summaryrefslogtreecommitdiff
path: root/addons/account/models/chart_template.py
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/account/models/chart_template.py
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/account/models/chart_template.py')
-rw-r--r--addons/account/models/chart_template.py1242
1 files changed, 1242 insertions, 0 deletions
diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py
new file mode 100644
index 00000000..ade616ce
--- /dev/null
+++ b/addons/account/models/chart_template.py
@@ -0,0 +1,1242 @@
+# -*- coding: utf-8 -*-
+
+from odoo.exceptions import AccessError
+from odoo import api, fields, models, _
+from odoo import SUPERUSER_ID
+from odoo.exceptions import UserError, ValidationError
+from odoo.http import request
+from odoo.addons.account.models.account_tax import TYPE_TAX_USE
+
+import logging
+
+_logger = logging.getLogger(__name__)
+
+def migrate_set_tags_and_taxes_updatable(cr, registry, module):
+ ''' This is a utility function used to manually set the flag noupdate to False on tags and account tax templates on localization modules
+ that need migration (for example in case of VAT report improvements)
+ '''
+ env = api.Environment(cr, SUPERUSER_ID, {})
+ xml_record_ids = env['ir.model.data'].search([
+ ('model', 'in', ['account.tax.template', 'account.account.tag']),
+ ('module', 'like', module)
+ ]).ids
+ if xml_record_ids:
+ cr.execute("update ir_model_data set noupdate = 'f' where id in %s", (tuple(xml_record_ids),))
+
+def preserve_existing_tags_on_taxes(cr, registry, module):
+ ''' This is a utility function used to preserve existing previous tags during upgrade of the module.'''
+ env = api.Environment(cr, SUPERUSER_ID, {})
+ xml_records = env['ir.model.data'].search([('model', '=', 'account.account.tag'), ('module', 'like', module)])
+ if xml_records:
+ cr.execute("update ir_model_data set noupdate = 't' where id in %s", [tuple(xml_records.ids)])
+
+# ---------------------------------------------------------------
+# Account Templates: Account, Tax, Tax Code and chart. + Wizard
+# ---------------------------------------------------------------
+
+
+class AccountGroupTemplate(models.Model):
+ _name = "account.group.template"
+ _description = 'Template for Account Groups'
+ _order = 'code_prefix_start'
+
+ parent_id = fields.Many2one('account.group.template', index=True, ondelete='cascade')
+ name = fields.Char(required=True)
+ code_prefix_start = fields.Char()
+ code_prefix_end = fields.Char()
+ chart_template_id = fields.Many2one('account.chart.template', string='Chart Template', required=True)
+
+
+class AccountAccountTemplate(models.Model):
+ _name = "account.account.template"
+ _description = 'Templates for Accounts'
+ _order = "code"
+
+ name = fields.Char(required=True, index=True)
+ currency_id = fields.Many2one('res.currency', string='Account Currency', help="Forces all moves for this account to have this secondary currency.")
+ code = fields.Char(size=64, required=True, index=True)
+ user_type_id = fields.Many2one('account.account.type', string='Type', required=True,
+ help="These types are defined according to your country. The type contains more information "\
+ "about the account and its specificities.")
+ reconcile = fields.Boolean(string='Allow Invoices & payments Matching', default=False,
+ help="Check this option if you want the user to reconcile entries in this account.")
+ note = fields.Text()
+ tax_ids = fields.Many2many('account.tax.template', 'account_account_template_tax_rel', 'account_id', 'tax_id', string='Default Taxes')
+ nocreate = fields.Boolean(string='Optional Create', default=False,
+ help="If checked, the new chart of accounts will not contain this by default.")
+ chart_template_id = fields.Many2one('account.chart.template', string='Chart Template',
+ help="This optional field allow you to link an account template to a specific chart template that may differ from the one its root parent belongs to. This allow you "
+ "to define chart templates that extend another and complete it with few new accounts (You don't need to define the whole structure that is common to both several times).")
+ tag_ids = fields.Many2many('account.account.tag', 'account_account_template_account_tag', string='Account tag', help="Optional tags you may want to assign for custom reporting")
+
+ @api.depends('name', 'code')
+ def name_get(self):
+ res = []
+ for record in self:
+ name = record.name
+ if record.code:
+ name = record.code + ' ' + name
+ res.append((record.id, name))
+ return res
+
+
+class AccountChartTemplate(models.Model):
+ _name = "account.chart.template"
+ _description = "Account Chart Template"
+
+ name = fields.Char(required=True)
+ parent_id = fields.Many2one('account.chart.template', string='Parent Chart Template')
+ code_digits = fields.Integer(string='# of Digits', required=True, default=6, help="No. of Digits to use for account code")
+ visible = fields.Boolean(string='Can be Visible?', default=True,
+ help="Set this to False if you don't want this template to be used actively in the wizard that generate Chart of Accounts from "
+ "templates, this is useful when you want to generate accounts of this template only when loading its child template.")
+ currency_id = fields.Many2one('res.currency', string='Currency', required=True)
+ use_anglo_saxon = fields.Boolean(string="Use Anglo-Saxon accounting", default=False)
+ complete_tax_set = fields.Boolean(string='Complete Set of Taxes', default=True,
+ help="This boolean helps you to choose if you want to propose to the user to encode the sale and purchase rates or choose from list "
+ "of taxes. This last choice assumes that the set of tax defined on this template is complete")
+ account_ids = fields.One2many('account.account.template', 'chart_template_id', string='Associated Account Templates')
+ tax_template_ids = fields.One2many('account.tax.template', 'chart_template_id', string='Tax Template List',
+ help='List of all the taxes that have to be installed by the wizard')
+ bank_account_code_prefix = fields.Char(string='Prefix of the bank accounts', required=True)
+ cash_account_code_prefix = fields.Char(string='Prefix of the main cash accounts', required=True)
+ transfer_account_code_prefix = fields.Char(string='Prefix of the main transfer accounts', required=True)
+ income_currency_exchange_account_id = fields.Many2one('account.account.template',
+ string="Gain Exchange Rate Account", domain=[('internal_type', '=', 'other'), ('deprecated', '=', False)])
+ expense_currency_exchange_account_id = fields.Many2one('account.account.template',
+ string="Loss Exchange Rate Account", domain=[('internal_type', '=', 'other'), ('deprecated', '=', False)])
+ account_journal_suspense_account_id = fields.Many2one('account.account.template', string='Journal Suspense Account')
+ default_cash_difference_income_account_id = fields.Many2one('account.account.template', string="Cash Difference Income Account")
+ default_cash_difference_expense_account_id = fields.Many2one('account.account.template', string="Cash Difference Expense Account")
+ default_pos_receivable_account_id = fields.Many2one('account.account.template', string="PoS receivable account")
+ property_account_receivable_id = fields.Many2one('account.account.template', string='Receivable Account')
+ property_account_payable_id = fields.Many2one('account.account.template', string='Payable Account')
+ property_account_expense_categ_id = fields.Many2one('account.account.template', string='Category of Expense Account')
+ property_account_income_categ_id = fields.Many2one('account.account.template', string='Category of Income Account')
+ property_account_expense_id = fields.Many2one('account.account.template', string='Expense Account on Product Template')
+ property_account_income_id = fields.Many2one('account.account.template', string='Income Account on Product Template')
+ property_stock_account_input_categ_id = fields.Many2one('account.account.template', string="Input Account for Stock Valuation")
+ property_stock_account_output_categ_id = fields.Many2one('account.account.template', string="Output Account for Stock Valuation")
+ property_stock_valuation_account_id = fields.Many2one('account.account.template', string="Account Template for Stock Valuation")
+ property_tax_payable_account_id = fields.Many2one('account.account.template', string="Tax current account (payable)")
+ property_tax_receivable_account_id = fields.Many2one('account.account.template', string="Tax current account (receivable)")
+ property_advance_tax_payment_account_id = fields.Many2one('account.account.template', string="Advance tax payment account")
+ property_cash_basis_base_account_id = fields.Many2one(
+ comodel_name='account.account.template',
+ domain=[('deprecated', '=', False)],
+ string="Base Tax Received Account",
+ help="Account that will be set on lines created in cash basis journal entry and used to keep track of the "
+ "tax base amount.")
+
+ @api.model
+ def _prepare_transfer_account_template(self, prefix=None):
+ ''' Prepare values to create the transfer account that is an intermediary account used when moving money
+ from a liquidity account to another.
+
+ :return: A dictionary of values to create a new account.account.
+ '''
+ digits = self.code_digits
+ prefix = prefix or self.transfer_account_code_prefix or ''
+ # Flatten the hierarchy of chart templates.
+ chart_template = self
+ chart_templates = self
+ while chart_template.parent_id:
+ chart_templates += chart_template.parent_id
+ chart_template = chart_template.parent_id
+ new_code = ''
+ for num in range(1, 100):
+ new_code = str(prefix.ljust(digits - 1, '0')) + str(num)
+ rec = self.env['account.account.template'].search(
+ [('code', '=', new_code), ('chart_template_id', 'in', chart_templates.ids)], limit=1)
+ if not rec:
+ break
+ else:
+ raise UserError(_('Cannot generate an unused account code.'))
+ current_assets_type = self.env.ref('account.data_account_type_current_assets', raise_if_not_found=False)
+ return {
+ 'name': _('Liquidity Transfer'),
+ 'code': new_code,
+ 'user_type_id': current_assets_type and current_assets_type.id or False,
+ 'reconcile': True,
+ 'chart_template_id': self.id,
+ }
+
+ @api.model
+ def _create_liquidity_journal_suspense_account(self, company, code_digits):
+ return self.env['account.account'].create({
+ 'name': _("Bank Suspense Account"),
+ 'code': self.env['account.account']._search_new_account_code(company, code_digits, company.bank_account_code_prefix or ''),
+ 'user_type_id': self.env.ref('account.data_account_type_current_liabilities').id,
+ 'company_id': company.id,
+ })
+
+ def try_loading(self, company=False):
+ """ Installs this chart of accounts for the current company if not chart
+ of accounts had been created for it yet.
+ """
+ # do not use `request.env` here, it can cause deadlocks
+ if not company:
+ if request and hasattr(request, 'allowed_company_ids'):
+ company = self.env['res.company'].browse(request.allowed_company_ids[0])
+ else:
+ company = self.env.company
+ # If we don't have any chart of account on this company, install this chart of account
+ if not company.chart_template_id and not self.existing_accounting(company):
+ for template in self:
+ template.with_context(default_company_id=company.id)._load(15.0, 15.0, company)
+
+
+ def _load(self, sale_tax_rate, purchase_tax_rate, company):
+ """ Installs this chart of accounts on the current company, replacing
+ the existing one if it had already one defined. If some accounting entries
+ had already been made, this function fails instead, triggering a UserError.
+
+ Also, note that this function can only be run by someone with administration
+ rights.
+ """
+ self.ensure_one()
+ # do not use `request.env` here, it can cause deadlocks
+ # Ensure everything is translated to the company's language, not the user's one.
+ self = self.with_context(lang=company.partner_id.lang).with_company(company)
+ if not self.env.is_admin():
+ raise AccessError(_("Only administrators can load a chart of accounts"))
+
+ existing_accounts = self.env['account.account'].search([('company_id', '=', company.id)])
+ if existing_accounts:
+ # we tolerate switching from accounting package (localization module) as long as there isn't yet any accounting
+ # entries created for the company.
+ if self.existing_accounting(company):
+ raise UserError(_('Could not install new chart of account as there are already accounting entries existing.'))
+
+ # delete accounting properties
+ prop_values = ['account.account,%s' % (account_id,) for account_id in existing_accounts.ids]
+ existing_journals = self.env['account.journal'].search([('company_id', '=', company.id)])
+ if existing_journals:
+ prop_values.extend(['account.journal,%s' % (journal_id,) for journal_id in existing_journals.ids])
+ self.env['ir.property'].sudo().search(
+ [('value_reference', 'in', prop_values)]
+ ).unlink()
+
+ # delete account, journal, tax, fiscal position and reconciliation model
+ models_to_delete = ['account.reconcile.model', 'account.fiscal.position', 'account.tax', 'account.move', 'account.journal', 'account.group']
+ for model in models_to_delete:
+ res = self.env[model].sudo().search([('company_id', '=', company.id)])
+ if len(res):
+ res.unlink()
+ existing_accounts.unlink()
+
+ company.write({'currency_id': self.currency_id.id,
+ 'anglo_saxon_accounting': self.use_anglo_saxon,
+ 'bank_account_code_prefix': self.bank_account_code_prefix,
+ 'cash_account_code_prefix': self.cash_account_code_prefix,
+ 'transfer_account_code_prefix': self.transfer_account_code_prefix,
+ 'chart_template_id': self.id
+ })
+
+ #set the coa currency to active
+ self.currency_id.write({'active': True})
+
+ # When we install the CoA of first company, set the currency to price types and pricelists
+ if company.id == 1:
+ for reference in ['product.list_price', 'product.standard_price', 'product.list0']:
+ try:
+ tmp2 = self.env.ref(reference).write({'currency_id': self.currency_id.id})
+ except ValueError:
+ pass
+
+ # If the floats for sale/purchase rates have been filled, create templates from them
+ self._create_tax_templates_from_rates(company.id, sale_tax_rate, purchase_tax_rate)
+
+ # Install all the templates objects and generate the real objects
+ acc_template_ref, taxes_ref = self._install_template(company, code_digits=self.code_digits)
+
+ # Set default cash difference account on company
+ if not company.account_journal_suspense_account_id:
+ company.account_journal_suspense_account_id = self._create_liquidity_journal_suspense_account(company, self.code_digits)
+
+ if not company.default_cash_difference_expense_account_id:
+ company.default_cash_difference_expense_account_id = self.env['account.account'].create({
+ 'name': _('Cash Difference Loss'),
+ 'code': self.env['account.account']._search_new_account_code(company, self.code_digits, '999'),
+ 'user_type_id': self.env.ref('account.data_account_type_expenses').id,
+ 'tag_ids': [(6, 0, self.env.ref('account.account_tag_investing').ids)],
+ 'company_id': company.id,
+ })
+
+ if not company.default_cash_difference_income_account_id:
+ company.default_cash_difference_income_account_id = self.env['account.account'].create({
+ 'name': _('Cash Difference Gain'),
+ 'code': self.env['account.account']._search_new_account_code(company, self.code_digits, '999'),
+ 'user_type_id': self.env.ref('account.data_account_type_revenue').id,
+ 'tag_ids': [(6, 0, self.env.ref('account.account_tag_investing').ids)],
+ 'company_id': company.id,
+ })
+
+ # Set the transfer account on the company
+ company.transfer_account_id = self.env['account.account'].search([
+ ('code', '=like', self.transfer_account_code_prefix + '%'), ('company_id', '=', company.id)], limit=1)
+
+ # Create Bank journals
+ self._create_bank_journals(company, acc_template_ref)
+
+ # Create the current year earning account if it wasn't present in the CoA
+ company.get_unaffected_earnings_account()
+
+ # set the default taxes on the company
+ company.account_sale_tax_id = self.env['account.tax'].search([('type_tax_use', 'in', ('sale', 'all')), ('company_id', '=', company.id)], limit=1).id
+ company.account_purchase_tax_id = self.env['account.tax'].search([('type_tax_use', 'in', ('purchase', 'all')), ('company_id', '=', company.id)], limit=1).id
+
+ return {}
+
+ @api.model
+ def existing_accounting(self, company_id):
+ """ Returns True iff some accounting entries have already been made for
+ the provided company (meaning hence that its chart of accounts cannot
+ be changed anymore).
+ """
+ model_to_check = ['account.move.line', 'account.payment', 'account.bank.statement']
+ for model in model_to_check:
+ if self.env[model].sudo().search([('company_id', '=', company_id.id)], limit=1):
+ return True
+ if self.env['account.move'].sudo().search([('company_id', '=', company_id.id), ('name', '!=', '/')], limit=1):
+ return True
+ return False
+
+ def _create_tax_templates_from_rates(self, company_id, sale_tax_rate, purchase_tax_rate):
+ '''
+ This function checks if this chart template is configured as containing a full set of taxes, and if
+ it's not the case, it creates the templates for account.tax object accordingly to the provided sale/purchase rates.
+ Then it saves the new tax templates as default taxes to use for this chart template.
+
+ :param company_id: id of the company for which the wizard is running
+ :param sale_tax_rate: the rate to use for created sales tax
+ :param purchase_tax_rate: the rate to use for created purchase tax
+ :return: True
+ '''
+ self.ensure_one()
+ obj_tax_temp = self.env['account.tax.template']
+ all_parents = self._get_chart_parent_ids()
+ # create tax templates from purchase_tax_rate and sale_tax_rate fields
+ if not self.complete_tax_set:
+ ref_taxs = obj_tax_temp.search([('type_tax_use', '=', 'sale'), ('chart_template_id', 'in', all_parents)], order="sequence, id desc", limit=1)
+ ref_taxs.write({'amount': sale_tax_rate, 'name': _('Tax %.2f%%') % sale_tax_rate, 'description': '%.2f%%' % sale_tax_rate})
+ ref_taxs = obj_tax_temp.search([('type_tax_use', '=', 'purchase'), ('chart_template_id', 'in', all_parents)], order="sequence, id desc", limit=1)
+ ref_taxs.write({'amount': purchase_tax_rate, 'name': _('Tax %.2f%%') % purchase_tax_rate, 'description': '%.2f%%' % purchase_tax_rate})
+ return True
+
+ def _get_chart_parent_ids(self):
+ """ Returns the IDs of all ancestor charts, including the chart itself.
+ (inverse of child_of operator)
+
+ :return: the IDS of all ancestor charts, including the chart itself.
+ """
+ chart_template = self
+ result = [chart_template.id]
+ while chart_template.parent_id:
+ chart_template = chart_template.parent_id
+ result.append(chart_template.id)
+ return result
+
+ def _create_bank_journals(self, company, acc_template_ref):
+ '''
+ This function creates bank journals and their account for each line
+ data returned by the function _get_default_bank_journals_data.
+
+ :param company: the company for which the wizard is running.
+ :param acc_template_ref: the dictionary containing the mapping between the ids of account templates and the ids
+ of the accounts that have been generated from them.
+ '''
+ self.ensure_one()
+ bank_journals = self.env['account.journal']
+ # Create the journals that will trigger the account.account creation
+ for acc in self._get_default_bank_journals_data():
+ bank_journals += self.env['account.journal'].create({
+ 'name': acc['acc_name'],
+ 'type': acc['account_type'],
+ 'company_id': company.id,
+ 'currency_id': acc.get('currency_id', self.env['res.currency']).id,
+ 'sequence': 10,
+ })
+
+ return bank_journals
+
+ @api.model
+ def _get_default_bank_journals_data(self):
+ """ Returns the data needed to create the default bank journals when
+ installing this chart of accounts, in the form of a list of dictionaries.
+ The allowed keys in these dictionaries are:
+ - acc_name: string (mandatory)
+ - account_type: 'cash' or 'bank' (mandatory)
+ - currency_id (optional, only to be specified if != company.currency_id)
+ """
+ return [{'acc_name': _('Cash'), 'account_type': 'cash'}, {'acc_name': _('Bank'), 'account_type': 'bank'}]
+
+ def open_select_template_wizard(self):
+ # Add action to open wizard to select between several templates
+ if not self.company_id.chart_template_id:
+ todo = self.env['ir.actions.todo']
+ action_rec = self.env['ir.model.data'].xmlid_to_object('account.action_wizard_multi_chart')
+ if action_rec:
+ todo.create({'action_id': action_rec.id, 'name': _('Choose Accounting Template')})
+ return True
+
+ @api.model
+ def _prepare_transfer_account_for_direct_creation(self, name, company):
+ """ Prepare values to create a transfer account directly, based on the
+ method _prepare_transfer_account_template().
+
+ This is needed when dealing with installation of payment modules
+ that requires the creation of their own transfer account.
+
+ :param name: The transfer account name.
+ :param company: The company owning this account.
+ :return: A dictionary of values to create a new account.account.
+ """
+ vals = self._prepare_transfer_account_template()
+ digits = self.code_digits or 6
+ prefix = self.transfer_account_code_prefix or ''
+ vals.update({
+ 'code': self.env['account.account']._search_new_account_code(company, digits, prefix),
+ 'name': name,
+ 'company_id': company.id,
+ })
+ del(vals['chart_template_id'])
+ return vals
+
+ @api.model
+ def generate_journals(self, acc_template_ref, company, journals_dict=None):
+ """
+ This method is used for creating journals.
+
+ :param acc_template_ref: Account templates reference.
+ :param company_id: company to generate journals for.
+ :returns: True
+ """
+ JournalObj = self.env['account.journal']
+ for vals_journal in self._prepare_all_journals(acc_template_ref, company, journals_dict=journals_dict):
+ journal = JournalObj.create(vals_journal)
+ if vals_journal['type'] == 'general' and vals_journal['code'] == _('EXCH'):
+ company.write({'currency_exchange_journal_id': journal.id})
+ if vals_journal['type'] == 'general' and vals_journal['code'] == _('CABA'):
+ company.write({'tax_cash_basis_journal_id': journal.id})
+ return True
+
+ def _prepare_all_journals(self, acc_template_ref, company, journals_dict=None):
+ def _get_default_account(journal_vals, type='debit'):
+ # Get the default accounts
+ default_account = False
+ if journal['type'] == 'sale':
+ default_account = acc_template_ref.get(self.property_account_income_categ_id.id)
+ elif journal['type'] == 'purchase':
+ default_account = acc_template_ref.get(self.property_account_expense_categ_id.id)
+
+ return default_account
+
+ journals = [{'name': _('Customer Invoices'), 'type': 'sale', 'code': _('INV'), 'favorite': True, 'color': 11, 'sequence': 5},
+ {'name': _('Vendor Bills'), 'type': 'purchase', 'code': _('BILL'), 'favorite': True, 'color': 11, 'sequence': 6},
+ {'name': _('Miscellaneous Operations'), 'type': 'general', 'code': _('MISC'), 'favorite': True, 'sequence': 7},
+ {'name': _('Exchange Difference'), 'type': 'general', 'code': _('EXCH'), 'favorite': False, 'sequence': 9},
+ {'name': _('Cash Basis Taxes'), 'type': 'general', 'code': _('CABA'), 'favorite': False, 'sequence': 10}]
+ if journals_dict != None:
+ journals.extend(journals_dict)
+
+ self.ensure_one()
+ journal_data = []
+ for journal in journals:
+ vals = {
+ 'type': journal['type'],
+ 'name': journal['name'],
+ 'code': journal['code'],
+ 'company_id': company.id,
+ 'default_account_id': _get_default_account(journal),
+ 'show_on_dashboard': journal['favorite'],
+ 'color': journal.get('color', False),
+ 'sequence': journal['sequence']
+ }
+ journal_data.append(vals)
+ return journal_data
+
+ def generate_properties(self, acc_template_ref, company):
+ """
+ This method used for creating properties.
+
+ :param acc_template_ref: Mapping between ids of account templates and real accounts created from them
+ :param company_id: company to generate properties for.
+ :returns: True
+ """
+ self.ensure_one()
+ PropertyObj = self.env['ir.property']
+ todo_list = [
+ ('property_account_receivable_id', 'res.partner'),
+ ('property_account_payable_id', 'res.partner'),
+ ('property_account_expense_categ_id', 'product.category'),
+ ('property_account_income_categ_id', 'product.category'),
+ ('property_account_expense_id', 'product.template'),
+ ('property_account_income_id', 'product.template'),
+ ('property_tax_payable_account_id', 'account.tax.group'),
+ ('property_tax_receivable_account_id', 'account.tax.group'),
+ ('property_advance_tax_payment_account_id', 'account.tax.group'),
+ ]
+ for field, model in todo_list:
+ account = self[field]
+ value = acc_template_ref[account.id] if account else False
+ if value:
+ PropertyObj._set_default(field, model, value, company=company)
+
+ stock_properties = [
+ 'property_stock_account_input_categ_id',
+ 'property_stock_account_output_categ_id',
+ 'property_stock_valuation_account_id',
+ ]
+ for stock_property in stock_properties:
+ account = getattr(self, stock_property)
+ value = account and acc_template_ref[account.id] or False
+ if value:
+ company.write({stock_property: value})
+ return True
+
+ def _install_template(self, company, code_digits=None, obj_wizard=None, acc_ref=None, taxes_ref=None):
+ """ Recursively load the template objects and create the real objects from them.
+
+ :param company: company the wizard is running for
+ :param code_digits: number of digits the accounts code should have in the COA
+ :param obj_wizard: the current wizard for generating the COA from the templates
+ :param acc_ref: Mapping between ids of account templates and real accounts created from them
+ :param taxes_ref: Mapping between ids of tax templates and real taxes created from them
+ :returns: tuple with a dictionary containing
+ * the mapping between the account template ids and the ids of the real accounts that have been generated
+ from them, as first item,
+ * a similar dictionary for mapping the tax templates and taxes, as second item,
+ :rtype: tuple(dict, dict, dict)
+ """
+ self.ensure_one()
+ if acc_ref is None:
+ acc_ref = {}
+ if taxes_ref is None:
+ taxes_ref = {}
+ if self.parent_id:
+ tmp1, tmp2 = self.parent_id._install_template(company, code_digits=code_digits, acc_ref=acc_ref, taxes_ref=taxes_ref)
+ acc_ref.update(tmp1)
+ taxes_ref.update(tmp2)
+ # Ensure, even if individually, that everything is translated according to the company's language.
+ tmp1, tmp2 = self.with_context(lang=company.partner_id.lang)._load_template(company, code_digits=code_digits, account_ref=acc_ref, taxes_ref=taxes_ref)
+ acc_ref.update(tmp1)
+ taxes_ref.update(tmp2)
+ return acc_ref, taxes_ref
+
+ def _load_template(self, company, code_digits=None, account_ref=None, taxes_ref=None):
+ """ Generate all the objects from the templates
+
+ :param company: company the wizard is running for
+ :param code_digits: number of digits the accounts code should have in the COA
+ :param acc_ref: Mapping between ids of account templates and real accounts created from them
+ :param taxes_ref: Mapping between ids of tax templates and real taxes created from them
+ :returns: tuple with a dictionary containing
+ * the mapping between the account template ids and the ids of the real accounts that have been generated
+ from them, as first item,
+ * a similar dictionary for mapping the tax templates and taxes, as second item,
+ :rtype: tuple(dict, dict, dict)
+ """
+ self.ensure_one()
+ if account_ref is None:
+ account_ref = {}
+ if taxes_ref is None:
+ taxes_ref = {}
+ if not code_digits:
+ code_digits = self.code_digits
+ AccountTaxObj = self.env['account.tax']
+
+ # Generate taxes from templates.
+ generated_tax_res = self.with_context(active_test=False).tax_template_ids._generate_tax(company)
+ taxes_ref.update(generated_tax_res['tax_template_to_tax'])
+
+ # Generating Accounts from templates.
+ account_template_ref = self.generate_account(taxes_ref, account_ref, code_digits, company)
+ account_ref.update(account_template_ref)
+
+ # Generate account groups, from template
+ self.generate_account_groups(company)
+
+ # writing account values after creation of accounts
+ for key, value in generated_tax_res['account_dict']['account.tax'].items():
+ if value['cash_basis_transition_account_id']:
+ AccountTaxObj.browse(key).write({
+ 'cash_basis_transition_account_id': account_ref.get(value['cash_basis_transition_account_id'], False),
+ })
+
+ AccountTaxRepartitionLineObj = self.env['account.tax.repartition.line']
+ for key, value in generated_tax_res['account_dict']['account.tax.repartition.line'].items():
+ if value['account_id']:
+ AccountTaxRepartitionLineObj.browse(key).write({
+ 'account_id': account_ref.get(value['account_id']),
+ })
+
+ # Set the company accounts
+ self._load_company_accounts(account_ref, company)
+
+ # Create Journals - Only done for root chart template
+ if not self.parent_id:
+ self.generate_journals(account_ref, company)
+
+ # generate properties function
+ self.generate_properties(account_ref, company)
+
+ # Generate Fiscal Position , Fiscal Position Accounts and Fiscal Position Taxes from templates
+ self.generate_fiscal_position(taxes_ref, account_ref, company)
+
+ # Generate account operation template templates
+ self.generate_account_reconcile_model(taxes_ref, account_ref, company)
+
+ return account_ref, taxes_ref
+
+ def _load_company_accounts(self, account_ref, company):
+ # Set the default accounts on the company
+ accounts = {
+ 'default_cash_difference_income_account_id': self.default_cash_difference_income_account_id.id,
+ 'default_cash_difference_expense_account_id': self.default_cash_difference_expense_account_id.id,
+ 'account_journal_suspense_account_id': self.account_journal_suspense_account_id.id,
+ 'account_cash_basis_base_account_id': self.property_cash_basis_base_account_id.id,
+ 'account_default_pos_receivable_account_id': self.default_pos_receivable_account_id.id,
+ 'income_currency_exchange_account_id': self.income_currency_exchange_account_id.id,
+ 'expense_currency_exchange_account_id': self.expense_currency_exchange_account_id.id,
+ }
+
+ values = {}
+
+ # The loop is to avoid writing when we have no values, thus avoiding erasing the account from the parent
+ for key, account in accounts.items():
+ if account_ref.get(account):
+ values[key] = account_ref.get(account)
+
+ company.write(values)
+
+ def create_record_with_xmlid(self, company, template, model, vals):
+ return self._create_records_with_xmlid(model, [(template, vals)], company).id
+
+ def _create_records_with_xmlid(self, model, template_vals, company):
+ """ Create records for the given model name with the given vals, and
+ create xml ids based on each record's template and company id.
+ """
+ if not template_vals:
+ return self.env[model]
+ template_model = template_vals[0][0]
+ template_ids = [template.id for template, vals in template_vals]
+ template_xmlids = template_model.browse(template_ids).get_external_id()
+ data_list = []
+ for template, vals in template_vals:
+ module, name = template_xmlids[template.id].split('.', 1)
+ xml_id = "%s.%s_%s" % (module, company.id, name)
+ data_list.append(dict(xml_id=xml_id, values=vals, noupdate=True))
+ return self.env[model]._load_records(data_list)
+
+ @api.model
+ def _load_records(self, data_list, update=False):
+ # When creating a chart template create, for the liquidity transfer account
+ # - an account.account.template: this allow to define account.reconcile.model.template objects refering that liquidity transfer
+ # account although it's not existing in any xml file
+ # - an entry in ir_model_data: this allow to still use the method create_record_with_xmlid() and don't make any difference between
+ # regular accounts created and that liquidity transfer account
+ records = super(AccountChartTemplate, self)._load_records(data_list, update)
+ account_data_list = []
+ for data, record in zip(data_list, records):
+ # Create the transfer account only for leaf chart template in the hierarchy.
+ if record.parent_id:
+ continue
+ if data.get('xml_id'):
+ account_xml_id = data['xml_id'] + '_liquidity_transfer'
+ if not self.env.ref(account_xml_id, raise_if_not_found=False):
+ account_vals = record._prepare_transfer_account_template()
+ account_data_list.append(dict(
+ xml_id=account_xml_id,
+ values=account_vals,
+ noupdate=data.get('noupdate'),
+ ))
+ self.env['account.account.template']._load_records(account_data_list, update)
+ return records
+
+ def _get_account_vals(self, company, account_template, code_acc, tax_template_ref):
+ """ This method generates a dictionary of all the values for the account that will be created.
+ """
+ self.ensure_one()
+ tax_ids = []
+ for tax in account_template.tax_ids:
+ tax_ids.append(tax_template_ref[tax.id])
+ val = {
+ 'name': account_template.name,
+ 'currency_id': account_template.currency_id and account_template.currency_id.id or False,
+ 'code': code_acc,
+ 'user_type_id': account_template.user_type_id and account_template.user_type_id.id or False,
+ 'reconcile': account_template.reconcile,
+ 'note': account_template.note,
+ 'tax_ids': [(6, 0, tax_ids)],
+ 'company_id': company.id,
+ 'tag_ids': [(6, 0, [t.id for t in account_template.tag_ids])],
+ }
+ return val
+
+ def generate_account(self, tax_template_ref, acc_template_ref, code_digits, company):
+ """ This method generates accounts from account templates.
+
+ :param tax_template_ref: Taxes templates reference for write taxes_id in account_account.
+ :param acc_template_ref: dictionary containing the mapping between the account templates and generated accounts (will be populated)
+ :param code_digits: number of digits to use for account code.
+ :param company_id: company to generate accounts for.
+ :returns: return acc_template_ref for reference purpose.
+ :rtype: dict
+ """
+ self.ensure_one()
+ account_tmpl_obj = self.env['account.account.template']
+ acc_template = account_tmpl_obj.search([('nocreate', '!=', True), ('chart_template_id', '=', self.id)], order='id')
+ template_vals = []
+ for account_template in acc_template:
+ code_main = account_template.code and len(account_template.code) or 0
+ code_acc = account_template.code or ''
+ if code_main > 0 and code_main <= code_digits:
+ code_acc = str(code_acc) + (str('0'*(code_digits-code_main)))
+ vals = self._get_account_vals(company, account_template, code_acc, tax_template_ref)
+ template_vals.append((account_template, vals))
+ accounts = self._create_records_with_xmlid('account.account', template_vals, company)
+ for template, account in zip(acc_template, accounts):
+ acc_template_ref[template.id] = account.id
+ return acc_template_ref
+
+ def generate_account_groups(self, company):
+ """ This method generates account groups from account groups templates.
+ :param company: company to generate the account groups for
+ """
+ self.ensure_one()
+ group_templates = self.env['account.group.template'].search([('chart_template_id', '=', self.id)])
+ template_vals = []
+ for group_template in group_templates:
+ vals = {
+ 'name': group_template.name,
+ 'code_prefix_start': group_template.code_prefix_start,
+ 'code_prefix_end': group_template.code_prefix_end,
+ 'company_id': company.id,
+ }
+ template_vals.append((group_template, vals))
+ groups = self._create_records_with_xmlid('account.group', template_vals, company)
+
+ def _prepare_reconcile_model_vals(self, company, account_reconcile_model, acc_template_ref, tax_template_ref):
+ """ This method generates a dictionary of all the values for the account.reconcile.model that will be created.
+ """
+ self.ensure_one()
+ account_reconcile_model_lines = self.env['account.reconcile.model.line.template'].search([
+ ('model_id', '=', account_reconcile_model.id)
+ ])
+ return {
+ 'name': account_reconcile_model.name,
+ 'sequence': account_reconcile_model.sequence,
+ 'company_id': company.id,
+ 'rule_type': account_reconcile_model.rule_type,
+ 'auto_reconcile': account_reconcile_model.auto_reconcile,
+ 'to_check': account_reconcile_model.to_check,
+ 'match_journal_ids': [(6, None, account_reconcile_model.match_journal_ids.ids)],
+ 'match_nature': account_reconcile_model.match_nature,
+ 'match_amount': account_reconcile_model.match_amount,
+ 'match_amount_min': account_reconcile_model.match_amount_min,
+ 'match_amount_max': account_reconcile_model.match_amount_max,
+ 'match_label': account_reconcile_model.match_label,
+ 'match_label_param': account_reconcile_model.match_label_param,
+ 'match_note': account_reconcile_model.match_note,
+ 'match_note_param': account_reconcile_model.match_note_param,
+ 'match_transaction_type': account_reconcile_model.match_transaction_type,
+ 'match_transaction_type_param': account_reconcile_model.match_transaction_type_param,
+ 'match_same_currency': account_reconcile_model.match_same_currency,
+ 'match_total_amount': account_reconcile_model.match_total_amount,
+ 'match_total_amount_param': account_reconcile_model.match_total_amount_param,
+ 'match_partner': account_reconcile_model.match_partner,
+ 'match_partner_ids': [(6, None, account_reconcile_model.match_partner_ids.ids)],
+ 'match_partner_category_ids': [(6, None, account_reconcile_model.match_partner_category_ids.ids)],
+ 'line_ids': [(0, 0, {
+ 'account_id': acc_template_ref[line.account_id.id],
+ 'label': line.label,
+ 'amount_type': line.amount_type,
+ 'force_tax_included': line.force_tax_included,
+ 'amount_string': line.amount_string,
+ 'tax_ids': [[4, tax_template_ref[tax.id], 0] for tax in line.tax_ids],
+ }) for line in account_reconcile_model_lines],
+ }
+
+ def generate_account_reconcile_model(self, tax_template_ref, acc_template_ref, company):
+ """ This method creates account reconcile models
+
+ :param tax_template_ref: Taxes templates reference for write taxes_id in account_account.
+ :param acc_template_ref: dictionary with the mapping between the account templates and the real accounts.
+ :param company_id: company to create models for
+ :returns: return new_account_reconcile_model for reference purpose.
+ :rtype: dict
+ """
+ self.ensure_one()
+ account_reconcile_models = self.env['account.reconcile.model.template'].search([
+ ('chart_template_id', '=', self.id)
+ ])
+ for account_reconcile_model in account_reconcile_models:
+ vals = self._prepare_reconcile_model_vals(company, account_reconcile_model, acc_template_ref, tax_template_ref)
+ self.create_record_with_xmlid(company, account_reconcile_model, 'account.reconcile.model', vals)
+ # Create a default rule for the reconciliation widget matching invoices automatically.
+ self.env['account.reconcile.model'].sudo().create({
+ "name": _('Invoices Matching Rule'),
+ "sequence": '1',
+ "rule_type": 'invoice_matching',
+ "auto_reconcile": False,
+ "match_nature": 'both',
+ "match_same_currency": True,
+ "match_total_amount": True,
+ "match_total_amount_param": 100,
+ "match_partner": True,
+ "company_id": company.id,
+ })
+ return True
+
+ def _get_fp_vals(self, company, position):
+ return {
+ 'company_id': company.id,
+ 'sequence': position.sequence,
+ 'name': position.name,
+ 'note': position.note,
+ 'auto_apply': position.auto_apply,
+ 'vat_required': position.vat_required,
+ 'country_id': position.country_id.id,
+ 'country_group_id': position.country_group_id.id,
+ 'state_ids': position.state_ids and [(6,0, position.state_ids.ids)] or [],
+ 'zip_from': position.zip_from,
+ 'zip_to': position.zip_to,
+ }
+
+ def generate_fiscal_position(self, tax_template_ref, acc_template_ref, company):
+ """ This method generates Fiscal Position, Fiscal Position Accounts
+ and Fiscal Position Taxes from templates.
+
+ :param taxes_ids: Taxes templates reference for generating account.fiscal.position.tax.
+ :param acc_template_ref: Account templates reference for generating account.fiscal.position.account.
+ :param company_id: the company to generate fiscal position data for
+ :returns: True
+ """
+ self.ensure_one()
+ positions = self.env['account.fiscal.position.template'].search([('chart_template_id', '=', self.id)])
+
+ # first create fiscal positions in batch
+ template_vals = []
+ for position in positions:
+ fp_vals = self._get_fp_vals(company, position)
+ template_vals.append((position, fp_vals))
+ fps = self._create_records_with_xmlid('account.fiscal.position', template_vals, company)
+
+ # then create fiscal position taxes and accounts
+ tax_template_vals = []
+ account_template_vals = []
+ for position, fp in zip(positions, fps):
+ for tax in position.tax_ids:
+ tax_template_vals.append((tax, {
+ 'tax_src_id': tax_template_ref[tax.tax_src_id.id],
+ 'tax_dest_id': tax.tax_dest_id and tax_template_ref[tax.tax_dest_id.id] or False,
+ 'position_id': fp.id,
+ }))
+ for acc in position.account_ids:
+ account_template_vals.append((acc, {
+ 'account_src_id': acc_template_ref[acc.account_src_id.id],
+ 'account_dest_id': acc_template_ref[acc.account_dest_id.id],
+ 'position_id': fp.id,
+ }))
+ self._create_records_with_xmlid('account.fiscal.position.tax', tax_template_vals, company)
+ self._create_records_with_xmlid('account.fiscal.position.account', account_template_vals, company)
+
+ return True
+
+
+class AccountTaxTemplate(models.Model):
+ _name = 'account.tax.template'
+ _description = 'Templates for Taxes'
+ _order = 'id'
+
+ chart_template_id = fields.Many2one('account.chart.template', string='Chart Template', required=True)
+
+ name = fields.Char(string='Tax Name', required=True)
+ type_tax_use = fields.Selection(TYPE_TAX_USE, string='Tax Type', required=True, default="sale",
+ help="Determines where the tax is selectable. Note : 'None' means a tax can't be used by itself, however it can still be used in a group.")
+ tax_scope = fields.Selection([('service', 'Service'), ('consu', 'Consumable')], help="Restrict the use of taxes to a type of product.")
+ amount_type = fields.Selection(default='percent', string="Tax Computation", required=True,
+ selection=[('group', 'Group of Taxes'), ('fixed', 'Fixed'), ('percent', 'Percentage of Price'), ('division', 'Percentage of Price Tax Included')])
+ active = fields.Boolean(default=True, help="Set active to false to hide the tax without removing it.")
+ children_tax_ids = fields.Many2many('account.tax.template', 'account_tax_template_filiation_rel', 'parent_tax', 'child_tax', string='Children Taxes')
+ sequence = fields.Integer(required=True, default=1,
+ help="The sequence field is used to define order in which the tax lines are applied.")
+ amount = fields.Float(required=True, digits=(16, 4), default=0)
+ description = fields.Char(string='Display on Invoices')
+ price_include = fields.Boolean(string='Included in Price', default=False,
+ help="Check this if the price you use on the product and invoices includes this tax.")
+ include_base_amount = fields.Boolean(string='Affect Subsequent Taxes', default=False,
+ help="If set, taxes which are computed after this one will be computed based on the price tax included.")
+ analytic = fields.Boolean(string="Analytic Cost", help="If set, the amount computed by this tax will be assigned to the same analytic account as the invoice line (if any)")
+ invoice_repartition_line_ids = fields.One2many(string="Repartition for Invoices", comodel_name="account.tax.repartition.line.template", inverse_name="invoice_tax_id", copy=True, help="Repartition when the tax is used on an invoice")
+ refund_repartition_line_ids = fields.One2many(string="Repartition for Refund Invoices", comodel_name="account.tax.repartition.line.template", inverse_name="refund_tax_id", copy=True, help="Repartition when the tax is used on a refund")
+ tax_group_id = fields.Many2one('account.tax.group', string="Tax Group")
+ tax_exigibility = fields.Selection(
+ [('on_invoice', 'Based on Invoice'),
+ ('on_payment', 'Based on Payment'),
+ ], string='Tax Due', default='on_invoice',
+ help="Based on Invoice: the tax is due as soon as the invoice is validated.\n"
+ "Based on Payment: the tax is due as soon as the payment of the invoice is received.")
+ cash_basis_transition_account_id = fields.Many2one(
+ comodel_name='account.account.template',
+ string="Cash Basis Transition Account",
+ domain=[('deprecated', '=', False)],
+ help="Account used to transition the tax amount for cash basis taxes. It will contain the tax amount as long as the original invoice has not been reconciled ; at reconciliation, this amount cancelled on this account and put on the regular tax account.")
+
+ _sql_constraints = [
+ ('name_company_uniq', 'unique(name, type_tax_use, tax_scope, chart_template_id)', 'Tax names must be unique !'),
+ ]
+
+ @api.depends('name', 'description')
+ def name_get(self):
+ res = []
+ for record in self:
+ name = record.description and record.description or record.name
+ res.append((record.id, name))
+ return res
+
+ def _get_tax_vals(self, company, tax_template_to_tax):
+ """ This method generates a dictionary of all the values for the tax that will be created.
+ """
+ # Compute children tax ids
+ children_ids = []
+ for child_tax in self.children_tax_ids:
+ if tax_template_to_tax.get(child_tax.id):
+ children_ids.append(tax_template_to_tax[child_tax.id])
+ self.ensure_one()
+ val = {
+ 'name': self.name,
+ 'type_tax_use': self.type_tax_use,
+ 'tax_scope': self.tax_scope,
+ 'amount_type': self.amount_type,
+ 'active': self.active,
+ 'company_id': company.id,
+ 'sequence': self.sequence,
+ 'amount': self.amount,
+ 'description': self.description,
+ 'price_include': self.price_include,
+ 'include_base_amount': self.include_base_amount,
+ 'analytic': self.analytic,
+ 'children_tax_ids': [(6, 0, children_ids)],
+ 'tax_exigibility': self.tax_exigibility,
+ }
+
+ # We add repartition lines if there are some, so that if there are none,
+ # default_get is called and creates the default ones properly.
+ if self.invoice_repartition_line_ids:
+ val['invoice_repartition_line_ids'] = self.invoice_repartition_line_ids.get_repartition_line_create_vals(company)
+ if self.refund_repartition_line_ids:
+ val['refund_repartition_line_ids'] = self.refund_repartition_line_ids.get_repartition_line_create_vals(company)
+
+ if self.tax_group_id:
+ val['tax_group_id'] = self.tax_group_id.id
+ return val
+
+ def _generate_tax(self, company):
+ """ This method generate taxes from templates.
+
+ :param company: the company for which the taxes should be created from templates in self
+ :returns: {
+ 'tax_template_to_tax': mapping between tax template and the newly generated taxes corresponding,
+ 'account_dict': dictionary containing a to-do list with all the accounts to assign on new taxes
+ }
+ """
+ # default_company_id is needed in context to allow creation of default
+ # repartition lines on taxes
+ ChartTemplate = self.env['account.chart.template'].with_context(default_company_id=company.id)
+ todo_dict = {'account.tax': {}, 'account.tax.repartition.line': {}}
+ tax_template_to_tax = {}
+
+ templates_todo = list(self)
+ while templates_todo:
+ templates = templates_todo
+ templates_todo = []
+
+ # create taxes in batch
+ 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)
+ tax_template_vals.append((template, vals))
+ else:
+ # defer the creation of this tax to the next batch
+ templates_todo.append(template)
+ taxes = ChartTemplate._create_records_with_xmlid('account.tax', tax_template_vals, company)
+
+ # fill in tax_template_to_tax and todo_dict
+ for tax, (template, vals) in zip(taxes, tax_template_vals):
+ tax_template_to_tax[template.id] = tax.id
+ # Since the accounts have not been created yet, we have to wait before filling these fields
+ todo_dict['account.tax'][tax.id] = {
+ 'cash_basis_transition_account_id': template.cash_basis_transition_account_id.id,
+ }
+
+ # We also have to delay the assignation of accounts to repartition lines
+ # The below code assigns the account_id to the repartition lines according
+ # to the corresponding repartition line in the template, based on the order.
+ # As we just created the repartition lines, tax.invoice_repartition_line_ids is not well sorted.
+ # But we can force the sort by calling sort()
+ all_tax_rep_lines = tax.invoice_repartition_line_ids.sorted() + tax.refund_repartition_line_ids.sorted()
+ all_template_rep_lines = template.invoice_repartition_line_ids + template.refund_repartition_line_ids
+ for i in range(0, len(all_template_rep_lines)):
+ # We assume template and tax repartition lines are in the same order
+ template_account = all_template_rep_lines[i].account_id
+ if template_account:
+ todo_dict['account.tax.repartition.line'][all_tax_rep_lines[i].id] = {
+ 'account_id': template_account.id,
+ }
+
+ if any(template.tax_exigibility == 'on_payment' for template in self):
+ # When a CoA is being installed automatically and if it is creating account tax(es) whose field `Use Cash Basis`(tax_exigibility) is set to True by default
+ # (example of such CoA's are l10n_fr and l10n_mx) then in the `Accounting Settings` the option `Cash Basis` should be checked by default.
+ company.tax_exigibility = True
+
+ return {
+ 'tax_template_to_tax': tax_template_to_tax,
+ 'account_dict': todo_dict
+ }
+
+# Tax Repartition Line Template
+
+
+class AccountTaxRepartitionLineTemplate(models.Model):
+ _name = "account.tax.repartition.line.template"
+ _description = "Tax Repartition Line Template"
+
+ factor_percent = fields.Float(string="%", required=True, help="Factor to apply on the account move lines generated from this distribution line, in percents")
+ repartition_type = fields.Selection(string="Based On", selection=[('base', 'Base'), ('tax', 'of tax')], required=True, default='tax', help="Base on which the factor will be applied.")
+ account_id = fields.Many2one(string="Account", comodel_name='account.account.template', help="Account on which to post the tax amount")
+ invoice_tax_id = fields.Many2one(comodel_name='account.tax.template', help="The tax set to apply this distribution on invoices. Mutually exclusive with refund_tax_id")
+ refund_tax_id = fields.Many2one(comodel_name='account.tax.template', help="The tax set to apply this distribution on refund invoices. Mutually exclusive with invoice_tax_id")
+ tag_ids = fields.Many2many(string="Financial Tags", relation='account_tax_repartition_financial_tags', comodel_name='account.account.tag', copy=True, help="Additional tags that will be assigned by this repartition line for use in financial reports")
+ use_in_tax_closing = fields.Boolean(string="Tax Closing Entry")
+
+ # These last two fields are helpers used to ease the declaration of account.account.tag objects in XML.
+ # They are directly linked to account.tax.report.line objects, which create corresponding + and - tags
+ # at creation. This way, we avoid declaring + and - separately every time.
+ plus_report_line_ids = fields.Many2many(string="Plus Tax Report Lines", relation='account_tax_repartition_plus_report_line', comodel_name='account.tax.report.line', copy=True, help="Tax report lines whose '+' tag will be assigned to move lines by this repartition line")
+ minus_report_line_ids = fields.Many2many(string="Minus Report Lines", relation='account_tax_repartition_minus_report_line', comodel_name='account.tax.report.line', copy=True, help="Tax report lines whose '-' tag will be assigned to move lines by this repartition line")
+
+ @api.model
+ def create(self, vals):
+ if vals.get('plus_report_line_ids'):
+ vals['plus_report_line_ids'] = self._convert_tag_syntax_to_orm(vals['plus_report_line_ids'])
+
+ if vals.get('minus_report_line_ids'):
+ vals['minus_report_line_ids'] = self._convert_tag_syntax_to_orm(vals['minus_report_line_ids'])
+
+ if vals.get('tag_ids'):
+ vals['tag_ids'] = self._convert_tag_syntax_to_orm(vals['tag_ids'])
+
+ if vals.get('use_in_tax_closing') is None:
+ if not vals.get('account_id'):
+ vals['use_in_tax_closing'] = False
+ else:
+ internal_group = self.env['account.account.template'].browse(vals.get('account_id')).user_type_id.internal_group
+ vals['use_in_tax_closing'] = not (internal_group == 'income' or internal_group == 'expense')
+
+ return super(AccountTaxRepartitionLineTemplate, self).create(vals)
+
+ @api.model
+ def _convert_tag_syntax_to_orm(self, tags_list):
+ """ Repartition lines give the possibility to directly give
+ a list of ids to create for tags instead of a list of ORM commands.
+
+ This function checks that tags_list uses this syntactic sugar and returns
+ an ORM-compliant version of it if it does.
+ """
+ if tags_list and all(isinstance(elem, int) for elem in tags_list):
+ return [(6, False, tags_list)]
+ return tags_list
+
+ @api.constrains('invoice_tax_id', 'refund_tax_id')
+ def validate_tax_template_link(self):
+ for record in self:
+ if record.invoice_tax_id and record.refund_tax_id:
+ raise ValidationError(_("Tax distribution line templates should apply to either invoices or refunds, not both at the same time. invoice_tax_id and refund_tax_id should not be set together."))
+
+ @api.constrains('plus_report_line_ids', 'minus_report_line_ids')
+ def validate_tags(self):
+ all_tax_rep_lines = self.mapped('plus_report_line_ids') + self.mapped('minus_report_line_ids')
+ lines_without_tag = all_tax_rep_lines.filtered(lambda x: not x.tag_name)
+ if lines_without_tag:
+ raise ValidationError(_("The following tax report lines are used in some tax distribution template though they don't generate any tag: %s . This probably means you forgot to set a tag_name on these lines.", str(lines_without_tag.mapped('name'))))
+
+ def get_repartition_line_create_vals(self, company):
+ rslt = [(5, 0, 0)]
+ for record in self:
+ tags_to_add = self.env['account.account.tag']
+ tags_to_add += record.plus_report_line_ids.mapped('tag_ids').filtered(lambda x: not x.tax_negate)
+ tags_to_add += record.minus_report_line_ids.mapped('tag_ids').filtered(lambda x: x.tax_negate)
+ tags_to_add += record.tag_ids
+
+ rslt.append((0, 0, {
+ 'factor_percent': record.factor_percent,
+ 'repartition_type': record.repartition_type,
+ 'tag_ids': [(6, 0, tags_to_add.ids)],
+ 'company_id': company.id,
+ 'use_in_tax_closing': record.use_in_tax_closing
+ }))
+ return rslt
+
+# Fiscal Position Templates
+
+class AccountFiscalPositionTemplate(models.Model):
+ _name = 'account.fiscal.position.template'
+ _description = 'Template for Fiscal Position'
+
+ sequence = fields.Integer()
+ name = fields.Char(string='Fiscal Position Template', required=True)
+ chart_template_id = fields.Many2one('account.chart.template', string='Chart Template', required=True)
+ account_ids = fields.One2many('account.fiscal.position.account.template', 'position_id', string='Account Mapping')
+ tax_ids = fields.One2many('account.fiscal.position.tax.template', 'position_id', string='Tax Mapping')
+ note = fields.Text(string='Notes')
+ auto_apply = fields.Boolean(string='Detect Automatically', help="Apply automatically this fiscal position.")
+ vat_required = fields.Boolean(string='VAT required', help="Apply only if partner has a VAT number.")
+ country_id = fields.Many2one('res.country', string='Country',
+ help="Apply only if delivery country matches.")
+ country_group_id = fields.Many2one('res.country.group', string='Country Group',
+ help="Apply only if delivery country matches the group.")
+ state_ids = fields.Many2many('res.country.state', string='Federal States')
+ zip_from = fields.Char(string='Zip Range From')
+ zip_to = fields.Char(string='Zip Range To')
+
+
+class AccountFiscalPositionTaxTemplate(models.Model):
+ _name = 'account.fiscal.position.tax.template'
+ _description = 'Tax Mapping Template of Fiscal Position'
+ _rec_name = 'position_id'
+
+ position_id = fields.Many2one('account.fiscal.position.template', string='Fiscal Position', required=True, ondelete='cascade')
+ tax_src_id = fields.Many2one('account.tax.template', string='Tax Source', required=True)
+ tax_dest_id = fields.Many2one('account.tax.template', string='Replacement Tax')
+
+
+class AccountFiscalPositionAccountTemplate(models.Model):
+ _name = 'account.fiscal.position.account.template'
+ _description = 'Accounts Mapping Template of Fiscal Position'
+ _rec_name = 'position_id'
+
+ position_id = fields.Many2one('account.fiscal.position.template', string='Fiscal Mapping', required=True, ondelete='cascade')
+ account_src_id = fields.Many2one('account.account.template', string='Account Source', required=True)
+ account_dest_id = fields.Many2one('account.account.template', string='Account Destination', required=True)
+
+
+class AccountReconcileModelTemplate(models.Model):
+ _name = "account.reconcile.model.template"
+ _description = 'Reconcile Model Template'
+
+ # Base fields.
+ chart_template_id = fields.Many2one('account.chart.template', string='Chart Template', required=True)
+ name = fields.Char(string='Button Label', required=True)
+ sequence = fields.Integer(required=True, default=10)
+
+ rule_type = fields.Selection(selection=[
+ ('writeoff_button', 'Manually create a write-off on clicked button'),
+ ('writeoff_suggestion', 'Suggest a write-off'),
+ ('invoice_matching', 'Match existing invoices/bills')
+ ], string='Type', default='writeoff_button', required=True)
+ auto_reconcile = fields.Boolean(string='Auto-validate',
+ help='Validate the statement line automatically (reconciliation based on your rule).')
+ to_check = fields.Boolean(string='To Check', default=False, help='This matching rule is used when the user is not certain of all the information of the counterpart.')
+ matching_order = fields.Selection(
+ selection=[
+ ('old_first', 'Oldest first'),
+ ('new_first', 'Newest first'),
+ ]
+ )
+
+ # ===== Conditions =====
+ match_text_location_label = fields.Boolean(
+ default=True,
+ help="Search in the Statement's Label to find the Invoice/Payment's reference",
+ )
+ match_text_location_note = fields.Boolean(
+ default=False,
+ help="Search in the Statement's Note to find the Invoice/Payment's reference",
+ )
+ match_text_location_reference = fields.Boolean(
+ default=False,
+ help="Search in the Statement's Reference to find the Invoice/Payment's reference",
+ )
+ match_journal_ids = fields.Many2many('account.journal', string='Journals',
+ domain="[('type', 'in', ('bank', 'cash'))]",
+ help='The reconciliation model will only be available from the selected journals.')
+ match_nature = fields.Selection(selection=[
+ ('amount_received', 'Amount Received'),
+ ('amount_paid', 'Amount Paid'),
+ ('both', 'Amount Paid/Received')
+ ], string='Amount Nature', required=True, default='both',
+ help='''The reconciliation model will only be applied to the selected transaction type:
+ * Amount Received: Only applied when receiving an amount.
+ * Amount Paid: Only applied when paying an amount.
+ * Amount Paid/Received: Applied in both cases.''')
+ match_amount = fields.Selection(selection=[
+ ('lower', 'Is Lower Than'),
+ ('greater', 'Is Greater Than'),
+ ('between', 'Is Between'),
+ ], string='Amount',
+ help='The reconciliation model will only be applied when the amount being lower than, greater than or between specified amount(s).')
+ match_amount_min = fields.Float(string='Amount Min Parameter')
+ match_amount_max = fields.Float(string='Amount Max Parameter')
+ match_label = fields.Selection(selection=[
+ ('contains', 'Contains'),
+ ('not_contains', 'Not Contains'),
+ ('match_regex', 'Match Regex'),
+ ], string='Label', help='''The reconciliation model will only be applied when the label:
+ * Contains: The proposition label must contains this string (case insensitive).
+ * Not Contains: Negation of "Contains".
+ * Match Regex: Define your own regular expression.''')
+ match_label_param = fields.Char(string='Label Parameter')
+ match_note = fields.Selection(selection=[
+ ('contains', 'Contains'),
+ ('not_contains', 'Not Contains'),
+ ('match_regex', 'Match Regex'),
+ ], string='Note', help='''The reconciliation model will only be applied when the note:
+ * Contains: The proposition note must contains this string (case insensitive).
+ * Not Contains: Negation of "Contains".
+ * Match Regex: Define your own regular expression.''')
+ match_note_param = fields.Char(string='Note Parameter')
+ match_transaction_type = fields.Selection(selection=[
+ ('contains', 'Contains'),
+ ('not_contains', 'Not Contains'),
+ ('match_regex', 'Match Regex'),
+ ], string='Transaction Type', help='''The reconciliation model will only be applied when the transaction type:
+ * Contains: The proposition transaction type must contains this string (case insensitive).
+ * Not Contains: Negation of "Contains".
+ * Match Regex: Define your own regular expression.''')
+ match_transaction_type_param = fields.Char(string='Transaction Type Parameter')
+ match_same_currency = fields.Boolean(string='Same Currency Matching', default=True,
+ help='Restrict to propositions having the same currency as the statement line.')
+ match_total_amount = fields.Boolean(string='Amount Matching', default=True,
+ help='The sum of total residual amount propositions matches the statement line amount.')
+ match_total_amount_param = fields.Float(string='Amount Matching %', default=100,
+ help='The sum of total residual amount propositions matches the statement line amount under this percentage.')
+ match_partner = fields.Boolean(string='Partner Is Set',
+ help='The reconciliation model will only be applied when a customer/vendor is set.')
+ match_partner_ids = fields.Many2many('res.partner', string='Restrict Partners to',
+ help='The reconciliation model will only be applied to the selected customers/vendors.')
+ match_partner_category_ids = fields.Many2many('res.partner.category', string='Restrict Partner Categories to',
+ help='The reconciliation model will only be applied to the selected customer/vendor categories.')
+
+ line_ids = fields.One2many('account.reconcile.model.line.template', 'model_id')
+ decimal_separator = fields.Char(help="Every character that is nor a digit nor this separator will be removed from the matching string")
+
+
+class AccountReconcileModelLineTemplate(models.Model):
+ _name = "account.reconcile.model.line.template"
+ _description = 'Reconcile Model Line Template'
+
+ model_id = fields.Many2one('account.reconcile.model.template')
+ sequence = fields.Integer(required=True, default=10)
+ account_id = fields.Many2one('account.account.template', string='Account', ondelete='cascade', domain=[('deprecated', '=', False)])
+ label = fields.Char(string='Journal Item Label')
+ amount_type = fields.Selection([
+ ('fixed', 'Fixed'),
+ ('percentage', 'Percentage of balance'),
+ ('regex', 'From label'),
+ ], required=True, default='percentage')
+ amount_string = fields.Char(string="Amount")
+ force_tax_included = fields.Boolean(string='Tax Included in Price', help='Force the tax to be managed as a price included tax.')
+ tax_ids = fields.Many2many('account.tax.template', string='Taxes', ondelete='restrict')