diff --git a/modules/purchase_trade/docs/business-rules.md b/modules/purchase_trade/docs/business-rules.md index 73a6b9b..af933f8 100644 --- a/modules/purchase_trade/docs/business-rules.md +++ b/modules/purchase_trade/docs/business-rules.md @@ -411,6 +411,29 @@ Owner technique: `a completer` - Priorite: - `importante` +### BR-PT-019 - Le padding de facture provisoire vente augmente la quantite facturee sans modifier le lot physique + +- Intent: permettre de constituer une provision sur une facture provisoire + vente tout en gardant la trace de l'ecart avec la quantite reelle du lot. +- Description: + - Le wizard `lot.invoice` expose un padding global uniquement pour les + factures provisoires cote vente. + - Ce padding global est reparti egalement entre les lots selectionnes. + - La quantite de chaque ligne de facture provisoire vente est augmentee de la + part de padding du lot. + - Le padding ne modifie pas la quantite physique du lot. +- Resultat attendu: + - deux lots factures ensemble avec un padding global de `1000` recoivent + chacun `500` de padding + - la ligne facture affiche la quantite augmentee + - la ligne facture expose `Included padding` + - le lot conserve sa part de padding dans `sale_invoice_padding` +- Hors scope: + - les ecritures comptables specifiques au padding seront traitees dans un + second temps +- Priorite: + - `importante` + ### BR-PT-012 - Fallback valuation basis sans summary: utiliser le prix economique de la ligne - Intent: eviter qu'une valuation `basis` ouverte sorte a zero alors que la diff --git a/modules/purchase_trade/invoice.xml b/modules/purchase_trade/invoice.xml index d65fe66..ca8a2b8 100644 --- a/modules/purchase_trade/invoice.xml +++ b/modules/purchase_trade/invoice.xml @@ -1,5 +1,16 @@ + + account.invoice.line + + invoice_line_form + + + account.invoice.line + + invoice_line_tree_sequence + + Packing List account.invoice diff --git a/modules/purchase_trade/lot.py b/modules/purchase_trade/lot.py index 060bd49..fca5fe1 100755 --- a/modules/purchase_trade/lot.py +++ b/modules/purchase_trade/lot.py @@ -53,9 +53,11 @@ class Lot(metaclass=PoolMeta): lot_move = fields.One2Many('lot.move','lot',"Move") invoice_line = fields.Many2One('account.invoice.line',"Purch.Invoice line") invoice_line_prov = fields.Many2One('account.invoice.line',"Purch. Invoice line prov") - sale_invoice_line = fields.Many2One('account.invoice.line',"Sale Invoice line") - sale_invoice_line_prov = fields.Many2One('account.invoice.line',"Sale Invoice line prov") - delta_qt = fields.Numeric("Delta Qt") + sale_invoice_line = fields.Many2One('account.invoice.line',"Sale Invoice line") + sale_invoice_line_prov = fields.Many2One('account.invoice.line',"Sale Invoice line prov") + sale_invoice_padding = fields.Numeric( + "Sale invoice padding", digits='lot_unit_line', readonly=True) + delta_qt = fields.Numeric("Delta Qt") delta_pr = fields.Numeric("Delta Pr") delta_amt = fields.Numeric("Delta Amt") warrant_nb = fields.Char("Warrant Nb") @@ -2820,6 +2822,7 @@ class LotInvoice(Wizard): break else: if sales: + self._apply_sale_invoice_padding(lots) Sale._process_invoice(sales, lots, action, self.inv.pp_sale) for lot in lots: lot = Lot(lot.id) @@ -2831,6 +2834,23 @@ class LotInvoice(Wizard): self.message.invoice = invoice_line.invoice return 'message' + + @classmethod + def _split_sale_padding(cls, padding, lots): + padding = Decimal(str(padding or 0)) + if not padding or not lots: + return {} + share = padding / Decimal(len(lots)) + return {lot.id: share for lot in lots} + + def _apply_sale_invoice_padding(self, lots): + Lot = Pool().get('lot.lot') + if self.inv.type != 'sale' or self.inv.action != 'prov': + return + padding_by_lot = self._split_sale_padding(self.inv.sale_padding, lots) + for lot in lots: + lot.sale_invoice_padding = padding_by_lot.get(lot.id, Decimal(0)) + Lot.save(lots) def default_message(self, fields): return { @@ -2870,10 +2890,14 @@ class LotInvoiceStart(ModelView): quantity = fields.Numeric("Quantity to invoice",digits='unit',readonly=True,states={'invisible': Eval('type')=='sale'}) amount = fields.Numeric("Amount to invoice",digits='unit',readonly=True,states={'invisible': Eval('type')=='sale'}) - quantity_s = fields.Numeric("Quantity to invoice",digits='unit',readonly=True,states={'invisible': Eval('type')=='purchase'}) - amount_s = fields.Numeric("Amount to invoice",digits='unit',readonly=True,states={'invisible': Eval('type')=='purchase'}) - - @fields.depends('type') + quantity_s = fields.Numeric("Quantity to invoice",digits='unit',readonly=True,states={'invisible': Eval('type')=='purchase'}) + amount_s = fields.Numeric("Amount to invoice",digits='unit',readonly=True,states={'invisible': Eval('type')=='purchase'}) + sale_padding = fields.Numeric("Global padding", digits='unit', + states={ + 'invisible': (Eval('type') == 'purchase') | (Eval('action') != 'prov'), + }) + + @fields.depends('type') def on_change_with_action(self, name=None): if self.lot_p and self.type == 'purchase': if self.lot_p[0].lot.line.purchase.wb.qt_type in [e.quantity_type for e in self.lot_p[0].lot.lot_hist]: @@ -2908,27 +2932,33 @@ class LotInvoiceStart(ModelView): amount += l.lot_amount return amount - @fields.depends('lot_s','type','quantity_s') - def on_change_with_quantity_s(self, name=None): - if self.lot_s and self.type == 'sale': - quantity = Decimal(0) - for l in self.lot_s: - if l.lot.sale_invoice_line_prov: - quantity += l.lot_diff_quantity - else: - quantity += l.lot_quantity - return quantity - - @fields.depends('lot_s','type','amount_s') - def on_change_with_amount_s(self, name=None): - if self.lot_s and self.type == 'sale': - amount = Decimal(0) - for l in self.lot_s: - if l.lot.sale_invoice_line_prov: - amount += l.lot_diff_amount - else: - amount += l.lot_amount - return amount + @fields.depends('lot_s','type','quantity_s','sale_padding') + def on_change_with_quantity_s(self, name=None): + if self.lot_s and self.type == 'sale': + quantity = Decimal(0) + for l in self.lot_s: + if l.lot.sale_invoice_line_prov: + quantity += l.lot_diff_quantity + else: + quantity += l.lot_quantity + quantity += Decimal(str(self.sale_padding or 0)) + return quantity + + @fields.depends('lot_s','type','amount_s','sale_padding') + def on_change_with_amount_s(self, name=None): + if self.lot_s and self.type == 'sale': + amount = Decimal(0) + padding = Decimal(str(self.sale_padding or 0)) + padding_share = ( + padding / Decimal(len(self.lot_s)) + if padding and self.lot_s else Decimal(0)) + for l in self.lot_s: + if l.lot.sale_invoice_line_prov: + amount += l.lot_diff_amount + else: + amount += l.lot_amount + amount += padding_share * Decimal(str(l.lot_price or 0)) + return amount @classmethod def default_action(cls): diff --git a/modules/purchase_trade/lot.xml b/modules/purchase_trade/lot.xml index 8f013a0..a6a32ff 100755 --- a/modules/purchase_trade/lot.xml +++ b/modules/purchase_trade/lot.xml @@ -25,16 +25,26 @@ this repository contains the full copyright notices and license terms. --> lot_report_graph - - lot.lot - graph - lot_graph - - - - Lots graph - lot.lot - + + lot.lot + graph + lot_graph + + + + lot.lot + + lot_form + + + lot.lot + + lot_tree_sequence + + + Lots graph + lot.lot + @@ -386,4 +396,4 @@ this repository contains the full copyright notices and license terms. --> id="menu_lot_fcr_form"/> - \ No newline at end of file + diff --git a/modules/purchase_trade/sale.py b/modules/purchase_trade/sale.py index ae41c13..803250d 100755 --- a/modules/purchase_trade/sale.py +++ b/modules/purchase_trade/sale.py @@ -1042,6 +1042,30 @@ class PriceComposition(metaclass=PoolMeta): class SaleLine(metaclass=PoolMeta): __name__ = 'sale.line' + def get_invoice_line(self, lots=None, action=None): + lines = super().get_invoice_line(lots, action) + if action != 'prov' or not lots: + return lines + + padding_by_lot = { + lot.id: Decimal(str(getattr(lot, 'sale_invoice_padding', 0) or 0)) + for lot in lots} + for invoice_line in lines: + if (getattr(invoice_line, 'invoice_type', None) != 'out' + or getattr(invoice_line, 'description', None) != 'Pro forma' + or Decimal(str(getattr(invoice_line, 'quantity', 0) or 0)) <= 0): + continue + lot = getattr(invoice_line, 'lot', None) + lot_id = getattr(lot, 'id', lot) + padding = padding_by_lot.get(lot_id, Decimal(0)) + if not padding: + continue + quantity = Decimal(str(invoice_line.quantity or 0)) + padding + if invoice_line.unit: + quantity = Decimal(str(invoice_line.unit.round(quantity))) + invoice_line.quantity = quantity + return lines + @classmethod def default_pricing_rule(cls): try: diff --git a/modules/purchase_trade/stock.py b/modules/purchase_trade/stock.py index 15944c5..8fd1543 100755 --- a/modules/purchase_trade/stock.py +++ b/modules/purchase_trade/stock.py @@ -100,14 +100,25 @@ class Move(metaclass=PoolMeta): LotMove.save([new_lm]) -class InvoiceLine(metaclass=PoolMeta): - __name__ = 'account.invoice.line' - - lot = fields.Many2One('lot.lot',"Lot") - fee = fields.Many2One('fee.fee',"Fee") - - @classmethod - def validate(cls, lines): +class InvoiceLine(metaclass=PoolMeta): + __name__ = 'account.invoice.line' + + lot = fields.Many2One('lot.lot',"Lot") + fee = fields.Many2One('fee.fee',"Fee") + included_padding = fields.Function( + fields.Numeric("Included padding", digits=(1, 5)), + 'get_included_padding') + + def get_included_padding(self, name): + if (self.lot + and self.invoice_type == 'out' + and self.description == 'Pro forma' + and Decimal(str(self.quantity or 0)) > 0): + return self.lot.sale_invoice_padding or Decimal(0) + return Decimal(0) + + @classmethod + def validate(cls, lines): super(InvoiceLine, cls).validate(lines) Lot = Pool().get('lot.lot') for line in lines: diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index 1d36da8..d145439 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -9,6 +9,7 @@ from trytond.pool import Pool from trytond.tests.test_tryton import ModuleTestCase, with_transaction from trytond.exceptions import UserError from trytond.modules.purchase_trade import valuation as valuation_module +from trytond.modules.purchase_trade import lot as lot_module from trytond.modules.purchase_trade.service import ContractFactory @@ -2148,5 +2149,25 @@ class PurchaseTradeTestCase(ModuleTestCase): self.assertIs(invoice_line.invoice, sale_invoice) + def test_lot_invoice_sale_padding_is_split_per_lot(self): + 'sale provisional padding is split equally across selected lots' + lots = [Mock(id=1), Mock(id=2)] + + self.assertEqual( + lot_module.LotInvoice._split_sale_padding(Decimal('1000'), lots), + {1: Decimal('500'), 2: Decimal('500')}) + + @with_transaction() + def test_invoice_line_included_padding_reads_sale_lot_padding(self): + 'invoice line exposes the sale provisional padding stored on the lot' + InvoiceLine = Pool().get('account.invoice.line') + line = InvoiceLine() + line.lot = Mock(sale_invoice_padding=Decimal('500')) + line.invoice_type = 'out' + line.description = 'Pro forma' + line.quantity = Decimal('10500') + + self.assertEqual(line.get_included_padding(None), Decimal('500')) + del ModuleTestCase diff --git a/modules/purchase_trade/view/invoice_line_form.xml b/modules/purchase_trade/view/invoice_line_form.xml new file mode 100644 index 0000000..7358592 --- /dev/null +++ b/modules/purchase_trade/view/invoice_line_form.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/modules/purchase_trade/view/invoice_line_tree_sequence.xml b/modules/purchase_trade/view/invoice_line_tree_sequence.xml new file mode 100644 index 0000000..729a5aa --- /dev/null +++ b/modules/purchase_trade/view/invoice_line_tree_sequence.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/modules/purchase_trade/view/lot_form.xml b/modules/purchase_trade/view/lot_form.xml new file mode 100644 index 0000000..43777fa --- /dev/null +++ b/modules/purchase_trade/view/lot_form.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/modules/purchase_trade/view/lot_invoice_start_form.xml b/modules/purchase_trade/view/lot_invoice_start_form.xml index 58bb1ec..24b5c8e 100755 --- a/modules/purchase_trade/view/lot_invoice_start_form.xml +++ b/modules/purchase_trade/view/lot_invoice_start_form.xml @@ -16,6 +16,8 @@