diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index 451a02e1bd..fd8018a077 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -1107,7 +1107,12 @@ class AccountMove(models.Model):
else:
tax_type = line.tax_ids[0].type_tax_use
is_refund = (tax_type == 'sale' and line.debit) or (tax_type == 'purchase' and line.credit)
- taxes = line.tax_ids.flatten_taxes_hierarchy()
+ taxes = line.tax_ids._origin.flatten_taxes_hierarchy().filtered(
+ lambda tax: (
+ tax.amount_type == 'fixed' and not invoice.company_id.currency_id.is_zero(tax.amount)
+ or not float_is_zero(tax.amount, precision_digits=4)
+ )
+ )
if is_refund:
tax_rep_lines = taxes.refund_repartition_line_ids._origin.filtered(lambda x: x.repartition_type == "tax")
else:
diff --git a/addons/account/tests/test_invoice_taxes.py b/addons/account/tests/test_invoice_taxes.py
index eeac01b81b..817cbf1b0e 100644
--- a/addons/account/tests/test_invoice_taxes.py
+++ b/addons/account/tests/test_invoice_taxes.py
@@ -711,3 +711,113 @@ class TestInvoiceTaxes(AccountTestInvoicingCommon):
self.assertRecordValues(invoice.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable'), [{
'balance': 686.54
}])
+
+ def test_change_tax_line_account_when_tax_zero_percent(self):
+ """
+ This test checks the following flow:
+ - Invoice with three invoice lines:
+ • One with tax > 0%
+ • One with tax == 0%
+ • One with tax == 0 fixed
+ - On line_ids, change the account of the tax line
+ The tax line should still be there and the account should be effectively changed
+ """
+ tax_0_percent, tax_0_fixed = self.env['account.tax'].create([
+ {
+ 'name': '0%',
+ 'amount_type': 'percent',
+ 'amount': 0,
+ 'sequence': 10,
+ },
+ {
+ 'name': '0 fixed',
+ 'amount_type': 'fixed',
+ 'amount': 0,
+ 'sequence': 10,
+ }
+ ])
+
+
+ new_account_revenue = self.company_data['default_account_revenue'].copy()
+
+ invoice = self._create_invoice([(500, self.percent_tax_1), (300, tax_0_percent), (100, tax_0_fixed)], inv_type='out_invoice')
+
+ self.assertRecordValues(invoice.line_ids, [
+ {
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'name': 'xxxx',
+ 'tax_ids': self.percent_tax_1.ids,
+ 'debit': 0.0,
+ 'credit': 500.0,
+ },
+ {
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'name': 'xxxx',
+ 'tax_ids': tax_0_percent.ids,
+ 'debit': 0.0,
+ 'credit': 300.0,
+ },
+ {
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'name': 'xxxx',
+ 'tax_ids': tax_0_fixed.ids,
+ 'debit': 0.0,
+ 'credit': 100.0,
+ },
+ {
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'name': '21%',
+ 'tax_ids': [],
+ 'debit': 0.0,
+ 'credit': 105.0,
+ },
+ {
+ 'account_id': self.company_data['default_account_receivable'].id,
+ 'name': '',
+ 'tax_ids': [],
+ 'debit': 1005.0,
+ 'credit': 0.0,
+ }
+ ])
+
+ with Form(invoice) as invoice_form:
+ with invoice_form.line_ids.edit(3) as line:
+ line.account_id = new_account_revenue
+
+ self.assertRecordValues(invoice.line_ids, [
+ {
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'name': 'xxxx',
+ 'tax_ids': self.percent_tax_1.ids,
+ 'debit': 0.0,
+ 'credit': 500.0,
+ },
+ {
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'name': 'xxxx',
+ 'tax_ids': tax_0_percent.ids,
+ 'debit': 0.0,
+ 'credit': 300.0,
+ },
+ {
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'name': 'xxxx',
+ 'tax_ids': tax_0_fixed.ids,
+ 'debit': 0.0,
+ 'credit': 100.0,
+ },
+ {
+ 'account_id': new_account_revenue.id,
+ 'name': '21%',
+ 'tax_ids': [],
+ 'debit': 0.0,
+ 'credit': 105.0,
+ },
+ {
+ 'account_id': self.company_data['default_account_receivable'].id,
+ 'name': '',
+ 'tax_ids': [],
+ 'debit': 1005.0,
+ 'credit': 0.0,
+ }
+ ])
diff --git a/addons/account_edi_facturx/data/facturx_templates.xml b/addons/account_edi_facturx/data/facturx_templates.xml
index ba0ba3ab12..3b994496ad 100644
--- a/addons/account_edi_facturx/data/facturx_templates.xml
+++ b/addons/account_edi_facturx/data/facturx_templates.xml
@@ -87,6 +87,16 @@
+
+
+
+
+
+
+
+
+
+
@@ -111,7 +121,7 @@
-->
- urn:cen.eu:en16931:2017
+ urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended
diff --git a/addons/account_edi_facturx/tests/test_facturx.py b/addons/account_edi_facturx/tests/test_facturx.py
index bfc0b80e1e..b41b5bc510 100644
--- a/addons/account_edi_facturx/tests/test_facturx.py
+++ b/addons/account_edi_facturx/tests/test_facturx.py
@@ -68,7 +68,7 @@ class TestAccountEdiFacturx(AccountEdiTestCommon):
- urn:cen.eu:en16931:2017
+ urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended
@@ -117,10 +117,16 @@ class TestAccountEdiFacturx(AccountEdiTestCommon):
company_1_data
+
+ company_1_data
+
partner_b
+
+ partner_b
+
@@ -130,6 +136,9 @@ class TestAccountEdiFacturx(AccountEdiTestCommon):
partner_b
+
+ partner_b
+
diff --git a/addons/hr_timesheet/models/hr_timesheet.py b/addons/hr_timesheet/models/hr_timesheet.py
index 2f6d2be20f..e8e4a98119 100644
--- a/addons/hr_timesheet/models/hr_timesheet.py
+++ b/addons/hr_timesheet/models/hr_timesheet.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
+from collections import defaultdict
from lxml import etree
import re
@@ -17,7 +18,7 @@ class AccountAnalyticLine(models.Model):
if 'encoding_uom_id' in field_list:
result['encoding_uom_id'] = self.env.company.timesheet_encode_uom_id.id
if not self.env.context.get('default_employee_id') and 'employee_id' in field_list and result.get('user_id'):
- result['employee_id'] = self.env['hr.employee'].search([('user_id', '=', result['user_id'])], limit=1).id
+ result['employee_id'] = self.env['hr.employee'].search([('user_id', '=', result['user_id']), ('company_id', '=', result.get('company_id', self.env.company.id))], limit=1).id
return result
def _domain_project_id(self):
@@ -45,7 +46,7 @@ class AccountAnalyticLine(models.Model):
'project.project', 'Project', compute='_compute_project_id', store=True, readonly=False,
domain=_domain_project_id)
user_id = fields.Many2one(compute='_compute_user_id', store=True, readonly=False)
- employee_id = fields.Many2one('hr.employee', "Employee", domain=_domain_employee_id)
+ employee_id = fields.Many2one('hr.employee', "Employee", domain=_domain_employee_id, context={'active_test': False})
department_id = fields.Many2one('hr.department', "Department", compute='_compute_department_id', store=True, compute_sudo=True)
encoding_uom_id = fields.Many2one('uom.uom', compute='_compute_encoding_uom_id')
partner_id = fields.Many2one(compute='_compute_partner_id', store=True, readonly=False)
@@ -92,18 +93,33 @@ class AccountAnalyticLine(models.Model):
def create(self, vals_list):
default_user_id = self._default_user()
user_ids = list(map(lambda x: x.get('user_id', default_user_id), filter(lambda x: not x.get('employee_id') and x.get('project_id'), vals_list)))
- employees = self.env['hr.employee'].search([('user_id', 'in', user_ids)])
- user_map = {employee.user_id.id: employee.id for employee in employees}
for vals in vals_list:
# when the name is not provide by the 'Add a line', we set a default one
if vals.get('project_id') and not vals.get('name'):
vals['name'] = '/'
- # compute employee only for timesheet lines, makes no sense for other lines
- if not vals.get('employee_id') and vals.get('project_id'):
- vals['employee_id'] = user_map.get(vals.get('user_id') or default_user_id)
vals.update(self._timesheet_preprocess(vals))
+ # Although this make a second loop on the vals, we need to wait the preprocess as it could change the company_id in the vals
+ # TODO To be refactored in master
+ employees = self.env['hr.employee'].sudo().search([('user_id', 'in', user_ids)])
+ employee_for_user_company = defaultdict(dict)
+ for employee in employees:
+ employee_for_user_company[employee.user_id.id][employee.company_id.id] = employee.id
+
+ employee_ids = set()
+ for vals in vals_list:
+ # compute employee only for timesheet lines, makes no sense for other lines
+ if not vals.get('employee_id') and vals.get('project_id'):
+ employee_for_company = employee_for_user_company.get(vals.get('user_id', default_user_id), False)
+ if not employee_for_company:
+ continue
+ company_id = list(employee_for_company)[0] if len(employee_for_company) == 1 else vals.get('company_id', self.env.company.id)
+ vals['employee_id'] = employee_for_company.get(company_id, False)
+ elif vals.get('employee_id'):
+ employee_ids.add(vals['employee_id'])
+ if any(not emp.active for emp in self.env['hr.employee'].browse(list(employee_ids))):
+ raise UserError(_('Timesheets must be created with an active employee.'))
lines = super(AccountAnalyticLine, self).create(vals_list)
for line, values in zip(lines, vals_list):
if line.project_id: # applied only for timesheet
@@ -116,6 +132,10 @@ class AccountAnalyticLine(models.Model):
raise AccessError(_("You cannot access timesheets that are not yours."))
values = self._timesheet_preprocess(values)
+ if values.get('employee_id'):
+ employee = self.env['hr.employee'].browse(values['employee_id'])
+ if not employee.active:
+ raise UserError(_('You cannot set an archived employee to the existing timesheets.'))
if 'name' in values and not values.get('name'):
values['name'] = '/'
result = super(AccountAnalyticLine, self).write(values)
diff --git a/addons/hr_timesheet/models/project.py b/addons/hr_timesheet/models/project.py
index 77789e2a39..2a105ae194 100644
--- a/addons/hr_timesheet/models/project.py
+++ b/addons/hr_timesheet/models/project.py
@@ -128,8 +128,14 @@ class Task(models.Model):
@api.depends('timesheet_ids.unit_amount')
def _compute_effective_hours(self):
+ if not any(self._ids):
+ for task in self:
+ task.effective_hours = round(sum(task.timesheet_ids.mapped('unit_amount')), 2)
+ return
+ timesheet_read_group = self.env['account.analytic.line'].read_group([('task_id', 'in', self.ids)], ['unit_amount', 'task_id'], ['task_id'])
+ timesheets_per_task = {res['task_id'][0]: res['unit_amount'] for res in timesheet_read_group}
for task in self:
- task.effective_hours = round(sum(task.timesheet_ids.mapped('unit_amount')), 2)
+ task.effective_hours = round(timesheets_per_task.get(task.id, 0.0), 2)
@api.depends('effective_hours', 'subtask_effective_hours', 'planned_hours')
def _compute_progress_hours(self):
diff --git a/addons/hr_timesheet/tests/test_timesheet.py b/addons/hr_timesheet/tests/test_timesheet.py
index 1ce30afee8..319c6a0e04 100644
--- a/addons/hr_timesheet/tests/test_timesheet.py
+++ b/addons/hr_timesheet/tests/test_timesheet.py
@@ -393,3 +393,68 @@ class TestTimesheet(TestCommonTimesheet):
timesheet1.unit_amount + timesheet2.unit_amount,
'The total timesheet time of this project should be equal to 4.'
)
+
+ def test_create_timesheet_employee_not_in_company(self):
+ ''' ts.employee_id only if the user has an employee in the company or one employee for all companies.
+ '''
+ company_2 = self.env['res.company'].create({'name': 'Company 2'})
+ company_3 = self.env['res.company'].create({'name': 'Company 3'})
+
+ analytic_account = self.env['account.analytic.account'].create({
+ 'name': 'Aa Aa',
+ 'company_id': company_3.id,
+ })
+ project = self.env['project.project'].create({
+ 'name': 'Aa Project',
+ 'company_id': company_3.id,
+ 'analytic_account_id': analytic_account.id,
+ })
+ task = self.env['project.task'].create({
+ 'name': 'Aa Task',
+ 'project_id': project.id,
+ })
+
+ Timesheet = self.env['account.analytic.line'].with_context(allowed_company_ids=[company_3.id, company_2.id, self.env.company.id])
+ timesheet = Timesheet.create({
+ 'name': 'Timesheet',
+ 'project_id': project.id,
+ 'task_id': task.id,
+ 'unit_amount': 2,
+ 'user_id': self.user_manager.id,
+ 'company_id': company_3.id,
+ })
+ self.assertEqual(timesheet.employee_id, self.user_manager.employee_id, 'As there is a unique employee for this user, it must be found')
+
+ self.env['hr.employee'].with_company(company_2).create({
+ 'name': 'Employee 2',
+ 'user_id': self.user_manager.id,
+ })
+ timesheet = Timesheet.create({
+ 'name': 'Timesheet',
+ 'project_id': project.id,
+ 'task_id': task.id,
+ 'unit_amount': 2,
+ 'user_id': self.user_manager.id,
+ 'company_id': company_3.id,
+ })
+ self.assertFalse(timesheet.employee_id, 'As there are several employees for this user, but none of them in this company, none must be found')
+
+ def test_create_timesheet_with_archived_employee(self):
+ ''' the timesheet can be created or edited only with an active employee
+ '''
+ self.empl_employee2.active = False
+ batch_vals = {
+ 'project_id': self.project_customer.id,
+ 'task_id': self.task1.id,
+ 'name': 'archived employee timesheet',
+ 'unit_amount': 3,
+ 'employee_id': self.empl_employee2.id
+ }
+
+ self.assertRaises(UserError, self.env['account.analytic.line'].create, batch_vals)
+
+ batch_vals["employee_id"] = self.empl_employee.id
+ timesheet = self.env['account.analytic.line'].create(batch_vals)
+
+ with self.assertRaises(UserError):
+ timesheet.employee_id = self.empl_employee2
diff --git a/addons/hr_timesheet/views/hr_timesheet_views.xml b/addons/hr_timesheet/views/hr_timesheet_views.xml
index c12d1039d7..d2f2192274 100644
--- a/addons/hr_timesheet/views/hr_timesheet_views.xml
+++ b/addons/hr_timesheet/views/hr_timesheet_views.xml
@@ -45,6 +45,7 @@
0
1
many2one_avatar_employee
+ {'active_test': True}
@@ -110,7 +111,7 @@
10
-
+
diff --git a/addons/hr_timesheet/views/project_views.xml b/addons/hr_timesheet/views/project_views.xml
index 8cec4d7613..d2ec42b7b0 100644
--- a/addons/hr_timesheet/views/project_views.xml
+++ b/addons/hr_timesheet/views/project_views.xml
@@ -107,7 +107,7 @@
-
+
@@ -117,7 +117,7 @@
-
+
@@ -152,7 +152,7 @@
-
+
diff --git a/addons/iap/models/iap_account.py b/addons/iap/models/iap_account.py
index 8656b0ac97..95a5fce408 100644
--- a/addons/iap/models/iap_account.py
+++ b/addons/iap/models/iap_account.py
@@ -31,6 +31,17 @@ class IapAccount(models.Model):
('company_ids', '=', False)
]
accounts = self.search(domain, order='id desc')
+ accounts_without_token = accounts.filtered(lambda acc: not acc.account_token)
+ if accounts_without_token:
+ with self.pool.cursor() as cr:
+ # In case of a further error that will rollback the database, we should
+ # use a different SQL cursor to avoid undo the accounts deletion.
+
+ # Flush the pending operations to avoid a deadlock.
+ self.flush()
+ IapAccount = self.with_env(self.env(cr=cr))
+ IapAccount.search(domain + [('account_token', '=', False)]).unlink()
+ accounts = accounts - accounts_without_token
if not accounts:
with self.pool.cursor() as cr:
# Since the account did not exist yet, we will encounter a NoCreditError,
diff --git a/addons/l10n_ar_website_sale/static/tests/tours/website_sale_buy.js b/addons/l10n_ar_website_sale/static/tests/tours/website_sale_buy.js
new file mode 100644
index 0000000000..bd212326ff
--- /dev/null
+++ b/addons/l10n_ar_website_sale/static/tests/tours/website_sale_buy.js
@@ -0,0 +1,47 @@
+flectra.define('l10n_ar_website_sale.shop_buy_product', function (require) {
+"use strict";
+
+ let tour = require('web_tour.tour');
+
+ var Session = require('web.session');
+
+ let session = Session;
+
+ console.log(session);
+ console.log(session.user_context);
+ console.log(session.user_context.allowed_company_ids);
+
+ let shop_buy_product = tour.tours.shop_buy_product;
+ // Remove the step related to other localisations
+ shop_buy_product.steps = _.filter(shop_buy_product.steps, step => !step.l10n || step.l10n === "ar");
+
+ let checkout_step_idx = _.findIndex(shop_buy_product.steps, step => step.trigger === 'a[href*="/shop/checkout"]');
+
+ shop_buy_product.steps.splice(checkout_step_idx + 1, 0, {
+ trigger: "#wizard-step20.active, #wizard-step40.active",
+ auto: true,
+ l10n: 'ar',
+ run: function (actions) {
+ let $wizard_step = $("div[id=wizard-step20]");
+ if ($wizard_step.length && $wizard_step.hasClass("active")) {
+ let $el = $('select[name="l10n_ar_afip_responsibility_type_id"]');
+ if ($el.length) {
+ $("select[name='l10n_ar_afip_responsibility_type_id'] option[value='5']").prop('selected', true);
+ $("select[name='l10n_latam_identification_type_id'] option[value='1']").prop('selected', true);
+ $("input[name='vat']").val('123456789');
+ $("a[role='button'] span:contains('Next')").click();
+ }
+ }
+ },
+ }, {
+ trigger: "span:contains('Confirm'), #wizard-step40.active",
+ auto: true,
+ l10n: 'ar',
+ run: function (actions) {
+ let $wizard_step = $("div[id=wizard-step20]");
+ if ($wizard_step.length && $wizard_step.hasClass("active")) {
+ $("a[role='button'] span:contains('Confirm')").click();
+ }
+ },
+ });
+});
diff --git a/addons/l10n_ar_website_sale/static/tests/tours/website_sale_complete_flow.js b/addons/l10n_ar_website_sale/static/tests/tours/website_sale_complete_flow.js
new file mode 100644
index 0000000000..5e241ceff1
--- /dev/null
+++ b/addons/l10n_ar_website_sale/static/tests/tours/website_sale_complete_flow.js
@@ -0,0 +1,19 @@
+flectra.define('l10n_ar_website_sale.website_sale_tour', function (require) {
+"use strict";
+
+ let tour = require('web_tour.tour');
+
+ let website_sale_tour = tour.tours.website_sale_tour;
+ // Remove the step related to other localisations
+ website_sale_tour.steps = _.filter(website_sale_tour.steps, step => !step.l10n || step.l10n === "ar");
+
+ let address_step = _.findIndex(website_sale_tour.steps, step => step.content === 'Fulfill billing address form');
+ let step_run = website_sale_tour.steps[address_step].run;
+
+ website_sale_tour.steps[address_step].run = function () {
+ step_run();
+ $("select[name='l10n_ar_afip_responsibility_type_id'] option[value='5']").prop('selected', true);
+ $("select[name='l10n_latam_identification_type_id'] option[value='1']").prop('selected', true);
+ $("input[name='vat']").val('123456789');
+ };
+});
diff --git a/addons/l10n_in_pos/models/pos_order.py b/addons/l10n_in_pos/models/pos_order.py
index 2a2b453dbf..cee5958028 100644
--- a/addons/l10n_in_pos/models/pos_order.py
+++ b/addons/l10n_in_pos/models/pos_order.py
@@ -6,7 +6,7 @@ from flectra import api, fields, models
class PosOrder(models.Model):
_inherit = 'pos.order'
-
+ #TODO: remove in master
@api.model
def _get_account_move_line_group_data_type_key(self, data_type, values, options={}):
res = super(PosOrder, self)._get_account_move_line_group_data_type_key(data_type, values, options)
@@ -14,7 +14,7 @@ class PosOrder(models.Model):
if self.env['account.tax'].browse(values['tax_line_id']).company_id.country_id.code == 'IN':
return res + (values['product_uom_id'], values['product_id'])
return res
-
+ #TODO: remove in master
def _prepare_account_move_line(self, line, partner_id, current_company, currency_id, rounding_method):
res = super(PosOrder, self)._prepare_account_move_line(line, partner_id, current_company, currency_id, rounding_method)
for line_values in res:
@@ -24,3 +24,15 @@ class PosOrder(models.Model):
'product_uom_id': line.product_id.uom_id.id
})
return res
+
+ def _prepare_invoice_vals(self):
+ vals = super()._prepare_invoice_vals()
+ if self.session_id.company_id.country_id.code == 'IN':
+ partner = self.partner_id
+ l10n_in_gst_treatment = partner.l10n_in_gst_treatment
+ if not l10n_in_gst_treatment and partner.country_id and partner.country_id.code != 'IN':
+ l10n_in_gst_treatment = 'overseas'
+ if not l10n_in_gst_treatment:
+ l10n_in_gst_treatment = partner.vat and 'regular' or 'consumer'
+ vals['l10n_in_gst_treatment'] = l10n_in_gst_treatment
+ return vals
diff --git a/addons/mail/static/src/models/partner/partner.js b/addons/mail/static/src/models/partner/partner.js
index 061747c555..f3b284a58f 100644
--- a/addons/mail/static/src/models/partner/partner.js
+++ b/addons/mail/static/src/models/partner/partner.js
@@ -189,8 +189,11 @@ function factory(dependencies) {
// partners (livechat guests), public partners (technical)
continue;
}
+ if (!partner.name) {
+ continue;
+ }
if (
- (partner.nameOrDisplayName && cleanSearchTerm(partner.nameOrDisplayName).includes(cleanedSearchTerm)) ||
+ (cleanSearchTerm(partner.name).includes(cleanedSearchTerm)) ||
(partner.email && cleanSearchTerm(partner.email).includes(cleanedSearchTerm))
) {
if (partner.user) {
@@ -303,8 +306,8 @@ function factory(dependencies) {
return 1;
}
}
- const cleanedAName = cleanSearchTerm(a.nameOrDisplayName || '');
- const cleanedBName = cleanSearchTerm(b.nameOrDisplayName || '');
+ const cleanedAName = cleanSearchTerm(a.name || '');
+ const cleanedBName = cleanSearchTerm(b.name || '');
if (cleanedAName.startsWith(cleanedSearchTerm) && !cleanedBName.startsWith(cleanedSearchTerm)) {
return -1;
}
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index e8a9dc971b..b02cb9bd7d 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -1524,7 +1524,7 @@ exports.PosModel = Backbone.Model.extend({
return taxes;
},
- get_taxes_after_fp: function(taxes_ids){
+ get_taxes_after_fp: function(taxes_ids, order = false){
var self = this;
var taxes = this.taxes;
var product_taxes = [];
@@ -1532,7 +1532,7 @@ exports.PosModel = Backbone.Model.extend({
var tax = _.detect(taxes, function(t){
return t.id === el;
});
- product_taxes.push.apply(product_taxes, self._map_tax_fiscal_position(tax));
+ product_taxes.push.apply(product_taxes, self._map_tax_fiscal_position(tax, order));
});
product_taxes = _.uniq(product_taxes, function(tax) { return tax.id; });
return product_taxes;
@@ -2245,7 +2245,7 @@ exports.Orderline = Backbone.Model.extend({
return round_pr(this.get_unit_price() * this.get_quantity() * (1 - this.get_discount()/100), rounding);
},
get_taxes_after_fp: function(taxes_ids){
- return this.pos.get_taxes_after_fp(taxes_ids);
+ return this.pos.get_taxes_after_fp(taxes_ids, this.order);
},
get_display_price_one: function(){
var rounding = this.pos.currency.rounding;
diff --git a/addons/pos_sale/report/sale_report.py b/addons/pos_sale/report/sale_report.py
index 0d3b4a771f..c2fd37c476 100644
--- a/addons/pos_sale/report/sale_report.py
+++ b/addons/pos_sale/report/sale_report.py
@@ -53,8 +53,8 @@ class SaleReport(models.Model):
partner.country_id AS country_id,
partner.industry_id AS industry_id,
partner.commercial_partner_id AS commercial_partner_id,
- (sum(t.weight) * l.qty / u.factor) AS weight,
- (sum(t.volume) * l.qty / u.factor) AS volume,
+ (sum(p.weight) * l.qty / u.factor) AS weight,
+ (sum(p.volume) * l.qty / u.factor) AS volume,
l.discount as discount,
sum((l.price_unit * l.discount * l.qty / 100.0 / CASE COALESCE(pos.currency_rate, 0) WHEN 0 THEN 1.0 ELSE pos.currency_rate END)) as discount_amount,
NULL as order_id
diff --git a/addons/pos_sale/tests/test_pos_sale_report.py b/addons/pos_sale/tests/test_pos_sale_report.py
index 751fc03b65..0fac7bc80d 100644
--- a/addons/pos_sale/tests/test_pos_sale_report.py
+++ b/addons/pos_sale/tests/test_pos_sale_report.py
@@ -37,3 +37,46 @@ class TestPoSSaleReport(TestPoSCommon):
self.assertEqual(reports[0].volume, 4)
self.assertEqual(reports[1].weight, 18)
self.assertEqual(reports[1].volume, 24)
+
+ def test_weight_and_volume_product_variant(self):
+ colors = ['red', 'blue']
+ prod_attr = self.env['product.attribute'].create({'name': 'Color', 'create_variant': 'dynamic'})
+ prod_attr_values = self.env['product.attribute.value'].create([{'name': color, 'attribute_id': prod_attr.id, 'sequence': 1} for color in colors])
+
+ uom_unit = self.env.ref('uom.product_uom_unit')
+ product_template = self.env['product.template'].create({
+ 'name': 'Sofa',
+ 'uom_id': uom_unit.id,
+ 'uom_po_id': uom_unit.id,
+ 'attribute_line_ids': [(0, 0, {
+ 'attribute_id': prod_attr.id,
+ 'value_ids': [(6, 0, prod_attr_values.ids)]
+ })]
+ })
+ prod_tmpl_attrs = self.env['product.template.attribute.value'].search([
+ ('attribute_line_id', '=', product_template.attribute_line_ids.id),
+ ('product_attribute_value_id', 'in', prod_attr_values.ids)
+ ])
+
+ product_1 = product_template._create_product_variant(prod_tmpl_attrs[0])
+ product_1.weight = 1
+ product_1.volume = 1
+
+ product_2 = product_template._create_product_variant(prod_tmpl_attrs[1])
+ product_2.weight = 2
+ product_2.volume = 2
+
+ self.open_new_session()
+ session = self.pos_session
+
+ order = self.create_ui_order_data([(product_1, 3), (product_2, 3)])
+ self.env['pos.order'].create_from_ui([order])
+
+ session.action_pos_session_closing_control()
+
+ report = self.env['sale.report'].sudo().search([('product_id', '=', product_1.id)], order='id', limit=1)
+ self.assertEqual(report.weight, 3)
+ self.assertEqual(report.weight, 3)
+ report = self.env['sale.report'].sudo().search([('product_id', '=', product_2.id)], order='id', limit=1)
+ self.assertEqual(report.weight, 6)
+ self.assertEqual(report.weight, 6)
diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py
index 1d980f76d9..400164a0b1 100644
--- a/addons/purchase/models/purchase.py
+++ b/addons/purchase/models/purchase.py
@@ -1082,7 +1082,7 @@ class PurchaseOrderLine(models.Model):
@api.onchange('product_qty', 'product_uom')
def _onchange_quantity(self):
- if not self.product_id or self.state in ('purchase', 'done'):
+ if not self.product_id or self.invoice_lines:
return
params = {'order_id': self.order_id}
seller = self.product_id._select_seller(
diff --git a/addons/purchase/tests/test_purchase_invoice.py b/addons/purchase/tests/test_purchase_invoice.py
index 581b5c785c..7613a37b83 100644
--- a/addons/purchase/tests/test_purchase_invoice.py
+++ b/addons/purchase/tests/test_purchase_invoice.py
@@ -467,3 +467,61 @@ class TestPurchaseToInvoice(AccountTestInvoicingCommon):
]
for line in invoice.invoice_line_ids.sorted('sequence'):
self.assertEqual(line.purchase_order_id, expected_purchase.pop(0))
+
+ def test_on_change_quantity_price_unit(self):
+ """ When a user changes the quantity of a product in a purchase order it
+ should only update the unit price if PO line has no invoice line. """
+
+ supplierinfo_vals = {
+ 'name': self.partner_a.id,
+ 'price': 10.0,
+ 'min_qty': 1,
+ "product_id": self.product_order.id,
+ "product_tmpl_id": self.product_order.product_tmpl_id.id,
+ }
+
+ supplierinfo = self.env["product.supplierinfo"].create(supplierinfo_vals)
+ po_form = Form(self.env['purchase.order'])
+ po_form.partner_id = self.partner_a
+ with po_form.order_line.new() as po_line_form:
+ po_line_form.product_id = self.product_order
+ po_line_form.product_qty = 1
+ po = po_form.save()
+ po_line = po.order_line[0]
+
+ self.assertEqual(10.0, po_line.price_unit, "Unit price should be set to 10.0 for 1 quantity")
+
+ # Ensure price unit is updated when changing quantity on a un-confirmed PO
+ supplierinfo.write({'min_qty': 2, 'price': 20.0})
+ po_line.write({'product_qty': 2})
+ po_line._onchange_quantity()
+ self.assertEqual(20.0, po_line.price_unit, "Unit price should be set to 20.0 for 2 quantity")
+
+ po.button_confirm()
+
+ # Ensure price unit is updated when changing quantity on a confirmed PO
+ supplierinfo.write({'min_qty': 3, 'price': 30.0})
+ po_line.write({'product_qty': 3})
+ po_line._onchange_quantity()
+ self.assertEqual(30.0, po_line.price_unit, "Unit price should be set to 30.0 for 3 quantity")
+
+ po.action_create_invoice()
+
+ # Ensure price unit is NOT updated when changing quantity on PO confirmed and line linked to an invoice line
+ supplierinfo.write({'min_qty': 4, 'price': 40.0})
+ po_line.write({'product_qty': 4})
+ po_line._onchange_quantity()
+ self.assertEqual(30.0, po_line.price_unit, "Unit price should be set to 30.0 for 3 quantity")
+
+ with po_form.order_line.new() as po_line_form:
+ po_line_form.product_id = self.product_order
+ po_line_form.product_qty = 1
+ po = po_form.save()
+ po_line = po.order_line[1]
+
+ self.assertEqual(235.0, po_line.price_unit, "Unit price should be reset to 235.0 since the supplier supplies minimum of 4 quantities")
+
+ # Ensure price unit is updated when changing quantity on PO confirmed and line NOT linked to an invoice line
+ po_line.write({'product_qty': 4})
+ po_line._onchange_quantity()
+ self.assertEqual(40.0, po_line.price_unit, "Unit price should be set to 40.0 for 4 quantity")
diff --git a/addons/sale_stock/models/account_move.py b/addons/sale_stock/models/account_move.py
index a7792cf96b..42225f8adc 100644
--- a/addons/sale_stock/models/account_move.py
+++ b/addons/sale_stock/models/account_move.py
@@ -68,18 +68,19 @@ class AccountMove(models.Model):
lot_values = []
for lot, qty in qties_per_lot.items():
- if float_is_zero(invoiced_qties[lot.product_id], precision_rounding=lot.product_uom_id.rounding) \
- or float_compare(qty, 0, precision_rounding=lot.product_uom_id.rounding) <= 0:
+ lot_sudo = lot.sudo()
+ if float_is_zero(invoiced_qties[lot_sudo.product_id], precision_rounding=lot_sudo.product_uom_id.rounding) \
+ or float_compare(qty, 0, precision_rounding=lot_sudo.product_uom_id.rounding) <= 0:
continue
- invoiced_lot_qty = min(qty, invoiced_qties[lot.product_id])
- invoiced_qties[lot.product_id] -= invoiced_lot_qty
+ invoiced_lot_qty = min(qty, invoiced_qties[lot_sudo.product_id])
+ invoiced_qties[lot_sudo.product_id] -= invoiced_lot_qty
lot_values.append({
- 'product_name': lot.product_id.display_name,
+ 'product_name': lot_sudo.product_id.display_name,
'quantity': formatLang(self.env, invoiced_lot_qty, dp='Product Unit of Measure'),
- 'uom_name': lot.product_uom_id.name,
- 'lot_name': lot.name,
+ 'uom_name': lot_sudo.product_uom_id.name,
+ 'lot_name': lot_sudo.name,
# The lot id is needed by localizations to inherit the method and add custom fields on the invoice's report.
- 'lot_id': lot.id,
+ 'lot_id': lot_sudo.id,
})
return lot_values
diff --git a/addons/sale_stock_margin/models/sale_order_line.py b/addons/sale_stock_margin/models/sale_order_line.py
index 78166f0a11..987679f790 100644
--- a/addons/sale_stock_margin/models/sale_order_line.py
+++ b/addons/sale_stock_margin/models/sale_order_line.py
@@ -7,7 +7,7 @@ from flectra import api, fields, models
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
- @api.depends('move_ids', 'move_ids.stock_valuation_layer_ids', 'order_id.picking_ids.state')
+ @api.depends('move_ids', 'move_ids.stock_valuation_layer_ids', 'move_ids.picking_id.state')
def _compute_purchase_price(self):
lines_without_moves = self.browse()
for line in self:
diff --git a/addons/sale_stock_margin/tests/test_sale_stock_margin.py b/addons/sale_stock_margin/tests/test_sale_stock_margin.py
index a1e5a7787d..e8f028cc85 100644
--- a/addons/sale_stock_margin/tests/test_sale_stock_margin.py
+++ b/addons/sale_stock_margin/tests/test_sale_stock_margin.py
@@ -166,6 +166,36 @@ class TestSaleStockMargin(TestStockValuationCommon):
self.assertAlmostEqual(order_line_2.margin, 30) # (20 - 12.5) * 4
self.assertAlmostEqual(sale_order.margin, 64)
+ def test_sale_stock_margin_6(self):
+ """ Test that the purchase price doesn't change when there is a service product in the SO"""
+ service = self.env['product.product'].create({
+ 'name': 'Service',
+ 'type': 'service',
+ 'list_price': 100.0,
+ 'standard_price': 50.0})
+ self.product1.list_price = 80.0
+ self.product1.standard_price = 40.0
+ sale_order = self._create_sale_order()
+ order_line_1 = self._create_sale_order_line(sale_order, service, 1, 100)
+ order_line_2 = self._create_sale_order_line(sale_order, self.product1, 1, 80)
+
+ self.assertEqual(order_line_1.purchase_price, 50, "Sales order line cost should be 50.00")
+ self.assertEqual(order_line_2.purchase_price, 40, "Sales order line cost should be 40.00")
+
+ self.assertEqual(order_line_1.margin, 50, "Sales order line profit should be 50.00")
+ self.assertEqual(order_line_2.margin, 40, "Sales order line profit should be 40.00")
+ self.assertEqual(sale_order.margin, 90, "Sales order profit should be 90.00")
+
+ # Change the purchase price of the service product.
+ order_line_1.purchase_price = 100.0
+ self.assertEqual(order_line_1.purchase_price, 100, "Sales order line cost should be 100.00")
+
+ # Confirm the sales order.
+ sale_order.action_confirm()
+
+ self.assertEqual(order_line_1.purchase_price, 100, "Sales order line cost should be 100.00")
+ self.assertEqual(order_line_2.purchase_price, 40, "Sales order line cost should be 40.00")
+
def test_so_and_multicurrency(self):
ResCurrencyRate = self.env['res.currency.rate']
company_currency = self.env.company.currency_id
diff --git a/addons/stock/views/stock_quant_views.xml b/addons/stock/views/stock_quant_views.xml
index 33256a95f8..8cb1f43d58 100644
--- a/addons/stock/views/stock_quant_views.xml
+++ b/addons/stock/views/stock_quant_views.xml
@@ -208,9 +208,10 @@
- Stock On Hand
- {'search_default_internal_loc': 1, 'search_default_productgroup':1, 'search_default_locationgroup':1}
+ Update Quantity
+ {'search_default_internal_loc': 1, 'search_default_productgroup':1, 'search_default_locationgroup':1, 'inventory_mode':True}
stock.quant
+
{'search_default_productgroup': 1}
diff --git a/addons/web/tests/test_serving_base.py b/addons/web/tests/test_serving_base.py
index 2d82ed7eb6..65e369d2a2 100644
--- a/addons/web/tests/test_serving_base.py
+++ b/addons/web/tests/test_serving_base.py
@@ -999,4 +999,4 @@ class TestStaticInheritancePerformance(TestStaticInheritanceCommon):
time_ratio = delta25000.total_seconds() / delta2500.total_seconds()
_logger.runbot('Static Templates Inheritance: 25000 templates treated in %s seconds' % delta25000.total_seconds())
_logger.runbot('Static Templates Inheritance: Computed linearity ratio: %s' % time_ratio)
- self.assertLessEqual(time_ratio, 12)
+ self.assertLessEqual(time_ratio, 14)
diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml
index 3a66b44866..9a53100538 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -631,8 +631,10 @@