Fee report
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -14,4 +14,7 @@
|
||||
<field name="r_state" width="80"/>
|
||||
<field name="r_inv" width="90"/>
|
||||
<field name="r_dn_cn" width="90"/>
|
||||
<field name="r_lot_invoice_status" width="110"/>
|
||||
<field name="r_lot_payment_status" width="110"/>
|
||||
<field name="r_fee_payment_status" width="110"/>
|
||||
</tree>
|
||||
|
||||
Reference in New Issue
Block a user