PluginProbe
Moloni / 5.0.09
Moloni v5.0.09
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 / Product.php

Product.php in Moloni 5.0.09, at src/Controllers/Product.php

551 lines 14.8 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\Curl;
6 use Moloni\Exceptions\APIException;
7 use Moloni\Exceptions\GenericException;
8 use Moloni\Storage;
9 use Moloni\Tools;
10 use WC_Product;
11 use WC_Tax;
12
13 /**
14 * Class Product
15 * @package Moloni\Controllers
16 */
17 class Product
18 {
19
20 /** @var WC_Product */
21 private $product;
22
23 /** @var WC_Product|false */
24 private $productParent = false;
25
26 private $moloniProduct = [];
27
28 public $product_id;
29 public $category_id;
30 protected $type;
31 public $reference;
32 public $name;
33 public $summary = '';
34 private $ean = '';
35 public $price;
36 private $unit_id;
37 public $has_stock;
38 public $stock;
39 private $warehouse_id = 0;
40 private $at_product_category = 'M';
41 protected $exemption_reason;
42 public $taxes;
43 public $visibility_id = 1;
44 public $fiscalZone;
45
46 public $composition_type = 0;
47 /** @var false|array */
48 public $child_products = false;
49
50 /**
51 * Product constructor.
52 * @param WC_Product $product
53 */
54 public function __construct($product)
55 {
56 $this->product = $product;
57
58 $parentId = $this->product->get_parent_id();
59
60 if ($parentId > 0) {
61 $this->productParent = wc_get_product($parentId);
62 }
63 }
64
65 /**
66 * Loads a product
67 *
68 * @throws APIException
69 */
70 public function loadByReference()
71 {
72 $this->setReference();
73
74 $searchProduct = Curl::simple('products/getByReference', ['reference' => $this->reference, 'with_invisible' => true, 'exact' => 1]);
75
76 if (!empty($searchProduct) && isset($searchProduct[0]['product_id'])) {
77 $this->moloniProduct = $searchProduct[0];
78
79 $this->product_id = $this->moloniProduct['product_id'];
80 $this->name = $this->moloniProduct['name'];
81 $this->summary = $this->moloniProduct['summary'];
82 $this->category_id = $this->moloniProduct['category_id'];
83 $this->has_stock = $this->moloniProduct['has_stock'];
84 $this->stock = $this->moloniProduct['stock'];
85 $this->warehouse_id = $this->moloniProduct['warehouse_id'];
86 $this->price = $this->moloniProduct['price'];
87 $this->child_products = $this->moloniProduct['child_products'];
88 $this->composition_type = $this->moloniProduct['composition_type'];
89 $this->taxes = $this->moloniProduct['taxes'];
90 $this->visibility_id = $this->moloniProduct['visibility_id'];
91
92 return $this;
93 }
94
95 return false;
96 }
97
98 /**
99 * Create a product based on a WooCommerce Product
100 * @return Product
101 *
102 * @throws APIException
103 * @throws GenericException
104 */
105 public function create()
106 {
107 $this->setProduct();
108
109 $props = $this->mapPropsToValues();
110 $props = apply_filters('moloni_before_moloni_product_insert', $props);
111
112 $insert = Curl::simple('products/insert', $props);
113
114 if (isset($insert['product_id'])) {
115 $this->product_id = $insert['product_id'];
116
117 Storage::$LOGGER->info(
118 str_replace('{0}', $this->reference, __('Produto criado no Moloni ({0})')),
119 [
120 'product_id' => $this->product_id,
121 'props' => $props
122 ]
123 );
124
125 return $this;
126 }
127
128 throw new GenericException(__('Erro ao inserir o produto') . $this->name);
129 }
130
131 /**
132 * Create a product based on a WooCommerce Product
133 *
134 * @return $this
135 *
136 * @throws APIException
137 * @throws GenericException
138 */
139 public function update(): Product
140 {
141 $this->setProduct();
142
143 $props = $this->mapPropsToValues();
144 $props = apply_filters('moloni_before_moloni_product_update', $props);
145
146 if (!$this->needsToUpdateProduct($props)) {
147 return $this;
148 }
149
150 $update = Curl::simple('products/update', $props);
151
152 if (isset($update['product_id'])) {
153 $this->product_id = $update['product_id'];
154
155 Storage::$LOGGER->info(
156 str_replace('{0}', $this->reference, __('Produto atualizado no Moloni ({0})')),
157 [
158 'product_id' => $this->product_id,
159 'props' => $props
160 ]
161 );
162
163 return $this;
164 }
165
166 throw new GenericException(__('Erro ao atualizar o produto') . $this->name);
167 }
168
169 // Gets //
170
171 /**
172 * @return bool|int
173 */
174 public function getProductId()
175 {
176 return $this->product_id ?: false;
177 }
178
179 public function getDefaultTax()
180 {
181 $moloniTax = Tools::getTaxFromRate(-1);
182
183 $tax = [];
184 $tax['tax_id'] = $moloniTax['tax_id'];
185 $tax['value'] = $moloniTax['value'];
186 $tax['order'] = 1;
187 $tax['cumulative'] = '0';
188
189 if ((float)$moloniTax['value'] > 0) {
190 return $tax;
191 }
192
193 return [];
194 }
195
196 // Privates //
197
198 /**
199 * @throws APIException
200 * @throws GenericException
201 */
202 private function setProduct()
203 {
204 $this
205 ->setReference()
206 ->setCategory()
207 ->setType()
208 ->setWarehouse()
209 ->setName()
210 ->setPrice()
211 ->setEan()
212 ->setUnitId()
213 ->setTaxes();
214 }
215
216 // Sets //
217
218 /**
219 * @return $this
220 */
221 private function setReference()
222 {
223 $this->reference = $this->product->get_sku();
224
225 if (empty($this->reference)) {
226 $this->reference = Tools::createReferenceFromString($this->product->get_name(), $this->product->get_id());
227 }
228
229 $this->reference = mb_substr($this->reference, 0, 30);
230
231 return $this;
232 }
233
234 /**
235 * @return Product
236 *
237 * @throws APIException
238 * @throws GenericException
239 */
240 private function setCategory()
241 {
242 $categories = $this->product->get_category_ids();
243
244 if (empty($categories) && $this->productParent) {
245 $categories = $this->productParent->get_category_ids();
246 }
247
248 // Get the deepest category from all the trees
249 if (!empty($categories) && is_array($categories)) {
250 $categoryTree = [];
251
252 foreach ($categories as $category) {
253 $parents = get_ancestors($category, 'product_cat');
254 $parents = array_reverse($parents);
255 $parents[] = $category;
256
257 if (is_array($parents) && count($parents) > count($categoryTree)) {
258 $categoryTree = $parents;
259 }
260 }
261
262 $this->category_id = 0;
263 foreach ($categoryTree as $categoryId) {
264 $category = get_term_by('id', $categoryId, 'product_cat');
265 if (!empty($category->name)) {
266 $categoryObj = new ProductCategory($category->name, $this->category_id);
267
268 if (!$categoryObj->loadByName()) {
269 $categoryObj->create();
270 }
271
272 $this->category_id = $categoryObj->category_id;
273 }
274 }
275 }
276
277 if ((int)$this->category_id === 0) {
278 $categoryObj = new ProductCategory('Loja Online', 0);
279
280 if (!$categoryObj->loadByName()) {
281 $categoryObj->create();
282 }
283
284 $this->category_id = $categoryObj->category_id;
285 }
286
287 return $this;
288 }
289
290 /**
291 * Available types:
292 * 1 Product
293 * 2 Service
294 * 3 Other
295 * 4 Tax (imposto)
296 * @return $this
297 */
298 protected function setType()
299 {
300 // If the product is virtual or downloadable then its a service
301 if ($this->product->is_virtual() || $this->product->is_downloadable()) {
302 $this->type = 2;
303 $this->has_stock = 0;
304 } else {
305 $this->type = 1;
306 $this->has_stock = $this->product->managing_stock() ? 1 : 0;
307 $this->stock = (float)$this->product->get_stock_quantity();
308 }
309
310 return $this;
311 }
312
313 /**
314 * Set the name of the product
315 * @return $this
316 */
317 protected function setName()
318 {
319 $this->name = strip_tags($this->product->get_name());
320
321 return $this;
322 }
323
324 /**
325 * Set the price of the product
326 * @return $this
327 */
328 private function setPrice()
329 {
330 $this->price = (float)wc_get_price_excluding_tax($this->product);
331
332 if ((float)$this->price === 0 && $this->productParent) {
333 $this->price = (float)wc_get_price_excluding_tax($this->productParent);
334 }
335
336 return $this;
337 }
338
339 /**
340 * @return $this
341 */
342 private function setEan()
343 {
344 foreach (['barcode', '_ywbc_barcode_display_value'] as $key) {
345 $metaBarcode = $this->product->get_meta($key, true);
346
347 if (!empty($metaBarcode)) {
348 $this->ean = $metaBarcode;
349 return $this;
350 }
351 }
352
353 if (method_exists($this->product, 'get_global_unique_id')) {
354 $metaBarcode = $this->product->get_global_unique_id();
355 } else {
356 $metaBarcode = $this->product->get_meta('_global_unique_id', true);
357 }
358
359 if (!empty($metaBarcode)) {
360 $this->ean = $metaBarcode;
361 }
362
363 return $this;
364 }
365
366 /**
367 * Set measurement unit
368 *
369 * @throws GenericException
370 */
371 private function setUnitId(): Product
372 {
373 if (defined('MEASURE_UNIT')) {
374 $this->unit_id = MEASURE_UNIT;
375 } else {
376 throw new GenericException(__('Unidade de medida não definida!'));
377 }
378
379 return $this;
380 }
381
382 /**
383 * Sets the taxes of a product or its exemption reason
384 *
385 * @throws APIException
386 */
387 protected function setTaxes()
388 {
389 $hasIVA = false;
390 $this->taxes = [];
391
392 if ($this->product->get_tax_status() === 'taxable') {
393 // Get taxes based on a tax class of a product
394 // If the tax class is empty it means the products uses the shop default
395 $productTaxes = $this->product->get_tax_class();
396 $taxRates = WC_Tax::get_base_tax_rates($productTaxes);
397
398 if (empty($this->fiscalZone)) {
399 $company = Curl::simple('companies/getOne', []);
400 $this->fiscalZone = strtoupper($company['country']['iso_3166_1']);
401 }
402
403 foreach ($taxRates as $order => $taxRate) {
404 $moloniTax = Tools::getTaxFromRate((float)$taxRate['rate'], $this->fiscalZone);
405
406 if (!$moloniTax) {
407 continue;
408 }
409
410 $tax = [];
411 $tax['tax_id'] = $moloniTax['tax_id'];
412 $tax['value'] = $taxRate['rate'];
413 $tax['order'] = $order;
414 $tax['cumulative'] = '0';
415
416 if ((float)$taxRate['rate'] > 0) {
417 $this->taxes[] = $tax;
418
419 if ((int)$moloniTax['saft_type'] === 1) {
420 $hasIVA = true;
421 }
422 }
423 }
424 }
425
426 if (!$hasIVA) {
427 if (defined('EXEMPTION_REASON') && EXEMPTION_REASON !== '') {
428 $this->exemption_reason = defined('EXEMPTION_REASON') ? EXEMPTION_REASON : '';
429 } else {
430 $this->taxes[] = $this->getDefaultTax();
431 }
432 }
433
434 return $this;
435 }
436
437 /**
438 * Set default warehouse
439 *
440 * @return $this
441 */
442 private function setWarehouse()
443 {
444 if ($this->warehouse_id > 0) {
445 return $this;
446 }
447
448 if (defined('MOLONI_PRODUCT_WAREHOUSE') && (int)MOLONI_PRODUCT_WAREHOUSE > 0) {
449 $this->warehouse_id = (int)MOLONI_PRODUCT_WAREHOUSE;
450 }
451
452 return $this;
453 }
454
455 // Auxiliary //
456
457 /**
458 * Map this object properties to an array to insert/update a moloni document
459 * @return array
460 */
461 private function mapPropsToValues()
462 {
463 $isNewProduct = empty($this->product_id);
464
465 $values = [];
466 $values['category_id'] = $this->category_id;
467 $values['type'] = $this->type;
468 $values['reference'] = $this->reference;
469 $values['name'] = $this->name;
470 $values['summary'] = $this->summary;
471
472 if (!empty($this->ean)) {
473 /** EAN is created from an external plugin so avoid to update it */
474 $values['ean'] = $this->ean;
475 }
476
477 $values['price'] = $this->price;
478 $values['unit_id'] = $this->unit_id;
479 $values['has_stock'] = $this->has_stock;
480 $values['exemption_reason'] = $this->exemption_reason;
481 $values['taxes'] = $this->taxes;
482 $values['visibility_id'] = $this->visibility_id;
483 $values['warehouse_id'] = $this->warehouse_id;
484
485 if ($isNewProduct) {
486 $values['at_product_category'] = $this->at_product_category;
487 } else {
488 $values['product_id'] = $this->product_id;
489 }
490
491 if ($this->warehouse_id > 0) {
492 $values['warehouses'][] = [
493 'warehouse_id' => $this->warehouse_id,
494 'stock' => $this->stock,
495 ];
496 } else {
497 $values['stock'] = $this->stock;
498 }
499
500 return $values;
501 }
502
503 /**
504 * Check if any attribute is outdated
505 *
506 * @param array $props
507 *
508 * @return true
509 */
510 private function needsToUpdateProduct(array $props): bool
511 {
512 if (empty($this->moloniProduct)) {
513 return true;
514 }
515
516 if (
517 (int)$props['category_id'] !== (int)$this->moloniProduct['category_id'] ||
518 (int)$props['unit_id'] !== (int)$this->moloniProduct['unit_id'] ||
519 (int)$props['visibility_id'] !== (int)$this->moloniProduct['visibility_id'] ||
520 round($props['price'], 5) !== round($this->moloniProduct['price'], 5) ||
521 ($props['name'] ?? '') !== ($this->moloniProduct['name'] ?? '') ||
522 ($props['summary'] ?? '') !== ($this->moloniProduct['summary'] ?? '') ||
523 ($props['ean'] ?? '') !== ($this->moloniProduct['ean'] ?? '') ||
524 ($props['exemption_reason'] ?? '') !== ($this->moloniProduct['exemption_reason'] ?? '')
525 ) {
526 return true;
527 }
528
529 $propsTaxCount = count($props['taxes'] ?? []);
530 $newTaxCount = count($this->taxes ?? []);
531
532 if ($propsTaxCount !== $newTaxCount) {
533 return true;
534 }
535
536 if ($propsTaxCount > 0 && $newTaxCount > 0) {
537 foreach ($props['taxes'] as $propTax) {
538 foreach ($this->taxes as $newTax) {
539 if ((int)$newTax['tax_id'] === (int)$propTax['tax_id']) {
540 continue 2;
541 }
542 }
543
544 return true;
545 }
546 }
547
548 return false;
549 }
550 }
551