Initial import from Docker volume

This commit is contained in:
root
2025-12-26 13:11:43 +00:00
commit 4998dc066a
13336 changed files with 1767801 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
# 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 . import account, sale
def register():
Pool.register(
sale.Sale,
account.Payment,
account.Invoice,
module='sale_payment', type_='model')

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

184
modules/sale_payment/account.py Executable file
View File

@@ -0,0 +1,184 @@
# 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)}
sales = Sale.browse(sales) # optimize cache
Sale.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'),
()),
]
@classmethod
def _get_origin(cls):
return super(Payment, cls)._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 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 = []
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,19 @@
#
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 ""
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr ""
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr ""

View File

@@ -0,0 +1,19 @@
#
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"
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."
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."

View File

@@ -0,0 +1,19 @@
#
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 ""
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr ""
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr ""

View File

@@ -0,0 +1,23 @@
#
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"
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."
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."

View File

@@ -0,0 +1,19 @@
#
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"
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."
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."

View File

@@ -0,0 +1,19 @@
#
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 ""
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr ""
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr ""

View File

@@ -0,0 +1,22 @@
#
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"
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."
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."

View File

@@ -0,0 +1,22 @@
#
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 "پرداخت ها"
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr "شما نمیتوانید فروش :\"%(sale)s\"راحذف کنید، چرا که پرداخت شده است."
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\" را به پیش نویس بازنشانی کنید، چرا که "
"پرداخت شده است."

View File

@@ -0,0 +1,19 @@
#
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 ""
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr ""
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr ""

View File

@@ -0,0 +1,22 @@
#
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"
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."
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."

View File

@@ -0,0 +1,19 @@
#
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 ""
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr ""
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr ""

View File

@@ -0,0 +1,23 @@
#
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"
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."
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."

View File

@@ -0,0 +1,21 @@
#
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"
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."
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."

View File

@@ -0,0 +1,19 @@
#
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 ""
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr ""
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr ""

View File

@@ -0,0 +1,19 @@
#
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 ""
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr ""
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr ""

View File

@@ -0,0 +1,23 @@
#
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"
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."
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."

View File

@@ -0,0 +1,20 @@
#
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"
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr ""
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr ""

View File

@@ -0,0 +1,20 @@
#
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"
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr ""
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr ""

View File

@@ -0,0 +1,19 @@
#
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"
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."
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."

View File

@@ -0,0 +1,19 @@
#
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 ""
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr ""
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr ""

View File

@@ -0,0 +1,19 @@
#
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 ""
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr ""
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr ""

View File

@@ -0,0 +1,19 @@
#
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 ""
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr ""
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr ""

View File

@@ -0,0 +1,19 @@
#
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 ""
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr ""
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
msgstr ""

View File

@@ -0,0 +1,19 @@
#
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 ""
msgctxt "model:ir.message,text:msg_sale_cancel_payment"
msgid "You cannot cancel sale \"%(sale)s\" because it has payments."
msgstr ""
msgctxt "model:ir.message,text:msg_sale_draft_payment"
msgid "You cannot reset to draft sale \"%(sale)s\" because it has payments."
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>

89
modules/sale_payment/sale.py Executable file
View File

@@ -0,0 +1,89 @@
# 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
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)),
If(Eval('total_amount', 0) >= 0,
('kind', '=', 'receivable'),
('kind', '=', 'payable'),
),
('party', '=', If(Bool(Eval('invoice_party')),
Eval('invoice_party'), Eval('party'))),
('currency', '=', Eval('currency')),
],
states={
'readonly': Eval('state') != 'quotation',
})
@classmethod
@ModelView.button
@Workflow.transition('cancelled')
@no_payment('sale_payment.msg_sale_cancel_payment')
def cancel(cls, sales):
super(Sale, cls).cancel(sales)
@classmethod
@ModelView.button
@Workflow.transition('draft')
@no_payment('sale_payment.msg_sale_draft_payment')
def draft(cls, sales):
super(Sale, cls).draft(sales)
@classmethod
def copy(cls, sales, default=None):
if default is None:
default = {}
else:
default = default.copy()
default.setdefault('payments', None)
return super(Sale, cls).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.is_authorized)
@property
def amount_to_pay(self):
"Amount to pay to confirm the sale"
return self.total_amount
@classmethod
def payment_confirm(cls, sales):
"Confirm the sale based on payment authorization"
to_confirm = []
for sale in sales:
if 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)

23
modules/sale_payment/sale.xml Executable file
View File

@@ -0,0 +1,23 @@
<?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>
</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,209 @@
=====================
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, get_company
>>> from trytond.tests.tools import activate_modules
Activate modules::
>>> config = activate_modules(['sale_payment', 'account_payment_clearing'])
Create company::
>>> _ = create_company()
>>> company = get_company()
Create fiscal year::
>>> fiscalyear = set_fiscalyear_invoice_sequences(
... create_fiscalyear(company))
>>> fiscalyear.click('create_period')
Create chart of accounts::
>>> _ = create_chart(company)
>>> accounts = get_accounts(company)
>>> 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,107 @@
=================================
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, get_company
>>> from trytond.tests.tools import activate_modules
Activate modules::
>>> config = activate_modules('sale_payment')
>>> 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 company::
>>> _ = create_company()
>>> company = get_company()
Create fiscal year::
>>> fiscalyear = set_fiscalyear_invoice_sequences(
... create_fiscalyear(company))
>>> fiscalyear.click('create_period')
Create chart of accounts::
>>> _ = create_chart(company)
>>> accounts = get_accounts(company)
>>> 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,12 @@
# 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 ModuleTestCase
class AccountPaymentTestCase(ModuleTestCase):
'Test Sale Payment module'
module = 'sale_payment'
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)

14
modules/sale_payment/tryton.cfg Executable file
View File

@@ -0,0 +1,14 @@
[tryton]
version=7.2.1
depends:
account_invoice
account_payment
ir
res
sale
extras_depend:
account_payment_clearing
sale_credit_limit
xml:
sale.xml
message.xml

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="//group[@id='links']" position="inside">
<link icon="tryton-payment" name="sale_payment.act_payments_relate"/>
</xpath>
</data>