1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* |
5
|
|
|
* |
6
|
|
|
* @category Mygento |
7
|
|
|
* @package Mygento_Payture |
8
|
|
|
* @copyright 2017 NKS LLC. (https://www.mygento.ru) |
9
|
|
|
*/ |
10
|
|
|
class Mygento_Payture_Helper_Discount extends Mage_Core_Helper_Abstract |
11
|
|
|
{ |
12
|
|
|
protected $_code = 'payture'; |
13
|
|
|
|
14
|
|
|
const VERSION = '1.0.9'; |
15
|
|
|
|
16
|
|
|
protected $generalHelper = null; |
17
|
|
|
|
18
|
|
|
protected $_entity = null; |
19
|
|
|
protected $_taxValue = null; |
20
|
|
|
protected $_taxAttributeCode = null; |
21
|
|
|
protected $_shippingTaxValue = null; |
22
|
|
|
|
23
|
|
|
protected $_discountlessSum = 0.00; |
24
|
|
|
|
25
|
|
|
protected $spreadDiscOnAllUnits = null; |
26
|
|
|
|
27
|
|
|
const NAME_UNIT_PRICE = 'disc_hlpr_price'; |
28
|
|
|
const NAME_SHIPPING_AMOUNT = 'disc_hlpr_shipping_amount'; |
29
|
|
|
|
30
|
|
|
/** Returns all items of the entity (order|invoice|creditmemo) with properly calculated discount and properly calculated Sum |
31
|
|
|
* @param $entity Mage_Sales_Model_Order | Mage_Sales_Model_Order_Invoice | Mage_Sales_Model_Order_Creditmemo |
32
|
|
|
* @param string $taxValue |
33
|
|
|
* @param string $taxAttributeCode Set it if info about tax is stored in product in certain attr |
34
|
|
|
* @param string $shippingTaxValue |
35
|
|
|
* @return array with calculated items and sum |
36
|
|
|
*/ |
37
|
|
|
public function getRecalculated($entity, $taxValue = '', $taxAttributeCode = '', $shippingTaxValue = '', $spreadDiscOnAllUnits = false) |
38
|
|
|
{ |
39
|
|
|
if (!$entity) { |
40
|
|
|
return; |
41
|
|
|
} |
42
|
|
|
|
43
|
|
|
$this->_entity = $entity; |
44
|
|
|
$this->_taxValue = $taxValue; |
45
|
|
|
$this->_taxAttributeCode = $taxAttributeCode; |
46
|
|
|
$this->_shippingTaxValue = $shippingTaxValue; |
47
|
|
|
$this->generalHelper = Mage::helper($this->_code); |
48
|
|
|
$this->spreadDiscOnAllUnits = $spreadDiscOnAllUnits; |
49
|
|
|
|
50
|
|
|
$generalHelper = $this->generalHelper; |
51
|
|
|
$generalHelper->addLog("== START == Recalculation of entity prices. Helper Version: " . self::VERSION . ". Entity class: " . get_class($entity) . ". Entity id: {$entity->getId()}"); |
52
|
|
|
|
53
|
|
|
//If there is no discounts - DO NOTHING |
54
|
|
|
if ($this->checkSpread()) { |
55
|
|
|
$this->applyDiscount(); |
56
|
|
|
} |
57
|
|
|
|
58
|
|
|
$this->generalHelper->addLog("== STOP == Recalculation. Entity class: " . get_class($entity) . ". Entity id: {$entity->getId()}"); |
59
|
|
|
|
60
|
|
|
return $this->buildFinalArray(); |
61
|
|
|
} |
62
|
|
|
|
63
|
|
|
public function applyDiscount() |
64
|
|
|
{ |
65
|
|
|
$subTotal = $this->_entity->getData('subtotal_incl_tax'); |
66
|
|
|
$shippingAmount = $this->_entity->getData('shipping_incl_tax'); |
67
|
|
|
$grandTotal = $this->_entity->getData('grand_total'); |
68
|
|
|
$grandDiscount = $grandTotal - $subTotal - $shippingAmount; |
69
|
|
|
|
70
|
|
|
$percentageSum = 0; |
71
|
|
|
|
72
|
|
|
$items = $this->getAllItems(); |
73
|
|
|
$itemsSum = 0.00; |
74
|
|
|
foreach ($items as $item) { |
75
|
|
|
if (!$this->isValidItem($item)) { |
76
|
|
|
continue; |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
$price = $item->getData('price_incl_tax'); |
80
|
|
|
$qty = $item->getQty() ?: $item->getQtyOrdered(); |
81
|
|
|
$rowTotal = $item->getData('row_total_incl_tax'); |
82
|
|
|
|
83
|
|
|
//Calculate Percentage. The heart of logic. |
84
|
|
|
$denominator = ($this->spreadDiscOnAllUnits || ($subTotal == $this->_discountlessSum)) ? $subTotal : ($subTotal - $this->_discountlessSum); |
85
|
|
|
$rowPercentage = $rowTotal / $denominator; |
86
|
|
|
|
87
|
|
|
if (!$this->spreadDiscOnAllUnits && (floatval($item->getDiscountAmount()) === 0.00)) { |
88
|
|
|
$rowPercentage = 0; |
89
|
|
|
} |
90
|
|
|
$percentageSum += $rowPercentage; |
91
|
|
|
|
92
|
|
|
$discountPerUnit = $rowPercentage * $grandDiscount / $qty; |
93
|
|
|
$priceWithDiscount = bcadd($price, $discountPerUnit, 2); |
94
|
|
|
|
95
|
|
|
//Set Recalculated unit price for the item |
96
|
|
|
$item->setData(self::NAME_UNIT_PRICE, $priceWithDiscount); |
97
|
|
|
|
98
|
|
|
$itemsSum += round($priceWithDiscount * $qty, 2); |
99
|
|
|
} |
100
|
|
|
|
101
|
|
|
$this->generalHelper->addLog("Sum of all percentages: {$percentageSum}"); |
102
|
|
|
|
103
|
|
|
//Calculate DIFF! |
104
|
|
|
$itemsSumDiff = round($this->slyFloor($grandTotal - $itemsSum - $shippingAmount, 3), 2); |
105
|
|
|
|
106
|
|
|
$this->generalHelper->addLog("Items sum: {$itemsSum}. All Discounts: {$grandDiscount} Diff value: {$itemsSumDiff}"); |
107
|
|
|
if (bccomp($itemsSumDiff, 0.00, 2) < 0) { |
108
|
|
|
//if: $itemsSumDiff < 0 |
|
|
|
|
109
|
|
|
$this->generalHelper->addLog("Notice: Sum of all items is greater than sumWithAllDiscount of entity. ItemsSumDiff: {$itemsSumDiff}"); |
110
|
|
|
$itemsSumDiff = 0.0; |
111
|
|
|
} |
112
|
|
|
|
113
|
|
|
//Set Recalculated Shipping Amount |
114
|
|
|
$this->_entity->setData(self::NAME_SHIPPING_AMOUNT, $this->_entity->getData('shipping_incl_tax') + $itemsSumDiff); |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
public function buildFinalArray() |
118
|
|
|
{ |
119
|
|
|
$grandTotal = $this->_entity->getData('grand_total'); |
120
|
|
|
|
121
|
|
|
$items = $this->getAllItems(); |
122
|
|
|
$itemsFinal = []; |
123
|
|
|
$itemsSum = 0.00; |
124
|
|
|
foreach ($items as $item) { |
125
|
|
|
if (!$this->isValidItem($item)) { |
126
|
|
|
continue; |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
$taxValue = $this->_taxAttributeCode ? $this->addTaxValue($this->_taxAttributeCode, $this->_entity, $item) : $this->_taxValue; |
130
|
|
|
$price = $item->getData(self::NAME_UNIT_PRICE) ?: $item->getData('price_incl_tax'); |
131
|
|
|
$entityItem = $this->_buildItem($item, $price, $taxValue); |
132
|
|
|
|
133
|
|
|
$itemsFinal[$item->getId()] = $entityItem; |
134
|
|
|
|
135
|
|
|
$itemsSum += $entityItem['sum']; |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
$receipt = [ |
139
|
|
|
'sum' => $itemsSum, |
140
|
|
|
'origGrandTotal' => floatval($grandTotal) |
141
|
|
|
]; |
142
|
|
|
|
143
|
|
|
$shippingAmount = $this->_entity->getData(self::NAME_SHIPPING_AMOUNT) ?: $this->_entity->getData('shipping_incl_tax') + 0.00; |
144
|
|
|
|
145
|
|
|
$shippingItem = [ |
146
|
|
|
'name' => $this->getShippingName($this->_entity), |
147
|
|
|
'price' => $shippingAmount, |
148
|
|
|
'quantity' => 1.0, |
149
|
|
|
'sum' => $shippingAmount, |
150
|
|
|
'tax' => $this->_shippingTaxValue, |
151
|
|
|
]; |
152
|
|
|
|
153
|
|
|
$itemsFinal['shipping'] = $shippingItem; |
154
|
|
|
$receipt['items'] = $itemsFinal; |
155
|
|
|
|
156
|
|
|
if (!$this->_checkReceipt($receipt)) { |
157
|
|
|
$this->generalHelper->addLog("WARNING: Calculation error! Sum of items is not equal to grandTotal!"); |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
$this->generalHelper->addLog("Final array:"); |
161
|
|
|
$this->generalHelper->addLog($receipt); |
162
|
|
|
|
163
|
|
|
$receiptObj = (object) $receipt; |
164
|
|
|
|
165
|
|
|
Mage::dispatchEvent('mygento_discount_recalculation_after', array('modulecode' => $this->_code, 'receipt' => $receiptObj)); |
166
|
|
|
|
167
|
|
|
return (array)$receiptObj; |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
protected function _buildItem($item, $price, $taxValue = '') |
171
|
|
|
{ |
172
|
|
|
$generalHelper = Mage::helper($this->_code); |
173
|
|
|
|
174
|
|
|
$qty = $item->getQty() ?: $item->getQtyOrdered(); |
175
|
|
|
if (!$qty) { |
176
|
|
|
throw new Exception('Divide by zero. Qty of the item is equal to zero! Item: ' . $item->getId()); |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
$entityItem = [ |
180
|
|
|
'price' => round($price, 2), |
181
|
|
|
'name' => $item->getName(), |
182
|
|
|
'quantity' => round($qty, 2), |
183
|
|
|
'sum' => round($price * $qty, 2), |
184
|
|
|
'tax' => $taxValue, |
185
|
|
|
]; |
186
|
|
|
|
187
|
|
|
$generalHelper->addLog("Item calculation details:"); |
188
|
|
|
$generalHelper->addLog("Item id: {$item->getId()}. Orig price: {$price} Item rowTotalInclTax: {$item->getData('row_total_incl_tax')} PriceInclTax of 1 piece: {$price}. Result of calc:"); |
189
|
|
|
$generalHelper->addLog($entityItem); |
190
|
|
|
|
191
|
|
|
return $entityItem; |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
public function getShippingName($entity) |
195
|
|
|
{ |
196
|
|
|
return $entity->getShippingDescription() |
197
|
|
|
?: ($entity->getOrder() ? $entity->getOrder()->getShippingDescription() : ''); |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
/**Validation method. It sums up all items and compares it to grandTotal. |
201
|
|
|
* @param array $receipt |
202
|
|
|
* @return bool True if all items price equal to grandTotal. False - if not. |
203
|
|
|
*/ |
204
|
|
|
protected function _checkReceipt(array $receipt) |
205
|
|
|
{ |
206
|
|
|
$sum = array_reduce($receipt['items'], function ($carry, $item) { |
207
|
|
|
$carry += $item['sum']; |
208
|
|
|
return $carry; |
209
|
|
|
}); |
210
|
|
|
|
211
|
|
|
return bcsub($sum, $receipt['origGrandTotal'], 2) === '0.00'; |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
public function isValidItem($item) |
215
|
|
|
{ |
216
|
|
|
return $item->getData('row_total_incl_tax') !== null; |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
public function slyFloor($val, $precision = 2) |
220
|
|
|
{ |
221
|
|
|
$factor = 1.00; |
222
|
|
|
$divider = pow(10, $precision); |
223
|
|
|
|
224
|
|
|
if ($val < 0) { |
225
|
|
|
$factor = -1.00; |
226
|
|
|
} |
227
|
|
|
|
228
|
|
|
return (floor(abs($val) * $divider) / $divider) * $factor; |
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
protected function addTaxValue($taxAttributeCode, $entity, $item) |
232
|
|
|
{ |
233
|
|
|
if (!$taxAttributeCode) { |
234
|
|
|
return ''; |
235
|
|
|
} |
236
|
|
|
$storeId = $entity->getStoreId(); |
237
|
|
|
$store = $storeId ? Mage::app()->getStore($storeId) : Mage::app()->getStore(); |
238
|
|
|
|
239
|
|
|
$taxValue = Mage::getResourceModel('catalog/product')->getAttributeRawValue( |
240
|
|
|
$item->getProductId(), |
241
|
|
|
$taxAttributeCode, |
242
|
|
|
$store |
243
|
|
|
); |
244
|
|
|
|
245
|
|
|
$attributeModel = Mage::getModel('eav/entity_attribute')->loadByCode('catalog_product', $taxAttributeCode); |
246
|
|
|
if ($attributeModel->getData('frontend_input') == 'select') { |
247
|
|
|
$taxValue = $attributeModel->getSource()->getOptionText($taxValue); |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
return $taxValue; |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
/** It checks do we need to spread dicount on all units and sets flag $this->spreadDiscOnAllUnits |
254
|
|
|
* @return nothing |
255
|
|
|
*/ |
256
|
|
|
public function checkSpread() |
257
|
|
|
{ |
258
|
|
|
$items = $this->getAllItems(); |
259
|
|
|
|
260
|
|
|
$sum = 0.00; |
261
|
|
|
$sumDiscountAmount = 0.00; |
262
|
|
|
$discountless = false; |
263
|
|
|
$this->_discountlessSum = 0.00; |
264
|
|
|
foreach ($items as $item) { |
265
|
|
|
$rowPrice = $item->getData('row_total_incl_tax') - $item->getData('discount_amount'); |
266
|
|
|
|
267
|
|
|
if (floatval($item->getData('discount_amount')) === 0.00) { |
268
|
|
|
$discountless = true; |
269
|
|
|
$this->_discountlessSum += $item->getData('row_total_incl_tax'); |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
$sum += $rowPrice; |
273
|
|
|
$sumDiscountAmount += $item->getData('discount_amount'); |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
$grandTotal = $this->_entity->getData('grand_total'); |
277
|
|
|
$shippingAmount = $this->_entity->getData('shipping_incl_tax'); |
278
|
|
|
|
279
|
|
|
//Есть ли общая скидка на Чек. bccomp returns 0 if operands are equal |
280
|
|
|
if (bccomp($grandTotal - $shippingAmount - $sum, 0.00, 2) !== 0) { |
281
|
|
|
$this->generalHelper->addLog("1. Global discount on whole cheque."); |
282
|
|
|
|
283
|
|
|
$this->spreadDiscOnAllUnits = true; |
284
|
|
|
return true; |
285
|
|
|
} |
286
|
|
|
|
287
|
|
|
//ок, нет скидки на заказ |
288
|
|
|
// Есть товар без скидок |
289
|
|
|
if ($discountless && ($sumDiscountAmount !== 0.00)) { |
290
|
|
|
$this->generalHelper->addLog("2. Item without discount."); |
291
|
|
|
|
292
|
|
|
return true; |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
// Все товары со скидками |
296
|
|
|
if ($sumDiscountAmount != 0.00) { |
297
|
|
|
$this->generalHelper->addLog("3. All items with discounts."); |
298
|
|
|
|
299
|
|
|
$this->spreadDiscOnAllUnits = true; |
300
|
|
|
return true; |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
return false; |
304
|
|
|
} |
305
|
|
|
|
306
|
|
|
public function getDecimalsCountAfterDiv($x, $y) |
307
|
|
|
{ |
308
|
|
|
$divRes = strval(round($x / $y, 3)); |
309
|
|
|
$decimals = strrchr($divRes, ".") ? strlen(strrchr($divRes, ".")) - 1 : 0; |
310
|
|
|
|
311
|
|
|
return $decimals; |
312
|
|
|
} |
313
|
|
|
|
314
|
|
|
public function getAllItems() |
315
|
|
|
{ |
316
|
|
|
return $this->_entity->getAllVisibleItems() ? $this->_entity->getAllVisibleItems() : $this->_entity->getAllItems(); |
317
|
|
|
} |
318
|
|
|
} |
319
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.