# This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import functools from decimal import Decimal from trytond.model import fields from trytond.pool import Pool, PoolMeta from trytond.pyson import Eval, If def sale_payment_confirm(func): @functools.wraps(func) def wrapper(cls, payments, *args, **kwargs): pool = Pool() Sale = pool.get('sale.sale') result = func(cls, payments, *args, **kwargs) sales = {p.origin for p in payments if isinstance(p.origin, Sale)} Sale.__queue__.payment_confirm(sales) return result return wrapper class Payment(metaclass=PoolMeta): __name__ = 'account.payment' @classmethod def __setup__(cls): super().__setup__() cls.origin.domain['sale.sale'] = [ If(~Eval('state').in_(['failed', 'succeeded']), ('state', '!=', 'draft'), ()), If(Eval('state') == 'draft', ('state', '!=', 'cancelled'), ()), ('company', '=', Eval('company', -1)), If(Eval('state') == 'draft', ['OR', ('invoice_party', '=', Eval('party', -1)), [ ('invoice_party', '=', None), ('party', '=', Eval('party', -1)), ], ], []), ('currency', '=', Eval('currency', -1)), ] @classmethod def _get_origin(cls): return super()._get_origin() + ['sale.sale'] @fields.depends('origin') def on_change_origin(self): pool = Pool() Sale = pool.get('sale.sale') try: super().on_change_origin() except AttributeError: pass if self.origin and isinstance(self.origin, Sale): sale = self.origin party = ( getattr(sale, 'invoice_party', None) or getattr(sale, 'party', None)) if party: self.party = party sale_amount = getattr(sale, 'total_amount', None) payment_amount = sum( (p.amount for p in getattr(sale, 'payments', []) if p.state != 'failed' and p != self), Decimal(0)) if sale_amount is not None: self.kind = 'receivable' if sale_amount > 0 else 'payable' self.amount = abs(sale_amount) - payment_amount currency = getattr(sale, 'currency', None) if currency is not None: self.currency = currency @classmethod def on_modification(cls, mode, payments, field_names=None): super().on_modification(mode, payments, field_names=field_names) if mode == 'create': cls.trigger_authorized([p for p in payments if p.is_authorized]) @classmethod def on_write(cls, payments, values): callback = super().on_write(payments, values) if unauthorized := {p for p in payments if not p.is_authorized}: def trigger(): authorized = {p for p in payments if p.is_authorized} cls.trigger_authorized(cls.browse(unauthorized & authorized)) callback.append(trigger) return callback @property def is_authorized(self): # TODO: move to account_payment return self.state == 'succeeded' @classmethod @sale_payment_confirm def trigger_authorized(cls, payments): pass class Invoice(metaclass=PoolMeta): __name__ = 'account.invoice' def add_payments(self, payments=None): "Add payments from sales lines to pay" if payments is None: payments = [] payments = set(payments) for sale in self.sales: payments.update(sale.payments) payments = list(payments) # Knapsack problem: # simple heuristic by trying to fill biggest amount first. payments.sort(key=lambda p: p.amount) lines_to_pay = sorted( self.lines_to_pay, key=lambda l: l.payment_amount) for line in lines_to_pay: if line.reconciliation: continue payment_amount = line.payment_amount for payment in payments: if payment.line or payment.state == 'failed': continue if ((payment.kind == 'receivable' and line.credit > 0) or (payment.kind == 'payable' and line.debit > 0)): continue if payment.party != line.party: continue if (getattr(payment, 'account', None) and payment.account != line.account): continue if payment.amount <= payment_amount: payment.line = line if hasattr(payment, 'account'): payment.account = None payment_amount -= payment.amount return payments def reconcile_payments(self): pool = Pool() Payment = pool.get('account.payment') Line = pool.get('account.move.line') if not hasattr(Payment, 'clearing_move'): return def balance(line): if self.currency == line.second_currency: return line.amount_second_currency elif self.currency == self.company.currency: return line.debit - line.credit else: return 0 to_reconcile = [] for line in self.lines_to_pay: if line.reconciliation: continue lines = [line] for payment in line.payments: if payment.state == 'succeeded' and payment.clearing_move: for pline in payment.clearing_move.lines: if (pline.account == line.account and not pline.reconciliation): lines.append(pline) if not sum(map(balance, lines)): to_reconcile.append(lines) for lines in to_reconcile: Line.reconcile(lines) @classmethod def _post(cls, invoices): pool = Pool() Payment = pool.get('account.payment') super()._post(invoices) payments = set() for invoice in invoices: payments.update(invoice.add_payments()) if payments: Payment.save(payments) if hasattr(Payment, 'clearing_move'): # Ensure clearing move is created as succeed may happen # before the payment has a line. Payment.set_clearing_move( [p for p in payments if p.state == 'succeeded']) for invoice in invoices: invoice.reconcile_payments()