From bb38c7047e739b250ceff9e90099e25d59bc2b3d Mon Sep 17 00:00:00 2001 From: "Lucas Perais (lpe)" Date: Wed, 18 Oct 2017 13:40:44 +0200 Subject: [PATCH 1/5] [FIX] im_livechat, mail: visitor to portal_user should keep livechat channel Before this commit, when starting a livechat session as a public user and continuing as a portal user, the conversation was lost (the poral user couldn't see previous messages posted as a public user) After this commit, the conversation is kept and security insured by the uuid in the cookie OPW 775568 closes #20279 --- addons/im_livechat/controllers/main.py | 3 ++- addons/mail/controllers/bus.py | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/addons/im_livechat/controllers/main.py b/addons/im_livechat/controllers/main.py index eaf54eaf399..25e5a946fbc 100644 --- a/addons/im_livechat/controllers/main.py +++ b/addons/im_livechat/controllers/main.py @@ -70,9 +70,10 @@ class LivechatController(http.Controller): anonymous_name = request.env.user.name return request.env["im_livechat.channel"].get_mail_channel(channel_id, anonymous_name) + # Do not forward port in 10.0 @http.route('/im_livechat/history', type="json", auth="public") def history(self, channel_id, limit): - return request.env["mail.channel"].browse(channel_id).channel_fetch_message(limit=limit) + return request.env["mail.channel"].browse(channel_id).sudo().channel_fetch_message(limit=limit) @http.route('/im_livechat/feedback', type='json', auth='public') def feedback(self, uuid, rate, reason=None, **kwargs): diff --git a/addons/mail/controllers/bus.py b/addons/mail/controllers/bus.py index 96d092f97b4..274cb722655 100644 --- a/addons/mail/controllers/bus.py +++ b/addons/mail/controllers/bus.py @@ -36,14 +36,13 @@ class MailChatController(openerp.addons.bus.controllers.main.BusController): # -------------------------- @openerp.http.route('/mail/chat_post', type="json", auth="none") def mail_chat_post(self, uuid, message_content, **kwargs): - request_uid = self._default_request_uid() # find the author from the user session, which can be None author_id = False # message_post accept 'False' author_id, but not 'None' if request.session.uid: author_id = request.env['res.users'].sudo().browse(request.session.uid).partner_id.id # post a message without adding followers to the channel. email_from=False avoid to get author from email data - mail_channel = request.env["mail.channel"].sudo(request_uid).search([('uuid', '=', uuid)], limit=1) - message = mail_channel.sudo(request_uid).with_context(mail_create_nosubscribe=True).message_post(author_id=author_id, email_from=False, body=message_content, message_type='comment', subtype='mail.mt_comment', content_subtype='plaintext', **kwargs) + mail_channel = request.env["mail.channel"].sudo().search([('uuid', '=', uuid)], limit=1) + message = mail_channel.sudo().with_context(mail_create_nosubscribe=True).message_post(author_id=author_id, email_from=False, body=message_content, message_type='comment', subtype='mail.mt_comment', content_subtype='plaintext') return message and message.id or False @openerp.http.route(['/mail/chat_history'], type="json", auth="none") From ab1ad384859e1ac1b9fb6d094cfec4c5750c210b Mon Sep 17 00:00:00 2001 From: "Yenthe V.G" Date: Thu, 9 Nov 2017 13:34:34 +0100 Subject: [PATCH 2/5] [FIX] stock,project: make planner translatable Titles of the sections was not present Closes #20825 Closes #20833 --- addons/project/data/web_planner_data.xml | 6 +++--- addons/project/i18n/project.pot | 12 ++++++++++++ addons/stock/data/web_planner_data.xml | 2 +- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/addons/project/data/web_planner_data.xml b/addons/project/data/web_planner_data.xml index ce1b1be2077..21b8e6d3dd6 100644 --- a/addons/project/data/web_planner_data.xml +++ b/addons/project/data/web_planner_data.xml @@ -39,7 +39,7 @@ Your Objectives - + Objectives

It's essential to be clear about why you want to use Odoo Project and what your goals are. Indeed, there are many ways to manage a project, to find the best one for you, you need to know exactly what you want to achieve. And later on, we will hopefully transform your objectives into real improvements for your company. @@ -83,7 +83,7 @@ Your Activities - + Activities

The Odoo Project app can be used to manage many activities, from the development of a new product to the daily operations of a customer support. With some creativity, it can even be used to manage your marketing communications or personal projects. But just because it can be done doesn't mean it's always a good idea: let's start by helping you understand what can be a good project.

@@ -116,7 +116,7 @@ Your Projects - + Projects

Depending on what you need and how you want to operate, there are several ways to work with Odoo. First, decide if you want to think in terms of tasks or issues. Then, activate the Timesheets app if you need it.

1. Learn about Tasks, Issues and Timesheets.

diff --git a/addons/project/i18n/project.pot b/addons/project/i18n/project.pot index df1aa4781dc..11026cf0f45 100644 --- a/addons/project/i18n/project.pot +++ b/addons/project/i18n/project.pot @@ -764,6 +764,11 @@ msgstr "" msgid "Active" msgstr "" +#. module: project +#: model:ir.ui.view,arch_db:project.project_planner +msgid "Activities" +msgstr "" + #. module: project #: model:ir.ui.view,arch_db:project.task_type_edit msgid "Add a description..." @@ -2210,6 +2215,12 @@ msgstr "" msgid "Number of documents attached" msgstr "" +#. module: project +#: model:ir.ui.view,arch_db:project.project_planner +msgid "Objectives" +msgstr "" + + #. module: project #: model:ir.ui.view,arch_db:project.project_planner msgid "Odoo Project is a super fast and easy way to make your activities and tasks visible to\n" @@ -2458,6 +2469,7 @@ msgstr "" #: model:ir.model.fields,field_description:project.field_project_task_type_project_ids #: model:ir.ui.menu,name:project.menu_projects_config #: model:ir.ui.view,arch_db:project.analytic_account_inherited_form +#: model:ir.ui.view,arch_db:project.project_planner #: model:ir.ui.view,arch_db:project.view_project msgid "Projects" msgstr "" diff --git a/addons/stock/data/web_planner_data.xml b/addons/stock/data/web_planner_data.xml index 592887fa992..cdde8a2e898 100644 --- a/addons/stock/data/web_planner_data.xml +++ b/addons/stock/data/web_planner_data.xml @@ -363,7 +363,7 @@ Reordering Rules - + Reordering Rules

A good inventory management aims to optimize stock levels: not too low (or you may find yourself out of stock) and not too high (your products occupy From a1ad6375efea21fd41cd15cb6749f409f17db123 Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Thu, 9 Nov 2017 13:00:47 +0100 Subject: [PATCH 3/5] [FIX] stock: reordering rules on service 1. Create a stockable product 2. Create a reordering rule 3. Ooops, change stockable into service 4. Run scheduler Odoo created procurement for service products. We prevent changing the product type if there are active reordering rules. We could have silently skipped these products when running the scheduler (in `_get_orderpoint_domain`), but that would have potentially broke existing installations incorrectly set up. opw-781128 --- addons/stock/models/product.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py index d581fde2855..2a0e96f58dd 100644 --- a/addons/stock/models/product.py +++ b/addons/stock/models/product.py @@ -505,6 +505,8 @@ class ProductTemplate(models.Model): done_moves = self.env['stock.move'].search([('product_id', 'in', updated.mapped('product_variant_ids').ids)], limit=1) if done_moves: raise UserError(_("You can not change the unit of measure of a product that has already been used in a done stock move. If you need to change the unit of measure, you may deactivate this product.")) + if 'type' in vals and vals['type'] != 'product' and sum(self.mapped('nbr_reordering_rules')) != 0: + raise UserError(_('You still have some active reordering rules on this product. Please archive or delete them first.')) return super(ProductTemplate, self).write(vals) @api.multi From e77fa0051c35d09e27fd5d233a9425d44a882c61 Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Fri, 10 Nov 2017 10:32:03 +0100 Subject: [PATCH 4/5] [FIX] models: blank export Exporting One2many files gives blank export. This reverts commit 2c61c54ffa67552f7731c9802853ef33197fc1fd. opw-781399 opw-781457 --- odoo/addons/test_impex/tests/test_export.py | 54 +++++++++------------ odoo/models.py | 10 +++- 2 files changed, 31 insertions(+), 33 deletions(-) diff --git a/odoo/addons/test_impex/tests/test_export.py b/odoo/addons/test_impex/tests/test_export.py index 861e7dd1661..226ba9943f6 100644 --- a/odoo/addons/test_impex/tests/test_export.py +++ b/odoo/addons/test_impex/tests/test_export.py @@ -304,12 +304,13 @@ class test_m2o(CreatorCase): [[False]]) def test_basic(self): - """ Exported value is empty + """ Exported value is the name_get of the related object """ record = self.env['export.integer'].create({'value': 42}) + name = dict(record.name_get())[record.id] self.assertEqual( self.export(record.id), - [['']]) + [[name]]) def test_path(self): """ Can recursively export fields of m2o via path @@ -351,13 +352,14 @@ class test_o2m(CreatorCase): def test_single(self): self.assertEqual( self.export([(0, False, {'value': 42})]), - [['']]) + # name_get result + [[u'export.one2many.child:42']]) def test_single_subfield(self): self.assertEqual( self.export([(0, False, {'value': 42})], fields=['value', 'value/value']), - [['', u'42']]) + [[u'export.one2many.child:42', u'42']]) def test_integrate_one_in_parent(self): self.assertEqual( @@ -379,13 +381,9 @@ class test_o2m(CreatorCase): def test_multiple_records_name(self): self.assertEqual( self.export(self.commands, fields=['const', 'value']), - [ - [u'4', ''], - ['', ''], - ['', ''], - ['', ''], - ['', ''], - ]) + [[ + u'4', u','.join(self.names) + ]]) def test_multiple_records_id(self): export = self.export(self.commands, fields=['const', 'value/.id']) @@ -403,18 +401,14 @@ class test_o2m(CreatorCase): def test_multiple_records_with_name_before(self): self.assertEqual( self.export(self.commands, fields=['const', 'value', 'value/value']), - [ - [u'4', '', u'4'], - ['', '', u'42'], - ['', '', u'36'], - ['', '', u'4'], - ['', '', u'13'], - ]) + [[ # exports sub-fields of very first o2m + u'4', u','.join(self.names), u'4' + ]]) def test_multiple_records_with_name_after(self): self.assertEqual( self.export(self.commands, fields=['const', 'value/value', 'value']), - [ + [ # completely ignores name_get request [u'4', u'4', ''], ['', u'42', ''], ['', u'36', ''], @@ -465,16 +459,16 @@ class test_o2m_multiple(CreatorCase): def test_single_per_side(self): self.assertEqual( self.export(child1=False, child2=[(0, False, {'value': 42})]), - [[False, '']]) + [[False, u'export.one2many.child.2:42']]) self.assertEqual( self.export(child1=[(0, False, {'value': 43})], child2=False), - [['', False]]) + [[u'export.one2many.child.1:43', False]]) self.assertEqual( self.export(child1=[(0, False, {'value': 43})], child2=[(0, False, {'value': 42})]), - [['', '']]) + [[u'export.one2many.child.1:43', u'export.one2many.child.2:42']]) def test_single_integrate_subfield(self): fields = ['const', 'child1/value', 'child2/value'] @@ -561,13 +555,14 @@ class test_m2m(CreatorCase): def test_single(self): self.assertEqual( self.export([(0, False, {'value': 42})]), - [['']]) + # name_get result + [[u'export.many2many.other:42']]) def test_single_subfield(self): self.assertEqual( self.export([(0, False, {'value': 42})], fields=['value', 'value/value']), - [['', u'42']]) + [[u'export.many2many.other:42', u'42']]) def test_integrate_one_in_parent(self): self.assertEqual( @@ -589,13 +584,10 @@ class test_m2m(CreatorCase): def test_multiple_records_name(self): self.assertEqual( self.export(self.commands, fields=['const', 'value']), - [ - [u'4', ''], - ['', ''], - ['', ''], - ['', ''], - ['', ''], - ]) + [[ # FIXME: hardcoded comma, import uses config.csv_internal_sep + # resolution: remove configurable csv_internal_sep + u'4', u','.join(self.names) + ]]) # essentially same as o2m, so boring diff --git a/odoo/models.py b/odoo/models.py index 89a80402cf3..9be314685cf 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -805,8 +805,14 @@ class BaseModel(object): for j, val in enumerate(lines2[0]): if val or isinstance(val, bool): current[j] = val - # append the other lines at the end - lines += lines2[1:] + # check value of current field + if not current[i] and not isinstance(current[i], bool): + # assign xml_ids, and forget about remaining lines + xml_ids = [item[1] for item in value.name_get()] + current[i] = ','.join(xml_ids) + else: + # append the other lines at the end + lines += lines2[1:] else: current[i] = False From ce818f1f8f0557409f6d001c2c64446f9cdc3bdb Mon Sep 17 00:00:00 2001 From: Laurent Smet Date: Fri, 10 Nov 2017 14:32:18 +0100 Subject: [PATCH 5/5] [REVERT] account, point_of_sale: revert complex taxes computation Reverts: f85c8843c90046ba3029f0f395ba1aa6778792b6 6c996723b212b8ea85aa7b5d6e935d80e37b83a9 455b5aaff82bdc286de05205a6bc39a87f5c36a2 c6a9bab074492beffeddcb320cfdc042f021a285 6366150484668ebfce98f9d7483db52103cca500 33d20a224164cd83f09018addcf02496f22bced2 47c0cfc6e790df0f655ac3aa096fb807a6306a46 6e46ba7b460b5f155735c91f957a4dca3dec8f96 5afb5ecd04cf49be6bedb655b187b19a65e1e02d --- addons/account/models/account.py | 207 +++----------- addons/account/tests/test_tax.py | 284 ++----------------- addons/point_of_sale/static/src/js/models.js | 180 +++--------- 3 files changed, 116 insertions(+), 555 deletions(-) diff --git a/addons/account/models/account.py b/addons/account/models/account.py index 846cc9ea132..c64473778ec 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -4,7 +4,7 @@ import time import math from openerp.osv import expression -from openerp.tools.float_utils import float_round as round, float_is_zero as is_zero +from openerp.tools.float_utils import float_round as round from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT from openerp.exceptions import UserError, ValidationError from openerp import api, fields, models, _ @@ -619,7 +619,6 @@ class AccountTax(models.Model): price_unit * quantity eventually affected by previous taxes (if tax is include_base_amount XOR price_include) """ self.ensure_one() - price_include = self._context.get('force_price_include', self.price_include) if self.amount_type == 'fixed': # Use copysign to take into account the sign of the base amount which includes the sign # of the quantity and the sign of the price_unit @@ -633,11 +632,11 @@ class AccountTax(models.Model): return math.copysign(quantity, base_amount) * self.amount else: return quantity * self.amount - if (self.amount_type == 'percent' and not price_include) or (self.amount_type == 'division' and self.price_include): + if (self.amount_type == 'percent' and not self.price_include) or (self.amount_type == 'division' and self.price_include): return base_amount * self.amount / 100 - if self.amount_type == 'percent' and price_include: + if self.amount_type == 'percent' and self.price_include: return base_amount - (base_amount / (1 + self.amount / 100)) - if self.amount_type == 'division' and not price_include: + if self.amount_type == 'division' and not self.price_include: return base_amount / (1 - self.amount / 100) - base_amount @api.v8 @@ -660,58 +659,13 @@ class AccountTax(models.Model): 'analytic': boolean, }] } """ - - # 1) Flatten the taxes. - - def collect_taxes(self, all_taxes=None): - # Collect all the taxes recursively ordered by the sequence. - # Example: - # group | seq | sub-group | - # ------------|-----------| - # | 1 | | - # ------------|-----------| - # t | 2 | | seq | | - # | | | 4 | | - # | | | 5 | | - # | | | 6 | | - # | | | - # ------------|-----------| - # | 3 | | - # ------------|-----------| - # Result: 1-4-5-6-3 - if not all_taxes: - all_taxes = self.env['account.tax'] - for tax in self.sorted(key=lambda r: r.sequence): - if tax.amount_type == 'group': - all_taxes = collect_taxes(tax.children_tax_ids, all_taxes) - else: - all_taxes += tax - return all_taxes - - taxes = collect_taxes(self) - - # 2) Avoid dealing with taxes mixing price_include=False && include_base_amount=True - # with price_include=True - - base_excluded_flag = False # price_include=False && include_base_amount=True - included_flag = False # price_include=True - for tax in taxes: - if tax.price_include: - included_flag = True - elif tax.include_base_amount: - base_excluded_flag = True - if base_excluded_flag and included_flag: - raise UserError(_('Unable to mix any taxes being price included with taxes affecting the base amount but not included in price.')) - - # 3) Deal with the rounding methods - if len(self) == 0: company_id = self.env.user.company_id else: company_id = self[0].company_id if not currency: currency = company_id.currency_id - + taxes = [] # By default, for each tax, tax amount will first be computed # and rounded at the 'Account' decimal precision for each # PO/SO/invoice line and then these rounded amounts will be @@ -736,133 +690,50 @@ class AccountTax(models.Model): if not round_tax: prec += 5 - # 4) Iterate the taxes in the reversed sequence order to retrieve the initial base of the computation. - # tax | base | amount | - # /\ ---------------------------- - # || tax_1 | XXXX | | <- we are looking for that, it's the total_excluded - # || tax_2 | | | - # || tax_3 | | | - # || ... | .. | .. | - # ---------------------------- - - base = round(price_unit * quantity, prec) - - # Keep track of subsequent recomputed bases in order to avoid some rounding issues. - # For example, 399.99 computed with a tax 20% price_include leads to - # base = 399.99 / 1.2 = 333.32500000000005 - # tax_amount = base * 0.2 = 66.665 - # round(base) + round(tax_amount) = 333.33 + 66.67 = 400.0 (!= 399.99: WRONG) - # - # To fix such issues, base_gaps will contains amount between two bases. - # In our example, the gap between 333.32500000000005 and 399.99 is 66.66499999999996 - # - # Then, when processing the tax and because 66.665 - 66.66499999999996 is close to zero, - # the real gap is returned and so: - # tax_amount = 66.66499999999996 - # round(base) + round(tax_amount) = 333.33 + 66.66 = 399.99 (CORRECT) - base_gaps = [] - - def recompute_base(base_amount, fixed_amount, percent_amount): - # Recompute the new base amount based on included fixed/percent amount and the current base amount. - # Example: - # tax | amount | - # ------------------ - # tax_1 | 10% | - # tax_2 | 15 | - # tax_3 | 20% | - # ------------------ - # if base_amount = 145, the new base is computed as: - # (145 - 15) / (1.0 + ((10 + 20) / 100.0)) = 130 / 1.3 = 100 - if fixed_amount == 0.0 and percent_amount == 0.0: - return base_amount - new_base = (base_amount - fixed_amount) / (1.0 + percent_amount / 100.0) - base_gaps.append(base_amount - new_base) - return new_base - - # For the computation of move lines, we could have a negative base value. - # In this case, compute all with positive values and negative them at the end. - if base < 0: - base = -base - sign = -1 + base_values = self.env.context.get('base_values') + if not base_values: + total_excluded = total_included = base = round(price_unit * quantity, prec) else: - sign = 1 + total_excluded, total_included, base = base_values - # Keep track of the accumulated included fixed/percent amount. - incl_fixed_amount = incl_percent_amount = 0 - for tax in reversed(taxes): - if tax.include_base_amount: - base = recompute_base(base, incl_fixed_amount, incl_percent_amount) - incl_fixed_amount = incl_percent_amount = 0 - if tax.price_include: - if tax.amount_type == 'fixed': - incl_fixed_amount += quantity * tax.amount - elif tax.amount_type == 'percent': - incl_percent_amount += tax.amount - # Start the computation of accumulated amounts at the total_excluded value. - total_excluded = total_included = base = recompute_base(base, incl_fixed_amount, incl_percent_amount) + # Sorting key is mandatory in this case. When no key is provided, sorted() will perform a + # search. However, the search method is overridden in account.tax in order to add a domain + # depending on the context. This domain might filter out some taxes from self, e.g. in the + # case of group taxes. + for tax in self.sorted(key=lambda r: r.sequence): + if tax.amount_type == 'group': + children = tax.children_tax_ids.with_context(base_values=(total_excluded, total_included, base)) + ret = children.compute_all(price_unit, currency, quantity, product, partner) + total_excluded = ret['total_excluded'] + base = ret['base'] if tax.include_base_amount else base + total_included = ret['total_included'] + tax_amount = total_included - total_excluded + taxes += ret['taxes'] + continue - # 5) Iterate the taxes in the sequence order to fill missing base/amount values. - # tax | base | amount | - # || ---------------------------- - # || tax_1 | OK | XXXX | - # || tax_2 | XXXX | XXXX | - # || tax_3 | XXXX | XXXX | - # \/ ... | .. | .. | - # ---------------------------- - - def compute_amount(tax): - # Compute the amount of the tax but don't deal with the price_include because it's already - # took into account on the base amount except for 'division' tax: - # (tax.amount_type == 'percent' && not tax.price_include) - # == (tax.amount_type == 'division' && tax.price_include) - # - # In case of price_included tax, subtract the amount to the corresponding - # gap between the current base and the next one. - amount = tax.with_context(force_price_include=False)._compute_amount( - base, price_unit, quantity, product, partner) - - if not tax.price_include or not base_gaps: - return amount - - # Compute the new gap after subtracting of the tax amount - new_gap = base_gaps[-1] - amount - - # If the newly computed gap is very close of zero, return the current gap to avoid - # rounding issues (see comments above base_gaps). - if is_zero(new_gap, prec): - return base_gaps.pop() - - # Update the current gap with the new one - base_gaps[-1] = new_gap - return amount - - taxes_vals = [] - for tax in taxes: - tax_amount = compute_amount(tax) + tax_amount = tax._compute_amount(base, price_unit, quantity, product, partner) if not round_tax: tax_amount = round(tax_amount, prec) else: tax_amount = currency.round(tax_amount) - # Suppose: - # seq | amount | incl | incl_base | base | amount - # ----------------------------------------------- - # 1 | 10 % | t | t | 100.0 | 10.0 - # ----------------------------------------------- - # ... the next computation must be done using 100.0 + 10.0 = 110.0 as base but - # the tax base of this tax will be 100.0. + if tax.price_include: + total_excluded -= tax_amount + base -= tax_amount + else: + total_included += tax_amount + + # Keep base amount used for the current tax tax_base = base + if tax.include_base_amount: base += tax_amount - # The total_included amount is computed as the sum of total_excluded with all tax_amount - total_included += tax_amount - - taxes_vals.append({ + taxes.append({ 'id': tax.id, 'name': tax.with_context(**{'lang': partner.lang} if partner else {}).name, - 'amount': sign * tax_amount, - 'base': round(sign * tax_base, prec), + 'amount': tax_amount, + 'base': tax_base, 'sequence': tax.sequence, 'account_id': tax.account_id.id, 'refund_account_id': tax.refund_account_id.id, @@ -870,10 +741,10 @@ class AccountTax(models.Model): }) return { - 'taxes': taxes_vals, - 'total_excluded': sign * (currency.round(total_excluded) if round_total else total_excluded), - 'total_included': sign * (currency.round(total_included) if round_total else total_included), - 'base': round(sign * base, prec), + 'taxes': sorted(taxes, key=lambda k: k['sequence']), + 'total_excluded': currency.round(total_excluded) if round_total else total_excluded, + 'total_included': currency.round(total_included) if round_total else total_included, + 'base': base, } @api.v7 diff --git a/addons/account/tests/test_tax.py b/addons/account/tests/test_tax.py index a214b8f35f8..02308fa6498 100644 --- a/addons/account/tests/test_tax.py +++ b/addons/account/tests/test_tax.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- - from openerp.addons.account.tests.account_test_users import AccountTestUsers - +from openerp.tests.common import TransactionCase +from openerp.tools import float_compare import time @@ -28,12 +27,6 @@ class TestTax(AccountTestUsers): 'amount': 10, 'sequence': 3, }) - self.percent_tax_bis = self.tax_model.create({ - 'name': "Percent tax bis", - 'amount_type': 'percent', - 'amount': 21, - 'sequence': 3, - }) self.division_tax = self.tax_model.create({ 'name': "Division tax", 'amount_type': 'division', @@ -74,125 +67,47 @@ class TestTax(AccountTestUsers): self.bank_account = self.bank_journal.default_debit_account_id self.expense_account = self.env['account.account'].search([('user_type_id.type', '=', 'payable')], limit=1) #Should be done by onchange later - def _check_compute_all_results(self, base, total_included, total_excluded, taxes, res): - self.assertAlmostEqual(res['base'], base) - self.assertAlmostEqual(res['total_included'], total_included) - self.assertAlmostEqual(res['total_excluded'], total_excluded) - for i in range(0, len(taxes)): - self.assertAlmostEqual(res['taxes'][i]['base'], taxes[i][0]) - self.assertAlmostEqual(res['taxes'][i]['amount'], taxes[i][1]) - def test_tax_group_of_group_tax(self): self.fixed_tax.include_base_amount = True + self.group_tax.include_base_amount = True + self.group_of_group_tax.include_base_amount = True res = self.group_of_group_tax.compute_all(200.0) - self._check_compute_all_results( - 220, # 'base' - 263, # 'total_included' - 200, # 'total_excluded' - [ - # base , amount | seq | amount | incl | incl_base - # --------------------------------------------------- - (200.0, 10.0), # | 1 | 10 | | t - (210.0, 21.0), # | 3 | 10% | | - (210.0, 10.0), # | 1 | 10 | | t - (220.0, 22.0), # | 3 | 10% | | - # --------------------------------------------------- - ], - res - ) + self.assertEquals(res['total_excluded'], 200.0) + # After calculation of first group + # base = 210 + # total_included = 231 + # Base of the first grouped is passed + # Base after the second group (220) is dropped. + # Base of the group of groups is passed out, + # so we obtain base as after first group + self.assertEquals(res['base'], 210.0) + self.assertEquals(res['total_included'], 263.0) def test_tax_group(self): res = self.group_tax.compute_all(200.0) - self._check_compute_all_results( - 200, # 'base' - 230, # 'total_included' - 200, # 'total_excluded' - [ - # base , amount | seq | amount | incl | incl_base - # --------------------------------------------------- - (200.0, 10.0), # | 1 | 10 | | - (200.0, 20.0), # | 3 | 10% | | - # --------------------------------------------------- - ], - res - ) + self.assertEquals(res['total_excluded'], 200.0) + self.assertEquals(res['total_included'], 230.0) + self.assertEquals(len(res['taxes']), 2) + self.assertEquals(res['taxes'][0]['amount'], 10.0) + self.assertEquals(res['taxes'][1]['amount'], 20.0) def test_tax_percent_division(self): self.division_tax.price_include = True self.division_tax.include_base_amount = True - res_division = self.division_tax.compute_all(200.0) - self._check_compute_all_results( - 220, # 'base' - 220, # 'total_included' - 200, # 'total_excluded' - [ - # base , amount | seq | amount | incl | incl_base - # --------------------------------------------------- - (200.0, 20.0), # | 4 | 10/ | t | t - # --------------------------------------------------- - ], - res_division - ) self.percent_tax.price_include = False self.percent_tax.include_base_amount = False + res_division = self.division_tax.compute_all(200.0) res_percent = self.percent_tax.compute_all(200.0) - self._check_compute_all_results( - 200, # 'base' - 220, # 'total_included' - 200, # 'total_excluded' - [ - # base , amount | seq | amount | incl | incl_base - # --------------------------------------------------- - (200.0, 20.0), # | 3 | 10% | | - # --------------------------------------------------- - ], - res_percent - ) + self.assertEquals(res_division['taxes'][0]['amount'], 20.0) + self.assertEquals(res_percent['taxes'][0]['amount'], 20.0) self.division_tax.price_include = False self.division_tax.include_base_amount = False - res_division = self.division_tax.compute_all(200.0) - self._check_compute_all_results( - 200, # 'base' - 222.22, # 'total_included' - 200, # 'total_excluded' - [ - # base , amount | seq | amount | incl | incl_base - # --------------------------------------------------- - (200.0, 22.22), # | 4 | 10/ | | - # --------------------------------------------------- - ], - res_division - ) self.percent_tax.price_include = True self.percent_tax.include_base_amount = True + res_division = self.division_tax.compute_all(200.0) res_percent = self.percent_tax.compute_all(200.0) - self._check_compute_all_results( - 200, # 'base' - 200, # 'total_included' - 181.82, # 'total_excluded' - [ - # base , amount | seq | amount | incl | incl_base - # --------------------------------------------------- - (181.82, 18.18), # | 3 | 10% | t | t - # --------------------------------------------------- - ], - res_percent - ) - self.percent_tax_bis.price_include = True - self.percent_tax_bis.include_base_amount = True - res_percent = self.percent_tax_bis.compute_all(7.0) - self._check_compute_all_results( - 7.0, # 'base' - 7.0, # 'total_included' - 5.79, # 'total_excluded' - [ - # base , amount | seq | amount | incl | incl_base - # --------------------------------------------------- - (5.79, 1.21), # | 3 | 21% | t | t - # --------------------------------------------------- - ], - res_percent - ) + self.assertEquals(res_division['taxes'][0]['amount'], 22.22) + self.assertEquals(res_percent['taxes'][0]['amount'], 18.18) def test_tax_sequence_normalized_set(self): self.division_tax.sequence = 1 @@ -200,103 +115,19 @@ class TestTax(AccountTestUsers): self.percent_tax.sequence = 3 taxes_set = (self.group_tax | self.division_tax) res = taxes_set.compute_all(200.0) - self._check_compute_all_results( - 200, # 'base' - 252.22, # 'total_included' - 200, # 'total_excluded' - [ - # base , amount | seq | amount | incl | incl_base - # --------------------------------------------------- - (200.0, 22.22), # | 1 | 10/ | | - (200.0, 10.0), # | 2 | 10 | | - (200.0, 20.0), # | 3 | 10% | | - # --------------------------------------------------- - ], - res - ) + self.assertEquals(res['taxes'][0]['amount'], 22.22) + self.assertEquals(res['taxes'][1]['amount'], 10.0) + self.assertEquals(res['taxes'][2]['amount'], 20.0) def test_tax_include_base_amount(self): self.fixed_tax.include_base_amount = True res = self.group_tax.compute_all(200.0) - self._check_compute_all_results( - 210, # 'base' - 231, # 'total_included' - 200, # 'total_excluded' - [ - # base , amount | seq | amount | incl | incl_base - # --------------------------------------------------- - (200.0, 10.0), # | 1 | 10 | | t - (210.0, 21.0), # | 3 | 10% | | - # --------------------------------------------------- - ], - res - ) - - self.fixed_tax.price_include = True - self.fixed_tax.include_base_amount = False - res = self.fixed_tax.compute_all(100.0, quantity=2.0) - self._check_compute_all_results( - 180, # 'base' - 200, # 'total_included' - 180, # 'total_excluded' - [ - # base , amount | seq | amount | incl | incl_base - # --------------------------------------------------- - (180.0, 20.0), # | 1 | 20 | | t - # --------------------------------------------------- - ], - res - ) - - def test_tax_include_base_amount_2(self): - self.percent_tax.price_include = True - self.percent_tax.amount = 21.0 - res = self.percent_tax.compute_all(7.0) - self._check_compute_all_results( - 5.79, # 'base' - 7.0, # 'total_included' - 5.79, # 'total_excluded' - [ - # base , amount | seq | amount | incl | incl_base - # --------------------------------------------------- - (5.79, 1.21), # | 3 | 10% | t | - # --------------------------------------------------- - ], - res - ) - - def test_tax_include_base_amount_3(self): - self.percent_tax.price_include = True - self.percent_tax.amount = 20.0 - res = self.percent_tax.compute_all(399.99) - self._check_compute_all_results( - 333.33, # 'base' - 399.99, # 'total_included' - 333.33, # 'total_excluded' - [ - # base , amount | seq | amount | incl | incl_base - # --------------------------------------------------- - (333.33, 66.66), # | 3 | 10% | t | - # --------------------------------------------------- - ], - res - ) + self.assertEquals(res['total_included'], 231.0) def test_tax_currency(self): self.division_tax.amount = 15.0 res = self.division_tax.compute_all(200.0, currency=self.env.ref('base.VEF')) - self._check_compute_all_results( - 200, # 'base' - 235.2941, # 'total_included' - 200, # 'total_excluded' - [ - # base , amount | seq | amount | incl | incl_base - # --------------------------------------------------- - (200.0, 35.2941), # | 4 | 15/ | | - # --------------------------------------------------- - ], - res - ) + self.assertAlmostEqual(res['total_included'], 235.2941) def test_tax_move_lines_creation(self): """ Test that creating a move.line with tax_ids generates the tax move lines and adjust line amount when a tax is price_include """ @@ -341,58 +172,3 @@ class TestTax(AccountTestUsers): aml_with_taxes = move.line_ids.filtered(lambda l: set(l.tax_ids.ids) == set([self.group_tax.id, self.fixed_tax_bis.id])) self.assertEquals(len(aml_with_taxes), 1) self.assertEquals(aml_with_taxes.credit, 190) - - def test_advanced_taxes_computation_0(self): - '''Test more advanced taxes computation (see issue 34471).''' - tax_1 = self.env['account.tax'].create({ - 'name': 'test_advanced_taxes_computation_0_1', - 'amount_type': 'percent', - 'amount': 10, - 'price_include': True, - 'include_base_amount': True, - 'sequence': 1, - }) - tax_2 = self.env['account.tax'].create({ - 'name': 'test_advanced_taxes_computation_0_2', - 'amount_type': 'percent', - 'amount': 10, - 'sequence': 2, - }) - tax_3 = self.env['account.tax'].create({ - 'name': 'test_advanced_taxes_computation_0_3', - 'amount_type': 'percent', - 'amount': 10, - 'price_include': True, - 'sequence': 3, - }) - tax_4 = self.env['account.tax'].create({ - 'name': 'test_advanced_taxes_computation_0_4', - 'amount_type': 'percent', - 'amount': 10, - 'sequence': 4, - }) - tax_5 = self.env['account.tax'].create({ - 'name': 'test_advanced_taxes_computation_0_5', - 'amount_type': 'percent', - 'amount': 10, - 'price_include': True, - 'sequence': 5, - }) - taxes = tax_1 + tax_2 + tax_3 + tax_4 + tax_5 - res = taxes.compute_all(132.0) - self._check_compute_all_results( - 110, # 'base' - 154, # 'total_included' - 100, # 'total_excluded' - [ - # base , amount | seq | amount | incl | incl_base - # --------------------------------------------------- - (100.0, 10.0), # | 1 | 10% | t | t - (110.0, 11.0), # | 2 | 10% | | - (110.0, 11.0), # | 3 | 10% | t | - (110.0, 11.0), # | 4 | 10% | | - (110.0, 11.0), # | 5 | 10% | t | - # --------------------------------------------------- - ], - res - ) diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index ab6d7517d3e..814856f4e5a 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -87,7 +87,7 @@ exports.PosModel = Backbone.Model.extend({ // We fetch the backend data on the server asynchronously. this is done only when the pos user interface is launched, // Any change on this data made on the server is thus not reflected on the point of sale until it is relaunched. - // when all the data has loaded, we compute some stuff, and declare the Pos ready to be used. + // when all the data has loaded, we compute some stuff, and declare the Pos ready to be used. this.ready = this.load_server_data().then(function(){ return self.after_load_server_data(); }); @@ -202,7 +202,7 @@ exports.PosModel = Backbone.Model.extend({ }, },{ model: 'account.tax', - fields: ['name','amount', 'price_include', 'include_base_amount', 'amount_type', 'children_tax_ids', 'sequence'], + fields: ['name','amount', 'price_include', 'include_base_amount', 'amount_type', 'children_tax_ids'], domain: null, loaded: function(self, taxes){ self.taxes = taxes; @@ -1332,159 +1332,74 @@ exports.Orderline = Backbone.Model.extend({ return tax; }, - /** - * Mirror JS method of: - * _compute_amount in addons/account/models/account.py - */ - _compute_all: function(tax, base_amount, quantity, price_include) { - if(price_include === undefined) - price_include = tax.price_include; + _compute_all: function(tax, base_amount, quantity) { if (tax.amount_type === 'fixed') { var sign_base_amount = base_amount >= 0 ? 1 : -1; return (Math.abs(tax.amount) * sign_base_amount) * quantity; } - if ((tax.amount_type === 'percent' && !price_include) || (tax.amount_type === 'division' && tax.price_include)){ + if ((tax.amount_type === 'percent' && !tax.price_include) || (tax.amount_type === 'division' && tax.price_include)){ return base_amount * tax.amount / 100; } - if (tax.amount_type === 'percent' && price_include){ + if (tax.amount_type === 'percent' && tax.price_include){ return base_amount - (base_amount / (1 + tax.amount / 100)); } - if (tax.amount_type === 'division' && !price_include) { + if (tax.amount_type === 'division' && !tax.price_include) { return base_amount / (1 - tax.amount / 100) - base_amount; } return false; }, - /** - * Mirror JS method of: - * compute_all in addons/account/models/account.py - * - * Read comments in the python side method for more details about each sub-methods. - */ compute_all: function(taxes, price_unit, quantity, currency_rounding, no_map_tax) { var self = this; - - // 1) Flatten the taxes. - - var _collect_taxes = function(taxes, all_taxes){ - taxes.sort(function (tax1, tax2) { - return tax1.sequence - tax2.sequence; - }); - _(taxes).each(function(tax){ - if(tax.amount_type === 'group') - all_taxes = _collect_taxes(tax.children_tax_ids, all_taxes); - else - all_taxes.push(tax); - }); - return all_taxes; - } - var collect_taxes = function(taxes){ - return _collect_taxes(taxes, []); - } - - taxes = collect_taxes(taxes); - - // 2) Avoid dealing with taxes mixing price_include=False && include_base_amount=True - // with price_include=True - - var base_excluded_flag = false; // price_include=False && include_base_amount=True - var included_flag = false; // price_include=True - _(taxes).each(function(tax){ - if(tax.price_include) - included_flag = true; - else if(tax.include_base_amount) - base_excluded_flag = true - if(base_excluded_flag && included_flag) - throw new Error('Unable to mix any taxes being price included with taxes affecting the base amount but not included in price.'); - }); - - // 3) Deal with the rounding methods - + var list_taxes = []; var currency_rounding_bak = currency_rounding; if (this.pos.company.tax_calculation_rounding_method == "round_globally"){ currency_rounding = currency_rounding * 0.00001; } - - // 4) Iterate the taxes in the reversed sequence order to retrieve the initial base of the computation. - - var base = round_pr(price_unit * quantity, currency_rounding); - - var base_gaps = []; - - var recompute_base = function(base_amount, fixed_amount, percent_amount){ - if(fixed_amount === 0.0 && percent_amount === 0.0) - return base_amount; - var new_base = (base_amount - fixed_amount) / (1.0 + percent_amount / 100.0); - base_gaps.push(base_amount - new_base); - return new_base; - } - - var sign = 1; - if(base < 0){ - base = -base; - sign = -1; - } - - var incl_fixed_amount = 0.0; - var incl_percent_amount = 0.0; - _(taxes.reverse()).each(function(tax){ - if(tax.include_base_amount){ - base = recompute_base(base, incl_fixed_amount, incl_percent_amount); - incl_fixed_amount = 0.0; - incl_percent_amount = 0.0; + var total_excluded = round_pr(price_unit * quantity, currency_rounding); + var total_included = total_excluded; + var base = total_excluded; + _(taxes).each(function(tax) { + if (!no_map_tax){ + tax = self._map_tax_fiscal_position(tax); } - if(tax.price_include){ - if(tax.amount_type === 'fixed') - incl_fixed_amount += quantity * tax.amount; - else if(tax.amount_type === 'percent') - incl_percent_amount += tax.amount; + if (!tax){ + return; } - }); + if (tax.amount_type === 'group'){ + var ret = self.compute_all(tax.children_tax_ids, price_unit, quantity, currency_rounding); + total_excluded = ret.total_excluded; + base = ret.total_excluded; + total_included = ret.total_included; + list_taxes = list_taxes.concat(ret.taxes); + } + else { + var tax_amount = self._compute_all(tax, base, quantity); + tax_amount = round_pr(tax_amount, currency_rounding); - base = recompute_base(base, incl_fixed_amount, incl_percent_amount); - var total_excluded = base; - var total_included = base; - - // 5) Iterate the taxes in the sequence order to fill missing base/amount values. - - var compute_amount = function(tax){ - var amount = self._compute_all(tax, base, quantity, false); - - if(!tax.price_include || base_gaps.length == 0) - return amount; - - var new_gap = base_gaps[base_gaps.length - 1] - amount; - - if(round_pr(new_gap, currency_rounding) === 0.0) - return base_gaps.pop(); - - base_gaps[base_gaps.length - 1] = new_gap; - return amount; - }; - - var taxes_vals = []; - _(taxes.reverse()).each(function(tax){ - var tax_amount = compute_amount(tax); - tax_amount = round_pr(tax_amount, currency_rounding); - - var tax_base = base; - - if(tax.include_base_amount) - base += tax_amount; - - total_included += tax_amount; - - var tax_vals = { - id: tax.id, - amount: sign * tax_amount, - name: tax.name, - }; - - taxes_vals.push(tax_vals); + if (tax_amount){ + if (tax.price_include) { + total_excluded -= tax_amount; + base -= tax_amount; + } + else { + total_included += tax_amount; + } + if (tax.include_base_amount) { + base += tax_amount; + } + var data = { + id: tax.id, + amount: tax_amount, + name: tax.name, + }; + list_taxes.push(data); + } + } }); return { - taxes: taxes_vals, - total_excluded: sign * round_pr(total_excluded, currency_rounding_bak), - total_included: sign * round_pr(total_included, currency_rounding_bak) + taxes: list_taxes, + total_excluded: round_pr(total_excluded, currency_rounding_bak), + total_included: round_pr(total_included, currency_rounding_bak) }; }, get_all_prices: function(){ @@ -1502,7 +1417,6 @@ exports.Orderline = Backbone.Model.extend({ return t.id === el; })); }); - product_taxes = _.map(product_taxes, this._map_tax_fiscal_position.bind(this)); var all_taxes = this.compute_all(product_taxes, price_unit, this.get_quantity(), this.pos.currency.rounding); _(all_taxes.taxes).each(function(tax) {