[PATCH] Upstream patch - 02072023

This commit is contained in:
Parthiv Patel
2023-07-02 08:34:44 +00:00
parent eb7b5cb3a7
commit 0a4a4bdc26
24 changed files with 431 additions and 116 deletions
+2 -2
View File
@@ -52,8 +52,8 @@ class PortalAccount(CustomerPortal):
searchbar_filters = {
'all': {'label': _('All'), 'domain': []},
'invoices': {'label': _('Invoices'), 'domain': [('move_type', '=', ('out_invoice', 'out_refund'))]},
'bills': {'label': _('Bills'), 'domain': [('move_type', '=', ('in_invoice', 'in_refund'))]},
'invoices': {'label': _('Invoices'), 'domain': [('move_type', 'in', ('out_invoice', 'out_refund'))]},
'bills': {'label': _('Bills'), 'domain': [('move_type', 'in', ('in_invoice', 'in_refund'))]},
}
# default filter by value
if not filterby:
@@ -495,7 +495,8 @@ class AccountPartialReconcile(models.Model):
partial = partial_values['partial']
# Init the journal entry.
move_date = partial.max_date if partial.max_date > (move.company_id.period_lock_date or date.min) else today
lock_date = move.company_id._get_user_fiscal_lock_date()
move_date = partial.max_date if partial.max_date > (lock_date or date.min) else today
move_vals = {
'move_type': 'entry',
'date': move_date,
+115 -80
View File
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
from collections import defaultdict
from flectra.exceptions import AccessError
from flectra import api, fields, models, _
@@ -43,15 +44,18 @@ def update_taxes_from_templates(cr, chart_template_xmlid):
This method is mainly used as a local upgrade script.
Returns a list of tuple (template_id, tax_id) of newly created records.
"""
def _create_tax_from_template(company, template, old_tax=None):
""" Create a new tax from template with template xmlid, if there was already an old tax with that xmlid we
def _create_taxes_from_template(company, template2tax_mapping):
""" Create a new taxes from templates. If an old tax already used the same xmlid, we
remove the xmlid from it but don't modify anything else.
:param company: the company of the tax to instantiate
:param template2tax_mapping: a list of tuples (template, existing_tax) where existing_tax can be None
:return: a list of tuples of ids (template.id, newly_created_tax.id)
"""
def _remove_xml_id(xml_id):
module, name = xml_id.split('.', 1)
env['ir.model.data'].search([('module', '=', module), ('name', '=', name)]).unlink()
def _avoid_name_conflict():
def _avoid_name_conflict(company, template):
conflict_taxes = env['account.tax'].search([
('name', '=', template.name), ('company_id', '=', company.id),
('type_tax_use', '=', template.type_tax_use), ('tax_scope', '=', template.tax_scope)
@@ -60,48 +64,61 @@ def update_taxes_from_templates(cr, chart_template_xmlid):
for index, conflict_taxes in enumerate(conflict_taxes):
conflict_taxes.name = f"[old{index if index > 0 else ''}] {conflict_taxes.name}"
template_vals = template._get_tax_vals_complete(company)
chart_template = env['account.chart.template'].with_context(default_company_id=company.id)
if old_tax:
xml_id = old_tax.get_xml_id().get(old_tax.id)
if xml_id:
_remove_xml_id(xml_id)
_avoid_name_conflict()
return chart_template.create_record_with_xmlid(company, template, 'account.tax', template_vals)
templates_to_create = env['account.tax.template'].with_context(active_test=False)
for template, old_tax in template2tax_mapping:
if old_tax:
xml_id = old_tax.get_external_id().get(old_tax.id)
if xml_id:
_remove_xml_id(xml_id)
_avoid_name_conflict(company, template)
templates_to_create += template
new_template2tax_company = templates_to_create._generate_tax(company, accounts_exist=True)['tax_template_to_tax']
return [(env['account.tax.template'].browse(template_id), env['account.tax'].browse(tax_id))
for template_id, tax_id in new_template2tax_company.items()]
def _update_tax_from_template(template, tax):
""" Update the tax's tags (and only tags!) based on template values. """
tax_rep_lines = tax.invoice_repartition_line_ids + tax.refund_repartition_line_ids
template_rep_lines = template.invoice_repartition_line_ids + template.refund_repartition_line_ids
for tax_line, template_line in zip(tax_rep_lines, template_rep_lines):
tags_to_add = template_line._get_tags_to_add()
tags_to_unlink = tax_line.tag_ids
if tags_to_add != tags_to_unlink:
tax_line.write({'tag_ids': [(6, 0, tags_to_add.ids)]})
_cleanup_tags(tags_to_unlink)
def _update_taxes_from_template(template2tax_mapping):
""" Update the taxes' tags (and only tags!) based on their corresponding template values.
:param template2tax_mapping: a list of tuples (template, existing_taxes)
"""
for template, existing_tax in template2tax_mapping:
tax_rep_lines = existing_tax.invoice_repartition_line_ids + existing_tax.refund_repartition_line_ids
template_rep_lines = template.invoice_repartition_line_ids + template.refund_repartition_line_ids
for tax_line, template_line in zip(tax_rep_lines, template_rep_lines):
tags_to_add = template_line._get_tags_to_add()
tags_to_unlink = tax_line.tag_ids
if tags_to_add != tags_to_unlink:
tax_line.write({'tag_ids': [(6, 0, tags_to_add.ids)]})
_cleanup_tags(tags_to_unlink)
def _get_template_to_real_xmlid_mapping(company, model):
def _get_template_to_real_xmlid_mapping(model, templates):
""" This function uses ir_model_data to return a mapping between the templates and the data, using their xmlid
:returns: {
account.tax.template.id: account.tax.id
}
company_id: { model.template.id1: model.id1, model.template.id2: model.id2 },
...
}
"""
template_xmlids = [xmlid.split('.', 1)[1] for xmlid in templates.get_external_id().values()]
res = defaultdict(dict)
if not template_xmlids:
return res
env['ir.model.data'].flush()
env.cr.execute(
"""
SELECT template.res_id AS template_res_id,
data.res_id AS data_res_id
SELECT substr(data.name, 0, strpos(data.name, '_'))::INTEGER AS data_company_id,
template.res_id AS template_res_id,
data.res_id AS data_res_id
FROM ir_model_data data
JOIN ir_model_data template
ON template.name = substr(data.name, strpos(data.name, '_') + 1)
WHERE data.model = %s
AND data.name LIKE %s
AND template.name IN %s
-- tax.name is of the form: {company_id}_{account.tax.template.name}
""",
[model, r"%s\_%%" % company.id],
[model, tuple(template_xmlids)],
)
tuples = env.cr.fetchall()
return dict(tuples)
for company_id, template_id, model_id in env.cr.fetchall():
res[company_id][template_id] = model_id
return res
def _is_tax_and_template_same(template, tax):
""" This function compares account.tax and account.tax.template repartition lines.
@@ -110,17 +127,23 @@ def update_taxes_from_templates(cr, chart_template_xmlid):
- amount
- repartition lines percentages in the same order
"""
tax_rep_lines = tax.invoice_repartition_line_ids + tax.refund_repartition_line_ids
template_rep_lines = template.invoice_repartition_line_ids + template.refund_repartition_line_ids
return (
tax.amount_type == template.amount_type
and tax.amount == template.amount
and len(tax_rep_lines) == len(template_rep_lines)
and all(
rep_line_tax.factor_percent == rep_line_template.factor_percent
for rep_line_tax, rep_line_template in zip(tax_rep_lines, template_rep_lines)
)
)
if tax.children_tax_ids:
# if the tax has children taxes we don't do checks on rep. lines nor amount
return tax.amount_type == template.amount_type
else:
tax_rep_lines = tax.invoice_repartition_line_ids + tax.refund_repartition_line_ids
template_rep_lines = template.invoice_repartition_line_ids + template.refund_repartition_line_ids
return (
tax.amount_type == template.amount_type
and tax.amount == template.amount
and (
len(tax_rep_lines) == len(template_rep_lines)
and all(
rep_line_tax.factor_percent == rep_line_template.factor_percent
for rep_line_tax, rep_line_template in zip(tax_rep_lines, template_rep_lines)
)
)
)
def _cleanup_tags(tags):
""" Checks if the tags are still used in taxes or move lines. If not we delete it. """
@@ -131,27 +154,33 @@ def update_taxes_from_templates(cr, chart_template_xmlid):
if not (aml_using_tag or tax_using_tag or report_line_using_tag):
tag.unlink()
def _update_fiscal_positions_from_templates(company, chart_template_id, new_taxes_template):
chart_template = env['account.chart.template'].browse(chart_template_id)
positions = env['account.fiscal.position.template'].search([('chart_template_id', '=', chart_template_id)])
tax_template_ref = _get_template_to_real_xmlid_mapping(company, 'account.tax')
fp_template_ref = _get_template_to_real_xmlid_mapping(company, 'account.fiscal.position')
def _update_fiscal_positions_from_templates(chart_template, new_tax_template_by_company, all_tax_templates):
fp_templates = env['account.fiscal.position.template'].search([('chart_template_id', '=', chart_template.id)])
template2tax = _get_template_to_real_xmlid_mapping('account.tax', all_tax_templates)
template2fp = _get_template_to_real_xmlid_mapping('account.fiscal.position', fp_templates)
tax_template_vals = []
for position_template in positions:
fp = env['account.fiscal.position'].browse(fp_template_ref.get(position_template.id))
if not fp:
continue
for position_tax in position_template.tax_ids:
position_tax_template_exist = fp.tax_ids.filtered_domain([('tax_src_id', '=', tax_template_ref[position_tax.tax_src_id.id]),
('tax_dest_id', '=', position_tax.tax_dest_id and tax_template_ref[position_tax.tax_dest_id.id] or False)])
if not position_tax_template_exist and (position_tax.tax_src_id in new_taxes_template or position_tax.tax_dest_id in new_taxes_template):
tax_template_vals.append((position_tax, {
'tax_src_id': tax_template_ref[position_tax.tax_src_id.id],
'tax_dest_id': position_tax.tax_dest_id and tax_template_ref[position_tax.tax_dest_id.id] or False,
'position_id': fp.id,
}))
chart_template._create_records_with_xmlid('account.fiscal.position.tax', tax_template_vals, company)
for company_id in new_tax_template_by_company:
fp_tax_template_vals = []
template2fp_company = template2fp.get(company_id)
for position_template in fp_templates:
fp = env['account.fiscal.position'].browse(template2fp_company.get(position_template.id)) if template2fp_company else None
if not fp:
continue
for position_tax in position_template.tax_ids:
position_tax_template_exist = fp.tax_ids.filtered(
lambda tax_fp: tax_fp.tax_src_id.id == template2tax.get(company_id).get(position_tax.tax_src_id.id)
and tax_fp.tax_dest_id.id == (position_tax.tax_dest_id and template2tax.get(company_id).get(position_tax.tax_dest_id.id) or False)
)
if not position_tax_template_exist and (
position_tax.tax_src_id in new_tax_template_by_company[company_id]
or position_tax.tax_dest_id in new_tax_template_by_company[company_id]
):
fp_tax_template_vals.append((position_tax, {
'tax_src_id': template2tax.get(company_id).get(position_tax.tax_src_id.id),
'tax_dest_id': position_tax.tax_dest_id and template2tax.get(company_id).get(position_tax.tax_dest_id.id) or False,
'position_id': fp.id,
}))
chart_template._create_records_with_xmlid('account.fiscal.position.tax', fp_tax_template_vals, env['res.company'].browse(company_id))
def _process_taxes_translations(chart_template, new_template_x_taxes):
"""
@@ -191,25 +220,30 @@ def update_taxes_from_templates(cr, chart_template_xmlid):
env = api.Environment(cr, SUPERUSER_ID, {})
chart_template = env.ref(chart_template_xmlid)
companies = env['res.company'].search(['|', ('chart_template_id', '=', chart_template.id), ('chart_template_id', 'child_of', chart_template.id)])
outdated_taxes = []
new_taxes_template = []
new_template2tax = []
companies = env['res.company'].search([('chart_template_id', 'child_of', chart_template.id)])
templates = env['account.tax.template'].with_context(active_test=False).search([('chart_template_id', '=', chart_template.id)])
template2tax = _get_template_to_real_xmlid_mapping('account.tax', templates)
outdated_taxes = env['account.tax']
new_tax_template_by_company = defaultdict(env['account.tax.template'].browse) # only contains completely new taxes (not previous taxe had the xmlid)
new_template2tax = [] # contains all created taxes
for company in companies:
template_to_tax = _get_template_to_real_xmlid_mapping(company, 'account.tax')
templates = env['account.tax.template'].with_context(active_test=False).search([('chart_template_id', '=', chart_template.id)])
templates_to_tax_create = []
templates_to_tax_update = []
template2oldtax_company = template2tax.get(company.id)
for template in templates:
tax = env['account.tax'].browse(template_to_tax.get(template.id))
tax = env['account.tax'].browse(template2oldtax_company.get(template.id)) if template2oldtax_company else None
if not tax or not _is_tax_and_template_same(template, tax):
new_tax_id = _create_tax_from_template(company, template, old_tax=tax)
new_template2tax.append((template.id, new_tax_id))
templates_to_tax_create.append((template, tax))
if tax:
outdated_taxes.append(tax)
outdated_taxes += tax
else:
new_taxes_template.append(template)
# we only want to update fiscal position if there is no previous tax with the mapping
new_tax_template_by_company[company.id] += template
else:
_update_tax_from_template(template, tax)
_update_fiscal_positions_from_templates(company, chart_template.id, new_taxes_template)
templates_to_tax_update.append((template, tax))
new_template2tax += _create_taxes_from_template(company, templates_to_tax_create)
_update_taxes_from_template(templates_to_tax_update)
_update_fiscal_positions_from_templates(chart_template, new_tax_template_by_company, templates)
if outdated_taxes:
_notify_accountant_managers(outdated_taxes)
if hasattr(chart_template, 'spoken_languages') and chart_template.spoken_languages:
@@ -1120,17 +1154,15 @@ class AccountTaxTemplate(models.Model):
val['tax_group_id'] = self.tax_group_id.id
return val
def _get_tax_vals_complete(self, company):
def _get_tax_vals_complete(self, company, tax_template_to_tax):
"""
Returns a dict of values to be used to create the tax corresponding to the template, assuming the
account.account objects were already created.
It differs from function _get_tax_vals because here, we replace the references to account.template by their
corresponding account.account ids ('cash_basis_transition_account_id' and 'account_id' in the invoice and
refund repartition lines)
(Used by upgrade/migrations/util/accounting)
"""
vals = self._get_tax_vals(company, {})
vals.pop("children_tax_ids", None)
vals = self._get_tax_vals(company, tax_template_to_tax)
if self.cash_basis_transition_account_id.code:
cash_basis_account_id = self.env['account.account'].search([
@@ -1146,7 +1178,7 @@ class AccountTaxTemplate(models.Model):
})
return vals
def _generate_tax(self, company):
def _generate_tax(self, company, accounts_exist=False):
""" This method generate taxes from templates.
:param company: the company for which the taxes should be created from templates in self
@@ -1170,7 +1202,10 @@ class AccountTaxTemplate(models.Model):
tax_template_vals = []
for template in templates:
if all(child.id in tax_template_to_tax for child in template.children_tax_ids):
vals = template._get_tax_vals(company, tax_template_to_tax)
if accounts_exist:
vals = template._get_tax_vals_complete(company, tax_template_to_tax)
else:
vals = template._get_tax_vals(company, tax_template_to_tax)
tax_template_vals.append((template, vals))
else:
# defer the creation of this tax to the next batch
@@ -3538,3 +3538,56 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon):
# Date of the reversal of the exchange move should be the last day of the month/year of the payment depending on the sequence format
self.assertEqual(exchange_move_reversal.date, fields.Date.to_date(expected_date))
@freeze_time('2023-05-01')
def test_caba_with_different_lock_dates(self):
"""
Test the date of the CABA move when reconciling a payment with an invoice
with date before fiscalyear_period but after period_lock_date when
having accountant rights.
"""
self.env.company.tax_exigibility = True
tax_waiting_account = self.env['account.account'].create({
'name': 'TAX_WAIT',
'code': 'TWAIT',
'user_type_id': self.env.ref('account.data_account_type_current_liabilities').id,
'reconcile': True,
})
tax = self.env['account.tax'].create({
'name': 'cash basis 10%',
'type_tax_use': 'sale',
'amount': 10,
'tax_exigibility': 'on_payment',
'cash_basis_transition_account_id': tax_waiting_account.id,
})
self.env['account.move'].search([('state', '=', 'draft')]).unlink()
self.env.company.fiscalyear_lock_date = fields.Date.from_string('2023-01-01')
self.env.company.period_lock_date = fields.Date.from_string('2023-02-01')
if not self.env.user.user_has_groups('account.group_account_manager'):
self.env.user.groups_id = [(4, [self.env.ref('account.group_account_manager').id], 0)]
invoice = self.init_invoice(move_type='out_invoice', products=self.product_a, invoice_date='2023-01-02', taxes=tax)
payment = self.env['account.payment'].create({
'partner_id': self.partner_a.id,
'payment_type': 'inbound',
'partner_type': 'customer',
'date': '2023-01-30',
'amount': invoice.amount_total,
})
(invoice + payment.move_id).action_post()
(invoice + payment.move_id).line_ids\
.filtered(lambda x: x.account_internal_type in ('receivable', 'payable'))\
.reconcile()
caba_move = self.env['account.move'].search([('tax_cash_basis_move_id', '=', invoice.id)])
self.assertRecordValues(caba_move, [{
'date': fields.Date.from_string('2023-01-30'),
}])
+114 -9
View File
@@ -15,19 +15,48 @@ class TestChartTemplate(SavepointCase):
"""
super().setUpClass()
be_country_id = cls.env.ref('base.be').id
us_country_id = cls.env.ref('base.us').id
cls.company = cls.env['res.company'].create({
'name': 'TestCompany1',
'country_id': be_country_id,
'account_tax_fiscal_country_id': be_country_id,
'country_id': us_country_id,
'account_tax_fiscal_country_id': us_country_id,
})
cls.chart_template = cls.env.ref('l10n_generic_coa.configurable_chart_template', raise_if_not_found=False)
if not cls.chart_template:
cls.skipTest(cls, "Accounting Tests skipped because the generic chart of accounts was not found")
cls.chart_template_xmlid = 'l10n_test.test_chart_template_xmlid'
cls.chart_template = cls.env['account.chart.template']._load_records([{
'xml_id': cls.chart_template_xmlid,
'values': {
'name': 'Test Chart Template US',
'currency_id': cls.env.ref('base.USD').id,
'bank_account_code_prefix': 1000,
'cash_account_code_prefix': 2000,
'transfer_account_code_prefix': 3000,
}
}])
account_templates = cls.env['account.account.template']._load_records([{
'xml_id': 'account.test_account_income_template',
'values':
{
'name': 'property_income_account',
'code': '222221',
'user_type_id': cls.env.ref('account.data_account_type_revenue').id,
'chart_template_id': cls.chart_template.id,
}
}, {
'xml_id': 'account.test_account_expense_template',
'values':
{
'name': 'property_expense_account',
'code': '222222',
'user_type_id': cls.env.ref('account.data_account_type_expenses').id,
'chart_template_id': cls.chart_template.id,
}
}])
cls.chart_template.property_account_income_categ_id = account_templates[0].id
cls.chart_template.property_account_expense_categ_id = account_templates[1].id
cls.fiscal_position_template = cls._create_fiscal_position_template('account.test_fiscal_position_template',
'BE fiscal position test', be_country_id)
'US fiscal position test', us_country_id)
cls.tax_template_1 = cls._create_tax_template('account.test_tax_template_1', 'Tax name 1', 1, 'tag_name_1')
cls.tax_template_2 = cls._create_tax_template('account.test_tax_template_2', 'Tax name 2', 2, 'tag_name_2')
cls.fiscal_position_tax_template_1 = cls._create_fiscal_position_tax_template(
@@ -48,19 +77,48 @@ class TestChartTemplate(SavepointCase):
return cls._create_tax_template(template_name, name, amount, tag_name=None)
@classmethod
def _create_tax_template(cls, tax_template_xmlid, name, amount, tag_name=None):
def _create_group_tax_template(cls, tax_template_xmlid, name, chart_template_id=None, active=True):
children_1 = cls._create_tax_template(f'{tax_template_xmlid}_children1', f'{name}_children_1', 10,
active=active)
children_2 = cls._create_tax_template(f'{tax_template_xmlid}_children2', f'{name}_children_2', 15,
active=active)
return cls.env['account.tax.template']._load_records([{
'xml_id': tax_template_xmlid,
'values': {
'name': name,
'amount_type': 'group',
'type_tax_use': 'none',
'active': active,
'chart_template_id': chart_template_id if chart_template_id else cls.chart_template.id,
'children_tax_ids': [(6, 0, (children_1 + children_2).ids)],
},
}])
@classmethod
def _create_tax_template(cls, tax_template_xmlid, name, amount, tag_name=None, account_data=None, active=True):
if tag_name:
tag = cls.env['account.account.tag'].create({
'name': tag_name,
'applicability': 'taxes',
'country_id': cls.company.account_tax_fiscal_country_id.id,
})
if account_data:
account_vals = {
'name': account_data['name'],
'code': account_data['code'],
'user_type_id': cls.env.ref('account.data_account_type_current_liabilities').id,
}
# We have to instantiate both the template and the record since we suppose accounts are already created.
account_template = cls.env['account.account.template'].create(account_vals)
account_vals.update({'company_id': cls.company.id})
cls.env['account.account'].create(account_vals)
return cls.env['account.tax.template']._load_records([{
'xml_id': tax_template_xmlid,
'values': {
'name': name,
'amount': amount,
'type_tax_use': 'none',
'active': active,
'chart_template_id': cls.chart_template.id,
'invoice_repartition_line_ids': [
(0, 0, {
@@ -70,6 +128,7 @@ class TestChartTemplate(SavepointCase):
}),
(0, 0, {
'factor_percent': 100,
'account_id': account_template.id if account_data else None,
'repartition_type': 'tax',
}),
],
@@ -81,6 +140,7 @@ class TestChartTemplate(SavepointCase):
}),
(0, 0, {
'factor_percent': 100,
'account_id': account_template.id if account_data else None,
'repartition_type': 'tax',
}),
],
@@ -115,7 +175,7 @@ class TestChartTemplate(SavepointCase):
creates this new tax and fiscal position line when updating
"""
tax_template_3 = self._create_tax_template('account.test_tax_3_template', 'Tax name 3', 3, 'tag_name_3')
tax_template_4 = self._create_tax_template('account.test_tax_4_template', 'Tax name 4', 4)
tax_template_4 = self._create_tax_template('account.test_tax_4_template', 'Tax name 4', 4, account_data={'name': 'account_name_4', 'code': 'TACT'})
self._create_fiscal_position_tax_template(self.fiscal_position_template, 'account.test_fiscal_position_tax_template', tax_template_3, tax_template_4)
update_taxes_from_templates(self.env.cr, self.chart_template_xmlid)
@@ -128,6 +188,7 @@ class TestChartTemplate(SavepointCase):
{'name': 'Tax name 4', 'amount': 4},
])
self.assertEqual(taxes.invoice_repartition_line_ids.tag_ids.name, 'tag_name_3')
self.assertEqual(taxes.invoice_repartition_line_ids.account_id.name, 'account_name_4')
self.assertRecordValues(self.fiscal_position.tax_ids.tax_src_id, [
{'name': 'Tax name 1'},
{'name': 'Tax name 3'},
@@ -265,3 +326,47 @@ class TestChartTemplate(SavepointCase):
('partner_ids', 'in', normal_user.partner_id.ids),
('body', 'like', f"%{self.tax_template_1.name}%"), # we look for taxes' name that have been sent in the message's body
]), 0)
def test_update_taxes_children_tax_ids(self):
""" Ensures children_tax_ids are correctly generated when updating taxes with
amount_type='group'.
"""
group_tax_name = 'Group Tax name 1 TEST'
self._create_group_tax_template('account.test_group_tax_test_template', group_tax_name,
chart_template_id=self.chart_template.id)
update_taxes_from_templates(self.env.cr, self.chart_template_xmlid)
parent_tax = self.env['account.tax'].search([
('company_id', '=', self.company.id),
('name', '=', group_tax_name),
])
children_taxes = self.env['account.tax'].search([
('company_id', '=', self.company.id),
('name', 'like', f'{group_tax_name}_%'),
])
self.assertEqual(len(parent_tax), 1, "The parent tax should have been created.")
self.assertEqual(len(children_taxes), 2, "Two children should have been created.")
self.assertEqual(parent_tax.children_tax_ids.ids, children_taxes.ids,
"The parent and its children taxes should be linked together.")
def test_update_taxes_children_tax_ids_inactive(self):
""" Ensure tax templates are correctly generated when updating taxes with children taxes,
even if templates are inactive.
"""
group_tax_name = 'Group Tax name 1 inactive TEST'
self._create_group_tax_template('account.test_group_tax_test_template_inactive', group_tax_name,
chart_template_id=self.chart_template.id, active=False)
update_taxes_from_templates(self.env.cr, self.chart_template_xmlid)
parent_tax = self.env['account.tax'].with_context(active_test=False).search([
('company_id', '=', self.company.id),
('name', '=', group_tax_name),
])
children_taxes = self.env['account.tax'].with_context(active_test=False).search([
('company_id', '=', self.company.id),
('name', 'like', f'{group_tax_name}_%'),
])
self.assertEqual(len(parent_tax), 1, "The parent tax should have been created, even if it is inactive.")
self.assertFalse(parent_tax.active, "The parent tax should be inactive.")
self.assertEqual(len(children_taxes), 2, "Two children should have been created, even if they are inactive.")
self.assertEqual(children_taxes.mapped('active'), [False] * 2, "Children taxes should be inactive.")
@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from flectra import models, fields, api
from flectra import models, fields, _
from flectra.tools.safe_eval import safe_eval
from flectra.exceptions import UserError
class AccountTaxPython(models.Model):
@@ -35,7 +36,10 @@ class AccountTaxPython(models.Model):
if self.amount_type == 'code':
company = self.env.company
localdict = {'base_amount': base_amount, 'price_unit':price_unit, 'quantity': quantity, 'product':product, 'partner':partner, 'company': company}
safe_eval(self.python_compute, localdict, mode="exec", nocopy=True)
try:
safe_eval(self.python_compute, localdict, mode="exec", nocopy=True)
except Exception as e:
raise UserError(_("You entered invalid code %r in %r taxes\n\nError : %s") % (self.python_compute, self.name, e)) from e
return localdict['result']
return super(AccountTaxPython, self)._compute_amount(base_amount, price_unit, quantity, product, partner)
@@ -47,7 +51,10 @@ class AccountTaxPython(models.Model):
for tax in self.filtered(lambda r: r.amount_type == 'code'):
localdict = self._context.get('tax_computation_context', {})
localdict.update({'price_unit': price_unit, 'quantity': quantity, 'product': product, 'partner': partner, 'company': company})
safe_eval(tax.python_applicable, localdict, mode="exec", nocopy=True)
try:
safe_eval(tax.python_applicable, localdict, mode="exec", nocopy=True)
except Exception as e:
raise UserError(_("You entered invalid code %r in %r taxes\n\nError : %s") % (tax.python_applicable, tax.name, e)) from e
if localdict.get('result', False):
taxes += tax
return super(AccountTaxPython, taxes).compute_all(price_unit, currency, quantity, product, partner, is_refund=is_refund, handle_price_include=handle_price_include)
@@ -178,9 +178,9 @@ var BarcodeParser = Class.extend({
match.base_code = base_code.join('')
}
if (base_pattern[0] !== '^') {
base_pattern = "^" + base_pattern;
}
base_pattern = base_pattern.split('|')
.map(part => part.startsWith('^') ? part : '^' + part)
.join('|');
match.match = match.base_code.match(base_pattern);
return match;
+1 -1
View File
@@ -424,7 +424,7 @@ Or send your receipts at <a href="mailto:%(email)s?subject=Lunch%%20with%%20cust
move_line_name = expense.employee_id.name + ': ' + expense.name.split('\n')[0][:64]
account_src = expense._get_expense_account_source()
account_dst = expense._get_expense_account_destination()
account_date = expense.sheet_id.accounting_date or expense.date or fields.Date.context_today(expense)
account_date = expense.date or expense.sheet_id.accounting_date or fields.Date.context_today(expense)
company_currency = expense.company_id.currency_id
+4 -4
View File
@@ -71,7 +71,7 @@ class TestExpenses(TestExpenseCommon):
# Receivable line (foreign currency):
{
'debit': 0.0,
'credit': 862.5,
'credit': 575.0,
'amount_currency': -1725.0,
'account_id': self.company_data['default_account_payable'].id,
'product_id': False,
@@ -81,7 +81,7 @@ class TestExpenses(TestExpenseCommon):
},
# Tax line (foreign currency):
{
'debit': 112.5,
'debit': 75.0,
'credit': 0.0,
'amount_currency': 225.0,
'account_id': self.company_data['default_account_tax_purchase'].id,
@@ -103,7 +103,7 @@ class TestExpenses(TestExpenseCommon):
},
# Product line (foreign currency):
{
'debit': 750.0,
'debit': 500.0,
'credit': 0.0,
'amount_currency': 1500.0,
'account_id': self.company_data['default_account_expense'].id,
@@ -134,7 +134,7 @@ class TestExpenses(TestExpenseCommon):
'currency_id': self.company_data['currency'].id,
},
{
'amount': -750.0,
'amount': -500.0,
'date': fields.Date.from_string('2017-01-01'),
'account_id': self.analytic_account_2.id,
'currency_id': self.company_data['currency'].id,
@@ -93,3 +93,5 @@ class ResCompany(models.Model):
'printing_date': format_date(self.env, Date.to_string( Date.today())),
'corrupted_orders': corrupted_orders or 'None'
}
else:
raise UserError(_('Accounting is not unalterable for the company %s. This mechanism is designed for companies where accounting is unalterable.') % self.env.company.name)
@@ -10,10 +10,8 @@ class ReportPosHashIntegrity(models.AbstractModel):
@api.model
def _get_report_values(self, docids, data=None):
if data:
data.update(self.env.company._check_pos_hash_integrity())
else:
data = self.env.company._check_hash_pos_integrity()
data = data or {}
data.update(self.env.company._check_pos_hash_integrity() or {})
return {
'doc_ids' : docids,
'doc_model' : self.env['res.company'],
@@ -6,7 +6,7 @@
<t t-foreach="ddt_dict" t-as="picking">
<DatiDDT>
<NumeroDDT t-esc="format_alphanumeric(picking.l10n_it_ddt_number[-20:])"/>
<DataDDT t-esc="format_date(picking.date)"/>
<DataDDT t-esc="format_date(picking.date_done)"/>
<t t-if="len(ddt_dict) > 1">
<t t-foreach="ddt_dict[picking]" t-as="line_ref">
<RiferimentoNumeroLinea t-esc="line_ref"/>
@@ -48,6 +48,10 @@
margin-right: $o-mail-discuss-sidebar-scrollbar-width;
}
.o_DiscussSidebar_newChannelAutocompleteSuggestions {
word-wrap: break-word;
}
.o_DiscussSidebar_quickSearch {
border-radius: 10px;
margin: 0 $o-mail-discuss-sidebar-scrollbar-width 10px;
@@ -130,6 +130,12 @@ class MicrosoftSync(models.AbstractModel):
if with_uid
else [('microsoft_id', '=', False)]
)
elif operator == '!=' and not value:
return (
[('microsoft_id', 'ilike', f'{IDS_SEPARATOR}_')]
if with_uid
else [('microsoft_id', '!=', False)]
)
return (
['|'] * (len(value) - 1) + [_domain(v) for v in value]
if operator.lower() == 'in'
@@ -288,3 +288,17 @@ class TestMicrosoftEvent(TestCommon):
# assert
self.assertEqual(len(matched._events), 0)
def test_search_set_ms_universal_event_id(self):
not_synced_events = self.env['calendar.event'].search([('ms_universal_event_id', '=', False)])
synced_events = self.env['calendar.event'].search([('ms_universal_event_id', '!=', False)])
self.assertIn(self.simple_event, synced_events)
self.assertNotIn(self.simple_event, not_synced_events)
self.simple_event.ms_universal_event_id = ''
not_synced_events = self.env['calendar.event'].search([('ms_universal_event_id', '=', False)])
synced_events = self.env['calendar.event'].search([('ms_universal_event_id', '!=', False)])
self.assertNotIn(self.simple_event, synced_events)
self.assertIn(self.simple_event, not_synced_events)
@@ -0,0 +1,31 @@
flectra.define('point_of_sale.tour.BarcodeScanning', function (require) {
'use strict';
const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods');
const { getSteps, startSteps } = require('point_of_sale.tour.utils');
const Tour = require('web_tour.tour');
startSteps();
// Add a product with its barcode
ProductScreen.do.scan_barcode("0123456789");
ProductScreen.check.selectedOrderlineHas('Monitor Stand');
ProductScreen.do.scan_barcode("0123456789");
ProductScreen.check.selectedOrderlineHas('Monitor Stand', 2);
// Test "Prices product" EAN-13 `23.....{NNNDD}` barcode pattern
ProductScreen.do.scan_ean13_barcode("2305000000004");
ProductScreen.check.selectedOrderlineHas('Magnetic Board', 1, "0.00");
ProductScreen.do.scan_ean13_barcode("2305000123454");
ProductScreen.check.selectedOrderlineHas('Magnetic Board', 1, "123.45");
// Test "Weighted product" EAN-13 `21.....{NNDDD}` barcode pattern
ProductScreen.do.scan_ean13_barcode("2100005000000");
ProductScreen.check.selectedOrderlineHas('Wall Shelf Unit', 0, "0.00");
ProductScreen.do.scan_ean13_barcode("2100005080000");
ProductScreen.check.selectedOrderlineHas('Wall Shelf Unit', 8);
Tour.register('BarcodeScanningTour', { test: true, url: '/pos/ui' }, getSteps());
});
@@ -146,6 +146,34 @@ flectra.define('point_of_sale.tour.ProductScreenTourMethods', function (require)
},
];
}
scan_barcode(barcode) {
return [
{
content: `input barcode '${barcode}'`,
trigger: "input.ean",
run: `text ${barcode}`,
},
{
content: `button scan barcode '${barcode}'`,
trigger: "li.barcode",
run: 'click',
}
];
}
scan_ean13_barcode(barcode) {
return [
{
content: `input barcode '${barcode}'`,
trigger: "input.ean",
run: `text ${barcode}`,
},
{
content: `button scan EAN-13 barcode '${barcode}'`,
trigger: "li.custom_ean",
run: 'click',
}
];
}
}
class Check {
@@ -70,6 +70,7 @@ class TestPointOfSaleHttpCommon(flectra.tests.HttpCase):
'available_in_pos': True,
'list_price': 1.98,
'taxes_id': False,
'barcode': '2100005000000',
})
small_shelf = env['product.product'].create({
'name': 'Small Shelf',
@@ -82,12 +83,14 @@ class TestPointOfSaleHttpCommon(flectra.tests.HttpCase):
'available_in_pos': True,
'list_price': 1.98,
'taxes_id': False,
'barcode': '2305000000004',
})
monitor_stand = env['product.product'].create({
'name': 'Monitor Stand',
'available_in_pos': True,
'list_price': 3.19,
'taxes_id': False,
'barcode': '0123456789', # No pattern in barcode nomenclature
})
desk_pad = env['product.product'].create({
'name': 'Desk Pad',
@@ -659,3 +662,12 @@ class TestUi(TestPointOfSaleHttpCommon):
self.main_pos_config.open_session_cb(check_coa=False)
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'ReceiptScreenDiscountWithPricelistTour', login="admin")
def test_07_pos_barcodes_scan(self):
barcode_rule = self.env.ref("point_of_sale.barcode_rule_client")
barcode_rule.pattern = barcode_rule.pattern + "|234"
# should in theory be changed in the JS code to `|^234`
# If not, it will fail as it will mistakenly match with the product barcode "0123456789"
self.main_pos_config.open_session_cb(check_coa=False)
self.start_tour("/pos/ui?debug=1&config_id=%d" % self.main_pos_config.id, 'BarcodeScanningTour', login="admin")
@@ -25,6 +25,7 @@
<script type="text/javascript" src="/point_of_sale/static/tests/tours/ReceiptScreen.tour.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/tours/Chrome.tour.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/tours/TicketScreen.tour.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/tours/BarcodeScanning.tour.js"></script>
</xpath>
</template>
+1 -1
View File
@@ -42,7 +42,7 @@ class StockMove(models.Model):
layers = self.origin_returned_move_id.sudo().stock_valuation_layer_ids
# dropshipping create additional positive svl to make sure there is no impact on the stock valuation
# We need to remove them from the computation of the price unit.
if self.origin_returned_move_id._is_dropshipped():
if self.origin_returned_move_id._is_dropshipped() or self.origin_returned_move_id._is_dropshipped_returned():
layers = layers.filtered(lambda l: float_compare(l.value, 0, precision_rounding=l.product_id.uom_id.rounding) <= 0)
layers |= layers.stock_valuation_layer_ids
quantity = sum(layers.mapped("quantity"))
@@ -324,3 +324,16 @@ class TestStockValuation(ValuationReconciliationTestCommon):
self.assertTrue(8 in return_pick.move_lines.stock_valuation_layer_ids.mapped('value'))
self.assertTrue(-8 in return_pick.move_lines.stock_valuation_layer_ids.mapped('value'))
# return again to have a new dropship picking from a dropship return
stock_return_picking_form_2 = Form(self.env['stock.return.picking']
.with_context(active_ids=return_pick.ids, active_id=return_pick.ids[0],
active_model='stock.picking'))
stock_return_picking_2 = stock_return_picking_form_2.save()
stock_return_picking_action_2 = stock_return_picking_2.create_returns()
return_pick_2 = self.env['stock.picking'].browse(stock_return_picking_action_2['res_id'])
return_pick_2.move_lines[0].move_line_ids[0].qty_done = 1.0
return_pick_2._action_done()
self.assertTrue(8 in return_pick_2.move_lines.stock_valuation_layer_ids.mapped('value'))
self.assertTrue(-8 in return_pick_2.move_lines.stock_valuation_layer_ids.mapped('value'))
+5 -1
View File
@@ -159,11 +159,15 @@ $.fn.extend({
compensateScrollbar(add = true, isScrollElement = true, cssProperty = 'padding-right') {
for (const el of this) {
// Compensate scrollbar
const scrollableEl = isScrollElement ? el : $(el).parent().closestScrollable()[0];
const isRTL = scrollableEl.matches(".o_rtl");
if (isRTL) {
cssProperty = cssProperty.replace("right", "left");
}
el.style.removeProperty(cssProperty);
if (!add) {
return;
}
const scrollableEl = isScrollElement ? el : $(el).parent().closestScrollable()[0];
const style = window.getComputedStyle(el);
const borderLeftWidth = parseInt(style.borderLeftWidth.replace('px', ''));
const borderRightWidth = parseInt(style.borderRightWidth.replace('px', ''));
@@ -46,6 +46,13 @@ publicWidget.registry.Channel = publicWidget.Widget.extend({
}
return this._super.apply(this, arguments);
},
/**
* @override
*/
destroy: function () {
this.el.classList.add('d-none');
this._super(...arguments);
},
//--------------------------------------------------------------------------
// Private
@@ -15,12 +15,6 @@ options.registry.Channel = options.Class.extend({
await this._super(...arguments);
this.publicChannels = await this._getPublicChannels();
},
/**
* @override
*/
cleanForSave: function () {
this.$target.addClass('d-none');
},
/**
* If we have already created channels => select the first one
* else => modal prompt (create a new channel)