diff --git a/addons/account/models/account.py b/addons/account/models/account.py index 561743fcd00..b81fd23cdda 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -4,7 +4,7 @@ import time import math from odoo.osv import expression -from odoo.tools.float_utils import float_round as round, float_is_zero as is_zero +from odoo.tools.float_utils import float_round as round from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, pycompat from odoo.exceptions import UserError, ValidationError from odoo import api, fields, models, _ @@ -649,7 +649,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 @@ -663,11 +662,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.multi @@ -701,58 +700,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 @@ -777,135 +731,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 self._context.get('force_price_include', 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) - # N.B: don't use the with_context if force_price_include already False in context - if 'force_price_include' not in self._context or self._context['force_price_include']: - tax = tax.with_context(force_price_include=False) - - # In case of price_included tax, subtract the amount to the corresponding - # gap between the current base and the next one. - amount = tax._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, @@ -913,10 +782,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.model diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 9b7e6c7c903..99b88952086 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -1181,7 +1181,9 @@ class AccountInvoice(models.Model): res.setdefault(line.tax_id.tax_group_id, 0.0) res[line.tax_id.tax_group_id] += line.amount res = sorted(res.items(), key=lambda l: l[0].sequence) - res = map(lambda l: (l[0].name, l[1]), res) + res = [( + r[0].name, r[1], formatLang(self.with_context(lang=self.partner_id.lang).env, r[1], currency_obj=currency) + ) for r in res] return res diff --git a/addons/account/tests/test_tax.py b/addons/account/tests/test_tax.py index 806c0e5cda4..e3d67432339 100644 --- a/addons/account/tests/test_tax.py +++ b/addons/account/tests/test_tax.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- - from odoo.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/account/views/report_invoice.xml b/addons/account/views/report_invoice.xml index dee6d66a055..dccec053980 100644 --- a/addons/account/views/report_invoice.xml +++ b/addons/account/views/report_invoice.xml @@ -112,7 +112,7 @@
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 @@
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. 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.
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
diff --git a/addons/stock/i18n/stock.pot b/addons/stock/i18n/stock.pot
index 0653df5098f..bd191d7f713 100644
--- a/addons/stock/i18n/stock.pot
+++ b/addons/stock/i18n/stock.pot
@@ -2543,6 +2543,11 @@ msgstr ""
msgid "Inventory Line"
msgstr ""
+#. module: stock
+#: model:ir.actions.act_window,name:stock.action_inventory_line_tree
+msgid "Inventory Lines"
+msgstr ""
+
#. module: stock
#: model:ir.model.fields,field_description:stock.field_product_product_property_stock_inventory
#: model:ir.model.fields,field_description:stock.field_product_template_property_stock_inventory
@@ -2823,6 +2828,11 @@ msgstr ""
msgid "Linked Quant"
msgstr ""
+#. module: stock
+#: model:ir.ui.view,arch_db:stock.view_inventory_form
+msgid "List view of lines"
+msgstr ""
+
#. module: stock
#: model:ir.ui.view,arch_db:stock.view_location_form
msgid "Localization"
@@ -4344,6 +4354,11 @@ msgstr ""
msgid "Quantity"
msgstr ""
+#. module: stock
+#: model:ir.model.fields,field_description:stock.field_stock_pack_operation_qty_done_uom_ordered
+msgid "Quantity Done"
+msgstr ""
+
#. module: stock
#: model:ir.ui.view,arch_db:stock.view_warehouse_orderpoint_form
msgid "Quantity Multiple"
@@ -4366,6 +4381,17 @@ msgstr ""
msgid "Quantity cannot be negative."
msgstr ""
+#. module: stock
+#: code:addons/stock/models/stock_move.py:394
+#, python-format
+msgid "Quantity decreased!"
+msgstr ""
+
+#. module: stock
+#: model:ir.model.fields,help:stock.field_stock_pack_operation_qty_done_uom_ordered
+msgid "Quantity done in UOM ordered"
+msgstr ""
+
#. module: stock
#: model:ir.model.fields,help:stock.field_stock_move_availability
msgid "Quantity in stock that can still be reserved for this move"
@@ -4883,6 +4909,11 @@ msgstr ""
msgid "Search Inventory"
msgstr ""
+#. module: stock
+#: model:ir.ui.view,arch_db:stock.stock_inventory_line_search
+msgid "Search Inventory Lines"
+msgstr ""
+
#. module: stock
#: model:ir.ui.view,arch_db:stock.stock_scrap_search_view
msgid "Search Scrap"
@@ -5510,6 +5541,12 @@ msgstr ""
msgid "The parent location that includes this location. Example : The 'Dispatch Zone' is the 'Gate 1' parent location."
msgstr ""
+#. module: stock
+#: code:addons/stock/models/stock_picking.py:795
+#, python-format
+msgid "The pick is already validated"
+msgstr ""
+
#. module: stock
#: code:addons/stock/models/stock_picking.py:787
#, python-format
diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py
index 0df4f407589..7d06ff73ac8 100644
--- a/addons/stock/models/product.py
+++ b/addons/stock/models/product.py
@@ -508,6 +508,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
diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py
index 087c111499a..c00834613a0 100644
--- a/addons/stock/models/stock_move.py
+++ b/addons/stock/models/stock_move.py
@@ -387,9 +387,13 @@ class StockMove(models.Model):
if not self.product_id or self.product_qty < 0.0:
self.product_qty = 0.0
if self.product_qty < self._origin.product_qty:
- return {'warning': _("By changing this quantity here, you accept the "
- "new quantity as complete: Odoo will not "
- "automatically generate a back order.")}
+ warning_mess = {
+ 'title': _('Quantity decreased!'),
+ 'message' : _("By changing this quantity here, you accept the "
+ "new quantity as complete: Odoo will not "
+ "automatically generate a back order."),
+ }
+ return {'warning': warning_mess}
@api.onchange('product_id')
def onchange_product_id(self):
diff --git a/addons/stock/views/stock_production_lot_views.xml b/addons/stock/views/stock_production_lot_views.xml
index cfa5d3929ce..694eebb6258 100644
--- a/addons/stock/views/stock_production_lot_views.xml
+++ b/addons/stock/views/stock_production_lot_views.xml
@@ -28,7 +28,7 @@
This promo code is not available1. Learn about Tasks, Issues and Timesheets.