2
0

account_chart_update: allows to set the matching value order

It allows to set the order of matching for Account, Taxes and
Fiscal position.
This commit is contained in:
ernesto 2019-01-29 12:07:29 -05:00 committed by Luis J. Salvatierra
parent 0fc24c8a74
commit 203c45b253
5 changed files with 453 additions and 39 deletions

View File

@ -8,7 +8,7 @@
{
'name': "Detect changes and update the Account Chart from a template",
"summary": "Wizard to update a company's account chart from a template",
'version': "11.0.1.1.0",
'version': "11.0.2.1.0",
'author': "Tecnativa, "
"BCIM, "
"Okia, "

View File

@ -396,8 +396,10 @@ positions that got modified on the template.</li>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#id1">Usage</a></h1>
<p>The wizard, accesible from <em>Accounting &gt; Settings &gt; Update chart of accounts</em>,
lets the user select what kind of objects must be checked/updated, and whether
<p>The wizard, accessible from
<em>Accounting &gt; Configuration &gt; Settings &gt; Update chart template</em>,
lets the user select what kind of objects must be checked/updated, allows to
set the order of fields matching to be checked/updated and whether
old records must be checked for changes and updates.</p>
<p>It will display all the objects to be created / updated / deactivated with some
information about the detected differences, and allow the user to exclude

View File

@ -46,6 +46,12 @@ class TestAccountChartUpdate(common.HttpCase):
self._create_xml_id(record)
return record
def _get_model_data(self, record):
return self.env['ir.model.data'].search([
('model', '=', record._name),
('res_id', '=', record.id),
])
def setUp(self):
super(TestAccountChartUpdate, self).setUp()
# Make sure user is in English
@ -355,3 +361,124 @@ class TestAccountChartUpdate(common.HttpCase):
self.assertEqual(wizard.rejected_new_account_number, 1)
self.assertEqual(wizard.new_accounts, 0)
wizard.unlink()
def test_matching(self):
# Test XML-ID matching
self.tax_template.name = "Test 1 tax name changed"
self.tax_template.description = "Test tax 1 description changed"
self.account_template.code = "200000"
self.fp_template.name = "Test 1 fp name changed"
wizard = self.wizard_obj.create(self.wizard_vals)
wizard.action_find_records()
self.assertEqual(wizard.tax_ids.tax_id, self.tax_template)
self.assertEqual(wizard.tax_ids.type, 'updated')
self.assertEqual(wizard.account_ids.account_id, self.account_template)
self.assertEqual(wizard.account_ids.type, 'updated')
self.assertTrue(wizard.fiscal_position_ids.type, 'updated')
self.assertEqual(wizard.fiscal_position_ids.fiscal_position_id,
self.fp_template)
wizard.action_update_records()
self.assertEqual(wizard.updated_taxes, 1)
self.assertEqual(wizard.updated_accounts, 1)
self.assertEqual(wizard.updated_fps, 1)
self.assertEqual(self.tax.name, self.tax_template.name)
self.assertEqual(self.tax.description, self.tax_template.description)
self.assertEqual(self.account.code, self.account_template.code)
self.assertEqual(self.fp.name, self.fp_template.name)
wizard.unlink()
# Test match by another field, there is no match by XML-ID
self._get_model_data(self.tax).unlink()
self._get_model_data(self.account).unlink()
self._get_model_data(self.fp).unlink()
self.tax_template.description = "Test 2 tax description changed"
self.account_template.name = "Test 2 account name changed"
self.fp_template.note = "Test 2 fp note changed"
wizard = self.wizard_obj.create(self.wizard_vals)
wizard.action_find_records()
self.assertEqual(wizard.tax_ids.tax_id, self.tax_template)
self.assertEqual(wizard.tax_ids.type, 'updated')
self.assertEqual(wizard.account_ids.account_id, self.account_template)
self.assertEqual(wizard.account_ids.type, 'updated')
self.assertTrue(wizard.fiscal_position_ids.type, 'updated')
self.assertEqual(wizard.fiscal_position_ids.fiscal_position_id,
self.fp_template)
wizard.action_update_records()
self.assertEqual(wizard.updated_taxes, 1)
self.assertEqual(wizard.updated_accounts, 1)
self.assertEqual(wizard.updated_fps, 1)
self.assertEqual(self.tax.description, self.tax_template.description)
self.assertEqual(self.account.name, self.account_template.name)
self.assertEqual(self.fp.note, self.fp_template.note)
wizard.unlink()
# Test match by name, there is no match by XML-ID or by code
self.account_template.code = "300000"
wizard = self.wizard_obj.create(self.wizard_vals)
wizard.action_find_records()
self.assertEqual(wizard.account_ids.account_id, self.account_template)
self.assertEqual(wizard.account_ids.type, 'updated')
wizard.action_update_records()
self.assertEqual(wizard.updated_accounts, 1)
self.assertEqual(self.account.code, self.account_template.code)
wizard.unlink()
# Test 1 recreate XML-ID
self.tax_template.description = "Test 4 tax description changed"
self.account_template.name = "Test 4 account name changed"
self.fp_template.note = "Test 4 fp note changed"
self.wizard_vals.update(recreate_xml_ids=True)
wizard = self.wizard_obj.create(self.wizard_vals)
wizard.action_find_records()
self.assertEqual(wizard.tax_ids.tax_id, self.tax_template)
self.assertEqual(wizard.tax_ids.type, 'updated')
self.assertEqual(wizard.account_ids.account_id, self.account_template)
self.assertEqual(wizard.account_ids.type, 'updated')
self.assertTrue(wizard.fiscal_position_ids.type, 'updated')
self.assertEqual(wizard.fiscal_position_ids.fiscal_position_id,
self.fp_template)
# There is no XML-ID
self.assertFalse(list(self.tax.get_xml_id().values())[0])
self.assertFalse(list(self.account.get_xml_id().values())[0])
self.assertFalse(list(self.fp.get_xml_id().values())[0])
# Update for recreating XML-ID
wizard.action_update_records()
self.assertEqual(wizard.updated_taxes, 1)
self.assertEqual(wizard.updated_accounts, 1)
self.assertEqual(wizard.updated_fps, 1)
self.assertEqual(self.tax.description, self.tax_template.description)
self.assertEqual(self.account.name, self.account_template.name)
self.assertEqual(self.fp.note, self.fp_template.note)
# There is XML-ID now
self.assertTrue(list(self.tax.get_xml_id().values())[0])
self.assertTrue(list(self.account.get_xml_id().values())[0])
self.assertTrue(list(self.fp.get_xml_id().values())[0])
wizard.unlink()
# Test 2 recreate XML-ID
self._get_model_data(self.tax).unlink()
self._get_model_data(self.account).unlink()
self._get_model_data(self.fp).unlink()
wizard = self.wizard_obj.create(self.wizard_vals)
wizard.action_find_records()
self.assertEqual(wizard.tax_ids.tax_id, self.tax_template)
self.assertEqual(wizard.tax_ids.type, 'updated')
self.assertEqual(wizard.account_ids.account_id, self.account_template)
self.assertEqual(wizard.account_ids.type, 'updated')
self.assertTrue(wizard.fiscal_position_ids.type, 'updated')
self.assertEqual(wizard.fiscal_position_ids.fiscal_position_id,
self.fp_template)
# There is no XML-ID
self.assertFalse(list(self.tax.get_xml_id().values())[0])
self.assertFalse(list(self.account.get_xml_id().values())[0])
self.assertFalse(list(self.fp.get_xml_id().values())[0])
# Update for recreating XML-ID
wizard.action_update_records()
self.assertEqual(wizard.updated_taxes, 1)
self.assertEqual(wizard.updated_accounts, 1)
self.assertEqual(wizard.updated_fps, 1)
# There is XML-ID now
self.assertTrue(list(self.tax.get_xml_id().values())[0])
self.assertTrue(list(self.account.get_xml_id().values())[0])
self.assertTrue(list(self.fp.get_xml_id().values())[0])
wizard.unlink()

View File

@ -62,6 +62,9 @@ class WizardUpdateChartsAccounts(models.TransientModel):
string="Continue on errors", default=False,
help="If set, the wizard will continue to the next step even if "
"there are minor errors.")
recreate_xml_ids = fields.Boolean(
string="Recreate missing XML-IDs",
)
tax_ids = fields.One2many(
comodel_name='wizard.update.charts.accounts.tax', ondelete='cascade',
inverse_name='update_chart_wizard_id', string='Taxes')
@ -117,6 +120,24 @@ class WizardUpdateChartsAccounts(models.TransientModel):
domain=lambda self: self._domain_fp_field_ids(),
default=lambda self: self._default_fp_field_ids(),
)
tax_matching_ids = fields.One2many(
comodel_name="wizard.tax.matching",
inverse_name="update_chart_wizard_id",
string="Taxes matching",
default=lambda self: self._default_tax_matching_ids(),
)
account_matching_ids = fields.One2many(
comodel_name="wizard.account.matching",
inverse_name="update_chart_wizard_id",
string="Taxes matching",
default=lambda self: self._default_account_matching_ids(),
)
fp_matching_ids = fields.One2many(
comodel_name="wizard.fp.matching",
inverse_name="update_chart_wizard_id",
string="Taxes matching",
default=lambda self: self._default_fp_matching_ids(),
)
def _domain_per_name(self, name):
return [
@ -148,6 +169,32 @@ class WizardUpdateChartsAccounts(models.TransientModel):
self._domain_fp_field_ids()
)]
def _get_matching_ids(self, model_name, ordered_opts):
vals = []
for seq, opt in enumerate(ordered_opts, 1):
vals.append((0, False, {'sequence': seq, 'matching_value': opt}))
all_options = self.env[model_name]._get_matching_selection()
all_options = map(lambda x: x[0], all_options)
all_options = list(set(all_options) - set(ordered_opts))
for seq, opt in enumerate(all_options, len(ordered_opts) + 1):
vals.append((0, False, {'sequence': seq, 'matching_value': opt}))
return vals
def _default_fp_matching_ids(self):
ordered_opts = ['xml_id', 'name']
return self._get_matching_ids('wizard.fp.matching', ordered_opts)
def _default_tax_matching_ids(self):
ordered_opts = ['xml_id', 'description', 'name']
return self._get_matching_ids('wizard.tax.matching', ordered_opts)
def _default_account_matching_ids(self):
ordered_opts = ['xml_id', 'code', 'name']
return self._get_matching_ids('wizard.account.matching', ordered_opts)
@api.model
def _get_lang_selection_options(self):
"""Gets the available languages for the selection."""
@ -298,33 +345,41 @@ class WizardUpdateChartsAccounts(models.TransientModel):
self.state = 'done'
return self._reopen()
def _get_real_xml_name(self, template):
[external_id] = template.get_external_id().values()
(name, module) = external_id.split('.')
return "%s.%d_%s" % (name, self.company_id.id, module)
@api.multi
@tools.ormcache("templates")
def find_tax_by_templates(self, templates):
"""Find a tax that matches the template."""
# search inactive taxes too, to avoid re-creating
# taxes that have been deactivated before
Tax = self.env['account.tax'].with_context(active_test=False)
result = Tax
tax_model = self.env['account.tax'].with_context(active_test=False)
for template in templates:
single = Tax
criteria = (
("name", "=", template.name),
("description", "=", template.name),
("name", "=", template.description),
("description", "=", template.description),
)
for domain in criteria:
if single:
break
if domain[2]:
single = Tax.search(
[domain,
("company_id", "=", self.company_id.id),
("type_tax_use", "=", template.type_tax_use)],
limit=1)
result |= single
return result[:1].id
for matching in self.tax_matching_ids.sorted('sequence'):
if matching.matching_value == 'xml_id':
real = self.env.ref(self._get_real_xml_name(template),
raise_if_not_found=False)
if not real:
continue
criteria = ('id', '=', real.id)
else:
field_name = matching.matching_value
if not template[field_name]:
continue
criteria = (field_name, '=', template[field_name])
result = tax_model.search([
criteria,
("company_id", "=", self.company_id.id),
("type_tax_use", "=", template.type_tax_use),
], limit=1)
if result:
return result.id
return False
@api.model
@tools.ormcache("code")
@ -336,19 +391,69 @@ class WizardUpdateChartsAccounts(models.TransientModel):
@tools.ormcache("templates")
def find_account_by_templates(self, templates):
"""Find an account that matches the template."""
return self.env['account.account'].search(
[('code', 'in',
list(map(self.padded_code, templates.mapped("code")))),
('company_id', '=', self.company_id.id)],
).id
account_model = self.env['account.account']
for matching in self.account_matching_ids.sorted('sequence'):
if matching.matching_value == 'xml_id':
real = self.env['account.account']
for template in templates:
try:
real |= self.env.ref(self._get_real_xml_name(template))
except:
pass
if not real:
continue
criteria = ('id', 'in', real.ids)
elif matching.matching_value == 'code':
codes = templates.mapped("code")
if not codes:
continue
criteria = ('code', 'in', list(map(self.padded_code, codes)))
else:
field_name = matching.matching_value
field_values = templates.mapped(field_name)
if not field_values:
continue
criteria = (field_name, 'in', field_values)
result = account_model.search(
[criteria, ('company_id', '=', self.company_id.id)])
if result:
return result.id
return False
@api.multi
@tools.ormcache("templates")
def find_fp_by_templates(self, templates):
"""Find a real fiscal position from a template."""
return self.env['account.fiscal.position'].search(
[('name', 'in', templates.mapped("name")),
('company_id', '=', self.company_id.id)], limit=1).id
fp_model = self.env['account.fiscal.position']
for matching in self.fp_matching_ids.sorted('sequence'):
if matching.matching_value == 'xml_id':
real = self.env['account.fiscal.position']
for template in templates:
try:
real |= self.env.ref(self._get_real_xml_name(template))
except:
pass
if not real:
continue
criteria = ('id', 'in', real.ids)
else:
field_name = matching.matching_value
field_values = templates.mapped(field_name)
if not field_values:
continue
criteria = (field_name, 'in', field_values)
result = fp_model.search([
criteria, ('company_id', '=', self.company_id.id)
], limit=1)
if result:
return result.id
return False
@api.multi
@tools.ormcache("templates", "current_fp_accounts")
@ -420,7 +525,6 @@ class WizardUpdateChartsAccounts(models.TransientModel):
},
"account.account.template": {
"chart_template_id",
"code",
},
"account.fiscal.position.template": {
"chart_template_id",
@ -483,8 +587,13 @@ class WizardUpdateChartsAccounts(models.TransientModel):
if expected is not None:
if expected != [] and expected != real[key]:
result[key] = expected
elif template[key] != real[key]:
result[key] = template[key]
else:
template_value = template[key]
if template._name == "account.account.template" \
and key == 'code':
template_value = self.padded_code(template['code'])
if template_value != real[key]:
result[key] = template_value
# Avoid to cache recordset references
if isinstance(real._fields[key], fields.Many2many):
result[key] = [(6, 0, result[key].ids)]
@ -521,6 +630,13 @@ class WizardUpdateChartsAccounts(models.TransientModel):
result.append(_("Tax is disabled."))
return "\n".join(result)
def missing_xml_id(self, real_obj):
return not self.env['ir.model.data'].search([
('res_id', '=', real_obj.id),
('model', '=', real_obj._name),
('module', '!=', '__export__'),
])
@api.multi
def _find_taxes(self):
"""Search for, and load, tax templates to create/update/delete."""
@ -544,6 +660,10 @@ class WizardUpdateChartsAccounts(models.TransientModel):
# Check the tax for changes
tax = self.env['account.tax'].browse(tax_id)
notes = self.diff_notes(template, tax)
if self.recreate_xml_ids and self.missing_xml_id(tax):
notes += (notes and "\n" or "") + _("Missing XML-ID.")
if notes:
# Tax to be updated
self.tax_ids.create({
@ -586,6 +706,10 @@ class WizardUpdateChartsAccounts(models.TransientModel):
# Check the account for changes
account = self.env['account.account'].browse(account_id)
notes = self.diff_notes(template, account)
if self.recreate_xml_ids and self.missing_xml_id(account):
notes += (notes and "\n" or "") + _("Missing XML-ID.")
if notes:
# Account to be updated
self.account_ids.create({
@ -620,6 +744,10 @@ class WizardUpdateChartsAccounts(models.TransientModel):
# Check the fiscal position for changes
fp = self.env['account.fiscal.position'].browse(fp_id)
notes = self.diff_notes(template, fp)
if self.recreate_xml_ids and self.missing_xml_id(fp):
notes += (notes and "\n" or "") + _("Missing XML-ID.")
if notes:
# Fiscal position template to be updated
wiz_fp.create({
@ -630,6 +758,27 @@ class WizardUpdateChartsAccounts(models.TransientModel):
'notes': notes,
})
def recreate_xml_id(self, template, real_obj):
ir_model_data = self.env['ir.model.data']
template_xmlid = ir_model_data.search([
('model', '=', template._name),
('res_id', '=', template.id),
])
new_xml_id = "%d_%s" % (self.company_id.id, template_xmlid.name)
real_xmlid = ir_model_data.search([
('model', '=', real_obj._name),
('res_id', '=', real_obj.id),
], limit=1)
if real_xmlid:
real_xmlid.unlink()
template_xmlid.copy({
'model': real_obj._name,
'res_id': real_obj.id,
'name': new_xml_id,
'noupdate': True,
})
@api.multi
def _update_taxes(self):
"""Process taxes to create/update/deactivate."""
@ -651,7 +800,11 @@ class WizardUpdateChartsAccounts(models.TransientModel):
if key in {'account_id', 'refund_account_id'}:
continue
tax[key] = value
_logger.info(_("Updated tax %s."), "'%s'" % template.name)
_logger.info(_("Updated tax %s."), "'%s'" % template.name)
if self.recreate_xml_ids and self.missing_xml_id(tax):
self.recreate_xml_id(template, tax)
_logger.info(_("Updated tax %s. (Recreated XML-IDs)"),
"'%s'" % template.name)
@api.multi
def _update_accounts(self):
@ -701,6 +854,14 @@ class WizardUpdateChartsAccounts(models.TransientModel):
_("Updated account %s."),
"'%s - %s'" % (account.code, account.name),
)
if self.recreate_xml_ids \
and self.missing_xml_id(account):
self.recreate_xml_id(template, account)
_logger.info(
_("Updated account %s. (Recreated XML-ID)"),
"'%s - %s'" % (account.code, account.name),
)
except Exception:
self.rejected_updated_account_number += 1
if config['test_enable']:
@ -772,13 +933,22 @@ class WizardUpdateChartsAccounts(models.TransientModel):
self.company_id, template, 'account.fiscal.position',
self._prepare_fp_vals(template),
)
_logger.info(
_("Created fiscal position %s."),
"'%s'" % template.name,
)
else:
# Update the given fiscal position
for key, value in self.diff_fields(template, fp).items():
fp[key] = value
_logger.info(
_("Created or updated fiscal position %s."),
"'%s'" % template.name)
_logger.info(_("Updated fiscal position %s."),
"'%s'" % template.name)
if self.recreate_xml_ids and self.missing_xml_id(fp):
self.recreate_xml_id(template, fp)
_logger.info(
_("Updated fiscal position %s. (Recreated XML-ID)"),
"'%s'" % template.name,
)
class WizardUpdateChartsAccountsTax(models.TransientModel):
@ -850,3 +1020,66 @@ class WizardUpdateChartsAccountsFiscalPosition(models.TransientModel):
comodel_name='account.fiscal.position', required=False,
string='Fiscal position to update', ondelete='set null')
notes = fields.Text('Notes', readonly=True)
class WizardMatching(models.TransientModel):
_name = 'wizard.matching'
_order = 'sequence'
update_chart_wizard_id = fields.Many2one(
comodel_name='wizard.update.charts.accounts',
string='Update chart wizard',
required=True,
ondelete='cascade',
)
sequence = fields.Integer(
required=True,
default=1,
)
matching_value = fields.Selection(
selection="_get_matching_selection",
)
def _get_matching_selection(self):
return [('xml_id', 'XML-ID')]
def _selection_from_files(self, model_name, field_opts):
result = []
for opt in field_opts:
model = self.env[model_name]
desc = model._fields[opt].get_description(self.env)["string"]
result.append((opt, "%s (%s)" % (desc, opt)))
return result
class WizardTaxMatching(models.TransientModel):
_name = "wizard.tax.matching"
_inherit = "wizard.matching"
def _get_matching_selection(self):
vals = super(WizardTaxMatching, self)._get_matching_selection()
vals += self._selection_from_files('account.tax.template',
['description', 'name'])
return vals
class WizardAccountMatching(models.TransientModel):
_name = "wizard.account.matching"
_inherit = "wizard.matching"
def _get_matching_selection(self):
vals = super(WizardAccountMatching, self)._get_matching_selection()
vals += self._selection_from_files('account.account.template',
['code', 'name'])
return vals
class WizardFpMatching(models.TransientModel):
_name = 'wizard.fp.matching'
_inherit = "wizard.matching"
def _get_matching_selection(self):
vals = super(WizardFpMatching, self)._get_matching_selection()
vals += self._selection_from_files('account.fiscal.position.template',
['name'])
return vals

View File

@ -42,6 +42,7 @@
</group>
<group string="Other options" attrs="{'invisible':[('state','!=','init')]}">
<field name="continue_on_errors" />
<field name="recreate_xml_ids" />
</group>
</group>
<group>
@ -87,6 +88,57 @@
</page>
</notebook>
</page>
<page string="Matching"
attrs="{'invisible': [('update_tax', '=', False), ('update_account', '=', False), ('update_fiscal_position', '=', False)]}"
>
<h3>
<p>Here you can set the matching order.</p>
</h3>
<notebook>
<page string="Taxes"
name="page_matching_taxes"
attrs="{'invisible': [('update_tax', '=', False)]}"
>
<field name="tax_matching_ids">
<tree create="false" edit="false" default_order="sequence">
<field name="sequence" widget="handle"/>
<field name="matching_value"/>
</tree>
<form>
<field name="matching_value" readonly="1"/>
</form>
</field>
</page>
<page string="Accounts"
name="page_matching_accounts"
attrs="{'invisible': [('update_account', '=', False)]}"
>
<field name="account_matching_ids">
<tree create="false" edit="false" default_order="sequence">
<field name="sequence" widget="handle"/>
<field name="matching_value"/>
</tree>
<form>
<field name="matching_value" readonly="1"/>
</form>
</field>
</page>
<page string="Fiscal positions"
name="page_matching_fps"
attrs="{'invisible': [('update_fiscal_position', '=', False)]}"
>
<field name="fp_matching_ids">
<tree create="false" edit="false" default_order="sequence">
<field name="sequence" widget="handle"/>
<field name="matching_value"/>
</tree>
<form>
<field name="matching_value" readonly="1"/>
</form>
</field>
</page>
</notebook>
</page>
</notebook>
<group attrs="{'invisible':[('state','!=','ready'),]}"
string="Records to create/update">