first commit

This commit is contained in:
root
2026-03-14 09:42:12 +00:00
commit 0adbd20c2c
10991 changed files with 1646955 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.

Binary file not shown.

Binary file not shown.

View 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()

View File

@@ -0,0 +1,14 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.pool import PoolMeta
class Cron(metaclass=PoolMeta):
__name__ = 'ir.cron'
@classmethod
def __setup__(cls):
super().__setup__()
cls.method.selection.append(
('sale.sale|payment_confirm', "Confirm sales based on payment"))

View File

@@ -0,0 +1,25 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:sale.sale,payments:"
msgid "Payments"
msgstr ""
msgctxt "model:ir.action,name:act_payments_relate"
msgid "Payments"
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr ""
msgctxt "selection:ir.cron,method:"
msgid "Confirm sales based on payment"
msgstr ""

View File

@@ -0,0 +1,25 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:sale.sale,payments:"
msgid "Payments"
msgstr "Cobraments"
msgctxt "model:ir.action,name:act_payments_relate"
msgid "Payments"
msgstr "Cobraments"
#, python-format
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr "No podeu cancel·lar la venda \"%(sale)s\" perquè té pagaments."
#, python-format
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr "No podeu restablir a esborrany la venda \"%(sale)s\" perquè té pagaments."
msgctxt "selection:ir.cron,method:"
msgid "Confirm sales based on payment"
msgstr "Confirma vendes pagades"

View File

@@ -0,0 +1,25 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:sale.sale,payments:"
msgid "Payments"
msgstr ""
msgctxt "model:ir.action,name:act_payments_relate"
msgid "Payments"
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr ""
msgctxt "selection:ir.cron,method:"
msgid "Confirm sales based on payment"
msgstr ""

View File

@@ -0,0 +1,29 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:sale.sale,payments:"
msgid "Payments"
msgstr "Zahlungen"
msgctxt "model:ir.action,name:act_payments_relate"
msgid "Payments"
msgstr "Zahlungen"
#, python-format
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr ""
"Der Verkauf \"%(sale)s\" kann nicht annulliert werden, weil es zugehörige "
"Zahlungen gibt."
#, python-format
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr ""
"Der Verkauf \"%(sale)s\" kann nicht auf den Entwurfsstatus zurückgesetzt "
"werden, weil es zu ihm gehörende Zahlungen gibt."
msgctxt "selection:ir.cron,method:"
msgid "Confirm sales based on payment"
msgstr "Verkäufe aufgrund von Zahlungen bestätigen"

View File

@@ -0,0 +1,25 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:sale.sale,payments:"
msgid "Payments"
msgstr "Cobros"
msgctxt "model:ir.action,name:act_payments_relate"
msgid "Payments"
msgstr "Cobros"
#, python-format
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr "No puede cancelar la venta \"%(sale)s\" porque tiene pagos."
#, python-format
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr "No puede restablecer a borrador la venta \"%(sale)s\" porque tiene pagos."
msgctxt "selection:ir.cron,method:"
msgid "Confirm sales based on payment"
msgstr "Confirmar ventas pagadas"

View File

@@ -0,0 +1,25 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:sale.sale,payments:"
msgid "Payments"
msgstr ""
msgctxt "model:ir.action,name:act_payments_relate"
msgid "Payments"
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr ""
msgctxt "selection:ir.cron,method:"
msgid "Confirm sales based on payment"
msgstr ""

View File

@@ -0,0 +1,28 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:sale.sale,payments:"
msgid "Payments"
msgstr "Laekumised"
#, fuzzy
msgctxt "model:ir.action,name:act_payments_relate"
msgid "Payments"
msgstr "Laekumised"
#, python-format
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr "Ei saa tühistada müüki \"%(sale)s\", kuna sellega on seotud laekumised."
#, python-format
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr ""
"Ei saa muuta müüki \"%(sale)s\" mustandiks, kuna sellega on seotud "
"laekumised."
msgctxt "selection:ir.cron,method:"
msgid "Confirm sales based on payment"
msgstr ""

View File

