196 lines
6.8 KiB
Python
196 lines
6.8 KiB
Python
# 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()
|