mirror of
https://gitlab.com/flectra-hq/flectra.git
synced 2026-10-26 07:50:05 -05:00
[PATCH] Upstream patch - 04092022
This commit is contained in:
1 parent
b5d3078476
commit
cb537cb6d4
25 files changed
+493
-40
No files matched your search
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 > 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 > 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 > 24"/>
|
||||
<field name="project_id" invisible="1"/>
|
||||
|
||||
@@ -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');
|
||||
};
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user