CN/DN commission
This commit is contained in:
@@ -4104,7 +4104,7 @@
|
|||||||
<text:p text:style-name="P17" loext:marker-style-name="T3"><text:span text:style-name="T7"><text:placeholder text:placeholder-type="text"><invoice.currency.name></text:placeholder></text:span><text:span text:style-name="T7"/></text:p>
|
<text:p text:style-name="P17" loext:marker-style-name="T3"><text:span text:style-name="T7"><text:placeholder text:placeholder-type="text"><invoice.currency.name></text:placeholder></text:span><text:span text:style-name="T7"/></text:p>
|
||||||
</table:table-cell>
|
</table:table-cell>
|
||||||
<table:table-cell table:style-name="Tableau6.B3" office:value-type="string">
|
<table:table-cell table:style-name="Tableau6.B3" office:value-type="string">
|
||||||
<text:p text:style-name="P19" loext:marker-style-name="T3"><text:span text:style-name="T7"><text:placeholder text:placeholder-type="text"><format_currency(invoice.total_amount, invoice.party.lang, invoice.currency)></text:placeholder></text:span><text:span text:style-name="T7"/></text:p>
|
<text:p text:style-name="P19" loext:marker-style-name="T3"><text:span text:style-name="T7"><text:placeholder text:placeholder-type="text"><format_number(invoice.total_amount, invoice.party.lang, digits=invoice.currency.digits)></text:placeholder></text:span><text:span text:style-name="T7"/></text:p>
|
||||||
</table:table-cell>
|
</table:table-cell>
|
||||||
</table:table-row>
|
</table:table-row>
|
||||||
</table:table>
|
</table:table>
|
||||||
|
|||||||
@@ -1915,8 +1915,11 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
|
|||||||
|
|
||||||
fee = fee[0]
|
fee = fee[0]
|
||||||
invoice_line.fee = fee
|
invoice_line.fee = fee
|
||||||
|
fee.warn_percent_price_partial_lots()
|
||||||
if fee.mode == 'lumpsum':
|
if fee.mode == 'lumpsum':
|
||||||
invoice_line.quantity = 1
|
invoice_line.quantity = 1
|
||||||
|
elif fee.mode == 'pprice':
|
||||||
|
invoice_line.quantity = 1
|
||||||
elif fee.mode == 'ppack':
|
elif fee.mode == 'ppack':
|
||||||
invoice_line.quantity = fee.quantity
|
invoice_line.quantity = fee.quantity
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -383,6 +383,7 @@ class Fee(ModelSQL,ModelView):
|
|||||||
"create a debit note or credit note by reversing the "
|
"create a debit note or credit note by reversing the "
|
||||||
"previous fee invoice line and adding a new line with "
|
"previous fee invoice line and adding a new line with "
|
||||||
"the current fee values. Do you want to continue?")
|
"the current fee values. Do you want to continue?")
|
||||||
|
fee.warn_percent_price_partial_lots()
|
||||||
fee.ensure_ordered_purchase()
|
fee.ensure_ordered_purchase()
|
||||||
if fee.purchase:
|
if fee.purchase:
|
||||||
fl = FeeLots.search([('fee','=',fee.id)])
|
fl = FeeLots.search([('fee','=',fee.id)])
|
||||||
@@ -498,6 +499,10 @@ class Fee(ModelSQL,ModelView):
|
|||||||
FeeLots = Pool().get('fee.lots')
|
FeeLots = Pool().get('fee.lots')
|
||||||
fee_lots = FeeLots.search([('fee', '=', self.id)])
|
fee_lots = FeeLots.search([('fee', '=', self.id)])
|
||||||
lots = [fee_lot.lot for fee_lot in fee_lots if fee_lot.lot]
|
lots = [fee_lot.lot for fee_lot in fee_lots if fee_lot.lot]
|
||||||
|
return self._select_effective_lots(lots)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _select_effective_lots(lots):
|
||||||
physical_lots = [
|
physical_lots = [
|
||||||
lot for lot in lots
|
lot for lot in lots
|
||||||
if getattr(lot, 'lot_type', None) == 'physic'
|
if getattr(lot, 'lot_type', None) == 'physic'
|
||||||
@@ -509,6 +514,42 @@ class Fee(ModelSQL,ModelView):
|
|||||||
if getattr(lot, 'lot_type', None) == 'virtual'
|
if getattr(lot, 'lot_type', None) == 'virtual'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _record_key(record):
|
||||||
|
return getattr(record, 'id', None) or id(record)
|
||||||
|
|
||||||
|
def _get_effective_line_lots(self):
|
||||||
|
line = self.line or self.sale_line
|
||||||
|
return self._select_effective_lots(
|
||||||
|
list(getattr(line, 'lots', []) or []))
|
||||||
|
|
||||||
|
def percent_price_lots_cover_line(self):
|
||||||
|
if self.mode != 'pprice':
|
||||||
|
return True
|
||||||
|
line_lots = self._get_effective_line_lots()
|
||||||
|
if not line_lots:
|
||||||
|
return True
|
||||||
|
fee_lots = self._get_effective_fee_lots()
|
||||||
|
fee_lot_keys = {self._record_key(lot) for lot in fee_lots}
|
||||||
|
return all(
|
||||||
|
self._record_key(lot) in fee_lot_keys
|
||||||
|
for lot in line_lots)
|
||||||
|
|
||||||
|
def warn_percent_price_partial_lots(self):
|
||||||
|
if self.percent_price_lots_cover_line():
|
||||||
|
return
|
||||||
|
Warning = Pool().get('res.user.warning')
|
||||||
|
warning_name = Warning.format(
|
||||||
|
"Partial % price fee lots", [self])
|
||||||
|
if Warning.check(warning_name):
|
||||||
|
raise UserWarning(
|
||||||
|
warning_name,
|
||||||
|
"This % price fee is linked to only part of the line lots. "
|
||||||
|
"The commission cannot be safely computed from a partial "
|
||||||
|
"quantity because lot quantities may change through BL, WR "
|
||||||
|
"or LR weighing states. Please link all lots to the fee or "
|
||||||
|
"review the fee amount manually before continuing.")
|
||||||
|
|
||||||
def is_effective_fee_lot(self, lot):
|
def is_effective_fee_lot(self, lot):
|
||||||
return bool(lot and lot in self._get_effective_fee_lots())
|
return bool(lot and lot in self._get_effective_fee_lots())
|
||||||
|
|
||||||
@@ -713,8 +754,16 @@ class Fee(ModelSQL,ModelView):
|
|||||||
return self._unsigned_amount(round((self.quantity if self.quantity else 0) * self.price * sign,2))
|
return self._unsigned_amount(round((self.quantity if self.quantity else 0) * self.price * sign,2))
|
||||||
elif self.mode == 'pprice':
|
elif self.mode == 'pprice':
|
||||||
if self.line:
|
if self.line:
|
||||||
|
if self.percent_price_lots_cover_line():
|
||||||
|
return self._unsigned_amount(round(
|
||||||
|
self.price / 100
|
||||||
|
* Decimal(self.line.amount or 0) * sign, 2))
|
||||||
return self._unsigned_amount(round(self.price / 100 * self.line.unit_price * (self.quantity if self.quantity else 0) * sign,2))
|
return self._unsigned_amount(round(self.price / 100 * self.line.unit_price * (self.quantity if self.quantity else 0) * sign,2))
|
||||||
if self.sale_line:
|
if self.sale_line:
|
||||||
|
if self.percent_price_lots_cover_line():
|
||||||
|
return self._unsigned_amount(round(
|
||||||
|
self.price / 100
|
||||||
|
* Decimal(self.sale_line.amount or 0) * sign, 2))
|
||||||
return self._unsigned_amount(round(self.price / 100 * self.sale_line.unit_price * (self.quantity if self.quantity else 0) * sign,2))
|
return self._unsigned_amount(round(self.price / 100 * self.sale_line.unit_price * (self.quantity if self.quantity else 0) * sign,2))
|
||||||
if self.shipment_in:
|
if self.shipment_in:
|
||||||
StockMove = Pool().get('stock.move')
|
StockMove = Pool().get('stock.move')
|
||||||
@@ -761,13 +810,18 @@ class Fee(ModelSQL,ModelView):
|
|||||||
if self.mode == 'lumpsum':
|
if self.mode == 'lumpsum':
|
||||||
if self.amount != self.purchase.lines[0].unit_price:
|
if self.amount != self.purchase.lines[0].unit_price:
|
||||||
self.purchase.lines[0].unit_price = self.amount
|
self.purchase.lines[0].unit_price = self.amount
|
||||||
elif self.mode == 'ppack':
|
elif self.mode == 'ppack':
|
||||||
if self.amount != self.purchase.lines[0].amount:
|
if self.amount != self.purchase.lines[0].amount:
|
||||||
self.purchase.lines[0].unit_price = self.price
|
self.purchase.lines[0].unit_price = self.price
|
||||||
self.purchase.lines[0].quantity = self.quantity
|
self.purchase.lines[0].quantity = self.quantity
|
||||||
else:
|
elif self.mode == 'pprice':
|
||||||
if self.get_price_per_qt() != self.purchase.lines[0].unit_price:
|
if self.amount != self.purchase.lines[0].unit_price:
|
||||||
self.purchase.lines[0].unit_price = self.get_price_per_qt()
|
self.purchase.lines[0].unit_price = self.amount
|
||||||
|
if self.purchase.lines[0].quantity != 1:
|
||||||
|
self.purchase.lines[0].quantity = 1
|
||||||
|
else:
|
||||||
|
if self.get_price_per_qt() != self.purchase.lines[0].unit_price:
|
||||||
|
self.purchase.lines[0].unit_price = self.get_price_per_qt()
|
||||||
if self.quantity != self.purchase.lines[0].quantity:
|
if self.quantity != self.purchase.lines[0].quantity:
|
||||||
self.purchase.lines[0].quantity = self.quantity
|
self.purchase.lines[0].quantity = self.quantity
|
||||||
if self.product != self.purchase.lines[0].product:
|
if self.product != self.purchase.lines[0].product:
|
||||||
@@ -912,6 +966,10 @@ class Fee(ModelSQL,ModelView):
|
|||||||
line.quantity = round(quantity, 5)
|
line.quantity = round(quantity, 5)
|
||||||
line.unit = unit
|
line.unit = unit
|
||||||
line.fee_ = self.id
|
line.fee_ = self.id
|
||||||
|
if self.mode == 'pprice':
|
||||||
|
line.quantity = 1
|
||||||
|
line.unit_price = round(Decimal(self.amount or 0), 4)
|
||||||
|
return
|
||||||
if getattr(self, 'price', None) is not None:
|
if getattr(self, 'price', None) is not None:
|
||||||
fee_price = self.get_price_per_qt()
|
fee_price = self.get_price_per_qt()
|
||||||
logger.info("GET_FEE_PRICE_PER_QT:%s", fee_price)
|
logger.info("GET_FEE_PRICE_PER_QT:%s", fee_price)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from xml.etree import ElementTree
|
|||||||
from trytond.pool import Pool
|
from trytond.pool import Pool
|
||||||
from trytond.pyson import Eval
|
from trytond.pyson import Eval
|
||||||
from trytond.tests.test_tryton import ModuleTestCase, with_transaction
|
from trytond.tests.test_tryton import ModuleTestCase, with_transaction
|
||||||
from trytond.exceptions import UserError
|
from trytond.exceptions import UserError, UserWarning
|
||||||
from trytond.transaction import Transaction
|
from trytond.transaction import Transaction
|
||||||
from trytond.modules.purchase import purchase as base_purchase_module
|
from trytond.modules.purchase import purchase as base_purchase_module
|
||||||
from trytond.modules.purchase_trade import valuation as valuation_module
|
from trytond.modules.purchase_trade import valuation as valuation_module
|
||||||
@@ -2690,6 +2690,118 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
line._get_service_fee_invoice_lines(invoice_line, lot), [])
|
line._get_service_fee_invoice_lines(invoice_line, lot), [])
|
||||||
|
|
||||||
|
def test_purchase_service_percent_price_fee_invoice_uses_amount_line(self):
|
||||||
|
'% price service fee invoice uses a single amount line'
|
||||||
|
Line = base_purchase_module.Line
|
||||||
|
line = Line()
|
||||||
|
line.purchase = Mock(id=20)
|
||||||
|
lot = Mock(id=30)
|
||||||
|
invoice_line = Mock(quantity=Decimal('12'), unit_price=Decimal('50'))
|
||||||
|
fee = Mock(
|
||||||
|
state='not invoiced',
|
||||||
|
mode='pprice',
|
||||||
|
warn_percent_price_partial_lots=Mock())
|
||||||
|
fee_model = Mock()
|
||||||
|
fee_model.search.return_value = [fee]
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
'trytond.modules.purchase.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, 1)
|
||||||
|
self.assertEqual(invoice_line.unit_price, Decimal('50'))
|
||||||
|
fee.warn_percent_price_partial_lots.assert_called_once_with()
|
||||||
|
|
||||||
|
def test_percent_price_fee_amount_uses_full_line_amount(self):
|
||||||
|
'% price fee ignores quantity when all line lots are linked'
|
||||||
|
Fee = Pool().get('fee.fee')
|
||||||
|
fee = Fee()
|
||||||
|
lot = SimpleNamespace(id=1, lot_type='physic')
|
||||||
|
fee.mode = 'pprice'
|
||||||
|
fee.price = Decimal('2')
|
||||||
|
fee.quantity = Decimal('999')
|
||||||
|
fee.sale_line = None
|
||||||
|
fee.shipment_in = None
|
||||||
|
fee.line = SimpleNamespace(
|
||||||
|
amount=Decimal('1000'),
|
||||||
|
unit_price=Decimal('10'),
|
||||||
|
lots=[lot])
|
||||||
|
|
||||||
|
with patch.object(
|
||||||
|
fee, '_get_effective_fee_lots', return_value=[lot]):
|
||||||
|
self.assertEqual(fee.get_amount(), Decimal('20.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')
|
||||||
|
fee = Fee()
|
||||||
|
lot_1 = SimpleNamespace(id=1, lot_type='physic')
|
||||||
|
lot_2 = SimpleNamespace(id=2, lot_type='physic')
|
||||||
|
fee.mode = 'pprice'
|
||||||
|
fee.line = SimpleNamespace(lots=[lot_1, lot_2])
|
||||||
|
fee.sale_line = None
|
||||||
|
warning = Mock()
|
||||||
|
warning.format.return_value = 'partial-pprice-fee'
|
||||||
|
warning.check.return_value = True
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
'trytond.modules.purchase_trade.fee.Pool'
|
||||||
|
) as PoolMock, patch.object(
|
||||||
|
fee, '_get_effective_fee_lots', return_value=[lot_1]):
|
||||||
|
PoolMock.return_value.get.side_effect = lambda name: {
|
||||||
|
'res.user.warning': warning,
|
||||||
|
}[name]
|
||||||
|
|
||||||
|
with self.assertRaises(UserWarning):
|
||||||
|
fee.warn_percent_price_partial_lots()
|
||||||
|
|
||||||
|
warning.format.assert_called_once_with(
|
||||||
|
"Partial % price fee lots", [fee])
|
||||||
|
warning.check.assert_called_once_with('partial-pprice-fee')
|
||||||
|
|
||||||
|
def test_percent_price_fee_adjusts_purchase_to_amount_line(self):
|
||||||
|
'% price fee keeps generated purchase as one amount line'
|
||||||
|
Fee = Pool().get('fee.fee')
|
||||||
|
fee = Fee()
|
||||||
|
fee.type = 'ordered'
|
||||||
|
fee.mode = 'pprice'
|
||||||
|
fee.amount = Decimal('20')
|
||||||
|
fee.product = Mock()
|
||||||
|
fee.supplier = Mock()
|
||||||
|
fee.currency = Mock()
|
||||||
|
purchase_line = Mock(
|
||||||
|
unit_price=Decimal('10'),
|
||||||
|
quantity=Decimal('999'),
|
||||||
|
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:
|
||||||
|
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('20'))
|
||||||
|
self.assertEqual(purchase_line.quantity, 1)
|
||||||
|
purchase_line_model.save.assert_called_once_with([purchase_line])
|
||||||
|
purchase_model.save.assert_called_once_with([fee.purchase])
|
||||||
|
|
||||||
def test_fee_get_non_cog_returns_zero_without_move_lines(self):
|
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 non-cog amount is zero before any accounting move line exists'
|
||||||
fee = fee_module.Fee()
|
fee = fee_module.Fee()
|
||||||
|
|||||||
Reference in New Issue
Block a user