@@ -0,0 +1,28 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:sale.sale,payments:"
msgid "Payments"
msgstr "پرداخت ها"
#, fuzzy
msgctxt "model:ir.action,name:act_payments_relate"
msgid "Payments"
msgstr "پرداخت ها"
#, python-format
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr "شما نمیتوانید فروش :\"%(sale)s\"راحذف کنید، چرا که پرداخت شده است."
#, python-format
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr ""
"شما نمیتوانید حالت فروش :\"%(sale)s\" را به پیش نویس بازنشانی کنید، چرا که "
"پرداخت شده است."
msgctxt "selection:ir.cron,method:"
msgid "Confirm sales based on payment"
msgstr ""

View File

@@ -0,0 +1,25 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:sale.sale,payments:"
msgid "Payments"
msgstr ""
msgctxt "model:ir.action,name:act_payments_relate"
msgid "Payments"
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr ""
msgctxt "selection:ir.cron,method:"
msgid "Confirm sales based on payment"
msgstr ""

View File

@@ -0,0 +1,28 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:sale.sale,payments:"
msgid "Payments"
msgstr "Paiements"
msgctxt "model:ir.action,name:act_payments_relate"
msgid "Payments"
msgstr "Paiements"
#, python-format
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr ""
"Vous ne pouvez pas annuler la vente « %(sale)s » car elle a des paiements."
#, python-format
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr ""
"Vous ne pouvez pas réinitialiser à l'état brouillon la vente « %(sale)s » "
"car elle a des paiements."
msgctxt "selection:ir.cron,method:"
msgid "Confirm sales based on payment"
msgstr "Confirmer les ventes en fonction des paiements"

View File

@@ -0,0 +1,25 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:sale.sale,payments:"
msgid "Payments"
msgstr ""
msgctxt "model:ir.action,name:act_payments_relate"
msgid "Payments"
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr ""
msgctxt "selection:ir.cron,method:"
msgid "Confirm sales based on payment"
msgstr ""

View File

@@ -0,0 +1,29 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:sale.sale,payments:"
msgid "Payments"
msgstr "Pembayaran"
msgctxt "model:ir.action,name:act_payments_relate"
msgid "Payments"
msgstr "Pembayaran"
#, python-format
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr ""
"Anda tidak dapat membatalkan penjualan \"%(sale)s\" karena memiliki "
"pembayaran."
#, python-format
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr ""
"Anda tidak dapat mengatur ulang ke konsep penjualan \"%(sale)s\" karena "
"memiliki pembayaran."
msgctxt "selection:ir.cron,method:"
msgid "Confirm sales based on payment"
msgstr ""

View File

@@ -0,0 +1,27 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:sale.sale,payments:"
msgid "Payments"
msgstr "Pagamenti"
msgctxt "model:ir.action,name:act_payments_relate"
msgid "Payments"
msgstr "Pagamenti"
#, python-format
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr "Non è possibile annullare la vendita \"%(sale)s\" perché ha pagamenti."
#, python-format
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr ""
"Non è possibile annullare la bozza di vendita \"%(sale)s\" perché ha "
"pagamenti."
msgctxt "selection:ir.cron,method:"
msgid "Confirm sales based on payment"
msgstr ""

View File

@@ -0,0 +1,25 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:sale.sale,payments:"
msgid "Payments"
msgstr ""
msgctxt "model:ir.action,name:act_payments_relate"
msgid "Payments"
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr ""
msgctxt "selection:ir.cron,method:"
msgid "Confirm sales based on payment"
msgstr ""

View File

@@ -0,0 +1,25 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:sale.sale,payments:"
msgid "Payments"
msgstr ""
msgctxt "model:ir.action,name:act_payments_relate"
msgid "Payments"
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr ""
msgctxt "selection:ir.cron,method:"
msgid "Confirm sales based on payment"
msgstr ""

View File

@@ -0,0 +1,29 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:sale.sale,payments:"
msgid "Payments"
msgstr "betalingen"
msgctxt "model:ir.action,name:act_payments_relate"
msgid "Payments"
msgstr "Betalingen"
#, python-format
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr ""
"U kunt de verkoop \"%(sale)s\" niet annuleren omdat deze een betalingen "
"heeft."
#, python-format
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr ""
"U kunt de verkoop \"%(sale)s\" niet terug zetten naar concept status omdat "
"deze betalingen heeft."
msgctxt "selection:ir.cron,method:"
msgid "Confirm sales based on payment"
msgstr "Bevestig verkopen gebaseerd op betaling"

