diff --git a/account_chart_update/__manifest__.py b/account_chart_update/__manifest__.py index 07708d61..4f15d87c 100644 --- a/account_chart_update/__manifest__.py +++ b/account_chart_update/__manifest__.py @@ -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, " diff --git a/account_chart_update/static/description/index.html b/account_chart_update/static/description/index.html index 3f777fe2..f8e62307 100644 --- a/account_chart_update/static/description/index.html +++ b/account_chart_update/static/description/index.html @@ -396,8 +396,10 @@ positions that got modified on the template.
The wizard, accesible from Accounting > Settings > Update chart of accounts, -lets the user select what kind of objects must be checked/updated, and whether +
The wizard, accessible from +Accounting > Configuration > Settings > Update chart template, +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.
It will display all the objects to be created / updated / deactivated with some
information about the detected differences, and allow the user to exclude
diff --git a/account_chart_update/tests/test_account_chart_update.py b/account_chart_update/tests/test_account_chart_update.py
index 7b2c4b63..ed8628d5 100644
--- a/account_chart_update/tests/test_account_chart_update.py
+++ b/account_chart_update/tests/test_account_chart_update.py
@@ -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()
diff --git a/account_chart_update/wizard/wizard_chart_update.py b/account_chart_update/wizard/wizard_chart_update.py
index f5693b47..67c71b72 100644
--- a/account_chart_update/wizard/wizard_chart_update.py
+++ b/account_chart_update/wizard/wizard_chart_update.py
@@ -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
diff --git a/account_chart_update/wizard/wizard_chart_update_view.xml b/account_chart_update/wizard/wizard_chart_update_view.xml
index 94bf2c59..3f6aa3ea 100644
--- a/account_chart_update/wizard/wizard_chart_update_view.xml
+++ b/account_chart_update/wizard/wizard_chart_update_view.xml
@@ -42,6 +42,7 @@
Here you can set the matching order.
+