diff --git a/modules/purchase_trade/docs/fees.md b/modules/purchase_trade/docs/fees.md
index cfb653f..3a60894 100644
--- a/modules/purchase_trade/docs/fees.md
+++ b/modules/purchase_trade/docs/fees.md
@@ -58,6 +58,18 @@ stocke sur `fee.fee.purchase`, cree pour facturer le service, ne doit jamais
etre presente comme le contrat d'achat metier.
Les statuts d'invoicing et de paiement des lots suivent les lots effectifs du
-fee. Le statut de paiement du fee consolide sa facture et son eventuelle
-DN/CN; il est `Paid` seulement si tous les documents concernes sont payes et
-`Partially paid` des qu'au moins un paiement existe sans paiement complet.
+fee. Le statut de paiement du fee distingue `Invoice paid`, `DN/CN paid`,
+`All paid` et `Not paid` selon le paiement complet de sa facture et de son
+eventuelle DN/CN.
+
+## Facturation depuis Invoice physical lots
+
+Le dialogue affiche les fees `ordered` des lignes correspondant au cote actif:
+
+- `Purchase`: fees des `purchase.line` des lots selectionnes;
+- `Sale`: fees des `sale.line` des lots selectionnes.
+
+Seuls les fees coches `To invoice` sont traites. En provisoire, un fee sans
+facture est facture normalement et un fee deja facture est ignore. En finale,
+un fee sans facture suit la facturation standard; un fee deja facture suit le
+flux DN/CN existant de `fee.fee.invoice`.
diff --git a/modules/purchase_trade/fee.py b/modules/purchase_trade/fee.py
index 58a5e1c..42150f3 100755
--- a/modules/purchase_trade/fee.py
+++ b/modules/purchase_trade/fee.py
@@ -1559,9 +1559,10 @@ class FeeReport(
('paid', 'Paid'),
], "Lot payment"), 'get_lot_payment_status')
r_fee_payment_status = fields.Function(fields.Selection([
- ('not', 'Not paid'),
- ('partial', 'Partially paid'),
- ('paid', 'Paid'),
+ ('not_paid', 'Not paid'),
+ ('invoice_paid', 'Invoice paid'),
+ ('dn_cn_paid', 'DN/CN paid'),
+ ('all_paid', 'All paid'),
], "Fee payment"), 'get_fee_payment_status')
r_state = fields.Selection([
('not invoiced', 'Not invoiced'),
@@ -1661,14 +1662,27 @@ class FeeReport(
_, _, invoices = self._lot_invoice_data(Fee(self.id))
return self._invoice_payment_status(invoices)
+ @classmethod
+ def _fee_payment_status(cls, invoice, dn_cn):
+ invoice_paid = (
+ cls._invoice_payment_status(cls._unique_invoices([invoice]))
+ == 'paid')
+ dn_cn_paid = (
+ cls._invoice_payment_status(cls._unique_invoices([dn_cn]))
+ == 'paid')
+ if invoice_paid and dn_cn_paid:
+ return 'all_paid'
+ if invoice_paid:
+ return 'invoice_paid'
+ if dn_cn_paid:
+ return 'dn_cn_paid'
+ return 'not_paid'
+
def get_fee_payment_status(self, name):
Fee = Pool().get('fee.fee')
fee = Fee(self.id)
- invoices = self._unique_invoices([
- fee.get_invoice('inv'),
- getattr(fee, 'dn_cn', None),
- ])
- return self._invoice_payment_status(invoices)
+ return self._fee_payment_status(
+ fee.get_invoice('inv'), getattr(fee, 'dn_cn', None))
def get_shipment_origin(self, name):
if self.r_shipment_in:
diff --git a/modules/purchase_trade/lot.py b/modules/purchase_trade/lot.py
index 6cd673f..8c887de 100755
--- a/modules/purchase_trade/lot.py
+++ b/modules/purchase_trade/lot.py
@@ -25,7 +25,16 @@ from trytond.modules.purchase_trade.service import ContractFactory
from trytond.modules.purchase_trade.company_defaults import (
default_itsa_unit, is_itsa_company)
-logger = logging.getLogger(__name__)
+logger = logging.getLogger(__name__)
+
+
+def _fee_invoice_action(fee, invoice_action):
+ invoice = fee.get_invoice('inv') if fee else None
+ if invoice_action == 'final' and invoice:
+ return 'dn_cn'
+ if invoice:
+ return 'already_invoiced'
+ return 'standard'
class LotAccountingGraph(ModelSQL,ModelView):
"Lot accounting graph"
@@ -4667,7 +4676,7 @@ class LotInvoice(Wizard):
def transition_start(self):
return 'inv'
- def default_inv(self, fields):
+ def default_inv(self, fields):
lot_p = []
lot_s = []
fee_pur = []
@@ -4675,8 +4684,10 @@ class LotInvoice(Wizard):
fee_sale = []
pp_sale = []
act = 'prov'
- line = None
- sale_line = None
+ line = None
+ sale_line = None
+ purchase_lines = []
+ sale_lines = []
val = {}
Lot = Pool().get('lot.lot')
LotQt = Pool().get('lot.qt')
@@ -4690,7 +4701,9 @@ class LotInvoice(Wizard):
lot = lqts[0].lot_p
else:
lot = Lot(i)
- line = lot.line
+ line = lot.line
+ if line and line not in purchase_lines:
+ purchase_lines.append(line)
if lot.line.purchase:
if lot.line.purchase.wb:
if lot.line.purchase.wb.qt_type in [e.quantity_type for e in lot.lot_hist]:
@@ -4715,7 +4728,9 @@ class LotInvoice(Wizard):
unit = val['lot_unit']
val['lot_currency'] = lot.lot_price_ct_symbol
lot_p.append(val)
- sale_line = lot.sale_line
+ sale_line = lot.sale_line
+ if sale_line and sale_line not in sale_lines:
+ sale_lines.append(sale_line)
val_s = val.copy() # ou utiliser deepcopy si certains champs sont des objets imbriqués
val_s['lot_price'] = lot.lot_price_sale
val_s['lot_amount'] = lot.get_current_quantity_converted() * lot.lot_price_sale if lot.lot_price_sale else Decimal(0)
@@ -4737,22 +4752,28 @@ class LotInvoice(Wizard):
val_s['lot_currency'] = lot.lot_price_ct_symbol_sale
val_s['lot_unit'] = sale_line.unit.id if sale_line else None
lot_s.append(val_s)
- if line:
- if line.fees:
- for f in line.fees:
- if f.type == 'ordered':
- val = {}
- val['fee'] = f.id
- val['fee_type'] = f.product.id
- val['fee_quantity'] = f.quantity
- val['fee_price'] = f.price
- val['fee_unit'] = line.unit.id
- val['fee_amount'] = f.amount
- val['fee_landed_cost'] = f.fee_landed_cost
- fee_pur.append(val)
- if line.purchase:
- Prep = Pool().get('account.invoice')
- prep = Prep.search([('description','=','Prepayment'),('party','=',line.purchase.party.id),('state','!=','draft')])
+ seen_fees = set()
+ for purchase_line in purchase_lines:
+ if purchase_line.fees:
+ for f in purchase_line.fees:
+ if f.type == 'ordered':
+ fee_key = getattr(f, 'id', None) or id(f)
+ if fee_key in seen_fees:
+ continue
+ seen_fees.add(fee_key)
+ val = {}
+ val['fee'] = f.id
+ val['fee_type'] = f.product.id
+ val['fee_quantity'] = f.quantity
+ val['fee_price'] = f.price
+ val['fee_unit'] = purchase_line.unit.id
+ val['fee_amount'] = f.amount
+ val['fee_landed_cost'] = f.fee_landed_cost
+ fee_pur.append(val)
+ if line:
+ if line.purchase:
+ Prep = Pool().get('account.invoice')
+ prep = Prep.search([('description','=','Prepayment'),('party','=',line.purchase.party.id),('state','!=','draft')])
if prep:
for p in prep:
for li in p.lines:
@@ -4760,21 +4781,27 @@ class LotInvoice(Wizard):
val['inv'] = li.id
val['inv_amount'] = li.amount
pp_pur.append(val)
- if sale_line:
- if sale_line.fees:
- for f in sale_line.fees:
- if f.type == 'ordered':
- val = {}
- val['fee'] = f.id
- val['fee_type'] = f.product.id
- val['fee_quantity'] = f.quantity
- val['fee_price'] = f.price
- val['fee_unit'] = sale_line.unit.id
- val['fee_amount'] = f.amount
- val['fee_landed_cost'] = f.fee_landed_cost
- fee_sale.append(val)
- if sale_line.sale:
- Prep = Pool().get('account.invoice')
+ seen_fees = set()
+ for current_sale_line in sale_lines:
+ if current_sale_line.fees:
+ for f in current_sale_line.fees:
+ if f.type == 'ordered':
+ fee_key = getattr(f, 'id', None) or id(f)
+ if fee_key in seen_fees:
+ continue
+ seen_fees.add(fee_key)
+ val = {}
+ val['fee'] = f.id
+ val['fee_type'] = f.product.id
+ val['fee_quantity'] = f.quantity
+ val['fee_price'] = f.price
+ val['fee_unit'] = current_sale_line.unit.id
+ val['fee_amount'] = f.amount
+ val['fee_landed_cost'] = f.fee_landed_cost
+ fee_sale.append(val)
+ if sale_line:
+ if sale_line.sale:
+ Prep = Pool().get('account.invoice')
prep = Prep.search([('description','=','Prepayment'),('party','=',sale_line.sale.party.id),('state','!=','draft')])
if prep:
for p in prep:
@@ -4842,10 +4869,33 @@ class LotInvoice(Wizard):
break
if not invoice_line:
raise UserError("No invoice line was generated from the selected lots.")
+ self._invoice_selected_fees()
self.message.invoice = invoice_line.invoice
return 'message'
+ def _invoice_selected_fees(self):
+ Fee = Pool().get('fee.fee')
+ fee_lines = (
+ self.inv.fee_pur
+ if self.inv.type == 'purchase' else self.inv.fee_sale)
+ fees = []
+ seen = set()
+ for line in fee_lines or []:
+ fee = getattr(line, 'fee', None)
+ if not getattr(line, 'to_invoice', False) or not fee:
+ continue
+ fee_key = getattr(fee, 'id', None) or id(fee)
+ if fee_key in seen:
+ continue
+ seen.add(fee_key)
+ action = _fee_invoice_action(fee, self.inv.action)
+ if action == 'already_invoiced':
+ continue
+ fees.append(fee)
+ if fees:
+ Fee.invoice(fees)
+
@classmethod
def _split_sale_padding(cls, padding, lots):
padding = Decimal(str(padding or 0))
@@ -4983,19 +5033,31 @@ class LotInvoiceStart(ModelView):
def default_type(cls):
return 'purchase'
-class LotInvoicingFee(ModelView):
+class LotInvoicingFee(ModelView):
"Fees"
__name__ = "lot.invoicing.fee"
lfs = fields.Many2One('lot.invoice.start',"Invoicing")
fee = fields.Many2One('fee.fee',"Fee")
- fee_type = fields.Many2One('product.product',"Fee type")
- to_invoice = fields.Boolean("To invoice")
+ fee_type = fields.Many2One('product.product',"Fee type")
+ invoice_action = fields.Function(fields.Selection([
+ ('standard', 'Standard invoice'),
+ ('dn_cn', 'DN/CN'),
+ ('already_invoiced', 'Already invoiced'),
+ ], "Invoice action"), 'on_change_with_invoice_action')
+ to_invoice = fields.Boolean("To invoice", states={
+ 'readonly': Eval('invoice_action') == 'already_invoiced',
+ }, depends=['invoice_action'])
fee_unit = fields.Many2One('product.uom',"Unit",readonly=True)
fee_quantity = fields.Numeric("Qt",digits=(1,5),readonly=True)
fee_price = fields.Numeric("Price",digits=(1,4),readonly=True)
fee_amount = fields.Numeric("Amount",digits=(1,2),readonly=True)
- fee_landed_cost = fields.Boolean("To inventory",readonly=True)
+ fee_landed_cost = fields.Boolean("To inventory",readonly=True)
+
+ @fields.depends('fee', '_parent_lfs.action')
+ def on_change_with_invoice_action(self, name=None):
+ action = self.lfs.action if self.lfs else None
+ return _fee_invoice_action(self.fee, action)
class LotInvoicingInv(ModelView):
"Fees"
diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py
index e4adc28..a9c54df 100644
--- a/modules/purchase_trade/tests/test_module.py
+++ b/modules/purchase_trade/tests/test_module.py
@@ -5897,7 +5897,7 @@ class PurchaseTradeTestCase(ModuleTestCase):
self.assertEqual(FeeReport._invoice_payment_status([unpaid]), 'not')
def test_fee_report_payment_status_includes_fee_dn_cn(self):
- 'fee payment status consolidates the invoice and its DN/CN'
+ 'fee payment status distinguishes invoice and DN/CN payments'
FeeReport = Pool().get('fee.report')
report = FeeReport()
report.id = 10
@@ -5916,10 +5916,28 @@ class PurchaseTradeTestCase(ModuleTestCase):
self.assertEqual(
report.get_fee_payment_status('r_fee_payment_status'),
- 'partial')
+ 'invoice_paid')
fee.get_invoice.assert_called_once_with('inv')
+ unpaid_dn_cn.payment_lines = [SimpleNamespace()]
+ unpaid_dn_cn.amount_to_pay = Decimal('0')
+ with patch('trytond.modules.purchase_trade.fee.Pool') as PoolMock:
+ PoolMock.return_value.get.return_value = fee_model
+
+ self.assertEqual(
+ report.get_fee_payment_status('r_fee_payment_status'),
+ 'all_paid')
+
+ unpaid = SimpleNamespace(
+ state='posted', payment_lines=[],
+ amount_to_pay=Decimal('5'))
+ self.assertEqual(
+ FeeReport._fee_payment_status(unpaid, unpaid), 'not_paid')
+ self.assertEqual(
+ FeeReport._fee_payment_status(unpaid, unpaid_dn_cn),
+ 'dn_cn_paid')
+
def test_sale_report_converts_mixed_units_for_total_and_words(self):
'sale report totals prefer the virtual lot unit as common unit'
Sale = Pool().get('sale.sale')
@@ -8315,6 +8333,68 @@ class PurchaseTradeTestCase(ModuleTestCase):
lot_module.LotInvoice._split_sale_padding(Decimal('1000'), lots),
{1: Decimal('500'), 2: Decimal('500')})
+ def test_lot_invoice_fee_action_follows_lot_invoice_stage(self):
+ 'fee action is standard in provisional and DN/CN in final'
+ not_invoiced = Mock()
+ not_invoiced.get_invoice.return_value = None
+ invoiced = Mock()
+ invoiced.get_invoice.return_value = Mock()
+
+ self.assertEqual(
+ lot_module._fee_invoice_action(not_invoiced, 'prov'),
+ 'standard')
+ self.assertEqual(
+ lot_module._fee_invoice_action(invoiced, 'prov'),
+ 'already_invoiced')
+ self.assertEqual(
+ lot_module._fee_invoice_action(not_invoiced, 'final'),
+ 'standard')
+ self.assertEqual(
+ lot_module._fee_invoice_action(invoiced, 'final'),
+ 'dn_cn')
+
+ def test_lot_invoice_only_invoices_checked_fees_for_selected_side(self):
+ 'lot invoice invoices only checked fees from purchase or sale side'
+ purchase_fee = Mock(id=1)
+ purchase_fee.get_invoice.return_value = None
+ ignored_purchase_fee = Mock(id=2)
+ ignored_purchase_fee.get_invoice.return_value = None
+ sale_fee = Mock(id=3)
+ sale_fee.get_invoice.return_value = None
+ wizard = SimpleNamespace(inv=SimpleNamespace(
+ type='purchase', action='prov',
+ fee_pur=[
+ SimpleNamespace(fee=purchase_fee, to_invoice=True),
+ SimpleNamespace(fee=ignored_purchase_fee, to_invoice=False),
+ ],
+ fee_sale=[SimpleNamespace(fee=sale_fee, to_invoice=True)],
+ ))
+ fee_model = Mock()
+
+ with patch('trytond.modules.purchase_trade.lot.Pool') as PoolMock:
+ PoolMock.return_value.get.return_value = fee_model
+
+ lot_module.LotInvoice._invoice_selected_fees(wizard)
+
+ fee_model.invoice.assert_called_once_with([purchase_fee])
+
+ def test_lot_invoice_final_invoices_checked_existing_fee_as_dn_cn(self):
+ 'final lot invoice sends an already invoiced checked fee to Fee.invoice'
+ fee = Mock(id=1)
+ fee.get_invoice.return_value = Mock()
+ wizard = SimpleNamespace(inv=SimpleNamespace(
+ type='sale', action='final', fee_pur=[],
+ fee_sale=[SimpleNamespace(fee=fee, to_invoice=True)],
+ ))
+ fee_model = Mock()
+
+ with patch('trytond.modules.purchase_trade.lot.Pool') as PoolMock:
+ PoolMock.return_value.get.return_value = fee_model
+
+ lot_module.LotInvoice._invoice_selected_fees(wizard)
+
+ fee_model.invoice.assert_called_once_with([fee])
+
@with_transaction()
def test_invoice_line_included_padding_reads_sale_lot_padding(self):
'invoice line exposes the sale provisional padding stored on the lot'
diff --git a/modules/purchase_trade/view/lot_invoicing_fee_tree.xml b/modules/purchase_trade/view/lot_invoicing_fee_tree.xml
index 9d33171..5b83394 100755
--- a/modules/purchase_trade/view/lot_invoicing_fee_tree.xml
+++ b/modules/purchase_trade/view/lot_invoicing_fee_tree.xml
@@ -5,5 +5,6 @@
+
diff --git a/modules/purchase_trade/view/lot_invoicing_lot_fee.xml b/modules/purchase_trade/view/lot_invoicing_lot_fee.xml
index 929eb1c..6386c19 100755
--- a/modules/purchase_trade/view/lot_invoicing_lot_fee.xml
+++ b/modules/purchase_trade/view/lot_invoicing_lot_fee.xml
@@ -6,5 +6,6 @@
+