View File

@@ -0,0 +1,26 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:sale.sale,payments:"
msgid "Payments"
msgstr "Płatności"
#, fuzzy
msgctxt "model:ir.action,name:act_payments_relate"
msgid "Payments"
msgstr "Płatności"
#, python-format
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr ""
msgctxt "selection:ir.cron,method:"
msgid "Confirm sales based on payment"
msgstr ""

View File

@@ -0,0 +1,26 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:sale.sale,payments:"
msgid "Payments"
msgstr "Pagamentos"
#, fuzzy
msgctxt "model:ir.action,name:act_payments_relate"
msgid "Payments"
msgstr "Pagamentos"
#, python-format
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr ""
msgctxt "selection:ir.cron,method:"
msgid "Confirm sales based on payment"
msgstr ""

View File

@@ -0,0 +1,25 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:sale.sale,payments:"
msgid "Payments"
msgstr "Plați"
msgctxt "model:ir.action,name:act_payments_relate"
msgid "Payments"
msgstr "Plați"
#, python-format
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr "Vânzarea \"%(sale)s\" nu se poate anula pentru că are plați."
#, python-format
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr "Nu se poate reseta vânzarea \"%(sale)s\" la draft pentru ca are plați."
msgctxt "selection:ir.cron,method:"
msgid "Confirm sales based on payment"
msgstr ""

View File

@@ -0,0 +1,25 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:sale.sale,payments:"
msgid "Payments"
msgstr ""
msgctxt "model:ir.action,name:act_payments_relate"
msgid "Payments"
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr ""
msgctxt "selection:ir.cron,method:"
msgid "Confirm sales based on payment"
msgstr ""

View File

@@ -0,0 +1,25 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:sale.sale,payments:"
msgid "Payments"
msgstr ""
msgctxt "model:ir.action,name:act_payments_relate"
msgid "Payments"
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr ""
msgctxt "selection:ir.cron,method:"
msgid "Confirm sales based on payment"
msgstr ""

View File

@@ -0,0 +1,25 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:sale.sale,payments:"
msgid "Payments"
msgstr ""
msgctxt "model:ir.action,name:act_payments_relate"
msgid "Payments"
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr ""
msgctxt "selection:ir.cron,method:"
msgid "Confirm sales based on payment"
msgstr ""

View File

@@ -0,0 +1,25 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:sale.sale,payments:"
msgid "Payments"
msgstr ""
msgctxt "model:ir.action,name:act_payments_relate"
msgid "Payments"
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr ""
msgctxt "selection:ir.cron,method:"
msgid "Confirm sales based on payment"
msgstr ""

View File

@@ -0,0 +1,25 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:sale.sale,payments:"
msgid "Payments"
msgstr ""
msgctxt "model:ir.action,name:act_payments_relate"
msgid "Payments"
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr ""
msgctxt "selection:ir.cron,method:"
msgid "Confirm sales based on payment"
msgstr ""

View File

@@ -0,0 +1,13 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tryton>
<data grouped="1">
<record model="ir.message" id="msg_sale_cancel_payment">
<field name="text">You cannot cancel sale "%(sale)s" because it has payments.</field>
</record>
<record model="ir.message" id="msg_sale_draft_payment">
<field name="text">You cannot reset to draft sale "%(sale)s" because it has payments.</field>
</record>
</data>
</tryton>

View File

