padding
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -53,9 +53,11 @@ class Lot(metaclass=PoolMeta):
|
|||||||
lot_move = fields.One2Many('lot.move','lot',"Move")
|
lot_move = fields.One2Many('lot.move','lot',"Move")
|
||||||
invoice_line = fields.Many2One('account.invoice.line',"Purch.Invoice line")
|
invoice_line = fields.Many2One('account.invoice.line',"Purch.Invoice line")
|
||||||
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")
|
||||||
delta_qt = fields.Numeric("Delta Qt")
|
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_pr = fields.Numeric("Delta Pr")
|
||||||
delta_amt = fields.Numeric("Delta Amt")
|
delta_amt = fields.Numeric("Delta Amt")
|
||||||
warrant_nb = fields.Char("Warrant Nb")
|
warrant_nb = fields.Char("Warrant Nb")
|
||||||
@@ -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)
|
||||||
@@ -2831,6 +2834,23 @@ class LotInvoice(Wizard):
|
|||||||
self.message.invoice = invoice_line.invoice
|
self.message.invoice = invoice_line.invoice
|
||||||
|
|
||||||
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 {
|
||||||
@@ -2870,10 +2890,14 @@ class LotInvoiceStart(ModelView):
|
|||||||
|
|
||||||
quantity = fields.Numeric("Quantity to invoice",digits='unit',readonly=True,states={'invisible': Eval('type')=='sale'})
|
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'})
|
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',
|
||||||
@fields.depends('type')
|
states={
|
||||||
|
'invisible': (Eval('type') == 'purchase') | (Eval('action') != 'prov'),
|
||||||
|
})
|
||||||
|
|
||||||
|
@fields.depends('type')
|
||||||
def on_change_with_action(self, name=None):
|
def on_change_with_action(self, name=None):
|
||||||
if self.lot_p and self.type == 'purchase':
|
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]:
|
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
|
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)
|
||||||
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:
|
||||||
quantity += l.lot_diff_quantity
|
quantity += l.lot_diff_quantity
|
||||||
else:
|
else:
|
||||||
quantity += l.lot_quantity
|
quantity += l.lot_quantity
|
||||||
return quantity
|
quantity += Decimal(str(self.sale_padding or 0))
|
||||||
|
return quantity
|
||||||
@fields.depends('lot_s','type','amount_s')
|
|
||||||
def on_change_with_amount_s(self, name=None):
|
@fields.depends('lot_s','type','amount_s','sale_padding')
|
||||||
if self.lot_s and self.type == 'sale':
|
def on_change_with_amount_s(self, name=None):
|
||||||
amount = Decimal(0)
|
if self.lot_s and self.type == 'sale':
|
||||||
for l in self.lot_s:
|
amount = Decimal(0)
|
||||||
if l.lot.sale_invoice_line_prov:
|
padding = Decimal(str(self.sale_padding or 0))
|
||||||
amount += l.lot_diff_amount
|
padding_share = (
|
||||||
else:
|
padding / Decimal(len(self.lot_s))
|
||||||
amount += l.lot_amount
|
if padding and self.lot_s else Decimal(0))
|
||||||
return amount
|
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
|
@classmethod
|
||||||
def default_action(cls):
|
def default_action(cls):
|
||||||
|
|||||||
@@ -25,16 +25,26 @@ this repository contains the full copyright notices and license terms. -->
|
|||||||
<field name="name">lot_report_graph</field>
|
<field name="name">lot_report_graph</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record model="ir.ui.view" id="view_lot_visgraph">
|
<record model="ir.ui.view" id="view_lot_visgraph">
|
||||||
<field name="model">lot.lot</field>
|
<field name="model">lot.lot</field>
|
||||||
<field name="type">graph</field>
|
<field name="type">graph</field>
|
||||||
<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.action.act_window" id="act_lot_visgraph">
|
<record model="ir.ui.view" id="lot_view_form">
|
||||||
<field name="name">Lots graph</field>
|
<field name="model">lot.lot</field>
|
||||||
<field name="res_model">lot.lot</field>
|
<field name="inherit" ref="lot.lot_view_form"/>
|
||||||
</record>
|
<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">
|
<record model="ir.action.act_window.view" id="act_lot_visgraph_form_view">
|
||||||
<field name="sequence" eval="80"/>
|
<field name="sequence" eval="80"/>
|
||||||
<field name="view" ref="view_lot_visgraph"/>
|
<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"/>
|
id="menu_lot_fcr_form"/>
|
||||||
|
|
||||||
</data>
|
</data>
|
||||||
</tryton>
|
</tryton>
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -100,14 +100,25 @@ class Move(metaclass=PoolMeta):
|
|||||||
|
|
||||||
LotMove.save([new_lm])
|
LotMove.save([new_lm])
|
||||||
|
|
||||||
class InvoiceLine(metaclass=PoolMeta):
|
class InvoiceLine(metaclass=PoolMeta):
|
||||||
__name__ = 'account.invoice.line'
|
__name__ = 'account.invoice.line'
|
||||||
|
|
||||||
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(
|
||||||
@classmethod
|
fields.Numeric("Included padding", digits=(1, 5)),
|
||||||
def validate(cls, lines):
|
'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)
|
super(InvoiceLine, cls).validate(lines)
|
||||||
Lot = Pool().get('lot.lot')
|
Lot = Pool().get('lot.lot')
|
||||||
for line in lines:
|
for line in lines:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
7
modules/purchase_trade/view/invoice_line_form.xml
Normal file
7
modules/purchase_trade/view/invoice_line_form.xml
Normal 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>
|
||||||
@@ -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>
|
||||||
7
modules/purchase_trade/view/lot_form.xml
Normal file
7
modules/purchase_trade/view/lot_form.xml
Normal 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>
|
||||||
@@ -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/>
|
||||||
|
|||||||
6
modules/purchase_trade/view/lot_tree_sequence.xml
Normal file
6
modules/purchase_trade/view/lot_tree_sequence.xml
Normal 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>
|
||||||
Reference in New Issue
Block a user