Padding logic in fee
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -73,3 +73,23 @@ Seuls les fees coches `To invoice` sont traites. En provisoire, un fee sans
|
||||
facture est facture normalement et un fee deja facture est ignore. En finale,
|
||||
un fee sans facture suit la facturation standard; un fee deja facture suit le
|
||||
flux DN/CN existant de `fee.fee.invoice`.
|
||||
|
||||
## Padding de facture vente dans les commissions broker
|
||||
|
||||
Le champ `lot.sale_invoice_padding` augmente la quantite facturee au client sans
|
||||
modifier le poids physique du lot. Pour les fees, `fee.quantity` reste donc la
|
||||
quantite reelle des lots effectifs.
|
||||
|
||||
Si un fee est coche `Add padding`, le calcul de facturation utilise une quantite
|
||||
dediee:
|
||||
|
||||
`quantite des lots effectifs du fee + padding vente des memes lots`
|
||||
|
||||
Cette quantite supplementaire s'applique uniquement aux modes:
|
||||
|
||||
- `Per qt`: montant = quantite avec padding * prix du fee;
|
||||
- `% price`: montant = quantite avec padding * prix unitaire de la ligne * %.
|
||||
|
||||
La purchase technique du fee et la ligne de facture service utilisent la meme
|
||||
base. Ainsi, une modification du padding apres une premiere facture est detectee
|
||||
par le flux DN/CN existant comme un changement de quantite ou de montant.
|
||||
|
||||
@@ -80,11 +80,12 @@ class Fee(ModelSQL,ModelView):
|
||||
('ppack', 'Per packing'),
|
||||
], 'Mode', required=True)
|
||||
auto_calculation = fields.Boolean("Auto",states={'readonly': (Eval('mode') != 'ppack')})
|
||||
inherit_qt = fields.Boolean("Inh Qt",states={'readonly': Eval('mode') != 'ppack'})
|
||||
quantity = fields.Numeric("Qt",digits='unit',states={'readonly': (Eval('mode') != 'ppack') | Bool(Eval('auto_calculation'))})
|
||||
unit = fields.Many2One('product.uom',"Unit",domain=[
|
||||
If(Eval('mode') == 'ppack',
|
||||
('category', '=', Eval('packing_category')),
|
||||
inherit_qt = fields.Boolean("Inh Qt",states={'readonly': Eval('mode') != 'ppack'})
|
||||
quantity = fields.Numeric("Qt",digits='unit',states={'readonly': (Eval('mode') != 'ppack') | Bool(Eval('auto_calculation'))})
|
||||
add_padding = fields.Boolean("Add padding")
|
||||
unit = fields.Many2One('product.uom',"Unit",domain=[
|
||||
If(Eval('mode') == 'ppack',
|
||||
('category', '=', Eval('packing_category')),
|
||||
()),
|
||||
],
|
||||
states={
|
||||
@@ -140,6 +141,10 @@ class Fee(ModelSQL,ModelView):
|
||||
@classmethod
|
||||
def default_generated_by_rule(cls):
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def default_add_padding(cls):
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def default_qt_state(cls):
|
||||
@@ -526,6 +531,8 @@ class Fee(ModelSQL,ModelView):
|
||||
def percent_price_lots_cover_line(self):
|
||||
if self.mode != 'pprice':
|
||||
return True
|
||||
if getattr(self, 'add_padding', False):
|
||||
return True
|
||||
line_lots = self._get_effective_line_lots()
|
||||
if not line_lots:
|
||||
return True
|
||||
@@ -644,6 +651,66 @@ class Fee(ModelSQL,ModelView):
|
||||
return None
|
||||
return sum(quantities)
|
||||
|
||||
def _convert_padding_quantity(self, quantity, from_unit):
|
||||
quantity = Decimal(str(quantity or 0))
|
||||
if not quantity:
|
||||
return Decimal(0)
|
||||
to_unit = getattr(self, 'unit', None)
|
||||
if not from_unit or not to_unit or from_unit == to_unit:
|
||||
return quantity
|
||||
Uom = Pool().get('product.uom')
|
||||
return Decimal(str(
|
||||
Uom.compute_qty(from_unit, float(quantity), to_unit) or 0))
|
||||
|
||||
def _get_lot_padding_quantity(self, lot):
|
||||
padding = Decimal(str(
|
||||
getattr(lot, 'sale_invoice_padding', 0) or 0))
|
||||
if not padding:
|
||||
return Decimal(0)
|
||||
source_line = (
|
||||
getattr(self, 'sale_line', None)
|
||||
or getattr(lot, 'sale_line', None)
|
||||
or getattr(self, 'line', None)
|
||||
or getattr(lot, 'line', None))
|
||||
source_unit = getattr(source_line, 'unit', None)
|
||||
if not source_unit:
|
||||
source_unit = getattr(lot, 'lot_unit_line', None)
|
||||
return self._convert_padding_quantity(padding, source_unit)
|
||||
|
||||
def get_padding_quantity(self):
|
||||
if not getattr(self, 'add_padding', False):
|
||||
return Decimal(0)
|
||||
return sum(
|
||||
self._get_lot_padding_quantity(lot)
|
||||
for lot in self._get_effective_fee_lots())
|
||||
|
||||
def get_billing_quantity(self):
|
||||
quantity = self._get_effective_fee_lots_quantity()
|
||||
if quantity is None:
|
||||
quantity = self.quantity
|
||||
if quantity is None:
|
||||
quantity = self.get_quantity()
|
||||
return Decimal(quantity or 0) + self.get_padding_quantity()
|
||||
|
||||
def _get_percent_price_unit_price(self):
|
||||
line = self.line or self.sale_line
|
||||
if line:
|
||||
return Decimal(str(getattr(line, 'unit_price', 0) or 0))
|
||||
lots = self._get_effective_fee_lots()
|
||||
lot = lots[0] if lots else None
|
||||
if lot:
|
||||
if getattr(lot, 'line', None):
|
||||
return Decimal(str(getattr(lot.line, 'unit_price', 0) or 0))
|
||||
if getattr(lot, 'sale_line', None):
|
||||
return Decimal(str(
|
||||
getattr(lot.sale_line, 'unit_price', 0) or 0))
|
||||
return Decimal(0)
|
||||
|
||||
def _get_padding_percent_price_amount(self):
|
||||
return (
|
||||
self.get_billing_quantity()
|
||||
* self._get_percent_price_unit_price())
|
||||
|
||||
def sync_quantity_from_lots(self):
|
||||
if self.mode == 'ppack' and self.auto_calculation:
|
||||
self.assert_quantity_consistency()
|
||||
@@ -740,6 +807,9 @@ class Fee(ModelSQL,ModelView):
|
||||
* self._get_amount_quantity() * sign, 2))
|
||||
|
||||
elif self.mode == 'perqt':
|
||||
if self.add_padding:
|
||||
return self._unsigned_amount(round(
|
||||
self.get_billing_quantity() * self.price * sign, 2))
|
||||
if self.shipment_in:
|
||||
StockMove = Pool().get('stock.move')
|
||||
sm = StockMove.search(['shipment','=','stock.shipment.in,'+str(self.shipment_in.id)])
|
||||
@@ -753,6 +823,11 @@ class Fee(ModelSQL,ModelView):
|
||||
|
||||
return self._unsigned_amount(round((self.quantity if self.quantity else 0) * self.price * sign,2))
|
||||
elif self.mode == 'pprice':
|
||||
if self.add_padding:
|
||||
return self._unsigned_amount(round(
|
||||
self.price / 100
|
||||
* self._get_padding_percent_price_amount()
|
||||
* sign, 2))
|
||||
if self.line:
|
||||
if self.percent_price_lots_cover_line():
|
||||
return self._unsigned_amount(round(
|
||||
@@ -819,6 +894,12 @@ class Fee(ModelSQL,ModelView):
|
||||
self.purchase.lines[0].unit_price = self.amount
|
||||
if self.purchase.lines[0].quantity != 1:
|
||||
self.purchase.lines[0].quantity = 1
|
||||
elif self.mode == 'perqt' and self.add_padding:
|
||||
quantity = self.get_billing_quantity()
|
||||
if self.price != self.purchase.lines[0].unit_price:
|
||||
self.purchase.lines[0].unit_price = self.price
|
||||
if quantity != self.purchase.lines[0].quantity:
|
||||
self.purchase.lines[0].quantity = quantity
|
||||
else:
|
||||
if self.get_price_per_qt() != self.purchase.lines[0].unit_price:
|
||||
self.purchase.lines[0].unit_price = self.get_price_per_qt()
|
||||
@@ -945,6 +1026,8 @@ class Fee(ModelSQL,ModelView):
|
||||
elif getattr(self, 'quantity', None) is not None:
|
||||
quantity = Decimal(self.quantity or 0)
|
||||
unit = getattr(self, 'unit', None)
|
||||
if self.mode == 'perqt' and getattr(self, 'add_padding', False):
|
||||
quantity = self.get_billing_quantity()
|
||||
return quantity, unit
|
||||
|
||||
def _get_ordered_purchase_locations(self):
|
||||
|
||||
@@ -1945,6 +1945,38 @@ class Line(metaclass=PoolMeta):
|
||||
super().__setup__()
|
||||
cls.quantity.readonly = True
|
||||
|
||||
def _get_service_fee_invoice_lines(self, invoice_line, lot):
|
||||
Fee = Pool().get('fee.fee')
|
||||
fee = Fee.search(['purchase', '=', self.purchase.id])
|
||||
if not fee:
|
||||
return [invoice_line]
|
||||
|
||||
fee = fee[0]
|
||||
invoice_line.fee = fee
|
||||
fee.warn_percent_price_partial_lots()
|
||||
if fee.mode == 'lumpsum':
|
||||
invoice_line.quantity = 1
|
||||
elif fee.mode == 'pprice':
|
||||
invoice_line.quantity = 1
|
||||
elif fee.mode == 'ppack':
|
||||
invoice_line.quantity = fee.quantity
|
||||
elif fee.mode == 'perqt' and getattr(fee, 'add_padding', False):
|
||||
invoice_line.quantity = fee.get_billing_quantity()
|
||||
else:
|
||||
invoice_line.quantity = fee.get_fee_lots_qt()
|
||||
|
||||
if getattr(fee, 'state', None) != 'invoiced':
|
||||
return [invoice_line]
|
||||
|
||||
previous_line = self._get_last_fee_invoice_line(fee, lot)
|
||||
if not self._fee_invoice_line_changed(previous_line, invoice_line):
|
||||
return []
|
||||
reversal_line = self._get_fee_reversal_invoice_line(
|
||||
previous_line, self)
|
||||
if reversal_line:
|
||||
return [reversal_line, invoice_line]
|
||||
return [invoice_line]
|
||||
|
||||
@staticmethod
|
||||
def _is_empty_quantity(quantity):
|
||||
if quantity in (None, ''):
|
||||
|
||||
@@ -3047,6 +3047,64 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
fee, '_get_effective_fee_lots', return_value=[lot]):
|
||||
self.assertEqual(fee.get_amount(), Decimal('20.00'))
|
||||
|
||||
def test_per_quantity_fee_with_padding_uses_lots_plus_padding(self):
|
||||
'per quantity fee can bill linked lots plus sale invoice padding'
|
||||
Fee = Pool().get('fee.fee')
|
||||
fee = Fee()
|
||||
unit = SimpleNamespace(id=1)
|
||||
sale_line = SimpleNamespace(unit=unit)
|
||||
lot = SimpleNamespace(
|
||||
id=1,
|
||||
lot_type='physic',
|
||||
sale_line=sale_line,
|
||||
sale_invoice_padding=Decimal('2'),
|
||||
lot_hist=[],
|
||||
get_current_quantity_converted=(
|
||||
lambda state=0, unit=None: Decimal('5')))
|
||||
fee.mode = 'perqt'
|
||||
fee.price = Decimal('10')
|
||||
fee.add_padding = True
|
||||
fee.unit = unit
|
||||
fee.quantity = Decimal('5')
|
||||
fee.line = None
|
||||
fee.sale_line = sale_line
|
||||
fee.shipment_in = None
|
||||
|
||||
with patch.object(
|
||||
fee, '_get_effective_fee_lots', return_value=[lot]):
|
||||
self.assertEqual(fee.get_billing_quantity(), Decimal('7'))
|
||||
self.assertEqual(fee.get_amount(), Decimal('70.00'))
|
||||
|
||||
def test_percent_price_fee_with_padding_uses_lots_padding_unit_price(self):
|
||||
'% price fee with padding uses fee lots plus padding as amount base'
|
||||
Fee = Pool().get('fee.fee')
|
||||
fee = Fee()
|
||||
unit = SimpleNamespace(id=1)
|
||||
sale_line = SimpleNamespace(
|
||||
unit=unit,
|
||||
unit_price=Decimal('100'),
|
||||
amount=Decimal('9999'))
|
||||
lot = SimpleNamespace(
|
||||
id=1,
|
||||
lot_type='physic',
|
||||
sale_line=sale_line,
|
||||
sale_invoice_padding=Decimal('2'),
|
||||
lot_hist=[],
|
||||
get_current_quantity_converted=(
|
||||
lambda state=0, unit=None: Decimal('5')))
|
||||
fee.mode = 'pprice'
|
||||
fee.price = Decimal('2')
|
||||
fee.add_padding = True
|
||||
fee.unit = unit
|
||||
fee.quantity = Decimal('5')
|
||||
fee.line = None
|
||||
fee.sale_line = sale_line
|
||||
fee.shipment_in = None
|
||||
|
||||
with patch.object(
|
||||
fee, '_get_effective_fee_lots', return_value=[lot]):
|
||||
self.assertEqual(fee.get_amount(), Decimal('14.00'))
|
||||
|
||||
def test_percent_price_fee_warns_when_line_lots_are_partial(self):
|
||||
'% price fee warns when it does not cover all line lots'
|
||||
Fee = Pool().get('fee.fee')
|
||||
@@ -3111,6 +3169,75 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
purchase_line_model.save.assert_called_once_with([purchase_line])
|
||||
purchase_model.save.assert_called_once_with([fee.purchase])
|
||||
|
||||
def test_per_quantity_fee_padding_adjusts_generated_purchase_quantity(self):
|
||||
'per quantity fee with padding updates the generated purchase quantity'
|
||||
Fee = Pool().get('fee.fee')
|
||||
fee = Fee()
|
||||
fee.type = 'ordered'
|
||||
fee.mode = 'perqt'
|
||||
fee.add_padding = True
|
||||
fee.price = Decimal('10')
|
||||
fee.product = Mock()
|
||||
fee.supplier = Mock()
|
||||
fee.currency = Mock()
|
||||
purchase_line = Mock(
|
||||
unit_price=Decimal('10'),
|
||||
quantity=Decimal('5'),
|
||||
product=fee.product)
|
||||
fee.purchase = Mock(
|
||||
lines=[purchase_line],
|
||||
party=fee.supplier,
|
||||
currency=fee.currency)
|
||||
purchase_model = Mock()
|
||||
purchase_line_model = Mock()
|
||||
|
||||
with patch(
|
||||
'trytond.modules.purchase_trade.fee.Pool'
|
||||
) as PoolMock, patch.object(
|
||||
fee, 'get_billing_quantity',
|
||||
return_value=Decimal('7')):
|
||||
PoolMock.return_value.get.side_effect = lambda name: {
|
||||
'purchase.purchase': purchase_model,
|
||||
'purchase.line': purchase_line_model,
|
||||
}[name]
|
||||
|
||||
fee.adjust_purchase_values()
|
||||
|
||||
self.assertEqual(purchase_line.unit_price, Decimal('10'))
|
||||
self.assertEqual(purchase_line.quantity, Decimal('7'))
|
||||
purchase_line_model.save.assert_called_once_with([purchase_line])
|
||||
purchase_model.save.assert_called_once_with([fee.purchase])
|
||||
|
||||
def test_purchase_trade_service_fee_invoice_uses_padding_quantity(self):
|
||||
'service fee invoice line uses billing quantity when padding is enabled'
|
||||
Line = purchase_module.Line
|
||||
line = Line()
|
||||
line.purchase = Mock(id=20)
|
||||
lot = Mock(id=30)
|
||||
invoice_line = Mock(quantity=Decimal('5'), unit_price=Decimal('10'))
|
||||
fee = Mock(
|
||||
state='not invoiced',
|
||||
mode='perqt',
|
||||
add_padding=True,
|
||||
warn_percent_price_partial_lots=Mock(),
|
||||
get_billing_quantity=Mock(return_value=Decimal('7')))
|
||||
fee_model = Mock()
|
||||
fee_model.search.return_value = [fee]
|
||||
|
||||
with patch(
|
||||
'trytond.modules.purchase_trade.purchase.Pool'
|
||||
) as PoolMock:
|
||||
PoolMock.return_value.get.side_effect = lambda name: {
|
||||
'fee.fee': fee_model,
|
||||
}[name]
|
||||
|
||||
result = line._get_service_fee_invoice_lines(invoice_line, lot)
|
||||
|
||||
self.assertEqual(result, [invoice_line])
|
||||
self.assertEqual(invoice_line.fee, fee)
|
||||
self.assertEqual(invoice_line.quantity, Decimal('7'))
|
||||
fee.get_billing_quantity.assert_called_once_with()
|
||||
|
||||
def test_fee_get_non_cog_returns_zero_without_move_lines(self):
|
||||
'fee non-cog amount is zero before any accounting move line exists'
|
||||
fee = fee_module.Fee()
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
<field name="currency"/>
|
||||
<label name="mode"/>
|
||||
<field name="mode"/>
|
||||
<label name="add_padding"/>
|
||||
<field name="add_padding"/>
|
||||
<label name="price"/>
|
||||
<field name="price"/>
|
||||
<label name="enable_linked_currency"/>
|
||||
|
||||
@@ -8,6 +8,7 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="mode"/>
|
||||
<field name="unit"/>
|
||||
<field name="auto_calculation" width="60"/>
|
||||
<field name="add_padding" width="80"/>
|
||||
<field name="enable_linked_currency" width="90"/>
|
||||
<field name="price"/>
|
||||
<field name="currency"/>
|
||||
|
||||
@@ -8,6 +8,7 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="mode"/>
|
||||
<field name="unit"/>
|
||||
<field name="auto_calculation" width="60"/>
|
||||
<field name="add_padding" width="80"/>
|
||||
<field name="enable_linked_currency" width="90"/>
|
||||
<field name="price"/>
|
||||
<field name="currency"/>
|
||||
|
||||
Reference in New Issue
Block a user