@@ -0,0 +1,105 @@
# 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.model import ModelView, Workflow, fields
from trytond.model.exceptions import AccessError
from trytond.pool import PoolMeta
from trytond.pyson import Bool, Eval, If
from trytond.transaction import Transaction
def no_payment(error):
def decorator(func):
@functools.wraps(func)
def wrapper(cls, sales, *args, **kwargs):
for sale in sales:
if not all((p.state == 'failed' for p in sale.payments)):
raise AccessError(gettext(error, sale=sale.rec_name))
return func(cls, sales, *args, **kwargs)
return wrapper
return decorator
class Sale(metaclass=PoolMeta):
__name__ = 'sale.sale'
payments = fields.One2Many(
'account.payment', 'origin', "Payments",
domain=[
('company', '=', Eval('company', -1)),
['OR',
('party', '=', If(Bool(Eval('invoice_party')),
Eval('invoice_party', -1), Eval('party', -1))),
('state', '!=', 'draft'),
],
('currency', '=', Eval('currency', -1)),
],
states={
'readonly': Eval('state') != 'quotation',
})
@classmethod
@ModelView.button
@Workflow.transition('cancelled')
@no_payment('sale_payment.msg_sale_cancel_payment')
def cancel(cls, sales):
super().cancel(sales)
@classmethod
@ModelView.button
@Workflow.transition('draft')
@no_payment('sale_payment.msg_sale_draft_payment')
def draft(cls, sales):
super().draft(sales)
@classmethod
def copy(cls, sales, default=None):
if default is None:
default = {}
else:
default = default.copy()
default.setdefault('payments', None)
return super().copy(sales, default=default)
@property
def payment_amount_authorized(self):
"Total amount of the authorized payments"
return (sum(
p.amount for p in self.payments
if p.kind == 'receivable' and p.is_authorized)
- sum(p.amount for p in self.payments if p.kind == 'payable'))
@property
def amount_to_pay(self):
"Amount to pay to confirm the sale"
return self.total_amount
@classmethod
def payment_confirm(cls, sales=None):
"Confirm the sale based on payment authorization"
if sales is None:
context = Transaction().context
sales = cls.search([
('state', '=', 'quotation'),
('payments', '!=', None),
('company', '=', context.get('company')),
])
def cover(authorized, amount):
return (
abs(authorized) >= abs(amount)
and (authorized * amount >= 0))
to_confirm = []
for sale in sales:
if cover(sale.payment_amount_authorized, sale.amount_to_pay):
to_confirm.append(sale)
if to_confirm:
to_confirm = cls.browse(to_confirm) # optimize cache
cls.confirm(to_confirm)
@property
def credit_limit_amount(self):
amount = super().credit_limit_amount
return max(0, amount - self.payment_amount_authorized)

View File

@@ -0,0 +1,29 @@
<?xml version="1.0"?>
<tryton>
<data>
<record model="ir.ui.view" id="sale_view_form">
<field name="model">sale.sale</field>
<field name="inherit" ref="sale.sale_view_form"/>
<field name="name">sale_form</field>
</record>
<record model="ir.action.act_window" id="act_payments_relate">
<field name="name">Payments</field>
<field name="res_model">account.payment</field>
<field
name="domain"
eval="[If(Eval('active_ids', []) == [Eval('active_id')], ('origin.id', '=', Eval('active_id'), 'sale.sale'), ('origin.id', 'in', Eval('active_ids'), 'sale.sale'))]"
pyson="1"/>
</record>
<record model="ir.action.keyword" id="act_payments_relate_keyword1">
<field name="keyword">form_relate</field>
<field name="model">sale.sale,-1</field>
<field name="action" ref="act_payments_relate"/>
</record>
<record model="ir.cron" id="cron_sale_payment_confirm">
<field name="method">sale.sale|payment_confirm</field>
<field name="interval_number" eval="15"/>
<field name="interval_type">minutes</field>
</record>
</data>
</tryton>

View File

@@ -0,0 +1,2 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.

View File

