Files

500 lines
17 KiB
Python
Raw Permalink Normal View History

2017-04-02 10:37:33 +02:00
# 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 decimal import Decimal
from itertools import chain
2017-04-02 10:37:33 +02:00
2021-12-11 17:58:43 +01:00
from simpleeval import simple_eval
from trytond import backend, config
2018-12-29 14:20:29 +01:00
from trytond.i18n import gettext
2018-03-26 14:14:39 +02:00
from trytond.model import (
DeactivableMixin, ModelSQL, ModelView, Workflow, fields, sort)
2020-03-01 12:33:53 +01:00
from trytond.modules.company.model import (
CompanyMultiValueMixin, CompanyValueMixin)
2021-08-21 09:09:56 +02:00
from trytond.modules.currency.fields import Monetary
2021-12-11 17:58:43 +01:00
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval
from trytond.tools import decistmt
from trytond.transaction import Transaction
2017-04-02 10:37:33 +02:00
2018-12-29 14:20:29 +01:00
from .exceptions import FormulaError
2017-04-02 10:37:33 +02:00
2018-03-26 14:14:39 +02:00
class AdvancePaymentTerm(
DeactivableMixin, ModelSQL, ModelView):
2017-04-02 10:37:33 +02:00
__name__ = 'sale.advance_payment_term'
name = fields.Char("Name", required=True, translate=True)
lines = fields.One2Many(
'sale.advance_payment_term.line', 'advance_payment_term', "Lines")
def get_advance_payment_context(self, sale):
return {
'total_amount': sale.total_amount,
'untaxed_amount': sale.untaxed_amount,
}
def get_lines(self, sale):
lines = []
2017-04-02 10:37:33 +02:00
term_context = self.get_advance_payment_context(sale)
for sale_line in self.lines:
line = sale_line.get_line(sale.currency, **term_context)
if line.amount > 0:
lines.append(line)
return lines
2017-04-02 10:37:33 +02:00
class AdvancePaymentTermLine(ModelView, ModelSQL, CompanyMultiValueMixin):
__name__ = 'sale.advance_payment_term.line'
_rec_name = 'description'
advance_payment_term = fields.Many2One(
'sale.advance_payment_term', "Advance Payment Term",
required=True, ondelete='CASCADE')
2017-04-02 10:37:33 +02:00
description = fields.Char(
"Description", required=True, translate=True,
help="Used as description for the invoice line.")
account = fields.MultiValue(
fields.Many2One('account.account', "Account", required=True,
domain=[
2019-03-24 18:00:01 +01:00
('type.unearned_revenue', '=', True),
2017-04-02 10:37:33 +02:00
],
help="Used for the line of advance payment invoice."))
accounts = fields.One2Many(
'sale.advance_payment_term.line.account', 'line', "Accounts")
block_supply = fields.Boolean(
"Block Supply",
help="Check to prevent any supply request before advance payment.")
block_shipping = fields.Boolean(
"Block Shipping",
help="Check to prevent the packing of the shipment "
"before advance payment.")
invoice_delay = fields.TimeDelta(
"Invoice Delay",
help="Delta to apply on the sale date for the date of "
"the advance payment invoice.")
formula = fields.Char('Formula', required=True,
help="A python expression used to compute the advance payment amount "
"that will be evaluated with:\n"
"- total_amount: The total amount of the sale.\n"
"- untaxed_amount: The total untaxed amount of the sale.")
2021-03-07 11:13:53 +01:00
@classmethod
def __setup__(cls):
super().__setup__()
cls.__access__.add('advance_payment_term')
2021-01-29 00:08:19 +01:00
@fields.depends('formula', 'description')
2017-04-02 10:37:33 +02:00
def pre_validate(self, **names):
2025-04-04 00:45:18 +02:00
super().pre_validate()
2017-04-02 10:37:33 +02:00
names['total_amount'] = names['untaxed_amount'] = 0
try:
if not isinstance(self.compute_amount(**names), Decimal):
raise Exception('The formula does not return a Decimal')
2018-06-26 19:11:52 +02:00
except Exception as exception:
2018-12-29 14:20:29 +01:00
raise FormulaError(
gettext('sale_advance_payment.msg_term_line_invalid_formula',
formula=self.formula,
2021-01-29 00:08:19 +01:00
term_line=self.description or '',
2018-12-29 14:20:29 +01:00
exception=exception)) from exception
2017-04-02 10:37:33 +02:00
def get_compute_amount_context(self, **names):
return {
'names': names,
'functions': {
'Decimal': Decimal,
},
}
def compute_amount(self, **names):
context = self.get_compute_amount_context(**names)
return simple_eval(decistmt(self.formula), **context)
def get_line(self, currency, **context):
2017-04-02 10:37:33 +02:00
pool = Pool()
Line = pool.get('sale.advance_payment.line')
2017-04-02 10:37:33 +02:00
return Line(
2017-04-02 10:37:33 +02:00
block_supply=self.block_supply,
block_shipping=self.block_shipping,
amount=currency.round(self.compute_amount(**context)),
account=self.account,
invoice_delay=self.invoice_delay,
description=self.description)
class AdvancePaymentTermLineAccount(ModelSQL, CompanyValueMixin):
__name__ = 'sale.advance_payment_term.line.account'
line = fields.Many2One(
'sale.advance_payment_term.line', "Line",
required=True, ondelete='CASCADE',
context={
'company': Eval('company', -1),
})
2017-04-02 10:37:33 +02:00
account = fields.Many2One(
'account.account', "Account", required=True,
domain=[
2019-03-24 18:00:01 +01:00
('type.unearned_revenue', '=', True),
2017-04-02 10:37:33 +02:00
('company', '=', Eval('company', -1)),
])
2017-04-02 10:37:33 +02:00
class AdvancePaymentLine(ModelSQL, ModelView):
__name__ = 'sale.advance_payment.line'
2017-04-02 10:37:33 +02:00
_rec_name = 'description'
_states = {
'readonly': Eval('sale_state') != 'draft',
}
sale = fields.Many2One(
'sale.sale', "Sale", required=True, ondelete='CASCADE',
2017-04-02 10:37:33 +02:00
states={
'readonly': ((Eval('sale_state') != 'draft')
& Bool(Eval('sale'))),
})
description = fields.Char("Description", required=True, states=_states)
2021-08-21 09:09:56 +02:00
amount = Monetary(
"Amount", currency='currency', digits='currency', states=_states)
2017-04-02 10:37:33 +02:00
account = fields.Many2One(
'account.account', "Account", required=True,
domain=[
2019-03-24 18:00:01 +01:00
('type.unearned_revenue', '=', True),
('company', '=', Eval('sale_company', -1)),
2017-04-02 10:37:33 +02:00
],
states=_states)
block_supply = fields.Boolean("Block Supply", states=_states)
block_shipping = fields.Boolean("Block Shipping", states=_states)
invoice_delay = fields.TimeDelta("Invoice Delay", states=_states)
2017-04-02 10:37:33 +02:00
invoice_lines = fields.One2Many(
'account.invoice.line', 'origin', "Invoice Lines", readonly=True)
completed = fields.Function(fields.Boolean("Completed"), 'get_completed')
sale_state = fields.Function(fields.Selection(
'get_sale_states', "Sale State"), 'on_change_with_sale_state')
sale_company = fields.Function(fields.Many2One(
'company.company', "Company"), 'on_change_with_sale_company')
2021-08-21 09:09:56 +02:00
currency = fields.Function(fields.Many2One(
'currency.currency', "Currency"),
'on_change_with_currency')
2017-04-02 10:37:33 +02:00
del _states
@classmethod
def __setup__(cls):
2025-04-04 00:45:18 +02:00
super().__setup__()
2017-04-02 10:37:33 +02:00
cls._order.insert(0, ('amount', 'ASC'))
cls.__access__.add('sale')
2017-04-02 10:37:33 +02:00
@classmethod
def __register__(cls, module):
# Migration from 7.0: rename condition into line
backend.TableHandler.table_rename(
config.get(
'table', 'sale.advance_payment.condition',
default='sale_advance_payment_condition'),
cls._table)
super().__register__(module)
2017-04-02 10:37:33 +02:00
@classmethod
def get_sale_states(cls):
pool = Pool()
Sale = pool.get('sale.sale')
return Sale.fields_get(['state'])['state']['selection']
@fields.depends('sale', '_parent_sale.state')
def on_change_with_sale_state(self, name=None):
if self.sale:
return self.sale.state
@fields.depends('sale', '_parent_sale.company')
def on_change_with_sale_company(self, name=None):
2023-03-26 09:49:00 +02:00
return self.sale.company if self.sale else None
2017-04-02 10:37:33 +02:00
2021-08-21 09:09:56 +02:00
@fields.depends('sale', '_parent_sale.currency')
def on_change_with_currency(self, name=None):
2023-03-26 09:49:00 +02:00
return self.sale.currency if self.sale else None
2021-08-21 09:09:56 +02:00
2017-04-02 10:37:33 +02:00
@classmethod
def copy(cls, lines, default=None):
2017-04-02 10:37:33 +02:00
if default is None:
default = {}
2018-09-01 01:02:04 +02:00
else:
default = default.copy()
default.setdefault('invoice_lines', [])
return super().copy(lines, default)
2017-04-02 10:37:33 +02:00
def create_invoice(self):
invoice = self.sale._get_invoice()
if self.invoice_delay is not None:
invoice.invoice_date = self.sale.sale_date + self.invoice_delay
2017-04-02 10:37:33 +02:00
invoice.payment_term = None
invoice_lines = self.get_invoice_advance_payment_lines(invoice)
if not invoice_lines:
return None
invoice.lines = invoice_lines
return invoice
def get_invoice_advance_payment_lines(self, invoice):
pool = Pool()
InvoiceLine = pool.get('account.invoice.line')
advance_amount = self._get_advance_amount()
advance_amount += self._get_ignored_amount()
if advance_amount >= self.amount:
return []
invoice_line = InvoiceLine()
invoice_line.invoice = invoice
invoice_line.type = 'line'
invoice_line.quantity = 1
invoice_line.account = self.account
invoice_line.unit_price = self.amount - advance_amount
invoice_line.description = self.description
invoice_line.origin = self
invoice_line.company = self.sale.company
2021-10-07 15:28:21 +02:00
invoice_line.currency = self.sale.currency
2017-04-02 10:37:33 +02:00
# Set taxes
invoice_line.on_change_account()
return [invoice_line]
def _get_advance_amount(self):
return sum(l.amount for l in self.invoice_lines
if l.invoice.state != 'cancelled')
2017-04-02 10:37:33 +02:00
def _get_ignored_amount(self):
skips = {l for i in self.sale.invoices_recreated for l in i.lines}
return sum(l.amount for l in self.invoice_lines
if l.invoice.state == 'cancelled' and l not in skips)
2017-04-02 10:37:33 +02:00
def get_completed(self, name):
advance_amount = 0
lines_ignored = set(l for i in self.sale.invoices_ignored
for l in i.lines)
for l in self.invoice_lines:
if l.invoice.state == 'paid' or l in lines_ignored:
advance_amount += l.amount
return advance_amount >= self.amount
2018-06-26 19:11:52 +02:00
class Sale(metaclass=PoolMeta):
2017-04-02 10:37:33 +02:00
__name__ = 'sale.sale'
advance_payment_term = fields.Many2One('sale.advance_payment_term',
'Advance Payment Term',
ondelete='RESTRICT', states={
'readonly': Eval('state') != 'draft',
})
advance_payment_lines = fields.One2Many(
'sale.advance_payment.line', 'sale', "Advance Payment Lines",
2017-04-02 10:37:33 +02:00
states={
'readonly': Eval('state') != 'draft',
})
advance_payment_invoices = fields.Function(fields.Many2Many(
'account.invoice', None, None, "Advance Payment Invoices"),
2017-04-02 10:37:33 +02:00
'get_advance_payment_invoices',
searcher='search_advance_payment_invoices')
@classmethod
def __setup__(cls):
super().__setup__()
cls.invoices_ignored.domain = [
'OR', cls.invoices_ignored.domain, [
('id', 'in', Eval('advance_payment_invoices', [])),
('state', '=', 'cancelled'),
],
]
2017-04-02 10:37:33 +02:00
@classmethod
@ModelView.button
@Workflow.transition('quotation')
def quote(cls, sales):
pool = Pool()
AdvancePaymentLine = pool.get('sale.advance_payment.line')
2017-04-02 10:37:33 +02:00
2025-04-04 00:45:18 +02:00
super().quote(sales)
2017-04-02 10:37:33 +02:00
AdvancePaymentLine.delete(
list(chain(*(s.advance_payment_lines for s in sales))))
2017-04-02 10:37:33 +02:00
for sale in sales:
sale.set_advance_payment_term()
cls.save(sales)
@classmethod
def copy(cls, sales, default=None):
if default is None:
default = {}
else:
default = default.copy()
default.setdefault('advance_payment_lines', None)
2025-04-04 00:45:18 +02:00
return super().copy(sales, default=default)
2017-04-02 10:37:33 +02:00
def set_advance_payment_term(self):
pool = Pool()
AdvancePaymentTerm = pool.get('sale.advance_payment_term')
if self.advance_payment_term:
if self.party and self.party.lang:
with Transaction().set_context(language=self.party.lang.code):
advance_payment_term = AdvancePaymentTerm(
self.advance_payment_term.id)
else:
advance_payment_term = self.advance_payment_term
self.advance_payment_lines = advance_payment_term.get_lines(self)
2017-04-02 10:37:33 +02:00
def get_advance_payment_invoices(self, name):
pool = Pool()
Invoice = pool.get('account.invoice')
field = getattr(self.__class__, name)
order = field.order if field.order is not None else Invoice._order
2017-04-02 10:37:33 +02:00
invoices = set()
for line in self.advance_payment_lines:
for invoice_line in line.invoice_lines:
2017-04-02 10:37:33 +02:00
if invoice_line.invoice:
invoices.add(invoice_line.invoice)
return sort(invoices, order)
2017-04-02 10:37:33 +02:00
@classmethod
def search_advance_payment_invoices(cls, name, clause):
return [('advance_payment_lines.invoice_lines.invoice'
+ clause[0][len(name):], *clause[1:])]
2017-04-02 10:37:33 +02:00
@property
def _invoices_for_state(self):
return super()._invoices_for_state + self.advance_payment_invoices
2017-04-02 10:37:33 +02:00
def get_recall_lines(self, invoice):
pool = Pool()
InvoiceLine = pool.get('account.invoice.line')
recall_lines = []
advance_lines = [
l
for c in self.advance_payment_lines
for l in c.invoice_lines
if l.type == 'line' and l.invoice.state == 'paid']
2017-04-02 10:37:33 +02:00
for advance_line in advance_lines:
amount = advance_line.amount
for recalled_line in advance_line.advance_payment_recalled_lines:
amount += recalled_line.amount
if amount:
line = InvoiceLine(
invoice=invoice,
company=invoice.company,
type='line',
quantity=-1,
account=advance_line.account,
unit_price=amount,
description=advance_line.description,
origin=advance_line,
taxes=advance_line.taxes,
2023-08-23 09:42:20 +02:00
taxes_date=advance_line.tax_date,
)
recall_lines.append(line)
2017-04-02 10:37:33 +02:00
return recall_lines
@classmethod
def _process_invoice(cls, sales):
pool = Pool()
Invoice = pool.get('account.invoice')
invoices = []
for sale in sales:
if (sale.advance_payment_eligible()
and not sale.advance_payment_completed):
for line in sale.advance_payment_lines:
invoice = line.create_invoice()
if invoice:
invoices.append(invoice)
Invoice.save(invoices)
super()._process_invoice(sales)
2017-04-02 10:37:33 +02:00
def create_invoice(self):
2025-04-04 00:45:18 +02:00
invoice = super().create_invoice()
2017-04-02 10:37:33 +02:00
if (invoice is not None
and self.advance_payment_eligible()
and self.advance_payment_completed):
invoice.lines = (
list(getattr(invoice, 'lines', ()))
+ self.get_recall_lines(invoice))
2017-04-02 10:37:33 +02:00
return invoice
def advance_payment_eligible(self, shipment_type=None):
"""
Returns True when the shipment_type is eligible to further processing
of the sale's advance payment.
"""
return bool((shipment_type == 'out' or shipment_type is None)
and self.advance_payment_lines)
2017-04-02 10:37:33 +02:00
@property
def advance_payment_completed(self):
"""
Returns True when the advance payment process is completed
"""
return (bool(self.advance_payment_lines)
and all(c.completed for c in self.advance_payment_lines))
2017-04-02 10:37:33 +02:00
@property
def supply_blocked(self):
for line in self.advance_payment_lines:
if not line.block_supply:
2017-04-02 10:37:33 +02:00
continue
if not line.completed:
2017-04-02 10:37:33 +02:00
return True
return False
@property
def shipping_blocked(self):
for line in self.advance_payment_lines:
if not line.block_shipping:
2017-04-02 10:37:33 +02:00
continue
if not line.completed:
2017-04-02 10:37:33 +02:00
return True
return False
2018-06-26 19:11:52 +02:00
class SaleLine(metaclass=PoolMeta):
2017-04-02 10:37:33 +02:00
__name__ = 'sale.line'
def get_move(self, shipment_type):
2025-04-04 00:45:18 +02:00
move = super().get_move(shipment_type)
2017-04-02 10:37:33 +02:00
if (self.sale.advance_payment_eligible(shipment_type)
and self.sale.supply_blocked):
return None
return move
2023-02-26 10:41:40 +01:00
def get_purchase_request(self, product_quantities):
request = super().get_purchase_request(product_quantities)
2017-04-02 10:37:33 +02:00
if (self.sale.advance_payment_eligible()
and self.sale.supply_blocked):
return None
return request
def get_invoice_line(self):
2025-04-04 00:45:18 +02:00
lines = super().get_invoice_line()
2017-04-02 10:37:33 +02:00
if (self.sale.advance_payment_eligible()
and not self.sale.advance_payment_completed):
return []
return lines
2018-06-26 19:11:52 +02:00
class HandleInvoiceException(metaclass=PoolMeta):
2017-04-02 10:37:33 +02:00
__name__ = 'sale.handle.invoice.exception'
def default_ask(self, fields):
2025-04-04 00:45:18 +02:00
default = super().default_ask(fields)
2017-04-02 10:37:33 +02:00
invoices = default['domain_invoices']
sale = self.record
2017-04-02 10:37:33 +02:00
skips = set(sale.invoices_ignored)
skips.update(sale.invoices_recreated)
for invoice in sale.advance_payment_invoices:
if invoice.state == 'cancelled' and invoice not in skips:
2017-04-02 10:37:33 +02:00
invoices.append(invoice.id)
return default