first commit
This commit is contained in:
195
modules/sale_payment/account.py
Normal file
195
modules/sale_payment/account.py
Normal file
@@ -0,0 +1,195 @@
|
||||
# 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()
|
||||
Reference in New Issue
Block a user