@@ -0,0 +1,204 @@
=====================
Sale Payment Scenario
=====================
Imports::
>>> from decimal import Decimal
>>> from proteus import Model
>>> from trytond.modules.account.tests.tools import (
... create_chart, create_fiscalyear, get_accounts)
>>> from trytond.modules.account_invoice.tests.tools import (
... set_fiscalyear_invoice_sequences)
>>> from trytond.modules.company.tests.tools import create_company
>>> from trytond.tests.tools import activate_modules
Activate modules::
>>> config = activate_modules(
... ['sale_payment', 'account_payment_clearing'],
... create_company, create_chart)
Create fiscal year::
>>> fiscalyear = set_fiscalyear_invoice_sequences(create_fiscalyear())
>>> fiscalyear.click('create_period')
Get accounts::
>>> accounts = get_accounts()
>>> revenue = accounts['revenue']
>>> payable = accounts['payable']
>>> Account = Model.get('account.account')
>>> bank_clearing = Account(parent=payable.parent)
>>> bank_clearing.name = 'Bank Clearing'
>>> bank_clearing.type = payable.type
>>> bank_clearing.reconcile = True
>>> bank_clearing.deferral = True
>>> bank_clearing.save()
>>> Journal = Model.get('account.journal')
>>> expense, = Journal.find([('code', '=', 'EXP')])
Create payment journal::
>>> PaymentJournal = Model.get('account.payment.journal')
>>> payment_journal = PaymentJournal(name='Manual',
... process_method='manual', clearing_journal=expense,
... clearing_account=bank_clearing)
>>> payment_journal.save()
Create parties::
>>> Party = Model.get('party.party')
>>> customer = Party(name='Customer')
>>> customer.save()
Default account product::
>>> AccountConfiguration = Model.get('account.configuration')
>>> account_configuration = AccountConfiguration(1)
>>> account_configuration.default_category_account_revenue = revenue
>>> account_configuration.save()
Create a sale quotation::
>>> Sale = Model.get('sale.sale')
>>> sale = Sale()
>>> sale.party = customer
>>> sale.invoice_method = 'order'
>>> sale_line = sale.lines.new()
>>> sale_line.description = "Test"
>>> sale_line.quantity = 1.0
>>> sale_line.unit_price = Decimal(100)
>>> sale.click('quote')
>>> sale.total_amount
Decimal('100.00')
>>> sale.state
'quotation'
Create a partial payment::
>>> Payment = Model.get('account.payment')
>>> payment = Payment()
>>> payment.journal = payment_journal
>>> payment.kind = 'receivable'
>>> payment.party = sale.party
>>> payment.origin = sale
>>> payment.amount = Decimal('40.00')
>>> payment.click('submit')
>>> payment.state
'submitted'
Attempt to put sale back to draft::
>>> sale.click('draft')
Traceback (most recent call last):
...
AccessError: ...
>>> sale.state
'quotation'
Attempt to cancel sale::
>>> sale.click('cancel')
Traceback (most recent call last):
...
AccessError: ...
>>> sale.state
'quotation'
Revert sale to draft after failed payment::
>>> process_payment = payment.click('process_wizard')
>>> payment.click('fail')
>>> payment.state
'failed'
>>> sale.click('draft')
>>> sale.state
'draft'
Attempt to add a second payment to draft sale::
>>> payment = Payment()
>>> payment.journal = payment_journal
>>> payment.kind = 'receivable'
>>> payment.party = sale.party
>>> payment.origin = sale
>>> payment.amount = Decimal('30.00')
>>> payment.save()
Traceback (most recent call last):
...
DomainValidationError: ...
Cancel the sale::
>>> sale.click('cancel')
>>> sale.state
'cancelled'
Attempt to add a second payment to the cancelled sale::
>>> payment = Payment()
>>> payment.journal = payment_journal
>>> payment.kind = 'receivable'
>>> payment.party = sale.party
>>> payment.origin = sale
>>> payment.amount = Decimal('30.00')
>>> payment.save()
Traceback (most recent call last):
...
DomainValidationError: ...
Revive the sale::
>>> sale.click('draft')
>>> sale.click('quote')
>>> sale.state
'quotation'
Change the first payment to succeed::
>>> payment, = sale.payments
>>> payment.click('succeed')
>>> sale.state
'quotation'
Create and process a final payment::
>>> payment = Payment()
>>> payment.journal = payment_journal
>>> payment.kind = 'receivable'
>>> payment.party = sale.party
>>> payment.origin = sale
>>> payment.amount = Decimal('60.00')
>>> payment.click('submit')
>>> process_payment = payment.click('process_wizard')
>>> payment.click('succeed')
The sale should be processing::
>>> sale.reload()
>>> sale.state
'processing'
Post the invoice and check amount to pay::
>>> sale.click('process')
>>> invoice, = sale.invoices
>>> invoice.total_amount
Decimal('100.00')
>>> invoice.click('post')
>>> invoice.amount_to_pay
Decimal('0')
>>> invoice.state
'paid'
Fail one payment and check invoice is no more paid::
>>> payment.click('fail')
>>> invoice.reload()
>>> invoice.state
'posted'

