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:
- `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

View File

@@ -1,5 +1,16 @@
<tryton>
<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">
<field name="name">Packing List</field>
<field name="model">account.invoice</field>

View File

@@ -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):

View File

@@ -25,16 +25,26 @@ this repository contains the full copyright notices and license terms. -->
<field name="name">lot_report_graph</field>
</record>
<record model="ir.ui.view" id="view_lot_visgraph">
<field name="model">lot.lot</field>
<field name="type">graph</field>
<field name="name">lot_graph</field>
<field name="priority" eval="10"/>
</record>
<record model="ir.action.act_window" id="act_lot_visgraph">
<field name="name">Lots graph</field>
<field name="res_model">lot.lot</field>
</record>
<record model="ir.ui.view" id="view_lot_visgraph">
<field name="model">lot.lot</field>
<field name="type">graph</field>
<field name="name">lot_graph</field>
<field name="priority" eval="10"/>
</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">
<field name="name">Lots graph</field>
<field name="res_model">lot.lot</field>
</record>
<record model="ir.action.act_window.view" id="act_lot_visgraph_form_view">
<field name="sequence" eval="80"/>
<field name="view" ref="view_lot_visgraph"/>
@@ -386,4 +396,4 @@ this repository contains the full copyright notices and license terms. -->
id="menu_lot_fcr_form"/>
</data>
</tryton>
</tryton>

View File

@@ -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:

View File

@@ -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:

View File

@@ -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

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"/>
<label name="quantity_s"/>
<field name="quantity_s"/>
<label name="sale_padding"/>
<field name="sale_padding"/>
<label name="amount_s"/>
<field name="amount_s"/>
<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>