closes odoo/odoo#59660 X-original-commit: fa5231983d1af929a26c83f5d64461cf657fc88a Related: odoo/enterprise#13964 Signed-off-by: oco-odoo <oco-odoo@users.noreply.github.com>
1243 lines
68 KiB
Python
1243 lines
68 KiB
Python
# -*- 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_assets').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 informations 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')
|