View File

@@ -0,0 +1,100 @@
=================================
Sale Payment Scenario No Clearing
=================================
Imports::
>>> from decimal import Decimal
>>> from proteus import Model
>>> from trytond.modules.account.tests.tools import (
... create_chart, create_fiscalyear, get_accounts)
>>> from trytond.modules.account_invoice.tests.tools import (
... set_fiscalyear_invoice_sequences)
>>> from trytond.modules.company.tests.tools import create_company
>>> from trytond.tests.tools import activate_modules
Activate modules::
>>> config = activate_modules('sale_payment', create_company, create_chart)
>>> PaymentJournal = Model.get('account.payment.journal')
>>> Party = Model.get('party.party')
>>> AccountConfiguration = Model.get('account.configuration')
>>> Sale = Model.get('sale.sale')
>>> Payment = Model.get('account.payment')
Create fiscal year::
>>> fiscalyear = set_fiscalyear_invoice_sequences(create_fiscalyear())
>>> fiscalyear.click('create_period')
Get accounts::
>>> accounts = get_accounts()
>>> revenue = accounts['revenue']
>>> payable = accounts['payable']
Create payment journal::
>>> payment_journal = PaymentJournal(
... name="Manual", process_method='manual')
>>> payment_journal.save()
Create parties::
>>> customer = Party(name="Customer")
>>> customer.save()
Default account product::
>>> account_configuration = AccountConfiguration(1)
>>> account_configuration.default_category_account_revenue = revenue
>>> account_configuration.save()
Create a sale quotation::
>>> sale = Sale()
>>> sale.party = customer
>>> sale.invoice_method = 'order'
>>> sale_line = sale.lines.new()
>>> sale_line.description = "Test"
>>> sale_line.quantity = 1.0
>>> sale_line.unit_price = Decimal(100)
>>> sale.click('quote')
>>> sale.total_amount
Decimal('100.00')
>>> sale.state
'quotation'
Pay the sale using payment::
>>> payment = Payment()
>>> payment.journal = payment_journal
>>> payment.kind = 'receivable'
>>> payment.party = sale.party
>>> payment.origin = sale
>>> payment.amount = Decimal('100.00')
>>> payment.click('submit')
>>> payment.state
'submitted'
>>> process_payment = payment.click('process_wizard')
>>> payment.click('succeed')
The sale should be processing::
>>> sale.reload()
>>> sale.state
'processing'
Post the invoice and check amount to pay::
>>> sale.click('process')
>>> invoice, = sale.invoices
>>> invoice.total_amount
Decimal('100.00')
>>> invoice.click('post')
>>> invoice.amount_to_pay
Decimal('0.00')
>>> invoice.state
'posted'

View File

@@ -0,0 +1,21 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.pool import Pool
from trytond.tests.test_tryton import ModuleTestCase, with_transaction
class AccountPaymentTestCase(ModuleTestCase):
'Test Sale Payment module'
module = 'sale_payment'
@with_transaction()
def test_sale_payment_confirm_cron(self):
"Test running sale payment_confirm without sales"
pool = Pool()
Sale = pool.get('sale.sale')
Sale.payment_confirm()
del ModuleTestCase

View File

@@ -0,0 +1,8 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.tests.test_tryton import load_doc_tests
def load_tests(*args, **kwargs):
return load_doc_tests(__name__, __file__, *args, **kwargs)

View File

@@ -0,0 +1,21 @@
[tryton]
version=7.8.0
depends:
account_invoice
account_payment
ir
res
sale
extras_depend:
account_payment_clearing
sale_credit_limit
xml:
sale.xml
message.xml
[register]
model:
ir.Cron
sale.Sale
account.Payment
account.Invoice

View File

@@ -0,0 +1,8 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<data>
<xpath expr="//link[@name='sale.act_invoice_form']" position="before">
<link icon="tryton-payment" name="sale_payment.act_payments_relate"/>
</xpath>
</data>