[PATCH] Upstream patch - 04092022

This commit is contained in:
Parthiv Patel committed 2022-09-04 08:34:56 +00:00
1 parent b5d3078476
commit cb537cb6d4
25 files changed
+493 -40

No files matched your search

+6 -1
View File
@@ -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:
+110
View File
@@ -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,
}
])
@@ -87,6 +87,16 @@
<ram:ID t-esc="SpecifiedLegalOrganization"/>
</ram:SpecifiedLegalOrganization>
<ram:DefinedTradeContact>
<ram:PersonName t-esc="partner.name"/>
<ram:TelephoneUniversalCommunication t-if="partner.phone or partner.mobile">
<ram:CompleteNumber t-esc="partner.phone or partner.mobile"/>
</ram:TelephoneUniversalCommunication>
<ram:EmailURIUniversalCommunication t-if="partner.email">
<ram:URIID schemeID='SMTP' t-esc="partner.email"/>
</ram:EmailURIUniversalCommunication>
</ram:DefinedTradeContact>
<!-- Address. -->
<ram:PostalTradeAddress>
<ram:PostcodeCode t-if="partner.zip" t-esc="partner.zip"/>
@@ -111,7 +121,7 @@
-->
<rsm:ExchangedDocumentContext>
<ram:GuidelineSpecifiedDocumentContextParameter>
<ram:ID>urn:cen.eu:en16931:2017</ram:ID>
<ram:ID>urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended</ram:ID>
</ram:GuidelineSpecifiedDocumentContextParameter>
</rsm:ExchangedDocumentContext>
@@ -68,7 +68,7 @@ class TestAccountEdiFacturx(AccountEdiTestCommon):
<CrossIndustryInvoice>
<ExchangedDocumentContext>
<GuidelineSpecifiedDocumentContextParameter>
<ID>urn:cen.eu:en16931:2017</ID>
<ID>urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended</ID>
</GuidelineSpecifiedDocumentContextParameter>
</ExchangedDocumentContext>
<ExchangedDocument>
@@ -117,10 +117,16 @@ class TestAccountEdiFacturx(AccountEdiTestCommon):
<ApplicableHeaderTradeAgreement>
<SellerTradeParty>
<Name>company_1_data</Name>
<DefinedTradeContact>
<PersonName>company_1_data</PersonName>
</DefinedTradeContact>
<PostalTradeAddress/>
</SellerTradeParty>
<BuyerTradeParty>
<Name>partner_b</Name>
<DefinedTradeContact>
<PersonName>partner_b</PersonName>
</DefinedTradeContact>
<PostalTradeAddress/>
</BuyerTradeParty>
<BuyerOrderReferencedDocument>
@@ -130,6 +136,9 @@ class TestAccountEdiFacturx(AccountEdiTestCommon):
<ApplicableHeaderTradeDelivery>
<ShipToTradeParty>
<Name>partner_b</Name>
<DefinedTradeContact>
<PersonName>partner_b</PersonName>
</DefinedTradeContact>
<PostalTradeAddress/>
</ShipToTradeParty>
</ApplicableHeaderTradeDelivery>
+27 -7
View File
@@ -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)
+7 -1
View File
@@ -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):
@@ -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
@@ -45,6 +45,7 @@
<attribute name="invisible">0</attribute>
<attribute name="required">1</attribute>
<attribute name="widget">many2one_avatar_employee</attribute>
<attribute name="context">{'active_test': True}</attribute>
</xpath>
</field>
</record>
@@ -110,7 +111,7 @@
<field name="priority">10</field>
<field name="arch" type="xml">
<xpath expr="//field[@name='company_id']" position="before">
<field name="employee_id" required="1" options='{"no_open": True}'/>
<field name="employee_id" required="1" options='{"no_open": True}' context="{'active_test': True}"/>
<field name="user_id" invisible="1"/>
</xpath>
</field>
+3 -3
View File
@@ -107,7 +107,7 @@
<tree editable="bottom" string="Timesheet Activities" default_order="date">
<field name="date"/>
<field name="user_id" invisible="1"/>
<field name="employee_id" required="1" widget="many2one_avatar_employee"/>
<field name="employee_id" required="1" widget="many2one_avatar_employee" context="{'active_test': True}"/>
<field name="name" required="0"/>
<field name="unit_amount" widget="timesheet_uom" decoration-danger="unit_amount &gt; 24"/>
<field name="project_id" invisible="1"/>
@@ -117,7 +117,7 @@
<kanban class="o_kanban_mobile">
<field name="date"/>
<field name="user_id"/>
<field name="employee_id" widget="many2one_avatar_employee"/>
<field name="employee_id" widget="many2one_avatar_employee" context="{'active_test': True}"/>
<field name="name"/>
<field name="unit_amount" decoration-danger="unit_amount &gt; 24"/>
<field name="project_id"/>
@@ -152,7 +152,7 @@
<group>
<field name="date"/>
<field name="user_id" invisible="1"/>
<field name="employee_id" required="1" widget="many2one_avatar_employee"/>
<field name="employee_id" required="1" widget="many2one_avatar_employee" context="{'active_test': True}"/>
<field name="name" required="0"/>
<field name="unit_amount" string="Duration" widget="float_time" decoration-danger="unit_amount &gt; 24"/>
<field name="project_id" invisible="1"/>
+11
View File
@@ -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,
@@ -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();
}
},
});
});
@@ -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');
};
});
+14 -2
View File
@@ -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
@@ -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;
}
+3 -3
View File
@@ -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;
+2 -2
View File
@@ -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
@@ -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)
+1 -1
View File
@@ -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(
@@ -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")
+9 -8
View File
@@ -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
@@ -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:
@@ -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
+3 -2
View File
@@ -208,9 +208,10 @@
</record>
<record model="ir.actions.act_window" id="dashboard_open_quants"> <!-- Used in dashboard -->
<field name="name">Stock On Hand</field>
<field name="context">{'search_default_internal_loc': 1, 'search_default_productgroup':1, 'search_default_locationgroup':1}</field>
<field name="name">Update Quantity</field>
<field name="context">{'search_default_internal_loc': 1, 'search_default_productgroup':1, 'search_default_locationgroup':1, 'inventory_mode':True}</field>
<field name="res_model">stock.quant</field>
<field name="view_id" ref="view_stock_quant_tree_editable"/>
</record>
<record model="ir.actions.act_window" id="location_open_quants"> <!-- Used in location -->
<field name="context">{'search_default_productgroup': 1}</field>
+1 -1
View File
@@ -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)
+4 -2
View File
@@ -631,8 +631,10 @@
<t t-call="website_sale.shop_product_carousel"/>
</div>
<div class="col-md-6 col-xl-4" id="product_details">
<t t-set="base_url" t-value="product.get_base_url()"/>
<h1 itemprop="name" t-field="product.name">Product Name</h1>
<span itemprop="url" style="display:none;" t-esc="product.website_url"/>
<span itemprop="url" style="display:none;" t-esc="base_url + product.website_url"/>
<span itemprop="image" style="display:none;" t-esc="base_url + website.image_url(product, 'image_1920')" />
<form t-if="product._is_add_to_cart_possible()" action="/shop/cart/update" method="POST">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()" />
<div class="js_product js_main_product">
@@ -1877,7 +1879,7 @@
<div t-if="product_image._name == 'product.image' and product_image.embed_code" class="d-flex align-items-center justify-content-center h-100 embed-responsive embed-responsive-16by9">
<t t-raw="product_image.embed_code"/>
</div>
<div t-else="" t-field="product_image.image_1920" class="d-flex align-items-center justify-content-center h-100" t-options='{"widget": "image", "preview_image": "image_1024", "class": "product_detail_img mh-100", "alt-field": "name", "zoom": product_image.can_image_1024_be_zoomed and "image_1920", "itemprop": "image"}'/>
<div t-else="" t-field="product_image.image_1920" class="d-flex align-items-center justify-content-center h-100" t-options='{"widget": "image", "preview_image": "image_1024", "class": "product_detail_img mh-100", "alt-field": "name", "zoom": product_image.can_image_1024_be_zoomed and "image_1920"}'/>
</div>
</t>
</div>