123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245 |
- # -*- coding: utf-8 -*-
- from odoo import api, fields, models, _, Command
- from odoo.exceptions import UserError, ValidationError
- from odoo.tools import format_date, formatLang, frozendict
- from dateutil.relativedelta import relativedelta
- class AccountPaymentTerm(models.Model):
- _name = "account.payment.term"
- _description = "Payment Terms"
- _order = "sequence, id"
- def _default_line_ids(self):
- return [Command.create({'value': 'balance', 'value_amount': 0.0, 'days': 0, 'end_month': False})]
- def _default_example_amount(self):
- return self._context.get('example_amount') or 100 # Force default value if the context is set to False
- def _default_example_date(self):
- return self._context.get('example_date') or fields.Date.today()
- name = fields.Char(string='Payment Terms', translate=True, required=True)
- active = fields.Boolean(default=True, help="If the active field is set to False, it will allow you to hide the payment terms without removing it.")
- note = fields.Html(string='Description on the Invoice', translate=True)
- line_ids = fields.One2many('account.payment.term.line', 'payment_id', string='Terms', copy=True, default=_default_line_ids)
- company_id = fields.Many2one('res.company', string='Company')
- sequence = fields.Integer(required=True, default=10)
- display_on_invoice = fields.Boolean(string='Display terms on invoice', help="If set, the payment deadlines and respective due amounts will be detailed on invoices.")
- example_amount = fields.Float(default=_default_example_amount, store=False)
- example_date = fields.Date(string='Date example', default=_default_example_date, store=False)
- example_invalid = fields.Boolean(compute='_compute_example_invalid')
- example_preview = fields.Html(compute='_compute_example_preview')
- @api.depends('line_ids')
- def _compute_example_invalid(self):
- for payment_term in self:
- payment_term.example_invalid = len(payment_term.line_ids.filtered(lambda l: l.value == 'balance')) != 1
- @api.depends('example_amount', 'example_date', 'line_ids.value', 'line_ids.value_amount',
- 'line_ids.months', 'line_ids.days', 'line_ids.end_month', 'line_ids.days_after')
- def _compute_example_preview(self):
- for record in self:
- example_preview = ""
- if not record.example_invalid:
- currency = self.env.company.currency_id
- terms = record._compute_terms(
- date_ref=record.example_date,
- currency=currency,
- company=self.env.company,
- tax_amount=0,
- tax_amount_currency=0,
- untaxed_amount=record.example_amount,
- untaxed_amount_currency=record.example_amount,
- sign=1)
- for i, info_by_dates in enumerate(record._get_amount_by_date(terms, currency).values()):
- date = info_by_dates['date']
- discount_date = info_by_dates['discount_date']
- amount = info_by_dates['amount']
- discount_amount = info_by_dates['discounted_amount'] or 0.0
- example_preview += "<div style='margin-left: 20px;'>"
- example_preview += _(
- "<b>%(count)s#</b> Installment of <b>%(amount)s</b> on <b style='color: #704A66;'>%(date)s</b>",
- count=i+1,
- amount=formatLang(self.env, amount, monetary=True, currency_obj=currency),
- date=date,
- )
- if discount_date:
- example_preview += _(
- " (<b>%(amount)s</b> if paid before <b>%(date)s</b>)",
- amount=formatLang(self.env, discount_amount, monetary=True, currency_obj=currency),
- date=format_date(self.env, terms[i].get('discount_date')),
- )
- example_preview += "</div>"
- record.example_preview = example_preview
- @api.model
- def _get_amount_by_date(self, terms, currency):
- """
- Returns a dictionary with the amount for each date of the payment term
- (grouped by date, discounted percentage and discount last date,
- sorted by date and ignoring null amounts).
- """
- terms = sorted(terms, key=lambda t: t.get('date'))
- amount_by_date = {}
- for term in terms:
- key = frozendict({
- 'date': term['date'],
- 'discount_date': term['discount_date'],
- 'discount_percentage': term['discount_percentage'],
- })
- results = amount_by_date.setdefault(key, {
- 'date': format_date(self.env, term['date']),
- 'amount': 0.0,
- 'discounted_amount': 0.0,
- 'discount_date': format_date(self.env, term['discount_date']),
- })
- results['amount'] += term['foreign_amount']
- results['discounted_amount'] += term['discount_amount_currency']
- return amount_by_date
- @api.constrains('line_ids')
- def _check_lines(self):
- for terms in self:
- if len(terms.line_ids.filtered(lambda r: r.value == 'balance')) != 1:
- raise ValidationError(_('The Payment Term must have one Balance line.'))
- if terms.line_ids.filtered(lambda r: r.value == 'fixed' and r.discount_percentage):
- raise ValidationError(_("You can't mix fixed amount with early payment percentage"))
- def _compute_terms(self, date_ref, currency, company, tax_amount, tax_amount_currency, sign, untaxed_amount, untaxed_amount_currency):
- """Get the distribution of this payment term.
- :param date_ref: The move date to take into account
- :param currency: the move's currency
- :param company: the company issuing the move
- :param tax_amount: the signed tax amount for the move
- :param tax_amount_currency: the signed tax amount for the move in the move's currency
- :param untaxed_amount: the signed untaxed amount for the move
- :param untaxed_amount_currency: the signed untaxed amount for the move in the move's currency
- :param sign: the sign of the move
- :return (list<tuple<datetime.date,tuple<float,float>>>): the amount in the company's currency and
- the document's currency, respectively for each required payment date
- """
- self.ensure_one()
- company_currency = company.currency_id
- tax_amount_left = tax_amount
- tax_amount_currency_left = tax_amount_currency
- untaxed_amount_left = untaxed_amount
- untaxed_amount_currency_left = untaxed_amount_currency
- total_amount = tax_amount + untaxed_amount
- total_amount_currency = tax_amount_currency + untaxed_amount_currency
- result = []
- for line in self.line_ids.sorted(lambda line: line.value == 'balance'):
- term_vals = {
- 'date': line._get_due_date(date_ref),
- 'has_discount': line.discount_percentage,
- 'discount_date': None,
- 'discount_amount_currency': 0.0,
- 'discount_balance': 0.0,
- 'discount_percentage': line.discount_percentage,
- }
- if line.value == 'fixed':
- term_vals['company_amount'] = sign * company_currency.round(line.value_amount)
- term_vals['foreign_amount'] = sign * currency.round(line.value_amount)
- company_proportion = tax_amount/untaxed_amount if untaxed_amount else 1
- foreign_proportion = tax_amount_currency/untaxed_amount_currency if untaxed_amount_currency else 1
- line_tax_amount = company_currency.round(line.value_amount * company_proportion) * sign
- line_tax_amount_currency = currency.round(line.value_amount * foreign_proportion) * sign
- line_untaxed_amount = term_vals['company_amount'] - line_tax_amount
- line_untaxed_amount_currency = term_vals['foreign_amount'] - line_tax_amount_currency
- elif line.value == 'percent':
- term_vals['company_amount'] = company_currency.round(total_amount * (line.value_amount / 100.0))
- term_vals['foreign_amount'] = currency.round(total_amount_currency * (line.value_amount / 100.0))
- line_tax_amount = company_currency.round(tax_amount * (line.value_amount / 100.0))
- line_tax_amount_currency = currency.round(tax_amount_currency * (line.value_amount / 100.0))
- line_untaxed_amount = term_vals['company_amount'] - line_tax_amount
- line_untaxed_amount_currency = term_vals['foreign_amount'] - line_tax_amount_currency
- else:
- line_tax_amount = line_tax_amount_currency = line_untaxed_amount = line_untaxed_amount_currency = 0.0
- tax_amount_left -= line_tax_amount
- tax_amount_currency_left -= line_tax_amount_currency
- untaxed_amount_left -= line_untaxed_amount
- untaxed_amount_currency_left -= line_untaxed_amount_currency
- if line.value == 'balance':
- term_vals['company_amount'] = tax_amount_left + untaxed_amount_left
- term_vals['foreign_amount'] = tax_amount_currency_left + untaxed_amount_currency_left
- line_tax_amount = tax_amount_left
- line_tax_amount_currency = tax_amount_currency_left
- line_untaxed_amount = untaxed_amount_left
- line_untaxed_amount_currency = untaxed_amount_currency_left
- if line.discount_percentage:
- if company.early_pay_discount_computation in ('excluded', 'mixed'):
- term_vals['discount_balance'] = company_currency.round(term_vals['company_amount'] - line_untaxed_amount * line.discount_percentage / 100.0)
- term_vals['discount_amount_currency'] = currency.round(term_vals['foreign_amount'] - line_untaxed_amount_currency * line.discount_percentage / 100.0)
- else:
- term_vals['discount_balance'] = company_currency.round(term_vals['company_amount'] * (1 - (line.discount_percentage / 100.0)))
- term_vals['discount_amount_currency'] = currency.round(term_vals['foreign_amount'] * (1 - (line.discount_percentage / 100.0)))
- term_vals['discount_date'] = date_ref + relativedelta(days=line.discount_days)
- result.append(term_vals)
- return result
- @api.ondelete(at_uninstall=False)
- def _unlink_except_referenced_terms(self):
- if self.env['account.move'].search([('invoice_payment_term_id', 'in', self.ids)]):
- raise UserError(_('You can not delete payment terms as other records still reference it. However, you can archive it.'))
- def unlink(self):
- for terms in self:
- self.env['ir.property'].sudo().search(
- [('value_reference', 'in', ['account.payment.term,%s'%payment_term.id for payment_term in terms])]
- ).unlink()
- return super(AccountPaymentTerm, self).unlink()
- class AccountPaymentTermLine(models.Model):
- _name = "account.payment.term.line"
- _description = "Payment Terms Line"
- _order = "id"
- value = fields.Selection([
- ('balance', 'Balance'),
- ('percent', 'Percent'),
- ('fixed', 'Fixed Amount')
- ], string='Type', required=True, default='percent',
- help="Select here the kind of valuation related to this payment terms line.")
- value_amount = fields.Float(string='Value', digits='Payment Terms', help="For percent enter a ratio between 0-100.")
- months = fields.Integer(string='Months', required=True, default=0)
- days = fields.Integer(string='Days', required=True, default=0)
- end_month = fields.Boolean(string='End of month', help="Switch to end of the month after having added months or days")
- days_after = fields.Integer(string='Days after End of month', help="Days to add after the end of the month")
- discount_percentage = fields.Float(string='Discount %', help='Early Payment Discount granted for this line')
- discount_days = fields.Integer(string='Discount Days', help='Number of days before the early payment proposition expires')
- payment_id = fields.Many2one('account.payment.term', string='Payment Terms', required=True, index=True, ondelete='cascade')
- def _get_due_date(self, date_ref):
- self.ensure_one()
- due_date = fields.Date.from_string(date_ref) or fields.Date.today()
- due_date += relativedelta(months=self.months)
- due_date += relativedelta(days=self.days)
- if self.end_month:
- due_date += relativedelta(day=31)
- due_date += relativedelta(days=self.days_after)
- return due_date
- @api.constrains('value', 'value_amount', 'discount_percentage')
- def _check_percent(self):
- for term_line in self:
- if term_line.value == 'percent' and (term_line.value_amount < 0.0 or term_line.value_amount > 100.0):
- raise ValidationError(_('Percentages on the Payment Terms lines must be between 0 and 100.'))
- if term_line.discount_percentage and (term_line.discount_percentage < 0.0 or term_line.discount_percentage > 100.0):
- raise ValidationError(_('Discount percentages on the Payment Terms lines must be between 0 and 100.'))
- @api.constrains('discount_days')
- def _check_positive(self):
- for term_line in self:
- if term_line.discount_days < 0:
- raise ValidationError(_('The discount days of the Payment Terms lines must be positive.'))
|