[ADD] sale_discount:

New implementation of sale_discount with full support
of tax reduction respecting also different taxes used
on sales positions
This commit is contained in:
Renzo Meister authored and Parthiv Patel committed 2021-02-12 10:58:50 +00:00
1 parent b318ce618a
commit 16b82d2414
17 files changed
+695

No files matched your search

+1
View File
@@ -0,0 +1 @@
from . import models
+25
View File
@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
{
'name': "Invoice Document Discount",
'summary': """
Use Document Discounts on Invoices
""",
'description': """
Use Document Discounts on Invoices
""",
'author': "Jamotion GmbH, Flectra HQ",
'website': "https://flectra-hq.com",
'category': 'Accounting',
'version': '0.1',
'depends': [
'account',
],
'data': [
'views/res_config_settings_views.xml',
'views/account_move_views.xml',
'reports/invoice_report_views.xml',
],
'demo': [
# 'demo/demo.xml',
],
}
@@ -0,0 +1,3 @@
from . import res_company
from . import res_config_settings
from . import account_move
@@ -0,0 +1,231 @@
import logging
from functools import partial
from flectra import models, fields, api, _
from flectra.tools import float_is_zero, float_round, float_compare
from flectra.tools.misc import formatLang
_logger = logging.getLogger(__name__)
class AccountMove(models.Model):
_inherit = 'account.move'
discount_type = fields.Selection(
selection=[
('fixed', 'Fixed'),
('percent', 'Percent')
],
string="Discount Type",
default="percent",
)
discount_value = fields.Float(
string='Discount',
tracking=True,
)
discount_value_percent = fields.Float(
string='Discount (%)',
digits='Discount',
compute='_compute_document_discount',
compute_sudo=True,
)
amount_gross = fields.Monetary(
string='Gross Amount',
store=True,
readonly=True,
compute='_compute_amount',
)
document_discount = fields.Monetary(
string='Document Discount',
store=True,
readonly=True,
compute='_compute_document_discount',
)
document_discount_tax_amount = fields.Monetary(
string='Document Discount Tax',
store=True,
readonly=True,
compute='_compute_document_discount',
)
has_document_discount = fields.Boolean(
string='Has Document Discount',
store=True,
readonly=True,
compute='_compute_document_discount',
)
@api.depends(
'line_ids.amount_currency',
'line_ids.price_subtotal',
'line_ids.price_total',
'discount_type',
'discount_value',
)
def _compute_document_discount(self):
for move in self:
document_discount = 0
discount_used = move.discount_type and not float_is_zero(move.discount_value, precision_digits=move.currency_id.decimal_places)
if discount_used:
if move.discount_type == 'fixed':
document_discount = move.discount_value * -1
else:
document_discount = (move.amount_gross * (move.discount_value / 100)) * -1
document_discount = float_round(document_discount, precision_digits=move.currency_id.decimal_places)
discount_lines = move.line_ids.filtered(lambda f: f.is_document_discount_line)
if discount_lines:
document_discount_tax_amount = float_round(
sum(line.price_total - line.price_subtotal for line in discount_lines),
precision_digits=move.currency_id.decimal_places
)
else:
document_discount_tax_amount = 0
move.update({
'document_discount': document_discount,
'document_discount_tax_amount': document_discount_tax_amount,
'has_document_discount': discount_used,
'discount_value_percent': move.discount_value,
})
@api.depends(
'line_ids.matched_debit_ids.debit_move_id.move_id.line_ids.amount_residual',
'line_ids.matched_debit_ids.debit_move_id.move_id.line_ids.amount_residual_currency',
'line_ids.matched_credit_ids.credit_move_id.move_id.line_ids.amount_residual',
'line_ids.matched_credit_ids.credit_move_id.move_id.line_ids.amount_residual_currency',
'line_ids.debit',
'line_ids.credit',
'line_ids.currency_id',
'line_ids.amount_currency',
'line_ids.amount_residual',
'line_ids.amount_residual_currency',
'line_ids.payment_id.state',
'line_ids.full_reconcile_id',
'line_ids.price_total',
'document_discount',
)
def _compute_amount(self):
super(AccountMove, self)._compute_amount()
for move in self:
if move.line_ids:
amount_gross = sum(move.line_ids.filtered(lambda f: not f.exclude_from_invoice_tab).mapped('price_subtotal'))
move.update({
'amount_gross': amount_gross,
'amount_untaxed': move.amount_gross + move.document_discount,
})
move._generate_document_discount_move_line()
move.update({
'amount_total': move.amount_untaxed + move.amount_tax,
})
def _generate_document_discount_move_line(self):
aml = self.env['account.move.line'].with_context(check_move_validity=False)
update_needed = False
for move in self:
update_needed = False
in_draft_mode = move != move._origin
if float_is_zero(move.document_discount, precision_digits=move.currency_id.decimal_places):
to_del = move.line_ids.filtered(lambda f: f.is_document_discount_line)
if in_draft_mode:
move.update({'line_ids': [(2, line.id) for line in to_del]})
update_needed = True
continue
currency = move.currency_id or move.company_id.currency_id
if move.discount_type == 'fixed':
account_id = move.company_id.document_discount_fixed_account_id
else:
account_id = move.company_id.document_discount_percent_account_id
create_method = in_draft_mode and aml.new or aml.create
distributions = move._get_document_tax_distribution()
for distribution in distributions.values():
existing_line = move.line_ids.filtered(lambda f: f.is_document_discount_line and f.tax_ids.ids == distribution['tax'].ids)
balance = currency._convert(
move.document_discount * distribution['factor'],
move.journal_id.company_id.currency_id,
move.journal_id.company_id,
move.date or fields.Date.context_today(move),
)
price_unit = move.document_discount * distribution['factor']
amount_currency = price_unit * -1
if existing_line:
line_values = {
'amount_currency': amount_currency,
'price_unit': price_unit,
'debit': balance < 0.0 and -balance or 0.0,
'credit': balance > 0.0 and balance or 0.0,
}
if existing_line.credit == line_values['credit'] and existing_line.debit == line_values['debit']:
continue
update_needed = True
if in_draft_mode:
existing_line.update(line_values)
else:
existing_line.write(line_values)
else:
line_values = {
'account_id': account_id.id,
'amount_currency': amount_currency,
'price_unit': price_unit,
'currency_id': currency.id,
'debit': balance < 0.0 and -balance or 0.0,
'credit': balance > 0.0 and balance or 0.0,
'name': _('Document Discount'),
'move_id': move.id,
'partner_id': move.partner_id.id,
'company_id': move.company_id.id,
'company_currency_id': currency.id,
'exclude_from_invoice_tab': True,
'is_document_discount_line': True,
'tax_ids': distribution['tax'].ids,
}
create_method(line_values)
update_needed = True
if update_needed:
self.with_context(check_move_validity=False)._recompute_dynamic_lines(recompute_all_taxes=True)
def _get_document_tax_distribution(self):
"""
This function calculates distribution factor for right split
of negative tax amount of document discount amount.
@param amount_gross: the base amount (total untaxed amount) used as 100%
@return: dict with tax.id as key and tax data includin the distribution factor
"""
self.ensure_one()
res = {}
tax_lines = self.line_ids.filtered(lambda f: f.tax_line_id)
if not tax_lines:
return res
total_amount = sum(tax_lines.mapped('tax_base_amount'))
for line in tax_lines:
key = line.tax_line_id.id
res.setdefault(key, {'tax': line.tax_line_id, 'factor': 0.0})
res[key]['factor'] = line.tax_base_amount / total_amount
return res
@api.model_create_multi
def create(self, vals_list):
rslt = super(AccountMove, self).create(vals_list)
rslt._compute_document_discount()
rslt._compute_amount()
rslt._generate_document_discount_move_line()
return rslt
class AccountMoveLine(models.Model):
_inherit = 'account.move.line'
is_document_discount_line = fields.Boolean(help="Technical field used to identify document discount lines.")
@@ -0,0 +1,18 @@
import logging
from flectra import models, fields, api, _
_logger = logging.getLogger(__name__)
class ResCompany(models.Model):
_inherit = 'res.company'
document_discount_fixed_account_id = fields.Many2one(
comodel_name='account.account',
string='Account for fixed Discount',
)
document_discount_percent_account_id = fields.Many2one(
comodel_name='account.account',
string='Account for percent Discount',
)
@@ -0,0 +1,18 @@
import logging
from flectra import models, fields, api, _
_logger = logging.getLogger(__name__)
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
document_discount_fixed_account_id = fields.Many2one(
related='company_id.document_discount_fixed_account_id',
readonly=False,
)
document_discount_percent_account_id = fields.Many2one(
related='company_id.document_discount_percent_account_id',
readonly=False,
)
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<flectra>
<template id="report_invoice_document" inherit_id="account.report_invoice_document">
<xpath expr="//div[@id='total']/div/table/tr[1]" position="before">
<t t-if="o.has_document_discount">
<tr class="border-black o_subtotal" style="">
<td>
<strong>Gross Amount</strong>
</td>
<td class="text-right">
<span t-field="o.amount_gross"/>
</td>
</tr>
<tr style="">
<td t-if="o.discount_type == 'fixed'">
Fixed Discount
</td>
<td t-if="o.discount_type == 'percent'">
<span t-field="o.discount_value_percent"/>% Discount
</td>
<td class="text-right">
<span t-field="o.document_discount"/>
</td>
</tr>
</t>
</xpath>
</template>
</flectra>
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<flectra>
<record id="view_move_form" model="ir.ui.view">
<field name="name">view.account.move.form.inherit</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_move_form"/>
<field name="arch" type="xml">
<field name="amount_untaxed" position="before">
<field name="has_document_discount" invisible="1"/>
<field name="amount_gross" attrs="{'invisible': [('has_document_discount', '=', False)]}"/>
<field name="document_discount" attrs="{'invisible': [('has_document_discount', '=', False)]}"/>
</field>
<field name="narration" position="after">
<group>
<group>
<label for="discount_type" string="Document Discount"/>
<div class="o_row">
<field name="discount_type"/>
<field name="discount_value" attrs="{'invisible': [('discount_type', '=', False)]}"/>
</div>
</group>
</group>
</field>
<xpath expr="//page[@id='aml_tab']//tree/field[@name='account_id']" position="after">
<field name="is_document_discount_line" invisible="True"/>
</xpath>
</field>
</record>
</flectra>
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<flectra>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.account.discount</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="account.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@id='default_incoterm']" position="after">
<div class="col-12 col-lg-6 o_setting_box" id="document_discount"
title="The following accounts are used for document discount items">
<div class="o_setting_left_pane"/>
<div class="o_setting_right_pane">
<span class="o_form_label">Document Discount Accounts</span>
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific."
aria-label="Values set here are company-specific."
groups="base.group_multi_company" role="img"/>
<div class="text-muted">
Accounts applied to document discount items
</div>
<div class="content-group">
<div class="row mt16">
<label string="Fixed Discount" for="document_discount_fixed_account_id" class="col-lg-3 o_light_label"/>
<field name="document_discount_fixed_account_id"/>
</div>
<div class="row">
<label string="Percent Discount" for="document_discount_percent_account_id" class="col-lg-3 o_light_label"/>
<field name="document_discount_percent_account_id"/>
</div>
</div>
</div>
</div>
</xpath>
</field>
</record>
</flectra>
+1
View File
@@ -0,0 +1 @@
from . import models
+27
View File
@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
{
'name': "Sale Document Discount",
'summary': """
Use Document Discounts in Sale
""",
'description': """
Use Document Discounts in Sale
""",
'author': "Jamotion GmbH",
'website': "https://jamotion.ch",
'category': 'Sales',
'version': '0.1',
'depends': [
'sale',
],
'data': [
# 'security/ir.model.access.csv',
# 'views/res_config_settings_views.xml',
# 'views/product_views.xml',
'views/sale_views.xml',
'reports/sale_report_templates.xml',
],
'demo': [
# 'demo/demo.xml',
],
}
+4
View File
@@ -0,0 +1,4 @@
from . import product
from . import sale
+13
View File
@@ -0,0 +1,13 @@
import logging
from flectra import models, fields, api, _
_logger = logging.getLogger(__name__)
class ProductTemplate(models.Model):
_inherit = 'product.template'
exclude_from_document_discount = fields.Boolean(
string='No Document Discount',
help='When enabled the position is excluded from Document Discount',
)
+189
View File
@@ -0,0 +1,189 @@
import logging
from functools import partial
from flectra import models, fields, api, _
from flectra.tools import float_is_zero
from flectra.tools.misc import formatLang
_logger = logging.getLogger(__name__)
class SaleOrder(models.Model):
_inherit = 'sale.order'
discount_type = fields.Selection(
selection=[
('fixed', 'Fixed'),
('percent', 'Percent')
],
string="Discount Type",
default="percent",
)
discount_value = fields.Float(
string='Discount',
tracking=True,
)
discount_value_percent = fields.Float(
string='Discount (%)',
digits='Discount',
compute='_amount_all',
compute_sudo=True,
)
amount_gross = fields.Monetary(
string='Gross Amount',
store=True,
readonly=True,
compute='_amount_all',
)
amount_discountable = fields.Monetary(
string='Base Discount Amount',
store=True,
readonly=True,
compute='_amount_all',
)
document_discount = fields.Monetary(
string='Document Discount',
store=True,
readonly=True,
compute='_amount_all',
)
document_discount_tax_amount = fields.Monetary(
string='Document Discount Tax',
store=True,
readonly=True,
compute='_amount_all',
)
@api.depends('order_line.price_total', 'discount_type', 'discount_value')
def _amount_all(self):
super(SaleOrder, self)._amount_all()
for order in self:
amount_gross = order.amount_untaxed
amount_discountable = 0
document_discount = 0
discount_tax_amount = 0
discount_used = not float_is_zero(order.discount_value, precision_digits=order.currency_id.decimal_places)
if discount_used:
amount_discountable = amount_gross
if order.discount_type == 'fixed':
document_discount = order.discount_value * -1
else:
excluded_lines = order.order_line.filtered(lambda f: f.product_id.exclude_from_document_discount)
if excluded_lines:
excluded_from_discount = sum(l.price_subtotal for l in excluded_lines)
amount_discountable -= excluded_from_discount
document_discount = (amount_discountable * (order.discount_value / 100)) * -1
for distribution in order._get_document_tax_distribution(amount_discountable).values():
taxes = distribution['tax'].compute_all(
document_discount * distribution['factor'],
partner=order.partner_shipping_id,
is_refund=True,
)
discount_tax_amount += sum(t.get('amount', 0.0) for t in taxes.get('taxes', []))
amount_untaxed = order.amount_untaxed + document_discount
amount_tax = order.amount_tax + discount_tax_amount
amount_total = amount_untaxed + amount_tax
order.update({
'discount_value_percent': order.discount_value,
'amount_discountable': amount_discountable,
'document_discount': document_discount,
'document_discount_tax_amount': discount_tax_amount,
'amount_gross': amount_gross,
'amount_untaxed': amount_untaxed,
'amount_tax': amount_tax,
'amount_total': amount_total,
})
def _amount_by_group(self):
for order in self:
if float_is_zero(order.document_discount, precision_digits=order.currency_id.decimal_places):
super(SaleOrder, order)._amount_by_group()
continue
currency = order.currency_id or order.company_id.currency_id
fmt = partial(formatLang, self.with_context(lang=order.partner_id.lang).env, currency_obj=currency)
res = {}
for line in order.order_line:
price_reduce = line.price_unit * (1.0 - line.discount / 100.0)
taxes = line.tax_id.compute_all(
price_reduce,
quantity=line.product_uom_qty,
product=line.product_id,
partner=order.partner_shipping_id,
)['taxes']
for tax in line.tax_id:
group = tax.tax_group_id
res.setdefault(group, {'amount': 0.0, 'base': 0.0})
for t in taxes:
if t['id'] == tax.id or t['id'] in tax.children_tax_ids.ids:
res[group]['amount'] += t['amount']
res[group]['base'] += t['base']
for distribution in order._get_document_tax_distribution(order.amount_discountable).values():
group = distribution['tax'].tax_group_id
taxes = distribution['tax'].compute_all(
order.document_discount * distribution['factor'],
partner=order.partner_shipping_id,
is_refund=True,
)['taxes']
for t in taxes:
if t['id'] == distribution['tax'].id or t['id'] in distribution['tax'].children_tax_ids.ids:
res[group]['amount'] += t['amount']
res[group]['base'] += t['base']
res = sorted(res.items(), key=lambda l: l[0].sequence)
order.amount_by_group = [(
l[0].name, l[1]['amount'], l[1]['base'],
fmt(l[1]['amount']), fmt(l[1]['base']),
len(res),
) for l in res]
def _get_document_tax_distribution(self, amount_discountable):
"""
This function calculates distribution factor for right split
of negative tax amount of document discount amount.
@param amount_discountable: the base amount (total untaxed amount) used as 100%
@return: dict with tax.id as key and tax data includin the distribution factor
"""
self.ensure_one()
res = {}
for line in self.order_line.filtered(lambda f: not f.product_id.exclude_from_document_discount):
price_reduce = line.price_unit * (1.0 - line.discount / 100.0)
taxes = line.tax_id.compute_all(
price_reduce,
quantity=line.product_uom_qty,
product=line.product_id,
partner=self.partner_shipping_id
)['taxes']
for tax in line.tax_id:
key = tax.id
res.setdefault(key, {'tax': tax, 'amount': 0.0, 'base': 0.0, 'factor': 0.0})
for t in taxes:
if t['id'] == tax.id or t['id'] in tax.children_tax_ids.ids:
res[key]['amount'] += t['amount']
res[key]['base'] += t['base']
res[key]['factor'] = res[key]['base'] / amount_discountable
return res
def _prepare_invoice(self):
invoice_vals = super(SaleOrder, self)._prepare_invoice()
# To apply fixed discount only on first real invoice (no downpayment)
# we need to check if any real order position was already invoiced
real_invoice_exists = len(self.order_line.filtered(lambda f: not f.is_downpayment).invoice_lines.move_id)
# On fixed document discount we apply the discount only on first invoice
# On percent document discount the discount is applied on all invoices
if self.discount_type == 'percent' or not real_invoice_exists:
invoice_vals['discount_type'] = self.discount_type
invoice_vals['discount_value'] = self.discount_value
return invoice_vals
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<flectra>
<template id="report_saleorder_document" inherit_id="sale.report_saleorder_document">
<xpath expr="//div[@id='total']/div/table/tr[1]" position="before">
<t t-if="doc.amount_gross != doc.amount_untaxed">
<tr class="border-black o_subtotal" style="">
<td name="td_amount_gross_label">
<strong>Gross</strong>
</td>
<td name="td_amount_gross" class="text-right">
<span t-field="doc.amount_gross"/>
</td>
</tr>
<tr style="">
<td name="td_document_discount_label">
<t t-if="doc.discount_type == 'percent'">
<span t-field="doc.discount_value_percent"/>
<span>% Discount</span>
</t>
<t t-else="">
<strong>Fixed Discount</strong>
</t>
</td>
<td name="td_document_discount" class="text-right">
<span t-field="doc.document_discount"/>
</td>
</tr>
</t>
</xpath>
</template>
</flectra>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<flectra>
<record id="product_template_sale_form_view" model="ir.ui.view">
<field name="name">view.product.template.form.inherit</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="sale.product_template_sale_form_view"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='sales']/group[@name='sale']" position="after">
<group name="document_discount" string="Document Discount">
<div>
<field name="exclude_from_document_discount" nolabel="1"/>
<label for="exclude_from_document_discount" string="No Discount"/>
</div>
</group>
</xpath>
</field>
</record>
</flectra>
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<flectra>
<data>
<record id="view_order_form" model="ir.ui.view">
<field name="name">view.sale.order.form.inherit</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<field name="note" position="before">
<label for="discount_type" string="Document Discount"/>
<div class="o_row">
<field name="discount_type"/>
<field name="discount_value" attrs="{'invisible': [('discount_type', '=', False)]}"/>
</div>
</field>
<field name="amount_untaxed" position="before">
<field name="amount_gross" widget='monetary' options="{'currency_field': 'currency_id'}"/>
<field name="document_discount" widget='monetary' options="{'currency_field': 'currency_id'}"/>
</field>
</field>
</record>
</data>
</flectra>