PluginProbe
Moloni / 5.0.06
Moloni v5.0.06
5.0.10 5.0.09 5.0.08 5.0.07 5.0.06 trunk 0.4.0.00 0.4.8.1 3.0.10 3.0.11 3.0.35 3.0.36 3.0.37 3.0.38 3.0.40 3.0.41 3.0.42 3.0.43 3.0.44 3.0.45 3.0.46 3.0.47 3.0.48 3.0.49 3.0.50 All 98 releases
moloni / src / Controllers / Documents.php

Documents.php in Moloni 5.0.06, at src/Controllers/Documents.php

1,174 lines 32.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Moloni\Controllers;
4
5 use Moloni\Exceptions\APIException;
6 use Moloni\Exceptions\DocumentError;
7 use Moloni\Exceptions\DocumentWarning;
8 use Moloni\Exceptions\GenericException;
9 use WC_Order;
10 use WC_Order_Item_Fee;
11 use WC_Order_Item_Product;
12 use Moloni\Curl;
13 use Moloni\Tools;
14 use Moloni\Storage;
15 use Moloni\Enums\Boolean;
16 use Moloni\Enums\DocumentTypes;
17 use Moloni\Enums\DocumentStatus;
18
19 /**
20 * Class Documents
21 * Used to create or update a Moloni Document
22 * @package Moloni\Controllers
23 */
24 class Documents
25 {
26 /**
27 * Moloni company data
28 *
29 * @var array
30 */
31 private $company;
32
33 /**
34 * Document fiscal zone
35 *
36 * @var array
37 */
38 private $fiscalData;
39
40 /**
41 * Associated documents
42 *
43 * @var array
44 */
45 private $associatedDocuments = [];
46
47 /**
48 * WooCommerce order object
49 *
50 * @var WC_Order
51 */
52 public $order;
53
54 /**
55 * WooCommerce order ID
56 *
57 * @var int
58 */
59 private $orderId;
60
61 /**
62 * Field used in filter to cancel document creation
63 *
64 * @var bool
65 */
66 public $stopProcess = false;
67
68 /**
69 * Created Moloni document data
70 *
71 * @var array
72 */
73 private $document = [];
74
75 /**
76 * Created Moloni document ID
77 *
78 * @var int
79 */
80 private $document_id = 0;
81
82 /**
83 * Moloni document total
84 *
85 * @var float
86 */
87 private $documentTotal = 0;
88
89 /**
90 * Moloni document exchange total total
91 *
92 * @var float
93 */
94 private $documentExchangeTotal = 0;
95
96 /**
97 * CAE ID
98 *
99 * @var int
100 */
101 private $caeId = 0;
102
103 /**
104 * Moloni customer ID
105 *
106 * @var int
107 */
108 public $customer_id;
109
110 /**
111 * Document set ID
112 *
113 * @var int
114 */
115 public $document_set_id;
116
117 /**
118 * Document reference
119 *
120 * @var string
121 */
122 public $our_reference = '';
123
124 /**
125 * Document reference
126 *
127 * @var string
128 */
129 public $your_reference = '';
130
131 /**
132 * Document data
133 *
134 * @var string in Y-m-d
135 */
136 public $date;
137
138 /**
139 * Document expiration date
140 *
141 * @var string in Y-m-d
142 */
143 public $expiration_date;
144
145 /**
146 * Document financial discount
147 *
148 * @var float
149 */
150 public $financial_discount = 0;
151
152 /**
153 * Document special discount
154 *
155 * @var float
156 */
157 public $special_discount = 0;
158
159 /**
160 * Document salesman ID
161 *
162 * @var int
163 */
164 public $salesman_id = 0;
165
166 /**
167 * Document salesman commission
168 *
169 * @var int
170 */
171 public $salesman_commission = 0;
172
173 // Delivery parameters being used if the option is set
174 public $delivery_datetime;
175 public $delivery_method_id = 0;
176
177 public $delivery_departure_address = '';
178 public $delivery_departure_city = '';
179 public $delivery_departure_zip_code = '';
180 public $delivery_departure_country = '';
181
182 public $delivery_destination_address = '';
183 public $delivery_destination_city = '';
184 public $delivery_destination_country = '';
185 public $delivery_destination_zip_code = '';
186 public $notes = '';
187
188 public $products = [];
189
190 public $payments = [];
191
192 public $documentType = '';
193 public $documentTypeName = '';
194 public $documentStatus = 0;
195
196 public $useShipping = 0;
197 public $sendEmail = 0;
198
199 /** @var int */
200 public $exchange_currency_id;
201 public $exchange_rate;
202
203 /**
204 * Constructor
205 *
206 * @param WC_Order $order
207 * @param array $company
208 *
209 * @throws DocumentError
210 */
211 public function __construct(WC_Order $order, array $company)
212 {
213 $this->order = $order;
214 $this->orderId = $order->get_id();
215 $this->company = $company;
216
217 $this->init();
218 }
219
220 /**
221 * Resets some values after cloning
222 *
223 * @return void
224 */
225 public function __clone()
226 {
227 $this->document = [];
228 $this->document_id = 0;
229
230 $this->documentTotal = 0;
231 $this->documentExchangeTotal = 0;
232
233 $this->associatedDocuments = [];
234 }
235
236 /**
237 * Associate a document with the current one
238 *
239 * @param int $documentId Document id to associate
240 * @param float $value Total value to associate
241 * @param array $products Document products
242 *
243 * @return $this
244 */
245 public function addAssociatedDocument(int $documentId, float $value, array $products = []): Documents
246 {
247 $this->associatedDocuments[] = [
248 'document_id' => $documentId,
249 'value' => $value,
250 'products' => $products
251 ];
252
253 return $this;
254 }
255
256 /**
257 * Create Moloni document
258 *
259 *
260 * @return Documents
261 *
262 * @throws DocumentError
263 * @throws DocumentWarning
264 */
265 public function createDocument(): Documents
266 {
267 apply_filters('moloni_before_insert_document', $this);
268
269 if ($this->stopProcess) {
270 return $this;
271 }
272
273 try {
274 $insertedDocument = Curl::simple($this->documentType . '/insert', $this->mapPropsToValues());
275 } catch (APIException $e) {
276 throw new DocumentError($e->getMessage(), $e->getData());
277 }
278
279 if (!isset($insertedDocument['document_id'])) {
280 throw new DocumentError(sprintf(__('Atenção, houve um erro ao inserir o documento %s'), $this->order->get_order_number()));
281 }
282
283 $this->document_id = $insertedDocument['document_id'];
284
285 $this->saveRecord();
286
287 try {
288 $this->document = Curl::simple('documents/getOne', ['document_id' => $insertedDocument['document_id'], 'with_bundle_products' => true]);
289 } catch (APIException $e) {
290 throw new DocumentError($e->getMessage(), $e->getData());
291 }
292
293 $this->documentTotal = (float)$this->document['net_value'];
294 $this->documentExchangeTotal = (float)$this->document['exchange_total_value'] > 0 ? (float)$this->document['exchange_total_value'] : $this->documentTotal;
295
296 apply_filters('moloni_after_insert_document', $this);
297
298 // If the documents are going to be inserted as closed
299 if ($this->shouldCloseDocument()) {
300 $this->closeDocument();
301 } else {
302 $note = __('Documento inserido como rascunho no Moloni');
303 $note .= " (" . $this->documentTypeName . ")";
304
305 $this->order->add_order_note($note);
306 }
307
308 $this->saveLog();
309
310 return $this;
311 }
312
313 /**
314 * Close Moloni document
315 *
316 * @throws DocumentWarning
317 * @throws DocumentError
318 */
319 public function closeDocument(): void
320 {
321 // Validate if the document totals match can be closed
322 $orderTotal = ((float)$this->order->get_total() - (float)$this->order->get_total_refunded());
323
324 if ($orderTotal !== $this->getDocumentExchangeTotal()) {
325 $note = __('Documento inserido como rascunho no Moloni');
326 $note .= " (" . $this->documentTypeName . ")";
327
328 $this->order->add_order_note($note);
329
330 $viewUrl = admin_url('admin.php?page=moloni&action=getInvoice&id=' . $this->document_id);
331
332 throw new DocumentWarning(
333 __('O documento foi inserido mas os totais não correspondem.') .
334 '<a href="' . esc_url($viewUrl) . '" target="_BLANK">Ver documento</a>'
335 );
336 }
337
338 $closeDocument = [
339 'document_id' => $this->document_id,
340 'status' => DocumentStatus::CLOSED
341 ];
342
343 // Associations need to be sent again when closing a document (but can skip product association)
344 if (!empty($this->associatedDocuments)) {
345 $this->associateDocuments($closeDocument, true);
346 }
347
348 // Send email to the client
349 if ($this->shouldSendEmail()) {
350 $this->order->add_order_note(__('Documento enviado por email para o cliente'));
351
352 $closeDocument['send_email'] = [];
353 $closeDocument['send_email'][] = [
354 'email' => $this->order->get_billing_email(),
355 'name' => $this->document['entity_name'],
356 'msg' => ''
357 ];
358 }
359
360 try {
361 $mutation = Curl::simple($this->documentType . '/update', $closeDocument);
362 } catch (APIException $e) {
363 throw new DocumentWarning($e->getMessage(), $e->getData());
364 }
365
366 if (!isset($mutation['document_id'])) {
367 if (str_starts_with($mutation[0]['code'] ?? '', '2 net_value')) {
368 $this->removeRecord();
369 }
370
371 throw new DocumentError(
372 sprintf(__('Atenção, houve um erro ao fechar o documento %s'), $this->order->get_order_number()),
373 $mutation
374 );
375 }
376
377 apply_filters('moloni_after_close_document', $this);
378
379 $note = __('Documento inserido no Moloni');
380 $note .= " (" . $this->documentTypeName . ")";
381
382 $this->order->add_order_note($note);
383 }
384
385 // PRIVATES //
386
387 /**
388 * Initialize document values
389 *
390 * @return void
391 *
392 * @throws DocumentError
393 */
394 private function init(): void
395 {
396 apply_filters('moloni_before_start_document', $this);
397
398 $this
399 ->setYourReference()
400 ->setDates()
401 ->setDocumentStatus()
402 ->setCustomer()
403 ->setDocumentType()
404 ->setDocumentSetId()
405 ->setSendEmail()
406 ->setFiscalData()
407 ->setProducts()
408 ->setShipping()
409 ->setFees()
410 ->setExchangeRate()
411 ->setCae()
412 ->setShippingInformation()
413 ->setDelivery()
414 ->setPaymentMethod()
415 ->setNotes();
416 }
417
418 /**
419 * Save document log
420 *
421 * @return void
422 */
423 private function saveLog(): void
424 {
425 $message = __('{0} foi gerado com sucesso ({1})');
426 $message = str_replace('{0}', $this->documentTypeName, $message);
427 $message = str_replace('{1}', $this->order->get_order_number(), $message);
428
429 Storage::$LOGGER->info($message, [
430 'order_id' => $this->orderId,
431 'document_id' => $this->document_id,
432 'document_status' => $this->documentStatus,
433 ]);
434 }
435
436 /**
437 * Save document id on order meta
438 *
439 * @return void
440 */
441 private function saveRecord(): void
442 {
443 $this->order->add_meta_data('_moloni_sent', $this->document_id);
444 $this->order->save();
445 }
446
447 /**
448 * Remove document id from order meta
449 *
450 * @return void
451 */
452 private function removeRecord(): void
453 {
454 $this->order->delete_meta_data_value('_moloni_sent', $this->document_id);
455 $this->order->save();
456 }
457
458 /**
459 * Map this object properties to an array to insert/update a moloni document
460 *
461 * @return array
462 */
463 private function mapPropsToValues(): array
464 {
465 $values = [];
466 $values['customer_id'] = $this->customer_id;
467 $values['document_set_id'] = $this->document_set_id;
468 $values['our_reference'] = $this->our_reference;
469 $values['your_reference'] = $this->your_reference;
470 $values['date'] = $this->date;
471 $values['expiration_date'] = $this->expiration_date;
472 $values['financial_discount'] = $this->financial_discount;
473 $values['special_discount'] = $this->special_discount;
474 $values['salesman_id'] = $this->salesman_id;
475 $values['salesman_commission'] = $this->salesman_commission;
476
477 $values['notes'] = $this->notes;
478 $values['status'] = DocumentStatus::DRAFT;
479 $values['eac_id'] = $this->caeId;
480 $values['products'] = $this->products;
481
482 if ($this->shouldAddShippingInformation()) {
483 $values['delivery_datetime'] = $this->delivery_datetime;
484 $values['delivery_method_id'] = $this->delivery_method_id;
485
486 $values['delivery_departure_address'] = $this->delivery_departure_address;
487 $values['delivery_departure_city'] = $this->delivery_departure_city;
488 $values['delivery_departure_zip_code'] = $this->delivery_departure_zip_code;
489 $values['delivery_departure_country'] = $this->delivery_departure_country;
490
491 $values['delivery_destination_address'] = $this->delivery_destination_address;
492 $values['delivery_destination_city'] = $this->delivery_destination_city;
493 $values['delivery_destination_zip_code'] = $this->delivery_destination_zip_code;
494 $values['delivery_destination_country'] = $this->delivery_destination_country;
495 }
496
497 if ($this->shouldAddPayment()) {
498 $values['payments'] = $this->payments;
499 }
500
501 if (!empty($this->exchange_currency_id)) {
502 $values['exchange_currency_id'] = $this->exchange_currency_id;
503 $values['exchange_rate'] = $this->exchange_rate;
504 }
505
506 if (!empty($this->associatedDocuments)) {
507 $this->associateDocuments($values);
508 }
509
510 return $values;
511 }
512
513 // AUXILIARY //
514
515 /**
516 * Auxiliary method to associate current document to associated list
517 *
518 * @param array $props API props
519 * @param bool|null $skipProducts Skip products association
520 *
521 * @return void
522 */
523 private function associateDocuments(array &$props, ?bool $skipProducts = false): void
524 {
525 // If multiple documents are associated, the need a global product counter
526 // Starts in -1 because the first thing we do is to increment its value
527 $currentProductIndex = -1;
528
529 $props['associated_documents'] = [];
530
531 foreach ($this->associatedDocuments as $associatedDocument) {
532 $newAssociation = [
533 'associated_id' => $associatedDocument['document_id'],
534 'value' => $associatedDocument['value']
535 ];
536
537 $props['associated_documents'][] = $newAssociation;
538
539 // Skip document product association
540 if ($skipProducts) {
541 continue;
542 }
543
544 if (!empty($associatedDocument['products'])) {
545 // Associate products from both documents
546 // We assume that the order of the documents is the same (beware if trying to do custom stuff)
547 foreach ($associatedDocument['products'] as $associatedProduct) {
548 $currentProductIndex++;
549
550 // To avoid errors, check length
551 if (!isset($props['products'][$currentProductIndex])) {
552 continue;
553 }
554
555 // Ids have to match
556 if ((int)$props['products'][$currentProductIndex]['product_id'] !== (int)$associatedProduct['product_id']) {
557 continue;
558 }
559
560 // Both have to be simple or bundle product
561 if (empty($associatedProduct['child_products']) !== empty($props['products'][$currentProductIndex]['child_products'])) {
562 continue;
563 }
564
565 if (empty($associatedProduct['child_products'])) {
566 $props['products'][$currentProductIndex]['origin_id'] = (int)$associatedDocument['document_id'];
567 $props['products'][$currentProductIndex]['related_id'] = (int)$associatedProduct['document_product_id'];
568 } else {
569 foreach ($associatedProduct['child_products'] as $childIndex => $childProduct) {
570 // To avoid errors, check length
571 if (!isset($props['products'][$currentProductIndex]['child_products'][$childIndex])) {
572 continue;
573 }
574
575 // Ids have to match
576 if ((int)$props['products'][$currentProductIndex]['child_products'][$childIndex]['product_id'] !== (int)$childProduct['product_id']) {
577 continue;
578 }
579
580 $props['products'][$currentProductIndex]['child_products'][$childIndex]['origin_id'] = (int)$associatedDocument['document_id'];
581 $props['products'][$currentProductIndex]['child_products'][$childIndex]['related_id'] = (int)$childProduct['document_product_id'];
582 }
583 }
584 }
585 }
586 }
587 }
588
589 // GETS //
590
591 /**
592 * Get document id
593 *
594 * @return int
595 */
596 public function getDocumentId(): int
597 {
598 return $this->document_id;
599 }
600
601 /**
602 * Get document total
603 *
604 * @return float|int
605 */
606 public function getDocumentTotal()
607 {
608 return $this->documentTotal;
609 }
610
611 /**
612 * Get created document products
613 *
614 * @return array
615 */
616 public function getDocumentProducts(): array
617 {
618 return $this->document['products'] ?? [];
619 }
620
621 /**
622 * Get document exchange total
623 *
624 * @return float|int
625 */
626 public function getDocumentExchangeTotal()
627 {
628 return $this->documentExchangeTotal;
629 }
630
631 // SETS //
632
633 /**
634 * Set document status
635 *
636 * @param $documentStatus
637 *
638 * @return $this
639 */
640 public function setDocumentStatus($documentStatus = null): Documents
641 {
642 switch (true) {
643 case $documentStatus !== null:
644 $this->documentStatus = (int)$documentStatus;
645
646 break;
647 case defined('DOCUMENT_STATUS'):
648 $this->documentStatus = (int)DOCUMENT_STATUS;
649
650 break;
651 default:
652 $this->documentStatus = DocumentStatus::DRAFT;
653
654 break;
655 }
656
657 return $this;
658 }
659
660 /**
661 * Set document type
662 *
663 * @param null $documentType
664 *
665 * @return $this
666 *
667 * @throws DocumentError
668 */
669 public function setDocumentType($documentType = null): Documents
670 {
671 switch (true) {
672 case !empty($documentType):
673 $this->documentType = $documentType;
674
675 break;
676 case defined('DOCUMENT_TYPE'):
677 $this->documentType = DOCUMENT_TYPE;
678 break;
679 default:
680 $this->documentType = '';
681
682 break;
683 }
684
685 if (empty($this->documentType)) {
686 throw new DocumentError(__('Tipo de documento não definido nas opções'));
687 }
688
689 $this->documentTypeName = DocumentTypes::getDocumentTypeName($this->documentType);
690
691 return $this;
692 }
693
694 /**
695 * Set send by email
696 *
697 * @param $sendByEmail
698 *
699 * @return $this
700 */
701 public function setSendEmail($sendByEmail = null): Documents
702 {
703 switch (true) {
704 case $sendByEmail !== null:
705 $this->sendEmail = (int)$sendByEmail;
706
707 break;
708 case defined('EMAIL_SEND'):
709 $this->sendEmail = (int)EMAIL_SEND;
710
711 break;
712 default:
713 $this->sendEmail = 0;
714
715 break;
716 }
717
718 return $this;
719 }
720
721 /**
722 * Set use CAE ID
723 *
724 * @return $this
725 */
726 public function setCae(): Documents
727 {
728 if (defined('DOCUMENT_SET_CAE_ID')) {
729 $this->caeId = (int)DOCUMENT_SET_CAE_ID;
730 } else {
731 $this->caeId = 0;
732 }
733
734 return $this;
735 }
736
737 /**
738 * Set use shipping information
739 *
740 * @return $this
741 */
742 public function setShippingInformation(): Documents
743 {
744 if (defined('SHIPPING_INFO')) {
745 $this->useShipping = (int)SHIPPING_INFO;
746 } else {
747 $this->useShipping = 0;
748 }
749
750 return $this;
751 }
752
753 /**
754 * Set document reference
755 *
756 * @return $this
757 */
758 public function setYourReference(): Documents
759 {
760 $this->your_reference = '#' . $this->order->get_order_number();
761
762 return $this;
763 }
764
765 /**
766 * Set dates
767 *
768 * @return $this
769 */
770 public function setDates(): Documents
771 {
772 $this->date = date('Y-m-d');
773 $this->expiration_date = date('Y-m-d');
774
775 return $this;
776 }
777
778 /**
779 * Set costumer
780 *
781 * @return $this
782 *
783 * @throws DocumentError
784 */
785 public function setCustomer(): Documents
786 {
787 try {
788 $this->customer_id = (new OrderCustomer($this->order))->create();
789 } catch (APIException|GenericException $e) {
790 throw new DocumentError($e->getMessage(), $e->getData());
791 }
792
793 return $this;
794 }
795
796 /**
797 * Set document set id
798 *
799 * @return $this
800 *
801 * @throws DocumentError
802 */
803 public function setDocumentSetId(): Documents
804 {
805 if (!defined('DOCUMENT_SET_ID') || (int)DOCUMENT_SET_ID === 0) {
806 throw new DocumentError(__('Série de documentos em falta. <br>Por favor selecione uma série nas opções do plugin'));
807 }
808
809 $this->document_set_id = DOCUMENT_SET_ID;
810
811 return $this;
812 }
813
814 /**
815 * Set fiscal data
816 *
817 * @return $this
818 */
819 public function setFiscalData(): Documents
820 {
821 switch (get_option('woocommerce_tax_based_on')) {
822 case 'billing':
823 $fiscalData = [
824 'code' => strtoupper($this->order->get_billing_country()),
825 'state' => $this->order->get_billing_state(),
826 'country' => 0,
827 ];
828
829 break;
830 case 'shipping':
831 $fiscalData = [
832 'code' => strtoupper($this->order->get_shipping_country()),
833 'state' => $this->order->get_shipping_state(),
834 'country' => 0,
835 ];
836
837 break;
838 default:
839 case 'base':
840 $fiscalData = [
841 'code' => strtoupper($this->company['country']['iso_3166_1']),
842 'state' => $this->company['city'],
843 'country' => $this->company['country_id'],
844 ];
845
846 break;
847 }
848
849 if (empty($fiscalData['code'])) {
850 $fiscalData['code'] = strtoupper($this->company['country']['iso_3166_1']);
851 }
852
853 $this->fiscalData = $fiscalData;
854
855 return $this;
856 }
857
858 /**
859 * Set products
860 *
861 * @return $this
862 *
863 * @throws DocumentError
864 */
865 public function setProducts(): Documents
866 {
867 foreach ($this->order->get_items() as $orderProduct) {
868 /** Skip "child" products created by "YITH WooCommerce Product Bundles" plugin */
869 if ($orderProduct->get_meta('_bundled_by')) {
870 continue;
871 }
872
873 /**
874 * @var $orderProduct WC_Order_Item_Product
875 */
876 $newOrderProduct = new OrderProduct($orderProduct, $this->order, count($this->products), $this->fiscalData);
877
878 try {
879 $newOrderProduct->create();
880 } catch (APIException|GenericException $e) {
881 throw new DocumentError($e->getMessage(), $e->getData());
882 }
883
884 if ($newOrderProduct->qty > 0) {
885 $this->products[] = $newOrderProduct->mapPropsToValues();
886 }
887 }
888
889 return $this;
890 }
891
892 /**
893 * Set shipping information
894 *
895 * @return $this
896 *
897 * @throws DocumentError
898 */
899 public function setShipping(): Documents
900 {
901 if ($this->order->get_shipping_method() && (float)$this->order->get_shipping_total() > 0) {
902 $newOrderShipping = new OrderShipping($this->order, count($this->products), $this->fiscalData);
903
904 try {
905 $newOrderShipping->create();
906 } catch (APIException|GenericException $e) {
907 throw new DocumentError($e->getMessage(), $e->getData());
908 }
909
910 if ($newOrderShipping->getPrice() > 0) {
911 $this->products[] = $newOrderShipping->mapPropsToValues();
912 }
913 }
914
915 return $this;
916 }
917
918 /**
919 * Set fees
920 *
921 * @return $this
922 *
923 * @throws DocumentError
924 */
925 public function setFees(): Documents
926 {
927 foreach ($this->order->get_fees() as $key => $item) {
928 /** @var $item WC_Order_Item_Fee */
929 $feePrice = abs($item['line_total']);
930
931 if ($feePrice > 0) {
932 $newOrderFee = new OrderFees($item, count($this->products), $this->fiscalData);
933
934 try {
935 $this->products[] = $newOrderFee->create()->mapPropsToValues();
936 } catch (APIException|GenericException $e) {
937 throw new DocumentError($e->getMessage(), $e->getData());
938 }
939 }
940 }
941
942 return $this;
943 }
944
945 /**
946 * Set exchange rate
947 *
948 * @return $this
949 *
950 * @throws DocumentError
951 */
952 public function setExchangeRate(): Documents
953 {
954 if ($this->company['currency']['iso4217'] !== $this->order->get_currency()) {
955
956 try {
957 $this->exchange_currency_id = Tools::getCurrencyIdFromCode($this->order->get_currency());
958 $this->exchange_rate = Tools::getCurrencyExchangeRate($this->company['currency']['currency_id'], $this->exchange_currency_id);
959 } catch (APIException $e) {
960 throw new DocumentError($e->getMessage(), $e->getData());
961 }
962
963 if (!empty($this->products) && is_array($this->products)) {
964 foreach ($this->products as &$product) {
965 $product['price'] /= $this->exchange_rate;
966
967 if (!empty($product['child_products'])) {
968 foreach ($product['child_products'] as &$child_product) {
969 $child_product['price'] /= $this->exchange_rate;
970 }
971 }
972 }
973 }
974 }
975
976 return $this;
977 }
978
979 /**
980 * Set the document Payment Method
981 *
982 * @return $this
983 *
984 * @throws DocumentError
985 */
986 public function setPaymentMethod(): Documents
987 {
988 $paymentMethodName = $this->order->get_payment_method_title();
989
990 if (!empty($paymentMethodName)) {
991 $paymentMethod = new Payment($paymentMethodName);
992
993 try {
994 if (!$paymentMethod->loadByName()) {
995 $paymentMethod->create();
996 }
997 } catch (APIException|GenericException $e) {
998 throw new DocumentError($e->getMessage(), $e->getData());
999 }
1000
1001 if ((int)$paymentMethod->payment_method_id > 0) {
1002 $orderTotal = (float)$this->order->get_total() - (float)$this->order->get_total_refunded();
1003
1004 //Use exchange rate value on payment method value
1005 if ($this->exchange_rate && $this->exchange_rate > 0) {
1006 $orderTotal /= $this->exchange_rate;
1007 }
1008
1009 $this->payments[] = [
1010 'payment_method_id' => (int)$paymentMethod->payment_method_id,
1011 'date' => date('Y-m-d H:i:s'),
1012 'value' => $orderTotal
1013 ];
1014 }
1015 }
1016
1017 return $this;
1018 }
1019
1020 /**
1021 * Populate the document's customer notes from the WooCommerce order.
1022 *
1023 * If the ADD_ORDER_NOTES constant is defined and set to Boolean::NO, this method is a no-op.
1024 * Otherwise it builds a single string from the order's customer order notes joined with
1025 * '<br>' between entries. If no customer order notes exist, it falls back to
1026 * the order's single customer note (or an empty string). The resulting text is stored
1027 * on $this->notes.
1028 *
1029 * @return $this
1030 */
1031 public function setNotes(): Documents
1032 {
1033 if (defined('ADD_ORDER_NOTES') && (int)ADD_ORDER_NOTES === Boolean::NO) {
1034 return $this;
1035 }
1036
1037 $notes = '';
1038
1039 $orderNotes = $this->order->get_customer_order_notes();
1040
1041 if (!empty($orderNotes)) {
1042 $lastOrderNoteIndex = count($orderNotes) - 1;
1043
1044 foreach ($orderNotes as $index => $note) {
1045 $notes .= $note->comment_content;
1046
1047 if ($index !== $lastOrderNoteIndex) {
1048 $notes .= '<br>';
1049 }
1050 }
1051 }
1052
1053 if (empty($notes)) {
1054 $notes = $this->order->get_customer_note() ?? '';
1055 }
1056
1057 $this->notes = $notes;
1058
1059 return $this;
1060 }
1061
1062 /**
1063 * Set delivery details
1064 *
1065 * @return $this
1066 *
1067 * @throws DocumentError
1068 */
1069 public function setDelivery(): Documents
1070 {
1071 $shippingName = $this->order->get_shipping_method();
1072
1073 if (empty($shippingName)) {
1074 return $this;
1075 }
1076
1077 $this->delivery_destination_zip_code = $this->order->get_shipping_postcode();
1078
1079 if ($this->order->get_shipping_country() === 'PT') {
1080 $this->delivery_destination_zip_code = Tools::zipCheck($this->delivery_destination_zip_code);
1081 }
1082
1083 $deliveryMethod = new DeliveryMethod($this->order->get_shipping_method());
1084
1085 try {
1086 if (!$deliveryMethod->loadByName()) {
1087 $deliveryMethod->create();
1088 }
1089 } catch (APIException|GenericException $e) {
1090 throw new DocumentError($e->getMessage(), $e->getData());
1091 }
1092
1093 $this->delivery_method_id = $deliveryMethod->delivery_method_id > 0 ?
1094 $deliveryMethod->delivery_method_id : $this->company['delivery_method_id'];
1095
1096 $this->delivery_datetime = date('Y-m-d H:i:s');
1097
1098 $loadSetting = defined('LOAD_ADDRESS') ? (int)LOAD_ADDRESS : 0;
1099
1100 if ($loadSetting === 1 &&
1101 defined('LOAD_ADDRESS_CUSTOM_ADDRESS') &&
1102 defined('LOAD_ADDRESS_CUSTOM_CITY') &&
1103 defined('LOAD_ADDRESS_CUSTOM_CODE') &&
1104 defined('LOAD_ADDRESS_CUSTOM_COUNTRY')) {
1105 $this->delivery_departure_address = LOAD_ADDRESS_CUSTOM_ADDRESS;
1106 $this->delivery_departure_city = LOAD_ADDRESS_CUSTOM_CITY;
1107 $this->delivery_departure_zip_code = LOAD_ADDRESS_CUSTOM_CODE;
1108 $this->delivery_departure_country = (int)LOAD_ADDRESS_CUSTOM_COUNTRY;
1109 } else {
1110 $this->delivery_departure_address = $this->company['address'];
1111 $this->delivery_departure_city = $this->company['city'];
1112 $this->delivery_departure_zip_code = $this->company['zip_code'];
1113 $this->delivery_departure_country = $this->company['country_id'];
1114 }
1115
1116 $this->delivery_destination_address = $this->order->get_shipping_address_1() . ' ' . $this->order->get_shipping_address_2();
1117 $this->delivery_destination_city = $this->order->get_shipping_city();
1118
1119 try {
1120 $this->delivery_destination_country = Tools::getCountryIdFromCode($this->order->get_shipping_country());
1121 } catch (APIException $e) {
1122 throw new DocumentError($e->getMessage(), $e->getData());
1123 }
1124
1125 return $this;
1126 }
1127
1128 // VERIFICATIONS //
1129
1130 /**
1131 * Checks if document should have payments
1132 *
1133 * @return bool
1134 */
1135 protected function shouldAddPayment(): bool
1136 {
1137 return DocumentTypes::hasPayments($this->documentType);
1138 }
1139
1140 /**
1141 * Checks if document should be closed
1142 *
1143 * @return bool
1144 */
1145 protected function shouldCloseDocument(): bool
1146 {
1147 return $this->documentStatus === DocumentStatus::CLOSED;
1148 }
1149
1150 /**
1151 * Checks if document should be sent via email
1152 *
1153 * @return bool
1154 */
1155 protected function shouldSendEmail(): bool
1156 {
1157 return $this->sendEmail === Boolean::YES;
1158 }
1159
1160 /**
1161 * Checks if document should have shipping information
1162 *
1163 * @return bool
1164 */
1165 protected function shouldAddShippingInformation(): bool
1166 {
1167 if (DocumentTypes::requiresDelivery($this->documentType)) {
1168 return true;
1169 }
1170
1171 return $this->useShipping === Boolean::YES;
1172 }
1173 }
1174