chart_template.py 95 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755
  1. # -*- coding: utf-8 -*-
  2. from collections import defaultdict
  3. from odoo.exceptions import AccessError
  4. from odoo import api, fields, models, Command, _, osv
  5. from odoo import SUPERUSER_ID
  6. from odoo.exceptions import UserError, ValidationError
  7. from odoo.http import request
  8. from odoo.addons.account.models.account_tax import TYPE_TAX_USE
  9. from odoo.addons.account.models.account_account import ACCOUNT_CODE_REGEX
  10. from odoo.tools import html_escape
  11. import logging
  12. import re
  13. _logger = logging.getLogger(__name__)
  14. def migrate_set_tags_and_taxes_updatable(cr, registry, module):
  15. ''' This is a utility function used to manually set the flag noupdate to False on tags and account tax templates on localization modules
  16. that need migration (for example in case of VAT report improvements)
  17. '''
  18. env = api.Environment(cr, SUPERUSER_ID, {})
  19. xml_record_ids = env['ir.model.data'].search([
  20. ('model', 'in', ['account.tax.template', 'account.account.tag']),
  21. ('module', 'like', module)
  22. ]).ids
  23. if xml_record_ids:
  24. cr.execute("update ir_model_data set noupdate = 'f' where id in %s", (tuple(xml_record_ids),))
  25. def preserve_existing_tags_on_taxes(cr, registry, module):
  26. ''' This is a utility function used to preserve existing previous tags during upgrade of the module.'''
  27. env = api.Environment(cr, SUPERUSER_ID, {})
  28. xml_records = env['ir.model.data'].search([('model', '=', 'account.account.tag'), ('module', 'like', module)])
  29. if xml_records:
  30. cr.execute("update ir_model_data set noupdate = 't' where id in %s", [tuple(xml_records.ids)])
  31. def update_taxes_from_templates(cr, chart_template_xmlid):
  32. """ This method will try to update taxes based on their template.
  33. Schematically there are three possible execution path:
  34. [do the template xmlid matches one tax xmlid ?]
  35. -NO--> we *create* a new tax based on the template values
  36. -YES-> [are the tax template and the matching tax similar enough (details see `_is_tax_and_template_same`) ?]
  37. -YES-> We *update* the existing tax's tag (and only tags).
  38. -NO--> We *create* a duplicated tax with template value, and related fiscal positions.
  39. This method is mainly used as a local upgrade script.
  40. Returns a list of tuple (template_id, tax_id) of newly created records.
  41. """
  42. def _create_taxes_from_template(company, template2tax_mapping, template2tax_to_update=None):
  43. """ Create a new taxes from templates. If an old tax already used the same xmlid, we
  44. remove the xmlid from it but don't modify anything else.
  45. :param company: the company of the tax to instantiate
  46. :param template2tax_mapping: a list of tuples (template, existing_tax) where existing_tax can be None
  47. :return: a list of tuples of ids (template.id, newly_created_tax.id)
  48. """
  49. def _remove_xml_id(xml_id):
  50. module, name = xml_id.split('.', 1)
  51. env['ir.model.data'].search([('module', '=', module), ('name', '=', name)]).unlink()
  52. def _avoid_name_conflict(company, template):
  53. conflict_taxes = env['account.tax'].search([
  54. ('name', '=', template.name), ('company_id', '=', company.id),
  55. ('type_tax_use', '=', template.type_tax_use), ('tax_scope', '=', template.tax_scope)
  56. ])
  57. if conflict_taxes:
  58. for index, conflict_taxes in enumerate(conflict_taxes):
  59. conflict_taxes.name = f"[old{index if index > 0 else ''}] {conflict_taxes.name}"
  60. templates_to_create = env['account.tax.template'].with_context(active_test=False)
  61. for template, old_tax in template2tax_mapping:
  62. if old_tax:
  63. xml_id = old_tax.get_external_id().get(old_tax.id)
  64. if xml_id:
  65. _remove_xml_id(xml_id)
  66. _avoid_name_conflict(company, template)
  67. templates_to_create += template
  68. new_template2tax_company = templates_to_create._generate_tax(
  69. company, accounts_exist=True, existing_template_to_tax=template2tax_to_update
  70. )['tax_template_to_tax']
  71. return [(template.id, tax.id) for template, tax in new_template2tax_company.items()]
  72. def _update_taxes_from_template(template2tax_mapping):
  73. """ Update the taxes' tags (and only tags!) based on their corresponding template values.
  74. :param template2tax_mapping: a list of tuples (template, existing_taxes)
  75. """
  76. for template, existing_tax in template2tax_mapping:
  77. tax_rep_lines = existing_tax.invoice_repartition_line_ids + existing_tax.refund_repartition_line_ids
  78. template_rep_lines = template.invoice_repartition_line_ids + template.refund_repartition_line_ids
  79. for tax_line, template_line in zip(tax_rep_lines, template_rep_lines):
  80. tags_to_add = template_line._get_tags_to_add()
  81. tags_to_unlink = tax_line.tag_ids
  82. if tags_to_add != tags_to_unlink:
  83. tax_line.write({'tag_ids': [(6, 0, tags_to_add.ids)]})
  84. _cleanup_tags(tags_to_unlink)
  85. def _get_template_to_real_xmlid_mapping(model, templates):
  86. """ This function uses ir_model_data to return a mapping between the templates and the data, using their xmlid
  87. :returns: {
  88. company_id: { model.template.id1: model.id1, model.template.id2: model.id2 },
  89. ...
  90. }
  91. """
  92. env['ir.model.data'].flush_model()
  93. template_xmlids = [xmlid.split('.', 1)[1] for xmlid in templates.get_external_id().values()]
  94. res = defaultdict(dict)
  95. if not template_xmlids:
  96. return res
  97. env.cr.execute(
  98. """
  99. SELECT substr(data.name, 0, strpos(data.name, '_'))::INTEGER AS data_company_id,
  100. template.res_id AS template_res_id,
  101. data.res_id AS data_res_id
  102. FROM ir_model_data data
  103. JOIN ir_model_data template
  104. ON template.name = substr(data.name, strpos(data.name, '_') + 1)
  105. WHERE data.model = %s
  106. AND template.name IN %s
  107. -- tax.name is of the form: {company_id}_{account.tax.template.name}
  108. """,
  109. [model, tuple(template_xmlids)],
  110. )
  111. for company_id, template_id, model_id in env.cr.fetchall():
  112. res[company_id][template_id] = model_id
  113. return res
  114. def _is_tax_and_template_same(template, tax):
  115. """ This function compares account.tax and account.tax.template repartition lines.
  116. A tax is considered the same as the template if they have the same:
  117. - amount_type
  118. - amount
  119. - repartition lines percentages in the same order
  120. """
  121. if tax.amount_type == 'group':
  122. # if the amount_type is group we don't do checks on rep. lines nor amount
  123. return tax.amount_type == template.amount_type
  124. else:
  125. tax_rep_lines = tax.invoice_repartition_line_ids + tax.refund_repartition_line_ids
  126. template_rep_lines = template.invoice_repartition_line_ids + template.refund_repartition_line_ids
  127. return (
  128. tax.amount_type == template.amount_type
  129. and tax.amount == template.amount
  130. and (
  131. len(tax_rep_lines) == len(template_rep_lines)
  132. and all(
  133. rep_line_tax.factor_percent == rep_line_template.factor_percent
  134. for rep_line_tax, rep_line_template in zip(tax_rep_lines, template_rep_lines)
  135. )
  136. )
  137. )
  138. def _cleanup_tags(tags):
  139. """ Checks if the tags are still used in taxes or move lines. If not we delete it. """
  140. for tag in tags:
  141. tax_using_tag = env['account.tax.repartition.line'].sudo().search([('tag_ids', 'in', tag.id)], limit=1)
  142. aml_using_tag = env['account.move.line'].sudo().search([('tax_tag_ids', 'in', tag.id)], limit=1)
  143. report_expr_using_tag = tag._get_related_tax_report_expressions()
  144. if not (aml_using_tag or tax_using_tag or report_expr_using_tag):
  145. tag.unlink()
  146. def _update_fiscal_positions_from_templates(chart_template, new_tax_template_by_company, all_tax_templates):
  147. fp_templates = env['account.fiscal.position.template'].search([('chart_template_id', '=', chart_template.id)])
  148. template2tax = _get_template_to_real_xmlid_mapping('account.tax', all_tax_templates)
  149. template2fp = _get_template_to_real_xmlid_mapping('account.fiscal.position', fp_templates)
  150. for company_id in new_tax_template_by_company:
  151. fp_tax_template_vals = []
  152. template2fp_company = template2fp.get(company_id)
  153. for position_template in fp_templates:
  154. fp = env['account.fiscal.position'].browse(template2fp_company.get(position_template.id)) if template2fp_company else None
  155. if not fp:
  156. continue
  157. for position_tax in position_template.tax_ids:
  158. src_id = template2tax.get(company_id).get(position_tax.tax_src_id.id)
  159. dest_id = position_tax.tax_dest_id and template2tax.get(company_id).get(position_tax.tax_dest_id.id) or False
  160. position_tax_template_exist = fp.tax_ids.filtered(
  161. lambda tax_fp: tax_fp.tax_src_id.id == src_id and tax_fp.tax_dest_id.id == dest_id
  162. )
  163. if not position_tax_template_exist and (
  164. position_tax.tax_src_id in new_tax_template_by_company[company_id]
  165. or position_tax.tax_dest_id in new_tax_template_by_company[company_id]):
  166. fp_tax_template_vals.append((position_tax, {
  167. 'tax_src_id': src_id,
  168. 'tax_dest_id': dest_id,
  169. 'position_id': fp.id,
  170. }))
  171. chart_template._create_records_with_xmlid('account.fiscal.position.tax', fp_tax_template_vals, env['res.company'].browse(company_id))
  172. def _process_taxes_translations(chart_template, new_template_x_taxes):
  173. """
  174. Retrieve translations for newly created taxes' name and description
  175. for languages of the chart_template.
  176. Those languages are the intersection of the spoken_languages of the chart_template
  177. and installed languages.
  178. """
  179. if not new_template_x_taxes:
  180. return
  181. langs = chart_template._get_langs()
  182. if langs:
  183. template_ids, tax_ids = zip(*new_template_x_taxes)
  184. in_ids = env['account.tax.template'].browse(template_ids)
  185. out_ids = env['account.tax'].browse(tax_ids)
  186. chart_template.process_translations(langs, 'name', in_ids, out_ids)
  187. chart_template.process_translations(langs, 'description', in_ids, out_ids)
  188. def _notify_accountant_managers(taxes_to_check):
  189. accountant_manager_group = env.ref("account.group_account_manager")
  190. partner_managers_ids = accountant_manager_group.users.partner_id.ids
  191. odoobot_id = env.ref('base.partner_root').id
  192. message_body = _(
  193. "Please check these taxes. They might be outdated. We did not update them. "
  194. "Indeed, they do not exactly match the taxes of the original version of the localization module.<br/>"
  195. "You might want to archive or adapt them.<br/><ul>"
  196. )
  197. for account_tax in taxes_to_check:
  198. message_body += f"<li>{html_escape(account_tax.name)}</li>"
  199. message_body += "</ul>"
  200. env['mail.thread'].message_notify(
  201. subject=_('Your taxes have been updated !'),
  202. author_id=odoobot_id,
  203. body=message_body,
  204. partner_ids=partner_managers_ids,
  205. )
  206. def _validate_taxes_country(chart_template, template2tax):
  207. """ Checks that existing taxes' country are either compatible with the company's
  208. fiscal country, or with the chart_template's country.
  209. """
  210. for company_id in template2tax:
  211. company = env['res.company'].browse(company_id)
  212. for template_id in template2tax[company_id]:
  213. tax = env['account.tax'].browse(template2tax[company_id][template_id])
  214. if (not chart_template.country_id or tax.country_id != chart_template.country_id) and tax.country_id != company.account_fiscal_country_id:
  215. raise ValidationError(_("Please check the fiscal country of company %s. (Settings > Accounting > Fiscal Country)"
  216. "Taxes can only be updated if they are in the company's fiscal country (%s) or the localization's country (%s).",
  217. company.name, company.account_fiscal_country_id.name, chart_template.country_id.name))
  218. env = api.Environment(cr, SUPERUSER_ID, {})
  219. chart_template = env.ref(chart_template_xmlid)
  220. companies = env['res.company'].search([('chart_template_id', 'child_of', chart_template.id)])
  221. templates = env['account.tax.template'].with_context(active_test=False).search([('chart_template_id', '=', chart_template.id)])
  222. template2tax = _get_template_to_real_xmlid_mapping('account.tax', templates)
  223. # adds companies that use the chart_template through fiscal position system
  224. companies = companies.union(env['res.company'].browse(template2tax.keys()))
  225. outdated_taxes = env['account.tax']
  226. new_tax_template_by_company = defaultdict(env['account.tax.template'].browse) # only contains completely new taxes (not previous taxe had the xmlid)
  227. new_template2tax = [] # contains all created taxes
  228. _validate_taxes_country(chart_template, template2tax)
  229. for company in companies:
  230. templates_to_tax_create = []
  231. templates_to_tax_update = []
  232. template2oldtax_company = template2tax.get(company.id)
  233. for template in templates:
  234. tax = env['account.tax'].browse(template2oldtax_company.get(template.id)) if template2oldtax_company else None
  235. if not tax or not _is_tax_and_template_same(template, tax):
  236. templates_to_tax_create.append((template, tax))
  237. if tax:
  238. outdated_taxes += tax
  239. else:
  240. # we only want to update fiscal position if there is no previous tax with the mapping
  241. new_tax_template_by_company[company.id] += template
  242. else:
  243. templates_to_tax_update.append((template, tax))
  244. new_template2tax += _create_taxes_from_template(company, templates_to_tax_create, templates_to_tax_update)
  245. _update_taxes_from_template(templates_to_tax_update)
  246. _update_fiscal_positions_from_templates(chart_template, new_tax_template_by_company, templates)
  247. if outdated_taxes:
  248. _notify_accountant_managers(outdated_taxes)
  249. if hasattr(chart_template, 'spoken_languages') and chart_template.spoken_languages:
  250. _process_taxes_translations(chart_template, new_template2tax)
  251. return new_template2tax
  252. # ---------------------------------------------------------------
  253. # Account Templates: Account, Tax, Tax Code and chart. + Wizard
  254. # ---------------------------------------------------------------
  255. class AccountGroupTemplate(models.Model):
  256. _name = "account.group.template"
  257. _description = 'Template for Account Groups'
  258. _order = 'code_prefix_start'
  259. parent_id = fields.Many2one('account.group.template', ondelete='cascade')
  260. name = fields.Char(required=True)
  261. code_prefix_start = fields.Char()
  262. code_prefix_end = fields.Char()
  263. chart_template_id = fields.Many2one('account.chart.template', string='Chart Template', required=True)
  264. class AccountAccountTemplate(models.Model):
  265. _name = "account.account.template"
  266. _inherit = ['mail.thread']
  267. _description = 'Templates for Accounts'
  268. _order = "code"
  269. name = fields.Char(required=True)
  270. currency_id = fields.Many2one('res.currency', string='Account Currency', help="Forces all moves for this account to have this secondary currency.")
  271. code = fields.Char(size=64, required=True)
  272. account_type = fields.Selection(
  273. selection=[
  274. ("asset_receivable", "Receivable"),
  275. ("asset_cash", "Bank and Cash"),
  276. ("asset_current", "Current Assets"),
  277. ("asset_non_current", "Non-current Assets"),
  278. ("asset_prepayments", "Prepayments"),
  279. ("asset_fixed", "Fixed Assets"),
  280. ("liability_payable", "Payable"),
  281. ("liability_credit_card", "Credit Card"),
  282. ("liability_current", "Current Liabilities"),
  283. ("liability_non_current", "Non-current Liabilities"),
  284. ("equity", "Equity"),
  285. ("equity_unaffected", "Current Year Earnings"),
  286. ("income", "Income"),
  287. ("income_other", "Other Income"),
  288. ("expense", "Expenses"),
  289. ("expense_depreciation", "Depreciation"),
  290. ("expense_direct_cost", "Cost of Revenue"),
  291. ("off_balance", "Off-Balance Sheet"),
  292. ],
  293. string="Type",
  294. help="These types are defined according to your country. The type contains more information "\
  295. "about the account and its specificities."
  296. )
  297. reconcile = fields.Boolean(string='Allow Invoices & payments Matching', default=False,
  298. help="Check this option if you want the user to reconcile entries in this account.")
  299. note = fields.Text()
  300. tax_ids = fields.Many2many('account.tax.template', 'account_account_template_tax_rel', 'account_id', 'tax_id', string='Default Taxes')
  301. nocreate = fields.Boolean(string='Optional Create', default=False,
  302. help="If checked, the new chart of accounts will not contain this by default.")
  303. chart_template_id = fields.Many2one('account.chart.template', string='Chart Template',
  304. 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 "
  305. "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).")
  306. 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")
  307. @api.depends('name', 'code')
  308. def name_get(self):
  309. res = []
  310. for record in self:
  311. name = record.name
  312. if record.code:
  313. name = record.code + ' ' + name
  314. res.append((record.id, name))
  315. return res
  316. @api.constrains('code')
  317. def _check_account_code(self):
  318. for account in self:
  319. if not re.match(ACCOUNT_CODE_REGEX, account.code):
  320. raise ValidationError(_(
  321. "The account code can only contain alphanumeric characters and dots."
  322. ))
  323. class AccountChartTemplate(models.Model):
  324. _name = "account.chart.template"
  325. _description = "Account Chart Template"
  326. name = fields.Char(required=True)
  327. parent_id = fields.Many2one('account.chart.template', string='Parent Chart Template')
  328. code_digits = fields.Integer(string='# of Digits', required=True, default=6, help="No. of Digits to use for account code")
  329. visible = fields.Boolean(string='Can be Visible?', default=True,
  330. 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 "
  331. "templates, this is useful when you want to generate accounts of this template only when loading its child template.")
  332. currency_id = fields.Many2one('res.currency', string='Currency', required=True)
  333. use_anglo_saxon = fields.Boolean(string="Use Anglo-Saxon accounting", default=False)
  334. use_storno_accounting = fields.Boolean(string="Use Storno accounting", default=False)
  335. account_ids = fields.One2many('account.account.template', 'chart_template_id', string='Associated Account Templates')
  336. tax_template_ids = fields.One2many('account.tax.template', 'chart_template_id', string='Tax Template List',
  337. help='List of all the taxes that have to be installed by the wizard')
  338. bank_account_code_prefix = fields.Char(string='Prefix of the bank accounts', required=True)
  339. cash_account_code_prefix = fields.Char(string='Prefix of the main cash accounts', required=True)
  340. transfer_account_code_prefix = fields.Char(string='Prefix of the main transfer accounts', required=True)
  341. income_currency_exchange_account_id = fields.Many2one('account.account.template',
  342. string="Gain Exchange Rate Account", domain=[('account_type', 'not in', ('asset_receivable', 'liability_payable', 'asset_cash', 'liability_credit_card')), ('deprecated', '=', False)])
  343. expense_currency_exchange_account_id = fields.Many2one('account.account.template',
  344. string="Loss Exchange Rate Account", domain=[('account_type', 'not in', ('asset_receivable', 'liability_payable', 'asset_cash', 'liability_credit_card')), ('deprecated', '=', False)])
  345. country_id = fields.Many2one(string="Country", comodel_name='res.country', help="The country this chart of accounts belongs to. None if it's generic.")
  346. account_journal_suspense_account_id = fields.Many2one('account.account.template', string='Journal Suspense Account')
  347. account_journal_payment_debit_account_id = fields.Many2one('account.account.template', string='Journal Outstanding Receipts Account')
  348. account_journal_payment_credit_account_id = fields.Many2one('account.account.template', string='Journal Outstanding Payments Account')
  349. default_cash_difference_income_account_id = fields.Many2one('account.account.template', string="Cash Difference Income Account")
  350. default_cash_difference_expense_account_id = fields.Many2one('account.account.template', string="Cash Difference Expense Account")
  351. default_pos_receivable_account_id = fields.Many2one('account.account.template', string="PoS receivable account")
  352. account_journal_early_pay_discount_loss_account_id = fields.Many2one(comodel_name='account.account.template', string='Cash Discount Write-Off Loss Account', )
  353. account_journal_early_pay_discount_gain_account_id = fields.Many2one(comodel_name='account.account.template', string='Cash Discount Write-Off Gain Account', )
  354. property_account_receivable_id = fields.Many2one('account.account.template', string='Receivable Account')
  355. property_account_payable_id = fields.Many2one('account.account.template', string='Payable Account')
  356. property_account_expense_categ_id = fields.Many2one('account.account.template', string='Category of Expense Account')
  357. property_account_income_categ_id = fields.Many2one('account.account.template', string='Category of Income Account')
  358. property_account_expense_id = fields.Many2one('account.account.template', string='Expense Account on Product Template')
  359. property_account_income_id = fields.Many2one('account.account.template', string='Income Account on Product Template')
  360. property_stock_account_input_categ_id = fields.Many2one('account.account.template', string="Input Account for Stock Valuation")
  361. property_stock_account_output_categ_id = fields.Many2one('account.account.template', string="Output Account for Stock Valuation")
  362. property_stock_valuation_account_id = fields.Many2one('account.account.template', string="Account Template for Stock Valuation")
  363. property_tax_payable_account_id = fields.Many2one('account.account.template', string="Tax current account (payable)")
  364. property_tax_receivable_account_id = fields.Many2one('account.account.template', string="Tax current account (receivable)")
  365. property_advance_tax_payment_account_id = fields.Many2one('account.account.template', string="Advance tax payment account")
  366. property_cash_basis_base_account_id = fields.Many2one(
  367. comodel_name='account.account.template',
  368. domain=[('deprecated', '=', False)],
  369. string="Base Tax Received Account",
  370. help="Account that will be set on lines created in cash basis journal entry and used to keep track of the "
  371. "tax base amount.")
  372. @api.model
  373. def _prepare_transfer_account_template(self, prefix=None):
  374. ''' Prepare values to create the transfer account that is an intermediary account used when moving money
  375. from a liquidity account to another.
  376. :return: A dictionary of values to create a new account.account.
  377. '''
  378. digits = self.code_digits
  379. prefix = prefix or self.transfer_account_code_prefix or ''
  380. # Flatten the hierarchy of chart templates.
  381. chart_template = self
  382. chart_templates = self
  383. while chart_template.parent_id:
  384. chart_templates += chart_template.parent_id
  385. chart_template = chart_template.parent_id
  386. new_code = ''
  387. for num in range(1, 100):
  388. new_code = str(prefix.ljust(digits - 1, '0')) + str(num)
  389. rec = self.env['account.account.template'].search(
  390. [('code', '=', new_code), ('chart_template_id', 'in', chart_templates.ids)], limit=1)
  391. if not rec:
  392. break
  393. else:
  394. raise UserError(_('Cannot generate an unused account code.'))
  395. return {
  396. 'name': _('Liquidity Transfer'),
  397. 'code': new_code,
  398. 'account_type': 'asset_current',
  399. 'reconcile': True,
  400. 'chart_template_id': self.id,
  401. }
  402. @api.model
  403. def _create_liquidity_journal_suspense_account(self, company, code_digits):
  404. return self.env['account.account'].create({
  405. 'name': _("Bank Suspense Account"),
  406. 'code': self.env['account.account']._search_new_account_code(company, code_digits, company.bank_account_code_prefix or ''),
  407. 'account_type': 'asset_current',
  408. 'company_id': company.id,
  409. })
  410. @api.model
  411. def _create_cash_discount_loss_account(self, company, code_digits):
  412. return self.env['account.account'].create({
  413. 'name': _("Cash Discount Loss"),
  414. 'code': 999998,
  415. 'account_type': 'expense',
  416. 'company_id': company.id,
  417. })
  418. @api.model
  419. def _create_cash_discount_gain_account(self, company, code_digits):
  420. return self.env['account.account'].create({
  421. 'name': _("Cash Discount Gain"),
  422. 'code': 999997,
  423. 'account_type': 'income_other',
  424. 'company_id': company.id,
  425. })
  426. def try_loading(self, company=False, install_demo=True):
  427. """ Installs this chart of accounts for the current company if not chart
  428. of accounts had been created for it yet.
  429. :param company (Model<res.company>): the company we try to load the chart template on.
  430. If not provided, it is retrieved from the context.
  431. :param install_demo (bool): whether or not we should load demo data right after loading the
  432. chart template.
  433. """
  434. # do not use `request.env` here, it can cause deadlocks
  435. if not company:
  436. if request and hasattr(request, 'allowed_company_ids'):
  437. company = self.env['res.company'].browse(request.allowed_company_ids[0])
  438. else:
  439. company = self.env.company
  440. # If we don't have any chart of account on this company, install this chart of account
  441. if not company.chart_template_id and not self.existing_accounting(company):
  442. for template in self:
  443. template.with_context(default_company_id=company.id)._load(company)
  444. # Install the demo data when the first localization is instanciated on the company
  445. if install_demo and self.env.ref('base.module_account').demo:
  446. self.with_context(
  447. default_company_id=company.id,
  448. allowed_company_ids=[company.id],
  449. )._create_demo_data()
  450. def _create_demo_data(self):
  451. try:
  452. with self.env.cr.savepoint():
  453. demo_data = self._get_demo_data()
  454. for model, data in demo_data:
  455. created = self.env[model]._load_records([{
  456. 'xml_id': "account.%s" % xml_id if '.' not in xml_id else xml_id,
  457. 'values': record,
  458. 'noupdate': True,
  459. } for xml_id, record in data.items()])
  460. self._post_create_demo_data(created)
  461. except Exception:
  462. # Do not rollback installation of CoA if demo data failed
  463. _logger.exception('Error while loading accounting demo data')
  464. def _load(self, company):
  465. """ Installs this chart of accounts on the current company, replacing
  466. the existing one if it had already one defined. If some accounting entries
  467. had already been made, this function fails instead, triggering a UserError.
  468. Also, note that this function can only be run by someone with administration
  469. rights.
  470. """
  471. self.ensure_one()
  472. # do not use `request.env` here, it can cause deadlocks
  473. # Ensure everything is translated to the company's language, not the user's one.
  474. self = self.with_context(lang=company.partner_id.lang).with_company(company)
  475. if not self.env.is_admin():
  476. raise AccessError(_("Only administrators can load a chart of accounts"))
  477. existing_accounts = self.env['account.account'].search([('company_id', '=', company.id)])
  478. if existing_accounts:
  479. # we tolerate switching from accounting package (localization module) as long as there isn't yet any accounting
  480. # entries created for the company.
  481. if self.existing_accounting(company):
  482. raise UserError(_('Could not install new chart of account as there are already accounting entries existing.'))
  483. # delete accounting properties
  484. prop_values = ['account.account,%s' % (account_id,) for account_id in existing_accounts.ids]
  485. existing_journals = self.env['account.journal'].search([('company_id', '=', company.id)])
  486. if existing_journals:
  487. prop_values.extend(['account.journal,%s' % (journal_id,) for journal_id in existing_journals.ids])
  488. self.env['ir.property'].sudo().search(
  489. [('value_reference', 'in', prop_values)]
  490. ).unlink()
  491. # delete account, journal, tax, fiscal position and reconciliation model
  492. models_to_delete = ['account.reconcile.model', 'account.fiscal.position', 'account.move.line', 'account.move', 'account.journal', 'account.tax', 'account.group']
  493. for model in models_to_delete:
  494. res = self.env[model].sudo().search([('company_id', '=', company.id)])
  495. if len(res):
  496. res.with_context(force_delete=True).unlink()
  497. existing_accounts.unlink()
  498. company.write({'currency_id': self.currency_id.id,
  499. 'anglo_saxon_accounting': self.use_anglo_saxon,
  500. 'account_storno': self.use_storno_accounting,
  501. 'bank_account_code_prefix': self.bank_account_code_prefix,
  502. 'cash_account_code_prefix': self.cash_account_code_prefix,
  503. 'transfer_account_code_prefix': self.transfer_account_code_prefix,
  504. 'chart_template_id': self.id
  505. })
  506. #set the coa currency to active
  507. self.currency_id.write({'active': True})
  508. # When we install the CoA of first company, set the currency to price types and pricelists
  509. if company.id == 1:
  510. for reference in ['product.list_price', 'product.standard_price', 'product.list0']:
  511. try:
  512. tmp2 = self.env.ref(reference).write({'currency_id': self.currency_id.id})
  513. except ValueError:
  514. pass
  515. # Set the fiscal country before generating taxes in case the company does not have a country_id set yet
  516. if self.country_id:
  517. # If this CoA is made for only one country, set it as the fiscal country of the company.
  518. company.account_fiscal_country_id = self.country_id
  519. elif not company.account_fiscal_country_id:
  520. company.account_fiscal_country_id = self.env.ref('base.us')
  521. # Install all the templates objects and generate the real objects
  522. acc_template_ref, taxes_ref = self._install_template(company, code_digits=self.code_digits)
  523. # Set default cash discount write-off accounts
  524. if not company.account_journal_early_pay_discount_loss_account_id:
  525. company.account_journal_early_pay_discount_loss_account_id = self._create_cash_discount_loss_account(
  526. company, self.code_digits)
  527. if not company.account_journal_early_pay_discount_gain_account_id:
  528. company.account_journal_early_pay_discount_gain_account_id = self._create_cash_discount_gain_account(
  529. company, self.code_digits)
  530. # Set default cash difference account on company
  531. if not company.account_journal_suspense_account_id:
  532. company.account_journal_suspense_account_id = self._create_liquidity_journal_suspense_account(company, self.code_digits)
  533. if not company.account_journal_payment_debit_account_id:
  534. company.account_journal_payment_debit_account_id = self.env['account.account'].create({
  535. 'name': _("Outstanding Receipts"),
  536. 'code': self.env['account.account']._search_new_account_code(company, self.code_digits, company.bank_account_code_prefix or ''),
  537. 'reconcile': True,
  538. 'account_type': 'asset_current',
  539. 'company_id': company.id,
  540. })
  541. if not company.account_journal_payment_credit_account_id:
  542. company.account_journal_payment_credit_account_id = self.env['account.account'].create({
  543. 'name': _("Outstanding Payments"),
  544. 'code': self.env['account.account']._search_new_account_code(company, self.code_digits, company.bank_account_code_prefix or ''),
  545. 'reconcile': True,
  546. 'account_type': 'asset_current',
  547. 'company_id': company.id,
  548. })
  549. if not company.default_cash_difference_expense_account_id:
  550. company.default_cash_difference_expense_account_id = self.env['account.account'].create({
  551. 'name': _('Cash Difference Loss'),
  552. 'code': self.env['account.account']._search_new_account_code(company, self.code_digits, '999'),
  553. 'account_type': 'expense',
  554. 'tag_ids': [(6, 0, self.env.ref('account.account_tag_investing').ids)],
  555. 'company_id': company.id,
  556. })
  557. if not company.default_cash_difference_income_account_id:
  558. company.default_cash_difference_income_account_id = self.env['account.account'].create({
  559. 'name': _('Cash Difference Gain'),
  560. 'code': self.env['account.account']._search_new_account_code(company, self.code_digits, '999'),
  561. 'account_type': 'income_other',
  562. 'tag_ids': [(6, 0, self.env.ref('account.account_tag_investing').ids)],
  563. 'company_id': company.id,
  564. })
  565. # Set the transfer account on the company
  566. company.transfer_account_id = self.env['account.account'].search([
  567. ('code', '=like', self.transfer_account_code_prefix + '%'), ('company_id', '=', company.id)], limit=1)
  568. # Create Bank journals
  569. self._create_bank_journals(company, acc_template_ref)
  570. # Create the current year earning account if it wasn't present in the CoA
  571. company.get_unaffected_earnings_account()
  572. # set the default taxes on the company
  573. company.account_sale_tax_id = self.env['account.tax'].search([('type_tax_use', 'in', ('sale', 'all')), ('company_id', '=', company.id)], limit=1).id
  574. company.account_purchase_tax_id = self.env['account.tax'].search([('type_tax_use', 'in', ('purchase', 'all')), ('company_id', '=', company.id)], limit=1).id
  575. return {}
  576. @api.model
  577. def existing_accounting(self, company_id):
  578. """ Returns True iff some accounting entries have already been made for
  579. the provided company (meaning hence that its chart of accounts cannot
  580. be changed anymore).
  581. """
  582. model_to_check = ['account.payment', 'account.bank.statement.line']
  583. for model in model_to_check:
  584. if self.env[model].sudo().search([('company_id', '=', company_id.id)], order="id DESC", limit=1):
  585. return True
  586. if self.env['account.move'].sudo().search([('company_id', '=', company_id.id), ('state', '!=', 'draft')], order="id DESC", limit=1):
  587. return True
  588. return False
  589. def _get_chart_parent_ids(self):
  590. """ Returns the IDs of all ancestor charts, including the chart itself.
  591. (inverse of child_of operator)
  592. :return: the IDS of all ancestor charts, including the chart itself.
  593. """
  594. chart_template = self
  595. result = [chart_template.id]
  596. while chart_template.parent_id:
  597. chart_template = chart_template.parent_id
  598. result.append(chart_template.id)
  599. return result
  600. def _create_bank_journals(self, company, acc_template_ref):
  601. '''
  602. This function creates bank journals and their account for each line
  603. data returned by the function _get_default_bank_journals_data.
  604. :param company: the company for which the wizard is running.
  605. :param acc_template_ref: the dictionary containing the mapping between the ids of account templates and the ids
  606. of the accounts that have been generated from them.
  607. '''
  608. self.ensure_one()
  609. bank_journals = self.env['account.journal']
  610. # Create the journals that will trigger the account.account creation
  611. for acc in self._get_default_bank_journals_data():
  612. bank_journals += self.env['account.journal'].create({
  613. 'name': acc['acc_name'],
  614. 'type': acc['account_type'],
  615. 'company_id': company.id,
  616. 'currency_id': acc.get('currency_id', self.env['res.currency']).id,
  617. 'sequence': 10,
  618. })
  619. return bank_journals
  620. @api.model
  621. def _get_default_bank_journals_data(self):
  622. """ Returns the data needed to create the default bank journals when
  623. installing this chart of accounts, in the form of a list of dictionaries.
  624. The allowed keys in these dictionaries are:
  625. - acc_name: string (mandatory)
  626. - account_type: 'cash' or 'bank' (mandatory)
  627. - currency_id (optional, only to be specified if != company.currency_id)
  628. """
  629. return [{'acc_name': _('Cash'), 'account_type': 'cash'}, {'acc_name': _('Bank'), 'account_type': 'bank'}]
  630. @api.model
  631. def generate_journals(self, acc_template_ref, company, journals_dict=None):
  632. """
  633. This method is used for creating journals.
  634. :param acc_template_ref: Account templates reference.
  635. :param company_id: company to generate journals for.
  636. :returns: True
  637. """
  638. JournalObj = self.env['account.journal']
  639. for vals_journal in self._prepare_all_journals(acc_template_ref, company, journals_dict=journals_dict):
  640. journal = JournalObj.create(vals_journal)
  641. if vals_journal['type'] == 'general' and vals_journal['code'] == _('EXCH'):
  642. company.write({'currency_exchange_journal_id': journal.id})
  643. if vals_journal['type'] == 'general' and vals_journal['code'] == _('CABA'):
  644. company.write({'tax_cash_basis_journal_id': journal.id})
  645. return True
  646. def _prepare_all_journals(self, acc_template_ref, company, journals_dict=None):
  647. def _get_default_account(journal_vals, type='debit'):
  648. # Get the default accounts
  649. default_account = False
  650. if journal['type'] == 'sale':
  651. default_account = acc_template_ref.get(self.property_account_income_categ_id).id
  652. elif journal['type'] == 'purchase':
  653. default_account = acc_template_ref.get(self.property_account_expense_categ_id).id
  654. return default_account
  655. journals = [{'name': _('Customer Invoices'), 'type': 'sale', 'code': _('INV'), 'favorite': True, 'color': 11, 'sequence': 5},
  656. {'name': _('Vendor Bills'), 'type': 'purchase', 'code': _('BILL'), 'favorite': True, 'color': 11, 'sequence': 6},
  657. {'name': _('Miscellaneous Operations'), 'type': 'general', 'code': _('MISC'), 'favorite': True, 'sequence': 7},
  658. {'name': _('Exchange Difference'), 'type': 'general', 'code': _('EXCH'), 'favorite': False, 'sequence': 9},
  659. {'name': _('Cash Basis Taxes'), 'type': 'general', 'code': _('CABA'), 'favorite': False, 'sequence': 10}]
  660. if journals_dict != None:
  661. journals.extend(journals_dict)
  662. self.ensure_one()
  663. journal_data = []
  664. for journal in journals:
  665. vals = {
  666. 'type': journal['type'],
  667. 'name': journal['name'],
  668. 'code': journal['code'],
  669. 'company_id': company.id,
  670. 'default_account_id': _get_default_account(journal),
  671. 'show_on_dashboard': journal['favorite'],
  672. 'color': journal.get('color', False),
  673. 'sequence': journal['sequence']
  674. }
  675. journal_data.append(vals)
  676. return journal_data
  677. def generate_properties(self, acc_template_ref, company):
  678. """
  679. This method used for creating properties.
  680. :param acc_template_ref: Mapping between ids of account templates and real accounts created from them
  681. :param company_id: company to generate properties for.
  682. :returns: True
  683. """
  684. self.ensure_one()
  685. PropertyObj = self.env['ir.property']
  686. todo_list = [
  687. ('property_account_receivable_id', 'res.partner'),
  688. ('property_account_payable_id', 'res.partner'),
  689. ('property_account_expense_categ_id', 'product.category'),
  690. ('property_account_income_categ_id', 'product.category'),
  691. ('property_account_expense_id', 'product.template'),
  692. ('property_account_income_id', 'product.template'),
  693. ('property_tax_payable_account_id', 'account.tax.group'),
  694. ('property_tax_receivable_account_id', 'account.tax.group'),
  695. ('property_advance_tax_payment_account_id', 'account.tax.group'),
  696. ]
  697. for field, model in todo_list:
  698. account = self[field]
  699. value = acc_template_ref[account].id if account else False
  700. if value:
  701. PropertyObj._set_default(field, model, value, company=company)
  702. stock_properties = [
  703. 'property_stock_account_input_categ_id',
  704. 'property_stock_account_output_categ_id',
  705. 'property_stock_valuation_account_id',
  706. ]
  707. for stock_property in stock_properties:
  708. account = getattr(self, stock_property)
  709. value = account and acc_template_ref[account].id or False
  710. if value:
  711. company.write({stock_property: value})
  712. return True
  713. def _install_template(self, company, code_digits=None, obj_wizard=None, acc_ref=None, taxes_ref=None):
  714. """ Recursively load the template objects and create the real objects from them.
  715. :param company: company the wizard is running for
  716. :param code_digits: number of digits the accounts code should have in the COA
  717. :param obj_wizard: the current wizard for generating the COA from the templates
  718. :param acc_ref: Mapping between ids of account templates and real accounts created from them
  719. :param taxes_ref: Mapping between ids of tax templates and real taxes created from them
  720. :returns: tuple with a dictionary containing
  721. * the mapping between the account template ids and the ids of the real accounts that have been generated
  722. from them, as first item,
  723. * a similar dictionary for mapping the tax templates and taxes, as second item,
  724. :rtype: tuple(dict, dict, dict)
  725. """
  726. self.ensure_one()
  727. if acc_ref is None:
  728. acc_ref = {}
  729. if taxes_ref is None:
  730. taxes_ref = {}
  731. if self.parent_id:
  732. tmp1, tmp2 = self.parent_id._install_template(company, code_digits=code_digits, acc_ref=acc_ref, taxes_ref=taxes_ref)
  733. acc_ref.update(tmp1)
  734. taxes_ref.update(tmp2)
  735. # Ensure, even if individually, that everything is translated according to the company's language.
  736. 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)
  737. acc_ref.update(tmp1)
  738. taxes_ref.update(tmp2)
  739. return acc_ref, taxes_ref
  740. def _load_template(self, company, code_digits=None, account_ref=None, taxes_ref=None):
  741. """ Generate all the objects from the templates
  742. :param company: company the wizard is running for
  743. :param code_digits: number of digits the accounts code should have in the COA
  744. :param acc_ref: Mapping between ids of account templates and real accounts created from them
  745. :param taxes_ref: Mapping between ids of tax templates and real taxes created from them
  746. :returns: tuple with a dictionary containing
  747. * the mapping between the account template ids and the ids of the real accounts that have been generated
  748. from them, as first item,
  749. * a similar dictionary for mapping the tax templates and taxes, as second item,
  750. :rtype: tuple(dict, dict, dict)
  751. """
  752. self.ensure_one()
  753. if account_ref is None:
  754. account_ref = {}
  755. if taxes_ref is None:
  756. taxes_ref = {}
  757. if not code_digits:
  758. code_digits = self.code_digits
  759. AccountTaxObj = self.env['account.tax']
  760. # Generate taxes from templates.
  761. generated_tax_res = self.with_context(active_test=False).tax_template_ids._generate_tax(company)
  762. taxes_ref.update(generated_tax_res['tax_template_to_tax'])
  763. # Generating Accounts from templates.
  764. account_template_ref = self.generate_account(taxes_ref, account_ref, code_digits, company)
  765. account_ref.update(account_template_ref)
  766. # Generate account groups, from template
  767. self.generate_account_groups(company)
  768. # writing account values after creation of accounts
  769. for tax, value in generated_tax_res['account_dict']['account.tax'].items():
  770. if value['cash_basis_transition_account_id']:
  771. tax.cash_basis_transition_account_id = account_ref.get(value['cash_basis_transition_account_id'])
  772. for repartition_line, value in generated_tax_res['account_dict']['account.tax.repartition.line'].items():
  773. if value['account_id']:
  774. repartition_line.account_id = account_ref.get(value['account_id'])
  775. # Set the company accounts
  776. self._load_company_accounts(account_ref, company)
  777. # Create Journals - Only done for root chart template
  778. if not self.parent_id:
  779. self.generate_journals(account_ref, company)
  780. # generate properties function
  781. self.generate_properties(account_ref, company)
  782. # Generate Fiscal Position , Fiscal Position Accounts and Fiscal Position Taxes from templates
  783. self.generate_fiscal_position(taxes_ref, account_ref, company)
  784. # Generate account operation template templates
  785. self.generate_account_reconcile_model(taxes_ref, account_ref, company)
  786. return account_ref, taxes_ref
  787. def _load_company_accounts(self, account_ref, company):
  788. # Set the default accounts on the company
  789. accounts = {
  790. 'default_cash_difference_income_account_id': self.default_cash_difference_income_account_id,
  791. 'default_cash_difference_expense_account_id': self.default_cash_difference_expense_account_id,
  792. 'account_journal_early_pay_discount_loss_account_id': self.account_journal_early_pay_discount_loss_account_id,
  793. 'account_journal_early_pay_discount_gain_account_id': self.account_journal_early_pay_discount_gain_account_id,
  794. 'account_journal_suspense_account_id': self.account_journal_suspense_account_id,
  795. 'account_journal_payment_debit_account_id': self.account_journal_payment_debit_account_id,
  796. 'account_journal_payment_credit_account_id': self.account_journal_payment_credit_account_id,
  797. 'account_cash_basis_base_account_id': self.property_cash_basis_base_account_id,
  798. 'account_default_pos_receivable_account_id': self.default_pos_receivable_account_id,
  799. 'income_currency_exchange_account_id': self.income_currency_exchange_account_id,
  800. 'expense_currency_exchange_account_id': self.expense_currency_exchange_account_id,
  801. }
  802. values = {}
  803. # The loop is to avoid writing when we have no values, thus avoiding erasing the account from the parent
  804. for key, account in accounts.items():
  805. if account_ref.get(account):
  806. values[key] = account_ref.get(account)
  807. company.write(values)
  808. def create_record_with_xmlid(self, company, template, model, vals):
  809. return self._create_records_with_xmlid(model, [(template, vals)], company).id
  810. def _create_records_with_xmlid(self, model, template_vals, company):
  811. """ Create records for the given model name with the given vals, and
  812. create xml ids based on each record's template and company id.
  813. """
  814. if not template_vals:
  815. return self.env[model]
  816. template_model = template_vals[0][0]
  817. template_ids = [template.id for template, vals in template_vals]
  818. template_xmlids = template_model.browse(template_ids).get_external_id()
  819. data_list = []
  820. for template, vals in template_vals:
  821. module, name = template_xmlids[template.id].split('.', 1)
  822. xml_id = "%s.%s_%s" % (module, company.id, name)
  823. data_list.append(dict(xml_id=xml_id, values=vals, noupdate=True))
  824. return self.env[model]._load_records(data_list)
  825. @api.model
  826. def _load_records(self, data_list, update=False):
  827. # When creating a chart template create, for the liquidity transfer account
  828. # - an account.account.template: this allow to define account.reconcile.model.template objects refering that liquidity transfer
  829. # account although it's not existing in any xml file
  830. # - an entry in ir_model_data: this allow to still use the method create_record_with_xmlid() and don't make any difference between
  831. # regular accounts created and that liquidity transfer account
  832. records = super(AccountChartTemplate, self)._load_records(data_list, update)
  833. account_data_list = []
  834. for data, record in zip(data_list, records):
  835. # Create the transfer account only for leaf chart template in the hierarchy.
  836. if record.parent_id:
  837. continue
  838. if data.get('xml_id'):
  839. account_xml_id = data['xml_id'] + '_liquidity_transfer'
  840. if not self.env.ref(account_xml_id, raise_if_not_found=False):
  841. account_vals = record._prepare_transfer_account_template()
  842. account_data_list.append(dict(
  843. xml_id=account_xml_id,
  844. values=account_vals,
  845. noupdate=data.get('noupdate'),
  846. ))
  847. self.env['account.account.template']._load_records(account_data_list, update)
  848. return records
  849. def _get_account_vals(self, company, account_template, code_acc, tax_template_ref):
  850. """ This method generates a dictionary of all the values for the account that will be created.
  851. """
  852. self.ensure_one()
  853. tax_ids = []
  854. for tax in account_template.tax_ids:
  855. tax_ids.append(tax_template_ref[tax].id)
  856. val = {
  857. 'name': account_template.name,
  858. 'currency_id': account_template.currency_id and account_template.currency_id.id or False,
  859. 'code': code_acc,
  860. 'account_type': account_template.account_type or False,
  861. 'reconcile': account_template.reconcile,
  862. 'note': account_template.note,
  863. 'tax_ids': [(6, 0, tax_ids)],
  864. 'company_id': company.id,
  865. 'tag_ids': [(6, 0, [t.id for t in account_template.tag_ids])],
  866. }
  867. return val
  868. def generate_account(self, tax_template_ref, acc_template_ref, code_digits, company):
  869. """ This method generates accounts from account templates.
  870. :param tax_template_ref: Taxes templates reference for write taxes_id in account_account.
  871. :param acc_template_ref: dictionary containing the mapping between the account templates and generated accounts (will be populated)
  872. :param code_digits: number of digits to use for account code.
  873. :param company_id: company to generate accounts for.
  874. :returns: return acc_template_ref for reference purpose.
  875. :rtype: dict
  876. """
  877. self.ensure_one()
  878. account_tmpl_obj = self.env['account.account.template']
  879. acc_template = account_tmpl_obj.search([('nocreate', '!=', True), ('chart_template_id', '=', self.id)], order='id')
  880. template_vals = []
  881. for account_template in acc_template:
  882. code_main = account_template.code and len(account_template.code) or 0
  883. code_acc = account_template.code or ''
  884. if code_main > 0 and code_main <= code_digits:
  885. code_acc = str(code_acc) + (str('0'*(code_digits-code_main)))
  886. vals = self._get_account_vals(company, account_template, code_acc, tax_template_ref)
  887. template_vals.append((account_template, vals))
  888. accounts = self._create_records_with_xmlid('account.account', template_vals, company)
  889. for template, account in zip(acc_template, accounts):
  890. acc_template_ref[template] = account
  891. return acc_template_ref
  892. def generate_account_groups(self, company):
  893. """ This method generates account groups from account groups templates.
  894. :param company: company to generate the account groups for
  895. """
  896. self.ensure_one()
  897. group_templates = self.env['account.group.template'].search([('chart_template_id', '=', self.id)])
  898. template_vals = []
  899. for group_template in group_templates:
  900. vals = {
  901. 'name': group_template.name,
  902. 'code_prefix_start': group_template.code_prefix_start,
  903. 'code_prefix_end': group_template.code_prefix_end,
  904. 'company_id': company.id,
  905. }
  906. template_vals.append((group_template, vals))
  907. groups = self._create_records_with_xmlid('account.group', template_vals, company)
  908. def _prepare_reconcile_model_vals(self, company, account_reconcile_model, acc_template_ref, tax_template_ref):
  909. """ This method generates a dictionary of all the values for the account.reconcile.model that will be created.
  910. """
  911. self.ensure_one()
  912. account_reconcile_model_lines = self.env['account.reconcile.model.line.template'].search([
  913. ('model_id', '=', account_reconcile_model.id)
  914. ])
  915. return {
  916. 'name': account_reconcile_model.name,
  917. 'sequence': account_reconcile_model.sequence,
  918. 'company_id': company.id,
  919. 'rule_type': account_reconcile_model.rule_type,
  920. 'auto_reconcile': account_reconcile_model.auto_reconcile,
  921. 'to_check': account_reconcile_model.to_check,
  922. 'match_journal_ids': [(6, None, account_reconcile_model.match_journal_ids.ids)],
  923. 'match_nature': account_reconcile_model.match_nature,
  924. 'match_amount': account_reconcile_model.match_amount,
  925. 'match_amount_min': account_reconcile_model.match_amount_min,
  926. 'match_amount_max': account_reconcile_model.match_amount_max,
  927. 'match_label': account_reconcile_model.match_label,
  928. 'match_label_param': account_reconcile_model.match_label_param,
  929. 'match_note': account_reconcile_model.match_note,
  930. 'match_note_param': account_reconcile_model.match_note_param,
  931. 'match_transaction_type': account_reconcile_model.match_transaction_type,
  932. 'match_transaction_type_param': account_reconcile_model.match_transaction_type_param,
  933. 'match_same_currency': account_reconcile_model.match_same_currency,
  934. 'allow_payment_tolerance': account_reconcile_model.allow_payment_tolerance,
  935. 'payment_tolerance_type': account_reconcile_model.payment_tolerance_type,
  936. 'payment_tolerance_param': account_reconcile_model.payment_tolerance_param,
  937. 'match_partner': account_reconcile_model.match_partner,
  938. 'match_partner_ids': [(6, None, account_reconcile_model.match_partner_ids.ids)],
  939. 'match_partner_category_ids': [(6, None, account_reconcile_model.match_partner_category_ids.ids)],
  940. 'line_ids': [(0, 0, {
  941. 'account_id': acc_template_ref[line.account_id].id,
  942. 'label': line.label,
  943. 'amount_type': line.amount_type,
  944. 'force_tax_included': line.force_tax_included,
  945. 'amount_string': line.amount_string,
  946. 'tax_ids': [[4, tax_template_ref[tax].id, 0] for tax in line.tax_ids],
  947. }) for line in account_reconcile_model_lines],
  948. }
  949. def generate_account_reconcile_model(self, tax_template_ref, acc_template_ref, company):
  950. """ This method creates account reconcile models
  951. :param tax_template_ref: Taxes templates reference for write taxes_id in account_account.
  952. :param acc_template_ref: dictionary with the mapping between the account templates and the real accounts.
  953. :param company_id: company to create models for
  954. :returns: return new_account_reconcile_model for reference purpose.
  955. :rtype: dict
  956. """
  957. self.ensure_one()
  958. account_reconcile_models = self.env['account.reconcile.model.template'].search([
  959. ('chart_template_id', '=', self.id)
  960. ])
  961. for account_reconcile_model in account_reconcile_models:
  962. vals = self._prepare_reconcile_model_vals(company, account_reconcile_model, acc_template_ref, tax_template_ref)
  963. self.create_record_with_xmlid(company, account_reconcile_model, 'account.reconcile.model', vals)
  964. # Create default rules for the reconciliation widget matching invoices automatically.
  965. if not self.parent_id:
  966. self.env['account.reconcile.model'].sudo().create({
  967. "name": _('Invoices/Bills Perfect Match'),
  968. "sequence": '1',
  969. "rule_type": 'invoice_matching',
  970. "auto_reconcile": True,
  971. "match_nature": 'both',
  972. "match_same_currency": True,
  973. "allow_payment_tolerance": True,
  974. "payment_tolerance_type": 'percentage',
  975. "payment_tolerance_param": 0,
  976. "match_partner": True,
  977. "company_id": company.id,
  978. })
  979. self.env['account.reconcile.model'].sudo().create({
  980. "name": _('Invoices/Bills Partial Match if Underpaid'),
  981. "sequence": '2',
  982. "rule_type": 'invoice_matching',
  983. "auto_reconcile": False,
  984. "match_nature": 'both',
  985. "match_same_currency": True,
  986. "allow_payment_tolerance": False,
  987. "match_partner": True,
  988. "company_id": company.id,
  989. })
  990. return True
  991. def _get_fp_vals(self, company, position):
  992. return {
  993. 'company_id': company.id,
  994. 'sequence': position.sequence,
  995. 'name': position.name,
  996. 'note': position.note,
  997. 'auto_apply': position.auto_apply,
  998. 'vat_required': position.vat_required,
  999. 'country_id': position.country_id.id,
  1000. 'country_group_id': position.country_group_id.id,
  1001. 'state_ids': position.state_ids and [(6,0, position.state_ids.ids)] or [],
  1002. 'zip_from': position.zip_from,
  1003. 'zip_to': position.zip_to,
  1004. }
  1005. def generate_fiscal_position(self, tax_template_ref, acc_template_ref, company):
  1006. """ This method generates Fiscal Position, Fiscal Position Accounts
  1007. and Fiscal Position Taxes from templates.
  1008. :param taxes_ids: Taxes templates reference for generating account.fiscal.position.tax.
  1009. :param acc_template_ref: Account templates reference for generating account.fiscal.position.account.
  1010. :param company_id: the company to generate fiscal position data for
  1011. :returns: True
  1012. """
  1013. self.ensure_one()
  1014. positions = self.env['account.fiscal.position.template'].search([('chart_template_id', '=', self.id)])
  1015. # first create fiscal positions in batch
  1016. template_vals = []
  1017. for position in positions:
  1018. fp_vals = self._get_fp_vals(company, position)
  1019. template_vals.append((position, fp_vals))
  1020. fps = self._create_records_with_xmlid('account.fiscal.position', template_vals, company)
  1021. # then create fiscal position taxes and accounts
  1022. tax_template_vals = []
  1023. account_template_vals = []
  1024. for position, fp in zip(positions, fps):
  1025. for tax in position.tax_ids:
  1026. tax_template_vals.append((tax, {
  1027. 'tax_src_id': tax_template_ref[tax.tax_src_id].id,
  1028. 'tax_dest_id': tax.tax_dest_id and tax_template_ref[tax.tax_dest_id].id or False,
  1029. 'position_id': fp.id,
  1030. }))
  1031. for acc in position.account_ids:
  1032. account_template_vals.append((acc, {
  1033. 'account_src_id': acc_template_ref[acc.account_src_id].id,
  1034. 'account_dest_id': acc_template_ref[acc.account_dest_id].id,
  1035. 'position_id': fp.id,
  1036. }))
  1037. self._create_records_with_xmlid('account.fiscal.position.tax', tax_template_vals, company)
  1038. self._create_records_with_xmlid('account.fiscal.position.account', account_template_vals, company)
  1039. return True
  1040. class AccountTaxTemplate(models.Model):
  1041. _name = 'account.tax.template'
  1042. _description = 'Templates for Taxes'
  1043. _order = 'id'
  1044. chart_template_id = fields.Many2one('account.chart.template', string='Chart Template', required=True)
  1045. name = fields.Char(string='Tax Name', required=True)
  1046. type_tax_use = fields.Selection(TYPE_TAX_USE, string='Tax Type', required=True, default="sale",
  1047. 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.")
  1048. tax_scope = fields.Selection([('service', 'Service'), ('consu', 'Consumable')], help="Restrict the use of taxes to a type of product.")
  1049. amount_type = fields.Selection(default='percent', string="Tax Computation", required=True,
  1050. selection=[('group', 'Group of Taxes'), ('fixed', 'Fixed'), ('percent', 'Percentage of Price'), ('division', 'Percentage of Price Tax Included')])
  1051. active = fields.Boolean(default=True, help="Set active to false to hide the tax without removing it.")
  1052. children_tax_ids = fields.Many2many('account.tax.template', 'account_tax_template_filiation_rel', 'parent_tax', 'child_tax', string='Children Taxes')
  1053. sequence = fields.Integer(required=True, default=1,
  1054. help="The sequence field is used to define order in which the tax lines are applied.")
  1055. amount = fields.Float(required=True, digits=(16, 4), default=0)
  1056. description = fields.Char(string='Display on Invoices')
  1057. price_include = fields.Boolean(string='Included in Price', default=False,
  1058. help="Check this if the price you use on the product and invoices includes this tax.")
  1059. include_base_amount = fields.Boolean(string='Affect Subsequent Taxes', default=False,
  1060. help="If set, taxes with a higher sequence than this one will be affected by it, provided they accept it.")
  1061. is_base_affected = fields.Boolean(
  1062. string="Base Affected by Previous Taxes",
  1063. default=True,
  1064. help="If set, taxes with a lower sequence might affect this one, provided they try to do it.")
  1065. 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)")
  1066. 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")
  1067. 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")
  1068. tax_group_id = fields.Many2one('account.tax.group', string="Tax Group")
  1069. tax_exigibility = fields.Selection(
  1070. [('on_invoice', 'Based on Invoice'),
  1071. ('on_payment', 'Based on Payment'),
  1072. ], string='Tax Due', default='on_invoice',
  1073. help="Based on Invoice: the tax is due as soon as the invoice is validated.\n"
  1074. "Based on Payment: the tax is due as soon as the payment of the invoice is received.")
  1075. cash_basis_transition_account_id = fields.Many2one(
  1076. comodel_name='account.account.template',
  1077. string="Cash Basis Transition Account",
  1078. domain=[('deprecated', '=', False)],
  1079. 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.")
  1080. _sql_constraints = [
  1081. ('name_company_uniq', 'unique(name, type_tax_use, tax_scope, chart_template_id)', 'Tax names must be unique !'),
  1082. ]
  1083. @api.depends('name', 'description')
  1084. def name_get(self):
  1085. res = []
  1086. for record in self:
  1087. name = record.description and record.description or record.name
  1088. res.append((record.id, name))
  1089. return res
  1090. @api.model
  1091. def _try_instantiating_foreign_taxes(self, country, company):
  1092. """ This function is called in multivat setup, when a company needs to submit a
  1093. tax report in a foreign country.
  1094. It searches for tax templates in the provided countries and instantiates the
  1095. ones it find in the provided company.
  1096. Tax accounts are not kept from the templates (this wouldn't make sense,
  1097. as they don't belong to the same CoA as the one installed on the company).
  1098. Instead, we search existing tax accounts for approximately equivalent accounts
  1099. and use their prefix to create new accounts. Doing this gives a roughly correct suggestion
  1100. that then needs to be reviewed by the user to ensure its consistency.
  1101. It is intended as a shortcut to avoid hours of encoding, not as an out-of-the-box, always
  1102. correct solution.
  1103. """
  1104. def create_foreign_tax_account(existing_account, additional_label):
  1105. new_code = self.env['account.account']._search_new_account_code(existing_account.company_id, len(existing_account.code), existing_account.code[:-2])
  1106. return self.env['account.account'].create({
  1107. 'name': f"{existing_account.name} - {additional_label}",
  1108. 'code': new_code,
  1109. 'account_type': existing_account.account_type,
  1110. 'company_id': existing_account.company_id.id,
  1111. })
  1112. def get_existing_tax_account(foreign_tax_rep_line, force_tax=None):
  1113. company = foreign_tax_rep_line.company_id
  1114. sign_comparator = '<' if foreign_tax_rep_line.factor_percent < 0 else '>'
  1115. search_domain = [
  1116. ('account_id', '!=', False),
  1117. ('factor_percent', sign_comparator, 0),
  1118. ('company_id', '=', company.id),
  1119. '|',
  1120. '&', ('invoice_tax_id.type_tax_use', '=', tax_rep_line.invoice_tax_id.type_tax_use),
  1121. ('invoice_tax_id.country_id', '=', company.account_fiscal_country_id.id),
  1122. '&', ('refund_tax_id.type_tax_use', '=', tax_rep_line.refund_tax_id.type_tax_use),
  1123. ('refund_tax_id.country_id', '=', company.account_fiscal_country_id.id),
  1124. ]
  1125. if force_tax:
  1126. search_domain += [
  1127. '|', ('invoice_tax_id', 'in', force_tax.ids),
  1128. ('refund_tax_id', 'in', force_tax.ids),
  1129. ]
  1130. return self.env['account.tax.repartition.line'].search(search_domain, limit=1).account_id
  1131. taxes_in_country = self.env['account.tax'].search([
  1132. ('country_id', '=', country.id),
  1133. ('company_id', '=', company.id)
  1134. ])
  1135. if taxes_in_country:
  1136. return
  1137. templates_to_instantiate = self.env['account.tax.template'].with_context(active_test=False).search([('chart_template_id.country_id', '=', country.id)])
  1138. default_company_taxes = company.account_sale_tax_id + company.account_purchase_tax_id
  1139. rep_lines_accounts = templates_to_instantiate._generate_tax(company)['account_dict']
  1140. new_accounts_map = {}
  1141. # Handle tax repartition line accounts
  1142. tax_rep_lines_accounts_dict = rep_lines_accounts['account.tax.repartition.line']
  1143. for tax_rep_line, account_dict in tax_rep_lines_accounts_dict.items():
  1144. account_template = account_dict['account_id']
  1145. rep_account = new_accounts_map.get(account_template)
  1146. if not rep_account:
  1147. existing_account = get_existing_tax_account(tax_rep_line, force_tax=default_company_taxes)
  1148. if not existing_account:
  1149. # If the default taxes were not enough to provide the account
  1150. # we need, search on all other taxes.
  1151. existing_account = get_existing_tax_account(tax_rep_line)
  1152. if existing_account:
  1153. rep_account = create_foreign_tax_account(existing_account, _("Foreign tax account (%s)", country.code))
  1154. new_accounts_map[account_template] = rep_account
  1155. tax_rep_line.account_id = rep_account
  1156. # Handle cash basis taxes transtion account
  1157. caba_transition_dict = rep_lines_accounts['account.tax']
  1158. for tax, account_dict in caba_transition_dict.items():
  1159. transition_account_template = account_dict['cash_basis_transition_account_id']
  1160. if transition_account_template:
  1161. transition_account = new_accounts_map.get(transition_account_template)
  1162. if not transition_account:
  1163. rep_lines = tax.invoice_repartition_line_ids + tax.refund_repartition_line_ids
  1164. tax_accounts = rep_lines.account_id
  1165. if tax_accounts:
  1166. transition_account = create_foreign_tax_account(tax_accounts[0], _("Cash basis transition account"))
  1167. tax.cash_basis_transition_account_id = transition_account
  1168. # Setup tax closing accounts on foreign tax groups ; we don't want to use the domestic accounts
  1169. groups = self.env['account.tax.group'].search([('country_id', '=', country.id)])
  1170. group_property_fields = [
  1171. 'property_tax_payable_account_id',
  1172. 'property_tax_receivable_account_id',
  1173. 'property_advance_tax_payment_account_id'
  1174. ]
  1175. property_company = self.env['ir.property'].with_company(company)
  1176. groups_company = groups.with_company(company)
  1177. for property_field in group_property_fields:
  1178. default_acc = property_company._get(property_field, 'account.tax.group')
  1179. if default_acc:
  1180. groups_company.write({
  1181. property_field: create_foreign_tax_account(default_acc, _("Foreign account (%s)", country.code))
  1182. })
  1183. def _get_tax_vals(self, company, tax_template_to_tax):
  1184. """ This method generates a dictionary of all the values for the tax that will be created.
  1185. """
  1186. # Compute children tax ids
  1187. children_ids = []
  1188. for child_tax in self.children_tax_ids:
  1189. if tax_template_to_tax.get(child_tax):
  1190. children_ids.append(tax_template_to_tax[child_tax].id)
  1191. self.ensure_one()
  1192. val = {
  1193. 'name': self.name,
  1194. 'type_tax_use': self.type_tax_use,
  1195. 'tax_scope': self.tax_scope,
  1196. 'amount_type': self.amount_type,
  1197. 'active': self.active,
  1198. 'company_id': company.id,
  1199. 'sequence': self.sequence,
  1200. 'amount': self.amount,
  1201. 'description': self.description,
  1202. 'price_include': self.price_include,
  1203. 'include_base_amount': self.include_base_amount,
  1204. 'is_base_affected': self.is_base_affected,
  1205. 'analytic': self.analytic,
  1206. 'children_tax_ids': [(6, 0, children_ids)],
  1207. 'tax_exigibility': self.tax_exigibility,
  1208. }
  1209. # We add repartition lines if there are some, so that if there are none,
  1210. # default_get is called and creates the default ones properly.
  1211. if self.invoice_repartition_line_ids:
  1212. val['invoice_repartition_line_ids'] = self.invoice_repartition_line_ids.get_repartition_line_create_vals(company)
  1213. if self.refund_repartition_line_ids:
  1214. val['refund_repartition_line_ids'] = self.refund_repartition_line_ids.get_repartition_line_create_vals(company)
  1215. if self.tax_group_id:
  1216. val['tax_group_id'] = self.tax_group_id.id
  1217. return val
  1218. def _get_tax_vals_complete(self, company, tax_template_to_tax):
  1219. """
  1220. Returns a dict of values to be used to create the tax corresponding to the template, assuming the
  1221. account.account objects were already created.
  1222. It differs from function _get_tax_vals because here, we replace the references to account.template by their
  1223. corresponding account.account ids ('cash_basis_transition_account_id' and 'account_id' in the invoice and
  1224. refund repartition lines)
  1225. """
  1226. vals = self._get_tax_vals(company, tax_template_to_tax)
  1227. if self.cash_basis_transition_account_id.code:
  1228. cash_basis_account_id = self.env['account.account'].search([
  1229. ('code', '=like', self.cash_basis_transition_account_id.code + '%'),
  1230. ('company_id', '=', company.id)
  1231. ], limit=1)
  1232. if cash_basis_account_id:
  1233. vals.update({"cash_basis_transition_account_id": cash_basis_account_id.id})
  1234. vals.update({
  1235. "invoice_repartition_line_ids": self.invoice_repartition_line_ids._get_repartition_line_create_vals_complete(company),
  1236. "refund_repartition_line_ids": self.refund_repartition_line_ids._get_repartition_line_create_vals_complete(company),
  1237. })
  1238. return vals
  1239. def _generate_tax(self, company, accounts_exist=False, existing_template_to_tax=None):
  1240. """ This method generate taxes from templates.
  1241. :param company: the company for which the taxes should be created from templates in self
  1242. :account_exist: whether accounts have already been created
  1243. :existing_template_to_tax: mapping of already existing templates to taxes [(template, tax), ...]
  1244. :returns: {
  1245. 'tax_template_to_tax': mapping between tax template and the newly generated taxes corresponding,
  1246. 'account_dict': dictionary containing a to-do list with all the accounts to assign on new taxes
  1247. }
  1248. """
  1249. # default_company_id is needed in context to allow creation of default
  1250. # repartition lines on taxes
  1251. ChartTemplate = self.env['account.chart.template'].with_context(default_company_id=company.id)
  1252. todo_dict = {'account.tax': {}, 'account.tax.repartition.line': {}}
  1253. if not existing_template_to_tax:
  1254. existing_template_to_tax = []
  1255. tax_template_to_tax = {template: tax for (template, tax) in existing_template_to_tax}
  1256. templates_todo = list(self)
  1257. while templates_todo:
  1258. templates = templates_todo
  1259. templates_todo = []
  1260. # create taxes in batch
  1261. tax_template_vals = []
  1262. for template in templates:
  1263. if all(child in tax_template_to_tax for child in template.children_tax_ids):
  1264. if accounts_exist:
  1265. vals = template._get_tax_vals_complete(company, tax_template_to_tax)
  1266. else:
  1267. vals = template._get_tax_vals(company, tax_template_to_tax)
  1268. if self.chart_template_id.country_id:
  1269. vals['country_id'] = self.chart_template_id.country_id.id
  1270. elif company.account_fiscal_country_id:
  1271. vals['country_id'] = company.account_fiscal_country_id.id
  1272. else:
  1273. # Will happen for generic CoAs such as syscohada (they are available for multiple countries, and don't have any country_id)
  1274. raise UserError(_("Please first define a fiscal country for company %s.", company.name))
  1275. tax_template_vals.append((template, vals))
  1276. else:
  1277. # defer the creation of this tax to the next batch
  1278. templates_todo.append(template)
  1279. taxes = ChartTemplate._create_records_with_xmlid('account.tax', tax_template_vals, company)
  1280. # fill in tax_template_to_tax and todo_dict
  1281. for tax, (template, vals) in zip(taxes, tax_template_vals):
  1282. tax_template_to_tax[template] = tax
  1283. # Since the accounts have not been created yet, we have to wait before filling these fields
  1284. todo_dict['account.tax'][tax] = {
  1285. 'cash_basis_transition_account_id': template.cash_basis_transition_account_id,
  1286. }
  1287. for existing_template, existing_tax in existing_template_to_tax:
  1288. if template in existing_template.children_tax_ids and tax not in existing_tax.children_tax_ids:
  1289. existing_tax.write({'children_tax_ids': [(4, tax.id, False)]})
  1290. if not accounts_exist:
  1291. # We also have to delay the assignation of accounts to repartition lines
  1292. # The below code assigns the account_id to the repartition lines according
  1293. # to the corresponding repartition line in the template, based on the order.
  1294. # As we just created the repartition lines, tax.invoice_repartition_line_ids is not well sorted.
  1295. # But we can force the sort by calling sort()
  1296. all_tax_rep_lines = tax.invoice_repartition_line_ids.sorted() + tax.refund_repartition_line_ids.sorted()
  1297. all_template_rep_lines = template.invoice_repartition_line_ids + template.refund_repartition_line_ids
  1298. for index, template_rep_line in enumerate(all_template_rep_lines):
  1299. # We assume template and tax repartition lines are in the same order
  1300. template_account = template_rep_line.account_id
  1301. if template_account:
  1302. todo_dict['account.tax.repartition.line'][all_tax_rep_lines[index]] = {
  1303. 'account_id': template_account,
  1304. }
  1305. if any(template.tax_exigibility == 'on_payment' for template in self):
  1306. # 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
  1307. # (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.
  1308. company.tax_exigibility = True
  1309. return {
  1310. 'tax_template_to_tax': tax_template_to_tax,
  1311. 'account_dict': todo_dict
  1312. }
  1313. # Tax Repartition Line Template
  1314. class AccountTaxRepartitionLineTemplate(models.Model):
  1315. _name = "account.tax.repartition.line.template"
  1316. _description = "Tax Repartition Line Template"
  1317. factor_percent = fields.Float(
  1318. string="%",
  1319. required=True,
  1320. default=100,
  1321. help="Factor to apply on the account move lines generated from this distribution line, in percents",
  1322. )
  1323. 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.")
  1324. account_id = fields.Many2one(string="Account", comodel_name='account.account.template', help="Account on which to post the tax amount")
  1325. 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")
  1326. 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")
  1327. 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 domains")
  1328. use_in_tax_closing = fields.Boolean(string="Tax Closing Entry")
  1329. # These last two fields are helpers used to ease the declaration of account.account.tag objects in XML.
  1330. # They are directly linked to account.tax.report.expression objects, which create corresponding + and - tags
  1331. # at creation. This way, we avoid declaring + and - separately every time.
  1332. plus_report_expression_ids = fields.Many2many(string="Plus Tax Report Expressions", relation='account_tax_rep_template_plus', comodel_name='account.report.expression', copy=True, help="Tax report expressions whose '+' tag will be assigned to move lines by this repartition line")
  1333. minus_report_expression_ids = fields.Many2many(string="Minus Report Expressions", relation='account_tax_rep_template_minus', comodel_name='account.report.expression', copy=True, help="Tax report expressions whose '-' tag will be assigned to move lines by this repartition line")
  1334. @api.model_create_multi
  1335. def create(self, vals_list):
  1336. for vals in vals_list:
  1337. if vals.get('use_in_tax_closing') is None:
  1338. vals['use_in_tax_closing'] = False
  1339. if vals.get('account_id'):
  1340. account_type = self.env['account.account.template'].browse(vals.get('account_id')).account_type
  1341. if account_type:
  1342. vals['use_in_tax_closing'] = not (account_type.startswith('income') or account_type.startswith('expense'))
  1343. return super().create(vals_list)
  1344. @api.constrains('invoice_tax_id', 'refund_tax_id')
  1345. def validate_tax_template_link(self):
  1346. for record in self:
  1347. if record.invoice_tax_id and record.refund_tax_id:
  1348. 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."))
  1349. @api.constrains('plus_report_expression_ids', 'minus_report_expression_ids')
  1350. def _validate_report_expressions(self):
  1351. for record in self:
  1352. all_engines = set((record.plus_report_expression_ids + record.minus_report_expression_ids).mapped('engine'))
  1353. if all_engines and all_engines != {'tax_tags'}:
  1354. raise ValidationError(_("Only 'tax_tags' expressions can be linked to a tax repartition line template."))
  1355. def get_repartition_line_create_vals(self, company):
  1356. rslt = [Command.clear()]
  1357. for record in self:
  1358. rslt.append(Command.create({
  1359. 'factor_percent': record.factor_percent,
  1360. 'repartition_type': record.repartition_type,
  1361. 'tag_ids': [Command.set(record._get_tags_to_add().ids)],
  1362. 'company_id': company.id,
  1363. 'use_in_tax_closing': record.use_in_tax_closing
  1364. }))
  1365. return rslt
  1366. def _get_repartition_line_create_vals_complete(self, company):
  1367. """
  1368. This function returns a list of values to create the repartition lines of a tax based on
  1369. one or several account.tax.repartition.line.template. It mimicks the function get_repartition_line_create_vals
  1370. but adds the missing field account_id (account.account)
  1371. Returns a list of (0,0, x) ORM commands to create the repartition lines starting with a (5,0,0)
  1372. command to clear the repartition.
  1373. """
  1374. rslt = self.get_repartition_line_create_vals(company)
  1375. for idx, template_line in zip(range(1, len(rslt)), self): # ignore first ORM command ( (5, 0, 0) )
  1376. account_id = False
  1377. if template_line.account_id:
  1378. # take the first account.account which code begins with the correct code
  1379. account_id = self.env['account.account'].search([
  1380. ('code', '=like', template_line.account_id.code + '%'),
  1381. ('company_id', '=', company.id)
  1382. ], limit=1).id
  1383. if not account_id:
  1384. _logger.warning("The account with code '%s' was not found but is supposed to be linked to a tax",
  1385. template_line.account_id.code)
  1386. rslt[idx][2].update({
  1387. "account_id": account_id,
  1388. })
  1389. return rslt
  1390. def _get_tags_to_add(self):
  1391. self.ensure_one()
  1392. tags_to_add = self.tag_ids
  1393. domains = []
  1394. for sign, report_expressions in (('+', self.plus_report_expression_ids), ('-', self.minus_report_expression_ids)):
  1395. for report_expression in report_expressions:
  1396. country = report_expression.report_line_id.report_id.country_id
  1397. domains.append(self.env['account.account.tag']._get_tax_tags_domain(report_expression.formula, country.id, sign=sign))
  1398. if domains:
  1399. tags_to_add |= self.env['account.account.tag'].with_context(active_test=False).search(osv.expression.OR(domains))
  1400. return tags_to_add
  1401. class AccountFiscalPositionTemplate(models.Model):
  1402. _name = 'account.fiscal.position.template'
  1403. _description = 'Template for Fiscal Position'
  1404. sequence = fields.Integer()
  1405. name = fields.Char(string='Fiscal Position Template', required=True)
  1406. chart_template_id = fields.Many2one('account.chart.template', string='Chart Template', required=True)
  1407. account_ids = fields.One2many('account.fiscal.position.account.template', 'position_id', string='Account Mapping')
  1408. tax_ids = fields.One2many('account.fiscal.position.tax.template', 'position_id', string='Tax Mapping')
  1409. note = fields.Text(string='Notes')
  1410. auto_apply = fields.Boolean(string='Detect Automatically', help="Apply automatically this fiscal position.")
  1411. vat_required = fields.Boolean(string='VAT required', help="Apply only if partner has a VAT number.")
  1412. country_id = fields.Many2one('res.country', string='Country',
  1413. help="Apply only if delivery country matches.")
  1414. country_group_id = fields.Many2one('res.country.group', string='Country Group',
  1415. help="Apply only if delivery country matches the group.")
  1416. state_ids = fields.Many2many('res.country.state', string='Federal States')
  1417. zip_from = fields.Char(string='Zip Range From')
  1418. zip_to = fields.Char(string='Zip Range To')
  1419. class AccountFiscalPositionTaxTemplate(models.Model):
  1420. _name = 'account.fiscal.position.tax.template'
  1421. _description = 'Tax Mapping Template of Fiscal Position'
  1422. _rec_name = 'position_id'
  1423. position_id = fields.Many2one('account.fiscal.position.template', string='Fiscal Position', required=True, ondelete='cascade')
  1424. tax_src_id = fields.Many2one('account.tax.template', string='Tax Source', required=True)
  1425. tax_dest_id = fields.Many2one('account.tax.template', string='Replacement Tax')
  1426. class AccountFiscalPositionAccountTemplate(models.Model):
  1427. _name = 'account.fiscal.position.account.template'
  1428. _description = 'Accounts Mapping Template of Fiscal Position'
  1429. _rec_name = 'position_id'
  1430. position_id = fields.Many2one('account.fiscal.position.template', string='Fiscal Mapping', required=True, ondelete='cascade')
  1431. account_src_id = fields.Many2one('account.account.template', string='Account Source', required=True)
  1432. account_dest_id = fields.Many2one('account.account.template', string='Account Destination', required=True)
  1433. class AccountReconcileModelTemplate(models.Model):
  1434. _name = "account.reconcile.model.template"
  1435. _description = 'Reconcile Model Template'
  1436. # Base fields.
  1437. chart_template_id = fields.Many2one('account.chart.template', string='Chart Template', required=True)
  1438. name = fields.Char(string='Button Label', required=True)
  1439. sequence = fields.Integer(required=True, default=10)
  1440. rule_type = fields.Selection(selection=[
  1441. ('writeoff_button', 'Button to generate counterpart entry'),
  1442. ('writeoff_suggestion', 'Rule to suggest counterpart entry'),
  1443. ('invoice_matching', 'Rule to match invoices/bills'),
  1444. ], string='Type', default='writeoff_button', required=True)
  1445. auto_reconcile = fields.Boolean(string='Auto-validate',
  1446. help='Validate the statement line automatically (reconciliation based on your rule).')
  1447. 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.')
  1448. matching_order = fields.Selection(
  1449. selection=[
  1450. ('old_first', 'Oldest first'),
  1451. ('new_first', 'Newest first'),
  1452. ]
  1453. )
  1454. # ===== Conditions =====
  1455. match_text_location_label = fields.Boolean(
  1456. default=True,
  1457. help="Search in the Statement's Label to find the Invoice/Payment's reference",
  1458. )
  1459. match_text_location_note = fields.Boolean(
  1460. default=False,
  1461. help="Search in the Statement's Note to find the Invoice/Payment's reference",
  1462. )
  1463. match_text_location_reference = fields.Boolean(
  1464. default=False,
  1465. help="Search in the Statement's Reference to find the Invoice/Payment's reference",
  1466. )
  1467. match_journal_ids = fields.Many2many('account.journal', string='Journals Availability',
  1468. domain="[('type', 'in', ('bank', 'cash'))]",
  1469. help='The reconciliation model will only be available from the selected journals.')
  1470. match_nature = fields.Selection(selection=[
  1471. ('amount_received', 'Amount Received'),
  1472. ('amount_paid', 'Amount Paid'),
  1473. ('both', 'Amount Paid/Received')
  1474. ], string='Amount Type', required=True, default='both',
  1475. help='''The reconciliation model will only be applied to the selected transaction type:
  1476. * Amount Received: Only applied when receiving an amount.
  1477. * Amount Paid: Only applied when paying an amount.
  1478. * Amount Paid/Received: Applied in both cases.''')
  1479. match_amount = fields.Selection(selection=[
  1480. ('lower', 'Is Lower Than'),
  1481. ('greater', 'Is Greater Than'),
  1482. ('between', 'Is Between'),
  1483. ], string='Amount Condition',
  1484. help='The reconciliation model will only be applied when the amount being lower than, greater than or between specified amount(s).')
  1485. match_amount_min = fields.Float(string='Amount Min Parameter')
  1486. match_amount_max = fields.Float(string='Amount Max Parameter')
  1487. match_label = fields.Selection(selection=[
  1488. ('contains', 'Contains'),
  1489. ('not_contains', 'Not Contains'),
  1490. ('match_regex', 'Match Regex'),
  1491. ], string='Label', help='''The reconciliation model will only be applied when the label:
  1492. * Contains: The proposition label must contains this string (case insensitive).
  1493. * Not Contains: Negation of "Contains".
  1494. * Match Regex: Define your own regular expression.''')
  1495. match_label_param = fields.Char(string='Label Parameter')
  1496. match_note = fields.Selection(selection=[
  1497. ('contains', 'Contains'),
  1498. ('not_contains', 'Not Contains'),
  1499. ('match_regex', 'Match Regex'),
  1500. ], string='Note', help='''The reconciliation model will only be applied when the note:
  1501. * Contains: The proposition note must contains this string (case insensitive).
  1502. * Not Contains: Negation of "Contains".
  1503. * Match Regex: Define your own regular expression.''')
  1504. match_note_param = fields.Char(string='Note Parameter')
  1505. match_transaction_type = fields.Selection(selection=[
  1506. ('contains', 'Contains'),
  1507. ('not_contains', 'Not Contains'),
  1508. ('match_regex', 'Match Regex'),
  1509. ], string='Transaction Type', help='''The reconciliation model will only be applied when the transaction type:
  1510. * Contains: The proposition transaction type must contains this string (case insensitive).
  1511. * Not Contains: Negation of "Contains".
  1512. * Match Regex: Define your own regular expression.''')
  1513. match_transaction_type_param = fields.Char(string='Transaction Type Parameter')
  1514. match_same_currency = fields.Boolean(string='Same Currency', default=True,
  1515. help='Restrict to propositions having the same currency as the statement line.')
  1516. allow_payment_tolerance = fields.Boolean(
  1517. string="Allow Payment Gap",
  1518. default=True,
  1519. help="Difference accepted in case of underpayment.",
  1520. )
  1521. payment_tolerance_param = fields.Float(
  1522. string="Gap",
  1523. default=0.0,
  1524. help="The sum of total residual amount propositions matches the statement line amount under this amount/percentage.",
  1525. )
  1526. payment_tolerance_type = fields.Selection(
  1527. selection=[('percentage', "in percentage"), ('fixed_amount', "in amount")],
  1528. required=True,
  1529. default='percentage',
  1530. help="The sum of total residual amount propositions and the statement line amount allowed gap type.",
  1531. )
  1532. match_partner = fields.Boolean(string='Partner Is Set',
  1533. help='The reconciliation model will only be applied when a customer/vendor is set.')
  1534. match_partner_ids = fields.Many2many('res.partner', string='Restrict Partners to',
  1535. help='The reconciliation model will only be applied to the selected customers/vendors.')
  1536. match_partner_category_ids = fields.Many2many('res.partner.category', string='Restrict Partner Categories to',
  1537. help='The reconciliation model will only be applied to the selected customer/vendor categories.')
  1538. line_ids = fields.One2many('account.reconcile.model.line.template', 'model_id')
  1539. decimal_separator = fields.Char(help="Every character that is nor a digit nor this separator will be removed from the matching string")
  1540. class AccountReconcileModelLineTemplate(models.Model):
  1541. _name = "account.reconcile.model.line.template"
  1542. _description = 'Reconcile Model Line Template'
  1543. model_id = fields.Many2one('account.reconcile.model.template')
  1544. sequence = fields.Integer(required=True, default=10)
  1545. account_id = fields.Many2one('account.account.template', string='Account', ondelete='cascade', domain=[('deprecated', '=', False)])
  1546. label = fields.Char(string='Journal Item Label')
  1547. amount_type = fields.Selection([
  1548. ('fixed', 'Fixed'),
  1549. ('percentage', 'Percentage of balance'),
  1550. ('regex', 'From label'),
  1551. ], required=True, default='percentage')
  1552. amount_string = fields.Char(string="Amount")
  1553. force_tax_included = fields.Boolean(string='Tax Included in Price', help='Force the tax to be managed as a price included tax.')
  1554. tax_ids = fields.Many2many('account.tax.template', string='Taxes', ondelete='restrict')