This commit is contained in:
2026-04-26 10:51:06 +02:00
parent 9879926f08
commit 530f2f9d97
12 changed files with 205 additions and 47 deletions

View File

@@ -411,6 +411,29 @@ Owner technique: `a completer`
- Priorite: - Priorite:
- `importante` - `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 ### 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 - Intent: eviter qu'une valuation `basis` ouverte sorte a zero alors que la

View File

@@ -1,5 +1,16 @@
<tryton> <tryton>
<data> <data>
<record model="ir.ui.view" id="invoice_line_view_form">
<field name="model">account.invoice.line</field>
<field name="inherit" ref="account_invoice.invoice_line_view_form"/>
<field name="name">invoice_line_form</field>
</record>
<record model="ir.ui.view" id="invoice_line_view_tree_sequence">
<field name="model">account.invoice.line</field>
<field name="inherit" ref="account_invoice.invoice_line_view_tree_sequence"/>
<field name="name">invoice_line_tree_sequence</field>
</record>
<record model="ir.action.report" id="report_invoice_packing_list"> <record model="ir.action.report" id="report_invoice_packing_list">
<field name="name">Packing List</field> <field name="name">Packing List</field>
<field name="model">account.invoice</field> <field name="model">account.invoice</field>

View File

@@ -55,6 +55,8 @@ class Lot(metaclass=PoolMeta):
invoice_line_prov = fields.Many2One('account.invoice.line',"Purch. Invoice line prov") 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 = fields.Many2One('account.invoice.line',"Sale Invoice line")
sale_invoice_line_prov = fields.Many2One('account.invoice.line',"Sale Invoice line prov") 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_qt = fields.Numeric("Delta Qt")
delta_pr = fields.Numeric("Delta Pr") delta_pr = fields.Numeric("Delta Pr")
delta_amt = fields.Numeric("Delta Amt") delta_amt = fields.Numeric("Delta Amt")
@@ -2820,6 +2822,7 @@ class LotInvoice(Wizard):
break break
else: else:
if sales: if sales:
self._apply_sale_invoice_padding(lots)
Sale._process_invoice(sales, lots, action, self.inv.pp_sale) Sale._process_invoice(sales, lots, action, self.inv.pp_sale)
for lot in lots: for lot in lots:
lot = Lot(lot.id) lot = Lot(lot.id)
@@ -2832,6 +2835,23 @@ class LotInvoice(Wizard):
return 'message' 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): def default_message(self, fields):
return { return {
'message': 'The invoice has been successfully created.', 'message': 'The invoice has been successfully created.',
@@ -2872,6 +2892,10 @@ class LotInvoiceStart(ModelView):
amount = fields.Numeric("Amount 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'}) 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'}) 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') @fields.depends('type')
def on_change_with_action(self, name=None): def on_change_with_action(self, name=None):
@@ -2908,7 +2932,7 @@ class LotInvoiceStart(ModelView):
amount += l.lot_amount amount += l.lot_amount
return amount return amount
@fields.depends('lot_s','type','quantity_s') @fields.depends('lot_s','type','quantity_s','sale_padding')
def on_change_with_quantity_s(self, name=None): def on_change_with_quantity_s(self, name=None):
if self.lot_s and self.type == 'sale': if self.lot_s and self.type == 'sale':
quantity = Decimal(0) quantity = Decimal(0)
@@ -2917,17 +2941,23 @@ class LotInvoiceStart(ModelView):
quantity += l.lot_diff_quantity quantity += l.lot_diff_quantity
else: else:
quantity += l.lot_quantity quantity += l.lot_quantity
quantity += Decimal(str(self.sale_padding or 0))
return quantity return quantity
@fields.depends('lot_s','type','amount_s') @fields.depends('lot_s','type','amount_s','sale_padding')
def on_change_with_amount_s(self, name=None): def on_change_with_amount_s(self, name=None):
if self.lot_s and self.type == 'sale': if self.lot_s and self.type == 'sale':
amount = Decimal(0) 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: for l in self.lot_s:
if l.lot.sale_invoice_line_prov: if l.lot.sale_invoice_line_prov:
amount += l.lot_diff_amount amount += l.lot_diff_amount
else: else:
amount += l.lot_amount amount += l.lot_amount
amount += padding_share * Decimal(str(l.lot_price or 0))
return amount return amount
@classmethod @classmethod

View File

@@ -31,6 +31,16 @@ this repository contains the full copyright notices and license terms. -->
<field name="name">lot_graph</field> <field name="name">lot_graph</field>
<field name="priority" eval="10"/> <field name="priority" eval="10"/>
</record> </record>
<record model="ir.ui.view" id="lot_view_form">
<field name="model">lot.lot</field>
<field name="inherit" ref="lot.lot_view_form"/>
<field name="name">lot_form</field>
</record>
<record model="ir.ui.view" id="lot_view_tree_sequence">
<field name="model">lot.lot</field>
<field name="inherit" ref="lot.lot_view_tree_sequence"/>
<field name="name">lot_tree_sequence</field>
</record>
<record model="ir.action.act_window" id="act_lot_visgraph"> <record model="ir.action.act_window" id="act_lot_visgraph">
<field name="name">Lots graph</field> <field name="name">Lots graph</field>
<field name="res_model">lot.lot</field> <field name="res_model">lot.lot</field>

View File

@@ -1042,6 +1042,30 @@ class PriceComposition(metaclass=PoolMeta):
class SaleLine(metaclass=PoolMeta): class SaleLine(metaclass=PoolMeta):
__name__ = 'sale.line' __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 @classmethod
def default_pricing_rule(cls): def default_pricing_rule(cls):
try: try:

View File

@@ -105,6 +105,17 @@ class InvoiceLine(metaclass=PoolMeta):
lot = fields.Many2One('lot.lot',"Lot") lot = fields.Many2One('lot.lot',"Lot")
fee = fields.Many2One('fee.fee',"Fee") 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 @classmethod
def validate(cls, lines): def validate(cls, lines):

View File

@@ -9,6 +9,7 @@ from trytond.pool import Pool
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
from trytond.modules.purchase_trade import valuation as valuation_module 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 from trytond.modules.purchase_trade.service import ContractFactory
@@ -2148,5 +2149,25 @@ class PurchaseTradeTestCase(ModuleTestCase):
self.assertIs(invoice_line.invoice, sale_invoice) 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 del ModuleTestCase

View File

@@ -0,0 +1,7 @@
<?xml version="1.0"?>
<data>
<xpath expr="/form/notebook/page[@id='general']/label[@name='quantity']" position="before">
<label name="included_padding"/>
<field name="included_padding"/>
</xpath>
</data>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0"?>
<data>
<xpath expr="/tree/field[@name='quantity']" position="after">
<field name="included_padding" optional="1"/>
</xpath>
</data>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0"?>
<data>
<xpath expr="/form/label[@name='warrant_nb']" position="before">
<label name="sale_invoice_padding"/>
<field name="sale_invoice_padding"/>
</xpath>
</data>

View File

@@ -16,6 +16,8 @@
<field name="amount"/> <field name="amount"/>
<label name="quantity_s"/> <label name="quantity_s"/>
<field name="quantity_s"/> <field name="quantity_s"/>
<label name="sale_padding"/>
<field name="sale_padding"/>
<label name="amount_s"/> <label name="amount_s"/>
<field name="amount_s"/> <field name="amount_s"/>
<newline/> <newline/>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0"?>
<data>
<xpath expr="/tree/field[@name='lot_quantity']" position="after">
<field name="sale_invoice_padding" optional="1"/>
</xpath>
</data>