diff --git a/modules/purchase_trade/docs/fees.md b/modules/purchase_trade/docs/fees.md
index a0d0545..cfb653f 100644
--- a/modules/purchase_trade/docs/fees.md
+++ b/modules/purchase_trade/docs/fees.md
@@ -49,3 +49,15 @@ Le recalcul est declenche apres:
`stock.shipment.in.validate()` recalcule deja le PnL depuis les lots du
shipment, mais ce workflow ne couvre pas les changements de fee saisis apres
coup dans l'onglet Fees.
+
+## Fee Report
+
+La colonne `Purchase` du Fee Report vient exclusivement du lot lie au fee
+(`fee.lots -> lot.line -> purchase.line.purchase`). Le purchase technique
+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.
diff --git a/modules/purchase_trade/fee.py b/modules/purchase_trade/fee.py
index f0ea47f..58a5e1c 100755
--- a/modules/purchase_trade/fee.py
+++ b/modules/purchase_trade/fee.py
@@ -1548,6 +1548,21 @@ class FeeReport(
r_inv = fields.Function(fields.Many2One('account.invoice',"Invoice"),'get_invoice')
r_dn_cn = fields.Function(fields.Many2One('account.invoice',"DN/CN"),
'get_dn_cn')
+ r_lot_invoice_status = fields.Function(fields.Selection([
+ ('not', 'Not invoiced'),
+ ('partial', 'Partially invoiced'),
+ ('invoiced', 'Invoiced'),
+ ], "Lot invoicing"), 'get_lot_invoice_status')
+ r_lot_payment_status = fields.Function(fields.Selection([
+ ('not', 'Not paid'),
+ ('partial', 'Partially paid'),
+ ('paid', 'Paid'),
+ ], "Lot payment"), 'get_lot_payment_status')
+ r_fee_payment_status = fields.Function(fields.Selection([
+ ('not', 'Not paid'),
+ ('partial', 'Partially paid'),
+ ('paid', 'Paid'),
+ ], "Fee payment"), 'get_fee_payment_status')
r_state = fields.Selection([
('not invoiced', 'Not invoiced'),
('invoiced', 'Invoiced'),
@@ -1580,6 +1595,80 @@ class FeeReport(
fee = Fee(self.id)
dn_cn = getattr(fee, 'dn_cn', None)
return getattr(dn_cn, 'id', dn_cn)
+
+ @staticmethod
+ def _unique_invoices(invoices):
+ unique = []
+ keys = set()
+ for invoice in invoices:
+ if not invoice or getattr(invoice, 'state', None) == 'cancelled':
+ continue
+ key = getattr(invoice, 'id', None) or id(invoice)
+ if key not in keys:
+ keys.add(key)
+ unique.append(invoice)
+ return unique
+
+ @classmethod
+ def _lot_invoice_data(cls, fee):
+ lots = fee._get_effective_fee_lots()
+ invoices = []
+ invoiced_lots = 0
+ for lot in lots:
+ lot_invoices = []
+ for name in ('invoice_line', 'invoice_line_prov'):
+ line = getattr(lot, name, None)
+ if line:
+ lot_invoices.append(getattr(line, 'invoice', None))
+ lot_invoices = cls._unique_invoices(lot_invoices)
+ if lot_invoices:
+ invoiced_lots += 1
+ invoices.extend(lot_invoices)
+ return lots, invoiced_lots, cls._unique_invoices(invoices)
+
+ @staticmethod
+ def _invoice_payment_status(invoices):
+ if not invoices:
+ return 'not'
+ statuses = []
+ for invoice in invoices:
+ payment_lines = list(
+ getattr(invoice, 'payment_lines', None) or [])
+ if not payment_lines:
+ statuses.append('not')
+ elif (getattr(invoice, 'state', None) == 'paid'
+ or getattr(invoice, 'amount_to_pay', None) == 0):
+ statuses.append('paid')
+ else:
+ statuses.append('partial')
+ if all(status == 'paid' for status in statuses):
+ return 'paid'
+ if any(status != 'not' for status in statuses):
+ return 'partial'
+ return 'not'
+
+ def get_lot_invoice_status(self, name):
+ Fee = Pool().get('fee.fee')
+ lots, invoiced_lots, _ = self._lot_invoice_data(Fee(self.id))
+ if not lots or not invoiced_lots:
+ return 'not'
+ if invoiced_lots == len(lots):
+ return 'invoiced'
+ return 'partial'
+
+ def get_lot_payment_status(self, name):
+ Fee = Pool().get('fee.fee')
+ _, _, invoices = self._lot_invoice_data(Fee(self.id))
+ return self._invoice_payment_status(invoices)
+
+ 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)
def get_shipment_origin(self, name):
if self.r_shipment_in:
@@ -1626,16 +1715,19 @@ class FeeReport(
pl = PurchaseLine.__table__()
SaleLine = Pool().get('sale.line')
sl = SaleLine.__table__()
+ Lot = Pool().get('lot.lot')
+ lot = Lot.__table__()
fee_lot_lines = (
fl
- .join(pl, 'LEFT', condition=fl.line == pl.id)
- .join(sl, 'LEFT', condition=fl.sale_line == sl.id)
+ .join(lot, 'LEFT', condition=fl.lot == lot.id)
+ .join(pl, 'LEFT', condition=lot.line == pl.id)
+ .join(sl, 'LEFT', condition=lot.sale_line == sl.id)
.select(
fl.fee.as_('fee'),
- Min(fl.line).as_('purchase_line'),
+ Min(lot.line).as_('purchase_line'),
Min(pl.purchase).as_('purchase'),
- Min(fl.sale_line).as_('sale_line'),
+ Min(lot.sale_line).as_('sale_line'),
Min(sl.sale).as_('sale'),
group_by=[fl.fee]))
@@ -1655,8 +1747,8 @@ class FeeReport(
wh &= (fr.fee_counterparty == party)
if fee_type:
wh &= (fr.fee_type == fee_type)
- purchase_expr = Coalesce(fee_lot_lines.purchase, fr.purchase)
- purchase_line_expr = Coalesce(fee_lot_lines.purchase_line, fr.line)
+ purchase_expr = fee_lot_lines.purchase
+ purchase_line_expr = fee_lot_lines.purchase_line
sale_expr = fee_lot_lines.sale
if purchase:
wh &= (purchase_expr == purchase)
diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py
index 4b34f66..e4adc28 100644
--- a/modules/purchase_trade/tests/test_module.py
+++ b/modules/purchase_trade/tests/test_module.py
@@ -5863,6 +5863,63 @@ class PurchaseTradeTestCase(ModuleTestCase):
fee_model.browse.assert_called_once_with([10])
fee_model.invoice.assert_called_once_with(fees)
+ def test_fee_report_lot_invoice_status_uses_effective_lots(self):
+ 'fee report consolidates invoicing over the effective linked lots'
+ FeeReport = Pool().get('fee.report')
+ invoice = SimpleNamespace(id=1, state='posted')
+ invoiced = SimpleNamespace(
+ invoice_line=SimpleNamespace(invoice=invoice),
+ invoice_line_prov=None)
+ not_invoiced = SimpleNamespace(
+ invoice_line=None, invoice_line_prov=None)
+ fee = SimpleNamespace(
+ _get_effective_fee_lots=lambda: [invoiced, not_invoiced])
+
+ lots, count, invoices = FeeReport._lot_invoice_data(fee)
+
+ self.assertEqual(lots, [invoiced, not_invoiced])
+ self.assertEqual(count, 1)
+ self.assertEqual(invoices, [invoice])
+
+ def test_fee_report_payment_status_consolidates_invoice_payments(self):
+ 'fee report marks mixed paid and unpaid invoices as partially paid'
+ FeeReport = Pool().get('fee.report')
+ paid = SimpleNamespace(
+ state='paid', payment_lines=[SimpleNamespace()],
+ amount_to_pay=Decimal('0'))
+ unpaid = SimpleNamespace(
+ state='posted', payment_lines=[],
+ amount_to_pay=Decimal('10'))
+
+ self.assertEqual(
+ FeeReport._invoice_payment_status([paid, unpaid]), 'partial')
+ self.assertEqual(FeeReport._invoice_payment_status([paid]), 'paid')
+ 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'
+ FeeReport = Pool().get('fee.report')
+ report = FeeReport()
+ report.id = 10
+ paid = SimpleNamespace(
+ id=1, state='paid', payment_lines=[SimpleNamespace()],
+ amount_to_pay=Decimal('0'))
+ unpaid_dn_cn = SimpleNamespace(
+ id=2, state='posted', payment_lines=[],
+ amount_to_pay=Decimal('5'))
+ fee = Mock(dn_cn=unpaid_dn_cn)
+ fee.get_invoice.return_value = paid
+ fee_model = Mock(return_value=fee)
+
+ 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'),
+ 'partial')
+
+ fee.get_invoice.assert_called_once_with('inv')
+
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')
diff --git a/modules/purchase_trade/view/fee_report_list.xml b/modules/purchase_trade/view/fee_report_list.xml
index b0e35dc..507e347 100755
--- a/modules/purchase_trade/view/fee_report_list.xml
+++ b/modules/purchase_trade/view/fee_report_list.xml
@@ -14,4 +14,7 @@
+
+
+