Files
tryton/account.py
T
2018-12-29 14:20:29 +01:00

171 lines
5.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 trytond.i18n import gettext
from trytond.pool import Pool, PoolMeta
from trytond.modules.account_payment.exceptions import PaymentValidationError
__all__ = ['Payment', 'Invoice']
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)]
sales = Sale.browse(sales) # optimize cache
Sale.payment_confirm(sales)
return result
return wrapper
class Payment(metaclass=PoolMeta):
__name__ = 'account.payment'
@classmethod
def _get_origin(cls):
return super(Payment, cls)._get_origin() + ['sale.sale']
@classmethod
def validate(cls, payments):
pool = Pool()
Sale = pool.get('sale.sale')
super(Payment, cls).validate(payments)
for payment in payments:
if isinstance(payment.origin, Sale):
payment.check_sale_state()
def check_sale_state(self):
assert isinstance(self.origin, Pool().get('sale.sale'))
if self.state == 'succeeded':
# Do not prevent succeeding payment
return
if self.state != 'failed' and self.origin.state == 'draft':
raise PaymentValidationError(
gettext('sale_payment.msg_payment_sale_draft',
sale=self.origin.rec_name,
payment=self.rec_name))
elif self.state == 'draft' and self.origin.state == 'cancel':
raise PaymentValidationError(
gettext('sale_payment.msg_payment_sale_cancel',
sale=self.origin.rec_name,
payment=self.rec_name))
@classmethod
def create(cls, vlist):
payments = super(Payment, cls).create(vlist)
cls.trigger_authorized([p for p in payments if p.is_authorized])
return payments
@classmethod
def write(cls, *args):
payments = sum(args[0:None:2], [])
unauthorized = {p for p in payments if not p.is_authorized}
super(Payment, cls).write(*args)
authorized = {p for p in payments if p.is_authorized}
cls.trigger_authorized(cls.browse(unauthorized & authorized))
@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 = []
else:
payments = payments[:]
for sale in self.sales:
payments.extend(sale.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 payment.amount <= payment_amount:
payment.line = line
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
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(l.debit - l.credit for l in 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')
Move = pool.get('account.move')
super(Invoice, cls).post(invoices)
payments = []
for invoice in invoices:
payments.extend(invoice.add_payments())
if payments:
Payment.save(payments)
if hasattr(Payment, 'clearing_move'):
moves = []
for payment in payments:
if payment.state == 'succeeded':
# Ensure clearing move is created as succeed may happen
# before the payment has a line.
move = payment.create_clearing_move()
if move:
moves.append(move)
if moves:
Move.save(moves)
Payment.write(*sum((([m.origin], {'clearing_move': m.id})
for m in moves), ()))
for invoice in invoices:
invoice.reconcile_payments()