PluginProbe
Moloni / trunk
Moloni vtrunk
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 trunk, at src/Controllers/Documents.php

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