1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
|
# -*- coding: utf-8 -*-
from odoo.exceptions import AccessError
from odoo import api, fields, models, _
from odoo import SUPERUSER_ID
from odoo.exceptions import UserError, ValidationError
from odoo.http import request
from odoo.addons.account.models.account_tax import TYPE_TAX_USE
import logging
_logger = logging.getLogger(__name__)
def migrate_set_tags_and_taxes_updatable(cr, registry, module):
''' This is a utility function used to manually set the flag noupdate to False on tags and account tax templates on localization modules
that need migration (for example in case of VAT report improvements)
'''
env = api.Environment(cr, SUPERUSER_ID, {})
xml_record_ids = env['ir.model.data'].search([
('model', 'in', ['account.tax.template', 'account.account.tag']),
('module', 'like', module)
]).ids
if xml_record_ids:
cr.execute("update ir_model_data set noupdate = 'f' where id in %s", (tuple(xml_record_ids),))
def preserve_existing_tags_on_taxes(cr, registry, module):
''' This is a utility function used to preserve existing previous tags during upgrade of the module.'''
env = api.Environment(cr, SUPERUSER_ID, {})
xml_records = env['ir.model.data'].search([('model', '=', 'account.account.tag'), ('module', 'like', module)])
if xml_records:
cr.execute("update ir_model_data set noupdate = 't' where id in %s", [tuple(xml_records.ids)])
# ---------------------------------------------------------------
# Account Templates: Account, Tax, Tax Code and chart. + Wizard
# ---------------------------------------------------------------
class AccountGroupTemplate(models.Model):
_name = "account.group.template"
_description = 'Template for Account Groups'
_order = 'code_prefix_start'
parent_id = fields.Many2one('account.group.template', index=True, ondelete='cascade')
name = fields.Char(required=True)
code_prefix_start = fields.Char()
code_prefix_end = fields.Char()
chart_template_id = fields.Many2one('account.chart.template', string='Chart Template', required=True)
class AccountAccountTemplate(models.Model):
_name = "account.account.template"
_description = 'Templates for Accounts'
_order = "code"
name = fields.Char(required=True, index=True)
currency_id = fields.Many2one('res.currency', string='Account Currency', help="Forces all moves for this account to have this secondary currency.")
code = fields.Char(size=64, required=True, index=True)
user_type_id = fields.Many2one('account.account.type', string='Type', required=True,
help="These types are defined according to your country. The type contains more information "\
"about the account and its specificities.")
reconcile = fields.Boolean(string='Allow Invoices & payments Matching', default=False,
help="Check this option if you want the user to reconcile entries in this account.")
note = fields.Text()
tax_ids = fields.Many2many('account.tax.template', 'account_account_template_tax_rel', 'account_id', 'tax_id', string='Default Taxes')
nocreate = fields.Boolean(string='Optional Create', default=False,
help="If checked, the new chart of accounts will not contain this by default.")
chart_template_id = fields.Many2one('account.chart.template', string='Chart Template',
help="This optional field allow you to link an account template to a specific chart template that may differ from the one its root parent belongs to. This allow you "
"to define chart templates that extend another and complete it with few new accounts (You don't need to define the whole structure that is common to both several times).")
tag_ids = fields.Many2many('account.account.tag', 'account_account_template_account_tag', string='Account tag', help="Optional tags you may want to assign for custom reporting")
@api.depends('name', 'code')
def name_get(self):
res = []
for record in self:
name = record.name
if record.code:
name = record.code + ' ' + name
res.append((record.id, name))
return res
class AccountChartTemplate(models.Model):
_name = "account.chart.template"
_description = "Account Chart Template"
name = fields.Char(required=True)
parent_id = fields.Many2one('account.chart.template', string='Parent Chart Template')
code_digits = fields.Integer(string='# of Digits', required=True, default=6, help="No. of Digits to use for account code")
visible = fields.Boolean(string='Can be Visible?', default=True,
help="Set this to False if you don't want this template to be used actively in the wizard that generate Chart of Accounts from "
"templates, this is useful when you want to generate accounts of this template only when loading its child template.")
currency_id = fields.Many2one('res.currency', string='Currency', required=True)
use_anglo_saxon = fields.Boolean(string="Use Anglo-Saxon accounting", default=False)
complete_tax_set = fields.Boolean(string='Complete Set of Taxes', default=True,
help="This boolean helps you to choose if you want to propose to the user to encode the sale and purchase rates or choose from list "
"of taxes. This last choice assumes that the set of tax defined on this template is complete")
account_ids = fields.One2many('account.account.template', 'chart_template_id', string='Associated Account Templates')
tax_template_ids = fields.One2many('account.tax.template', 'chart_template_id', string='Tax Template List',
help='List of all the taxes that have to be installed by the wizard')
bank_account_code_prefix = fields.Char(string='Prefix of the bank accounts', required=True)
cash_account_code_prefix = fields.Char(string='Prefix of the main cash accounts', required=True)
transfer_account_code_prefix = fields.Char(string='Prefix of the main transfer accounts', required=True)
income_currency_exchange_account_id = fields.Many2one('account.account.template',
string="Gain Exchange Rate Account", domain=[('internal_type', '=', 'other'), ('deprecated', '=', False)])
expense_currency_exchange_account_id = fields.Many2one('account.account.template',
string="Loss Exchange Rate Account", domain=[('internal_type', '=', 'other'), ('deprecated', '=', False)])
account_journal_suspense_account_id = fields.Many2one('account.account.template', string='Journal Suspense Account')
default_cash_difference_income_account_id = fields.Many2one('account.account.template', string="Cash Difference Income Account")
default_cash_difference_expense_account_id = fields.Many2one('account.account.template', string="Cash Difference Expense Account")
default_pos_receivable_account_id = fields.Many2one('account.account.template', string="PoS receivable account")
property_account_receivable_id = fields.Many2one('account.account.template', string='Receivable Account')
property_account_payable_id = fields.Many2one('account.account.template', string='Payable Account')
property_account_expense_categ_id = fields.Many2one('account.account.template', string='Category of Expense Account')
property_account_income_categ_id = fields.Many2one('account.account.template', string='Category of Income Account')
property_account_expense_id = fields.Many2one('account.account.template', string='Expense Account on Product Template')
property_account_income_id = fields.Many2one('account.account.template', string='Income Account on Product Template')
property_stock_account_input_categ_id = fields.Many2one('account.account.template', string="Input Account for Stock Valuation")
property_stock_account_output_categ_id = fields.Many2one('account.account.template', string="Output Account for Stock Valuation")
property_stock_valuation_account_id = fields.Many2one('account.account.template', string="Account Template for Stock Valuation")
property_tax_payable_account_id = fields.Many2one('account.account.template', string="Tax current account (payable)")
property_tax_receivable_account_id = fields.Many2one('account.account.template', string="Tax current account (receivable)")
property_advance_tax_payment_account_id = fields.Many2one('account.account.template', string="Advance tax payment account")
property_cash_basis_base_account_id = fields.Many2one(
comodel_name='account.account.template',
domain=[('deprecated', '=', False)],
string="Base Tax Received Account",
help="Account that will be set on lines created in cash basis journal entry and used to keep track of the "
"tax base amount.")
@api.model
def _prepare_transfer_account_template(self, prefix=None):
''' Prepare values to create the transfer account that is an intermediary account used when moving money
from a liquidity account to another.
:return: A dictionary of values to create a new account.account.
'''
digits = self.code_digits
prefix = prefix or self.transfer_account_code_prefix or ''
# Flatten the hierarchy of chart templates.
chart_template = self
chart_templates = self
while chart_template.parent_id:
chart_templates += chart_template.parent_id
chart_template = chart_template.parent_id
new_code = ''
for num in range(1, 100):
new_code = str(prefix.ljust(digits - 1, '0')) + str(num)
rec = self.env['account.account.template'].search(
[('code', '=', new_code), ('chart_template_id', 'in', chart_templates.ids)], limit=1)
if not rec:
break
else:
raise UserError(_('Cannot generate an unused account code.'))
current_assets_type = self.env.ref('account.data_account_type_current_assets', raise_if_not_found=False)
return {
'name': _('Liquidity Transfer'),
'code': new_code,
'user_type_id': current_assets_type and current_assets_type.id or False,
'reconcile': True,
'chart_template_id': self.id,
}
@api.model
def _create_liquidity_journal_suspense_account(self, company, code_digits):
return self.env['account.account'].create({
'name': _("Bank Suspense Account"),
'code': self.env['account.account']._search_new_account_code(company, code_digits, company.bank_account_code_prefix or ''),
'user_type_id': self.env.ref('account.data_account_type_current_liabilities').id,
'company_id': company.id,
})
def try_loading(self, company=False):
""" Installs this chart of accounts for the current company if not chart
of accounts had been created for it yet.
"""
# do not use `request.env` here, it can cause deadlocks
if not company:
if request and hasattr(request, 'allowed_company_ids'):
company = self.env['res.company'].browse(request.allowed_company_ids[0])
else:
company = self.env.company
# If we don't have any chart of account on this company, install this chart of account
if not company.chart_template_id and not self.existing_accounting(company):
for template in self:
template.with_context(default_company_id=company.id)._load(15.0, 15.0, company)
def _load(self, sale_tax_rate, purchase_tax_rate, company):
""" Installs this chart of accounts on the current company, replacing
the existing one if it had already one defined. If some accounting entries
had already been made, this function fails instead, triggering a UserError.
Also, note that this function can only be run by someone with administration
rights.
"""
self.ensure_one()
# do not use `request.env` here, it can cause deadlocks
# Ensure everything is translated to the company's language, not the user's one.
self = self.with_context(lang=company.partner_id.lang).with_company(company)
if not self.env.is_admin():
raise AccessError(_("Only administrators can load a chart of accounts"))
existing_accounts = self.env['account.account'].search([('company_id', '=', company.id)])
if existing_accounts:
# we tolerate switching from accounting package (localization module) as long as there isn't yet any accounting
# entries created for the company.
if self.existing_accounting(company):
raise UserError(_('Could not install new chart of account as there are already accounting entries existing.'))
# delete accounting properties
prop_values = ['account.account,%s' % (account_id,) for account_id in existing_accounts.ids]
existing_journals = self.env['account.journal'].search([('company_id', '=', company.id)])
if existing_journals:
prop_values.extend(['account.journal,%s' % (journal_id,) for journal_id in existing_journals.ids])
self.env['ir.property'].sudo().search(
[('value_reference', 'in', prop_values)]
).unlink()
# delete account, journal, tax, fiscal position and reconciliation model
models_to_delete = ['account.reconcile.model', 'account.fiscal.position', 'account.tax', 'account.move', 'account.journal', 'account.group']
for model in models_to_delete:
res = self.env[model].sudo().search([('company_id', '=', company.id)])
if len(res):
res.unlink()
existing_accounts.unlink()
company.write({'currency_id': self.currency_id.id,
'anglo_saxon_accounting': self.use_anglo_saxon,
'bank_account_code_prefix': self.bank_account_code_prefix,
'cash_account_code_prefix': self.cash_account_code_prefix,
'transfer_account_code_prefix': self.transfer_account_code_prefix,
'chart_template_id': self.id
})
#set the coa currency to active
self.currency_id.write({'active': True})
# When we install the CoA of first company, set the currency to price types and pricelists
if company.id == 1:
for reference in ['product.list_price', 'product.standard_price', 'product.list0']:
try:
tmp2 = self.env.ref(reference).write({'currency_id': self.currency_id.id})
except ValueError:
pass
# If the floats for sale/purchase rates have been filled, create templates from them
self._create_tax_templates_from_rates(company.id, sale_tax_rate, purchase_tax_rate)
# Install all the templates objects and generate the real objects
acc_template_ref, taxes_ref = self._install_template(company, code_digits=self.code_digits)
# Set default cash difference account on company
if not company.account_journal_suspense_account_id:
company.account_journal_suspense_account_id = self._create_liquidity_journal_suspense_account(company, self.code_digits)
if not company.default_cash_difference_expense_account_id:
company.default_cash_difference_expense_account_id = self.env['account.account'].create({
'name': _('Cash Difference Loss'),
'code': self.env['account.account']._search_new_account_code(company, self.code_digits, '999'),
'user_type_id': self.env.ref('account.data_account_type_expenses').id,
'tag_ids': [(6, 0, self.env.ref('account.account_tag_investing').ids)],
'company_id': company.id,
})
if not company.default_cash_difference_income_account_id:
company.default_cash_difference_income_account_id = self.env['account.account'].create({
'name': _('Cash Difference Gain'),
'code': self.env['account.account']._search_new_account_code(company, self.code_digits, '999'),
'user_type_id': self.env.ref('account.data_account_type_revenue').id,
'tag_ids': [(6, 0, self.env.ref('account.account_tag_investing').ids)],
'company_id': company.id,
})
# Set the transfer account on the company
company.transfer_account_id = self.env['account.account'].search([
('code', '=like', self.transfer_account_code_prefix + '%'), ('company_id', '=', company.id)], limit=1)
# Create Bank journals
self._create_bank_journals(company, acc_template_ref)
# Create the current year earning account if it wasn't present in the CoA
company.get_unaffected_earnings_account()
# set the default taxes on the company
company.account_sale_tax_id = self.env['account.tax'].search([('type_tax_use', 'in', ('sale', 'all')), ('company_id', '=', company.id)], limit=1).id
company.account_purchase_tax_id = self.env['account.tax'].search([('type_tax_use', 'in', ('purchase', 'all')), ('company_id', '=', company.id)], limit=1).id
return {}
@api.model
def existing_accounting(self, company_id):
""" Returns True iff some accounting entries have already been made for
the provided company (meaning hence that its chart of accounts cannot
be changed anymore).
"""
model_to_check = ['account.move.line', 'account.payment', 'account.bank.statement']
for model in model_to_check:
if self.env[model].sudo().search([('company_id', '=', company_id.id)], limit=1):
return True
if self.env['account.move'].sudo().search([('company_id', '=', company_id.id), ('name', '!=', '/')], limit=1):
return True
return False
def _create_tax_templates_from_rates(self, company_id, sale_tax_rate, purchase_tax_rate):
'''
This function checks if this chart template is configured as containing a full set of taxes, and if
it's not the case, it creates the templates for account.tax object accordingly to the provided sale/purchase rates.
Then it saves the new tax templates as default taxes to use for this chart template.
:param company_id: id of the company for which the wizard is running
:param sale_tax_rate: the rate to use for created sales tax
:param purchase_tax_rate: the rate to use for created purchase tax
:return: True
'''
self.ensure_one()
obj_tax_temp = self.env['account.tax.template']
all_parents = self._get_chart_parent_ids()
# create tax templates from purchase_tax_rate and sale_tax_rate fields
if not self.complete_tax_set:
ref_taxs = obj_tax_temp.search([('type_tax_use', '=', 'sale'), ('chart_template_id', 'in', all_parents)], order="sequence, id desc", limit=1)
ref_taxs.write({'amount': sale_tax_rate, 'name': _('Tax %.2f%%') % sale_tax_rate, 'description': '%.2f%%' % sale_tax_rate})
ref_taxs = obj_tax_temp.search([('type_tax_use', '=', 'purchase'), ('chart_template_id', 'in', all_parents)], order="sequence, id desc", limit=1)
ref_taxs.write({'amount': purchase_tax_rate, 'name': _('Tax %.2f%%') % purchase_tax_rate, 'description': '%.2f%%' % purchase_tax_rate})
return True
def _get_chart_parent_ids(self):
""" Returns the IDs of all ancestor charts, including the chart itself.
(inverse of child_of operator)
:return: the IDS of all ancestor charts, including the chart itself.
"""
chart_template = self
result = [chart_template.id]
while chart_template.parent_id:
chart_template = chart_template.parent_id
result.append(chart_template.id)
return result
def _create_bank_journals(self, company, acc_template_ref):
'''
This function creates bank journals and their account for each line
data returned by the function _get_default_bank_journals_data.
:param company: the company for which the wizard is running.
:param acc_template_ref: the dictionary containing the mapping between the ids of account templates and the ids
of the accounts that have been generated from them.
'''
self.ensure_one()
bank_journals = self.env['account.journal']
# Create the journals that will trigger the account.account creation
for acc in self._get_default_bank_journals_data():
bank_journals += self.env['account.journal'].create({
'name': acc['acc_name'],
'type': acc['account_type'],
'company_id': company.id,
'currency_id': acc.get('currency_id', self.env['res.currency']).id,
'sequence': 10,
})
return bank_journals
@api.model
def _get_default_bank_journals_data(self):
""" Returns the data needed to create the default bank journals when
installing this chart of accounts, in the form of a list of dictionaries.
The allowed keys in these dictionaries are:
- acc_name: string (mandatory)
- account_type: 'cash' or 'bank' (mandatory)
- currency_id (optional, only to be specified if != company.currency_id)
"""
return [{'acc_name': _('Cash'), 'account_type': 'cash'}, {'acc_name': _('Bank'), 'account_type': 'bank'}]
def open_select_template_wizard(self):
# Add action to open wizard to select between several templates
if not self.company_id.chart_template_id:
todo = self.env['ir.actions.todo']
action_rec = self.env['ir.model.data'].xmlid_to_object('account.action_wizard_multi_chart')
if action_rec:
todo.create({'action_id': action_rec.id, 'name': _('Choose Accounting Template')})
return True
@api.model
def _prepare_transfer_account_for_direct_creation(self, name, company):
""" Prepare values to create a transfer account directly, based on the
method _prepare_transfer_account_template().
This is needed when dealing with installation of payment modules
that requires the creation of their own transfer account.
:param name: The transfer account name.
:param company: The company owning this account.
:return: A dictionary of values to create a new account.account.
"""
vals = self._prepare_transfer_account_template()
digits = self.code_digits or 6
prefix = self.transfer_account_code_prefix or ''
vals.update({
'code': self.env['account.account']._search_new_account_code(company, digits, prefix),
'name': name,
'company_id': company.id,
})
del(vals['chart_template_id'])
return vals
@api.model
def generate_journals(self, acc_template_ref, company, journals_dict=None):
"""
This method is used for creating journals.
:param acc_template_ref: Account templates reference.
:param company_id: company to generate journals for.
:returns: True
"""
JournalObj = self.env['account.journal']
for vals_journal in self._prepare_all_journals(acc_template_ref, company, journals_dict=journals_dict):
journal = JournalObj.create(vals_journal)
if vals_journal['type'] == 'general' and vals_journal['code'] == _('EXCH'):
company.write({'currency_exchange_journal_id': journal.id})
if vals_journal['type'] == 'general' and vals_journal['code'] == _('CABA'):
company.write({'tax_cash_basis_journal_id': journal.id})
return True
def _prepare_all_journals(self, acc_template_ref, company, journals_dict=None):
def _get_default_account(journal_vals, type='debit'):
# Get the default accounts
default_account = False
if journal['type'] == 'sale':
default_account = acc_template_ref.get(self.property_account_income_categ_id.id)
elif journal['type'] == 'purchase':
default_account = acc_template_ref.get(self.property_account_expense_categ_id.id)
return default_account
journals = [{'name': _('Customer Invoices'), 'type': 'sale', 'code': _('INV'), 'favorite': True, 'color': 11, 'sequence': 5},
{'name': _('Vendor Bills'), 'type': 'purchase', 'code': _('BILL'), 'favorite': True, 'color': 11, 'sequence': 6},
{'name': _('Miscellaneous Operations'), 'type': 'general', 'code': _('MISC'), 'favorite': True, 'sequence': 7},
{'name': _('Exchange Difference'), 'type': 'general', 'code': _('EXCH'), 'favorite': False, 'sequence': 9},
{'name': _('Cash Basis Taxes'), 'type': 'general', 'code': _('CABA'), 'favorite': False, 'sequence': 10}]
if journals_dict != None:
journals.extend(journals_dict)
self.ensure_one()
journal_data = []
for journal in journals:
vals = {
'type': journal['type'],
'name': journal['name'],
'code': journal['code'],
'company_id': company.id,
'default_account_id': _get_default_account(journal),
'show_on_dashboard': journal['favorite'],
'color': journal.get('color', False),
'sequence': journal['sequence']
}
journal_data.append(vals)
return journal_data
def generate_properties(self, acc_template_ref, company):
"""
This method used for creating properties.
:param acc_template_ref: Mapping between ids of account templates and real accounts created from them
:param company_id: company to generate properties for.
:returns: True
"""
self.ensure_one()
PropertyObj = self.env['ir.property']
todo_list = [
('property_account_receivable_id', 'res.partner'),
('property_account_payable_id', 'res.partner'),
('property_account_expense_categ_id', 'product.category'),
('property_account_income_categ_id', 'product.category'),
('property_account_expense_id', 'product.template'),
('property_account_income_id', 'product.template'),
('property_tax_payable_account_id', 'account.tax.group'),
('property_tax_receivable_account_id', 'account.tax.group'),
('property_advance_tax_payment_account_id', 'account.tax.group'),
]
for field, model in todo_list:
account = self[field]
value = acc_template_ref[account.id] if account else False
if value:
PropertyObj._set_default(field, model, value, company=company)
stock_properties = [
'property_stock_account_input_categ_id',
'property_stock_account_output_categ_id',
'property_stock_valuation_account_id',
]
for stock_property in stock_properties:
account = getattr(self, stock_property)
value = account and acc_template_ref[account.id] or False
if value:
company.write({stock_property: value})
return True
def _install_template(self, company, code_digits=None, obj_wizard=None, acc_ref=None, taxes_ref=None):
""" Recursively load the template objects and create the real objects from them.
:param company: company the wizard is running for
:param code_digits: number of digits the accounts code should have in the COA
:param obj_wizard: the current wizard for generating the COA from the templates
:param acc_ref: Mapping between ids of account templates and real accounts created from them
:param taxes_ref: Mapping between ids of tax templates and real taxes created from them
:returns: tuple with a dictionary containing
* the mapping between the account template ids and the ids of the real accounts that have been generated
from them, as first item,
* a similar dictionary for mapping the tax templates and taxes, as second item,
:rtype: tuple(dict, dict, dict)
"""
self.ensure_one()
if acc_ref is None:
acc_ref = {}
if taxes_ref is None:
taxes_ref = {}
if self.parent_id:
tmp1, tmp2 = self.parent_id._install_template(company, code_digits=code_digits, acc_ref=acc_ref, taxes_ref=taxes_ref)
acc_ref.update(tmp1)
taxes_ref.update(tmp2)
# Ensure, even if individually, that everything is translated according to the company's language.
tmp1, tmp2 = self.with_context(lang=company.partner_id.lang)._load_template(company, code_digits=code_digits, account_ref=acc_ref, taxes_ref=taxes_ref)
acc_ref.update(tmp1)
taxes_ref.update(tmp2)
return acc_ref, taxes_ref
def _load_template(self, company, code_digits=None, account_ref=None, taxes_ref=None):
""" Generate all the objects from the templates
:param company: company the wizard is running for
:param code_digits: number of digits the accounts code should have in the COA
:param acc_ref: Mapping between ids of account templates and real accounts created from them
:param taxes_ref: Mapping between ids of tax templates and real taxes created from them
:returns: tuple with a dictionary containing
* the mapping between the account template ids and the ids of the real accounts that have been generated
from them, as first item,
* a similar dictionary for mapping the tax templates and taxes, as second item,
:rtype: tuple(dict, dict, dict)
"""
self.ensure_one()
if account_ref is None:
account_ref = {}
if taxes_ref is None:
taxes_ref = {}
if not code_digits:
code_digits = self.code_digits
AccountTaxObj = self.env['account.tax']
# Generate taxes from templates.
generated_tax_res = self.with_context(active_test=False).tax_template_ids._generate_tax(company)
taxes_ref.update(generated_tax_res['tax_template_to_tax'])
# Generating Accounts from templates.
account_template_ref = self.generate_account(taxes_ref, account_ref, code_digits, company)
account_ref.update(account_template_ref)
# Generate account groups, from template
self.generate_account_groups(company)
# writing account values after creation of accounts
for key, value in generated_tax_res['account_dict']['account.tax'].items():
if value['cash_basis_transition_account_id']:
AccountTaxObj.browse(key).write({
'cash_basis_transition_account_id': account_ref.get(value['cash_basis_transition_account_id'], False),
})
AccountTaxRepartitionLineObj = self.env['account.tax.repartition.line']
for key, value in generated_tax_res['account_dict']['account.tax.repartition.line'].items():
if value['account_id']:
AccountTaxRepartitionLineObj.browse(key).write({
'account_id': account_ref.get(value['account_id']),
})
# Set the company accounts
self._load_company_accounts(account_ref, company)
# Create Journals - Only done for root chart template
if not self.parent_id:
self.generate_journals(account_ref, company)
# generate properties function
self.generate_properties(account_ref, company)
# Generate Fiscal Position , Fiscal Position Accounts and Fiscal Position Taxes from templates
self.generate_fiscal_position(taxes_ref, account_ref, company)
# Generate account operation template templates
self.generate_account_reconcile_model(taxes_ref, account_ref, company)
return account_ref, taxes_ref
def _load_company_accounts(self, account_ref, company):
# Set the default accounts on the company
accounts = {
'default_cash_difference_income_account_id': self.default_cash_difference_income_account_id.id,
'default_cash_difference_expense_account_id': self.default_cash_difference_expense_account_id.id,
'account_journal_suspense_account_id': self.account_journal_suspense_account_id.id,
'account_cash_basis_base_account_id': self.property_cash_basis_base_account_id.id,
'account_default_pos_receivable_account_id': self.default_pos_receivable_account_id.id,
'income_currency_exchange_account_id': self.income_currency_exchange_account_id.id,
'expense_currency_exchange_account_id': self.expense_currency_exchange_account_id.id,
}
values = {}
# The loop is to avoid writing when we have no values, thus avoiding erasing the account from the parent
for key, account in accounts.items():
if account_ref.get(account):
values[key] = account_ref.get(account)
company.write(values)
def create_record_with_xmlid(self, company, template, model, vals):
return self._create_records_with_xmlid(model, [(template, vals)], company).id
def _create_records_with_xmlid(self, model, template_vals, company):
""" Create records for the given model name with the given vals, and
create xml ids based on each record's template and company id.
"""
if not template_vals:
return self.env[model]
template_model = template_vals[0][0]
template_ids = [template.id for template, vals in template_vals]
template_xmlids = template_model.browse(template_ids).get_external_id()
data_list = []
for template, vals in template_vals:
module, name = template_xmlids[template.id].split('.', 1)
xml_id = "%s.%s_%s" % (module, company.id, name)
data_list.append(dict(xml_id=xml_id, values=vals, noupdate=True))
return self.env[model]._load_records(data_list)
@api.model
def _load_records(self, data_list, update=False):
# When creating a chart template create, for the liquidity transfer account
# - an account.account.template: this allow to define account.reconcile.model.template objects refering that liquidity transfer
# account although it's not existing in any xml file
# - an entry in ir_model_data: this allow to still use the method create_record_with_xmlid() and don't make any difference between
# regular accounts created and that liquidity transfer account
records = super(AccountChartTemplate, self)._load_records(data_list, update)
account_data_list = []
for data, record in zip(data_list, records):
# Create the transfer account only for leaf chart template in the hierarchy.
if record.parent_id:
continue
if data.get('xml_id'):
account_xml_id = data['xml_id'] + '_liquidity_transfer'
if not self.env.ref(account_xml_id, raise_if_not_found=False):
account_vals = record._prepare_transfer_account_template()
account_data_list.append(dict(
xml_id=account_xml_id,
values=account_vals,
noupdate=data.get('noupdate'),
))
self.env['account.account.template']._load_records(account_data_list, update)
return records
def _get_account_vals(self, company, account_template, code_acc, tax_template_ref):
""" This method generates a dictionary of all the values for the account that will be created.
"""
self.ensure_one()
tax_ids = []
for tax in account_template.tax_ids:
tax_ids.append(tax_template_ref[tax.id])
val = {
'name': account_template.name,
'currency_id': account_template.currency_id and account_template.currency_id.id or False,
'code': code_acc,
'user_type_id': account_template.user_type_id and account_template.user_type_id.id or False,
'reconcile': account_template.reconcile,
'note': account_template.note,
'tax_ids': [(6, 0, tax_ids)],
'company_id': company.id,
'tag_ids': [(6, 0, [t.id for t in account_template.tag_ids])],
}
return val
def generate_account(self, tax_template_ref, acc_template_ref, code_digits, company):
""" This method generates accounts from account templates.
:param tax_template_ref: Taxes templates reference for write taxes_id in account_account.
:param acc_template_ref: dictionary containing the mapping between the account templates and generated accounts (will be populated)
:param code_digits: number of digits to use for account code.
:param company_id: company to generate accounts for.
:returns: return acc_template_ref for reference purpose.
:rtype: dict
"""
self.ensure_one()
account_tmpl_obj = self.env['account.account.template']
acc_template = account_tmpl_obj.search([('nocreate', '!=', True), ('chart_template_id', '=', self.id)], order='id')
template_vals = []
for account_template in acc_template:
code_main = account_template.code and len(account_template.code) or 0
code_acc = account_template.code or ''
if code_main > 0 and code_main <= code_digits:
code_acc = str(code_acc) + (str('0'*(code_digits-code_main)))
vals = self._get_account_vals(company, account_template, code_acc, tax_template_ref)
template_vals.append((account_template, vals))
accounts = self._create_records_with_xmlid('account.account', template_vals, company)
for template, account in zip(acc_template, accounts):
acc_template_ref[template.id] = account.id
return acc_template_ref
def generate_account_groups(self, company):
""" This method generates account groups from account groups templates.
:param company: company to generate the account groups for
"""
self.ensure_one()
group_templates = self.env['account.group.template'].search([('chart_template_id', '=', self.id)])
template_vals = []
for group_template in group_templates:
vals = {
'name': group_template.name,
'code_prefix_start': group_template.code_prefix_start,
'code_prefix_end': group_template.code_prefix_end,
'company_id': company.id,
}
template_vals.append((group_template, vals))
groups = self._create_records_with_xmlid('account.group', template_vals, company)
def _prepare_reconcile_model_vals(self, company, account_reconcile_model, acc_template_ref, tax_template_ref):
""" This method generates a dictionary of all the values for the account.reconcile.model that will be created.
"""
self.ensure_one()
account_reconcile_model_lines = self.env['account.reconcile.model.line.template'].search([
('model_id', '=', account_reconcile_model.id)
])
return {
'name': account_reconcile_model.name,
'sequence': account_reconcile_model.sequence,
'company_id': company.id,
'rule_type': account_reconcile_model.rule_type,
'auto_reconcile': account_reconcile_model.auto_reconcile,
'to_check': account_reconcile_model.to_check,
'match_journal_ids': [(6, None, account_reconcile_model.match_journal_ids.ids)],
'match_nature': account_reconcile_model.match_nature,
'match_amount': account_reconcile_model.match_amount,
'match_amount_min': account_reconcile_model.match_amount_min,
'match_amount_max': account_reconcile_model.match_amount_max,
'match_label': account_reconcile_model.match_label,
'match_label_param': account_reconcile_model.match_label_param,
'match_note': account_reconcile_model.match_note,
'match_note_param': account_reconcile_model.match_note_param,
'match_transaction_type': account_reconcile_model.match_transaction_type,
'match_transaction_type_param': account_reconcile_model.match_transaction_type_param,
'match_same_currency': account_reconcile_model.match_same_currency,
'match_total_amount': account_reconcile_model.match_total_amount,
'match_total_amount_param': account_reconcile_model.match_total_amount_param,
'match_partner': account_reconcile_model.match_partner,
'match_partner_ids': [(6, None, account_reconcile_model.match_partner_ids.ids)],
'match_partner_category_ids': [(6, None, account_reconcile_model.match_partner_category_ids.ids)],
'line_ids': [(0, 0, {
'account_id': acc_template_ref[line.account_id.id],
'label': line.label,
'amount_type': line.amount_type,
'force_tax_included': line.force_tax_included,
'amount_string': line.amount_string,
'tax_ids': [[4, tax_template_ref[tax.id], 0] for tax in line.tax_ids],
}) for line in account_reconcile_model_lines],
}
def generate_account_reconcile_model(self, tax_template_ref, acc_template_ref, company):
""" This method creates account reconcile models
:param tax_template_ref: Taxes templates reference for write taxes_id in account_account.
:param acc_template_ref: dictionary with the mapping between the account templates and the real accounts.
:param company_id: company to create models for
:returns: return new_account_reconcile_model for reference purpose.
:rtype: dict
"""
self.ensure_one()
account_reconcile_models = self.env['account.reconcile.model.template'].search([
('chart_template_id', '=', self.id)
])
for account_reconcile_model in account_reconcile_models:
vals = self._prepare_reconcile_model_vals(company, account_reconcile_model, acc_template_ref, tax_template_ref)
self.create_record_with_xmlid(company, account_reconcile_model, 'account.reconcile.model', vals)
# Create a default rule for the reconciliation widget matching invoices automatically.
self.env['account.reconcile.model'].sudo().create({
"name": _('Invoices Matching Rule'),
"sequence": '1',
"rule_type": 'invoice_matching',
"auto_reconcile": False,
"match_nature": 'both',
"match_same_currency": True,
"match_total_amount": True,
"match_total_amount_param": 100,
"match_partner": True,
"company_id": company.id,
})
return True
def _get_fp_vals(self, company, position):
return {
'company_id': company.id,
'sequence': position.sequence,
'name': position.name,
'note': position.note,
'auto_apply': position.auto_apply,
'vat_required': position.vat_required,
'country_id': position.country_id.id,
'country_group_id': position.country_group_id.id,
'state_ids': position.state_ids and [(6,0, position.state_ids.ids)] or [],
'zip_from': position.zip_from,
'zip_to': position.zip_to,
}
def generate_fiscal_position(self, tax_template_ref, acc_template_ref, company):
""" This method generates Fiscal Position, Fiscal Position Accounts
and Fiscal Position Taxes from templates.
:param taxes_ids: Taxes templates reference for generating account.fiscal.position.tax.
:param acc_template_ref: Account templates reference for generating account.fiscal.position.account.
:param company_id: the company to generate fiscal position data for
:returns: True
"""
self.ensure_one()
positions = self.env['account.fiscal.position.template'].search([('chart_template_id', '=', self.id)])
# first create fiscal positions in batch
template_vals = []
for position in positions:
fp_vals = self._get_fp_vals(company, position)
template_vals.append((position, fp_vals))
fps = self._create_records_with_xmlid('account.fiscal.position', template_vals, company)
# then create fiscal position taxes and accounts
tax_template_vals = []
account_template_vals = []
for position, fp in zip(positions, fps):
for tax in position.tax_ids:
tax_template_vals.append((tax, {
'tax_src_id': tax_template_ref[tax.tax_src_id.id],
'tax_dest_id': tax.tax_dest_id and tax_template_ref[tax.tax_dest_id.id] or False,
'position_id': fp.id,
}))
for acc in position.account_ids:
account_template_vals.append((acc, {
'account_src_id': acc_template_ref[acc.account_src_id.id],
'account_dest_id': acc_template_ref[acc.account_dest_id.id],
'position_id': fp.id,
}))
self._create_records_with_xmlid('account.fiscal.position.tax', tax_template_vals, company)
self._create_records_with_xmlid('account.fiscal.position.account', account_template_vals, company)
return True
class AccountTaxTemplate(models.Model):
_name = 'account.tax.template'
_description = 'Templates for Taxes'
_order = 'id'
chart_template_id = fields.Many2one('account.chart.template', string='Chart Template', required=True)
name = fields.Char(string='Tax Name', required=True)
type_tax_use = fields.Selection(TYPE_TAX_USE, string='Tax Type', required=True, default="sale",
help="Determines where the tax is selectable. Note : 'None' means a tax can't be used by itself, however it can still be used in a group.")
tax_scope = fields.Selection([('service', 'Service'), ('consu', 'Consumable')], help="Restrict the use of taxes to a type of product.")
amount_type = fields.Selection(default='percent', string="Tax Computation", required=True,
selection=[('group', 'Group of Taxes'), ('fixed', 'Fixed'), ('percent', 'Percentage of Price'), ('division', 'Percentage of Price Tax Included')])
active = fields.Boolean(default=True, help="Set active to false to hide the tax without removing it.")
children_tax_ids = fields.Many2many('account.tax.template', 'account_tax_template_filiation_rel', 'parent_tax', 'child_tax', string='Children Taxes')
sequence = fields.Integer(required=True, default=1,
help="The sequence field is used to define order in which the tax lines are applied.")
amount = fields.Float(required=True, digits=(16, 4), default=0)
description = fields.Char(string='Display on Invoices')
price_include = fields.Boolean(string='Included in Price', default=False,
help="Check this if the price you use on the product and invoices includes this tax.")
include_base_amount = fields.Boolean(string='Affect Subsequent Taxes', default=False,
help="If set, taxes which are computed after this one will be computed based on the price tax included.")
analytic = fields.Boolean(string="Analytic Cost", help="If set, the amount computed by this tax will be assigned to the same analytic account as the invoice line (if any)")
invoice_repartition_line_ids = fields.One2many(string="Repartition for Invoices", comodel_name="account.tax.repartition.line.template", inverse_name="invoice_tax_id", copy=True, help="Repartition when the tax is used on an invoice")
refund_repartition_line_ids = fields.One2many(string="Repartition for Refund Invoices", comodel_name="account.tax.repartition.line.template", inverse_name="refund_tax_id", copy=True, help="Repartition when the tax is used on a refund")
tax_group_id = fields.Many2one('account.tax.group', string="Tax Group")
tax_exigibility = fields.Selection(
[('on_invoice', 'Based on Invoice'),
('on_payment', 'Based on Payment'),
], string='Tax Due', default='on_invoice',
help="Based on Invoice: the tax is due as soon as the invoice is validated.\n"
"Based on Payment: the tax is due as soon as the payment of the invoice is received.")
cash_basis_transition_account_id = fields.Many2one(
comodel_name='account.account.template',
string="Cash Basis Transition Account",
domain=[('deprecated', '=', False)],
help="Account used to transition the tax amount for cash basis taxes. It will contain the tax amount as long as the original invoice has not been reconciled ; at reconciliation, this amount cancelled on this account and put on the regular tax account.")
_sql_constraints = [
('name_company_uniq', 'unique(name, type_tax_use, tax_scope, chart_template_id)', 'Tax names must be unique !'),
]
@api.depends('name', 'description')
def name_get(self):
res = []
for record in self:
name = record.description and record.description or record.name
res.append((record.id, name))
return res
def _get_tax_vals(self, company, tax_template_to_tax):
""" This method generates a dictionary of all the values for the tax that will be created.
"""
# Compute children tax ids
children_ids = []
for child_tax in self.children_tax_ids:
if tax_template_to_tax.get(child_tax.id):
children_ids.append(tax_template_to_tax[child_tax.id])
self.ensure_one()
val = {
'name': self.name,
'type_tax_use': self.type_tax_use,
'tax_scope': self.tax_scope,
'amount_type': self.amount_type,
'active': self.active,
'company_id': company.id,
'sequence': self.sequence,
'amount': self.amount,
'description': self.description,
'price_include': self.price_include,
'include_base_amount': self.include_base_amount,
'analytic': self.analytic,
'children_tax_ids': [(6, 0, children_ids)],
'tax_exigibility': self.tax_exigibility,
}
# We add repartition lines if there are some, so that if there are none,
# default_get is called and creates the default ones properly.
if self.invoice_repartition_line_ids:
val['invoice_repartition_line_ids'] = self.invoice_repartition_line_ids.get_repartition_line_create_vals(company)
if self.refund_repartition_line_ids:
val['refund_repartition_line_ids'] = self.refund_repartition_line_ids.get_repartition_line_create_vals(company)
if self.tax_group_id:
val['tax_group_id'] = self.tax_group_id.id
return val
def _generate_tax(self, company):
""" This method generate taxes from templates.
:param company: the company for which the taxes should be created from templates in self
:returns: {
'tax_template_to_tax': mapping between tax template and the newly generated taxes corresponding,
'account_dict': dictionary containing a to-do list with all the accounts to assign on new taxes
}
"""
# default_company_id is needed in context to allow creation of default
# repartition lines on taxes
ChartTemplate = self.env['account.chart.template'].with_context(default_company_id=company.id)
todo_dict = {'account.tax': {}, 'account.tax.repartition.line': {}}
tax_template_to_tax = {}
templates_todo = list(self)
while templates_todo:
templates = templates_todo
templates_todo = []
# create taxes in batch
tax_template_vals = []
for template in templates:
if all(child.id in tax_template_to_tax for child in template.children_tax_ids):
vals = template._get_tax_vals(company, tax_template_to_tax)
tax_template_vals.append((template, vals))
else:
# defer the creation of this tax to the next batch
templates_todo.append(template)
taxes = ChartTemplate._create_records_with_xmlid('account.tax', tax_template_vals, company)
# fill in tax_template_to_tax and todo_dict
for tax, (template, vals) in zip(taxes, tax_template_vals):
tax_template_to_tax[template.id] = tax.id
# Since the accounts have not been created yet, we have to wait before filling these fields
todo_dict['account.tax'][tax.id] = {
'cash_basis_transition_account_id': template.cash_basis_transition_account_id.id,
}
# We also have to delay the assignation of accounts to repartition lines
# The below code assigns the account_id to the repartition lines according
# to the corresponding repartition line in the template, based on the order.
# As we just created the repartition lines, tax.invoice_repartition_line_ids is not well sorted.
# But we can force the sort by calling sort()
all_tax_rep_lines = tax.invoice_repartition_line_ids.sorted() + tax.refund_repartition_line_ids.sorted()
all_template_rep_lines = template.invoice_repartition_line_ids + template.refund_repartition_line_ids
for i in range(0, len(all_template_rep_lines)):
# We assume template and tax repartition lines are in the same order
template_account = all_template_rep_lines[i].account_id
if template_account:
todo_dict['account.tax.repartition.line'][all_tax_rep_lines[i].id] = {
'account_id': template_account.id,
}
if any(template.tax_exigibility == 'on_payment' for template in self):
# When a CoA is being installed automatically and if it is creating account tax(es) whose field `Use Cash Basis`(tax_exigibility) is set to True by default
# (example of such CoA's are l10n_fr and l10n_mx) then in the `Accounting Settings` the option `Cash Basis` should be checked by default.
company.tax_exigibility = True
return {
'tax_template_to_tax': tax_template_to_tax,
'account_dict': todo_dict
}
# Tax Repartition Line Template
class AccountTaxRepartitionLineTemplate(models.Model):
_name = "account.tax.repartition.line.template"
_description = "Tax Repartition Line Template"
factor_percent = fields.Float(string="%", required=True, help="Factor to apply on the account move lines generated from this distribution line, in percents")
repartition_type = fields.Selection(string="Based On", selection=[('base', 'Base'), ('tax', 'of tax')], required=True, default='tax', help="Base on which the factor will be applied.")
account_id = fields.Many2one(string="Account", comodel_name='account.account.template', help="Account on which to post the tax amount")
invoice_tax_id = fields.Many2one(comodel_name='account.tax.template', help="The tax set to apply this distribution on invoices. Mutually exclusive with refund_tax_id")
refund_tax_id = fields.Many2one(comodel_name='account.tax.template', help="The tax set to apply this distribution on refund invoices. Mutually exclusive with invoice_tax_id")
tag_ids = fields.Many2many(string="Financial Tags", relation='account_tax_repartition_financial_tags', comodel_name='account.account.tag', copy=True, help="Additional tags that will be assigned by this repartition line for use in financial reports")
use_in_tax_closing = fields.Boolean(string="Tax Closing Entry")
# These last two fields are helpers used to ease the declaration of account.account.tag objects in XML.
# They are directly linked to account.tax.report.line objects, which create corresponding + and - tags
# at creation. This way, we avoid declaring + and - separately every time.
plus_report_line_ids = fields.Many2many(string="Plus Tax Report Lines", relation='account_tax_repartition_plus_report_line', comodel_name='account.tax.report.line', copy=True, help="Tax report lines whose '+' tag will be assigned to move lines by this repartition line")
minus_report_line_ids = fields.Many2many(string="Minus Report Lines", relation='account_tax_repartition_minus_report_line', comodel_name='account.tax.report.line', copy=True, help="Tax report lines whose '-' tag will be assigned to move lines by this repartition line")
@api.model
def create(self, vals):
if vals.get('plus_report_line_ids'):
vals['plus_report_line_ids'] = self._convert_tag_syntax_to_orm(vals['plus_report_line_ids'])
if vals.get('minus_report_line_ids'):
vals['minus_report_line_ids'] = self._convert_tag_syntax_to_orm(vals['minus_report_line_ids'])
if vals.get('tag_ids'):
vals['tag_ids'] = self._convert_tag_syntax_to_orm(vals['tag_ids'])
if vals.get('use_in_tax_closing') is None:
if not vals.get('account_id'):
vals['use_in_tax_closing'] = False
else:
internal_group = self.env['account.account.template'].browse(vals.get('account_id')).user_type_id.internal_group
vals['use_in_tax_closing'] = not (internal_group == 'income' or internal_group == 'expense')
return super(AccountTaxRepartitionLineTemplate, self).create(vals)
@api.model
def _convert_tag_syntax_to_orm(self, tags_list):
""" Repartition lines give the possibility to directly give
a list of ids to create for tags instead of a list of ORM commands.
This function checks that tags_list uses this syntactic sugar and returns
an ORM-compliant version of it if it does.
"""
if tags_list and all(isinstance(elem, int) for elem in tags_list):
return [(6, False, tags_list)]
return tags_list
@api.constrains('invoice_tax_id', 'refund_tax_id')
def validate_tax_template_link(self):
for record in self:
if record.invoice_tax_id and record.refund_tax_id:
raise ValidationError(_("Tax distribution line templates should apply to either invoices or refunds, not both at the same time. invoice_tax_id and refund_tax_id should not be set together."))
@api.constrains('plus_report_line_ids', 'minus_report_line_ids')
def validate_tags(self):
all_tax_rep_lines = self.mapped('plus_report_line_ids') + self.mapped('minus_report_line_ids')
lines_without_tag = all_tax_rep_lines.filtered(lambda x: not x.tag_name)
if lines_without_tag:
raise ValidationError(_("The following tax report lines are used in some tax distribution template though they don't generate any tag: %s . This probably means you forgot to set a tag_name on these lines.", str(lines_without_tag.mapped('name'))))
def get_repartition_line_create_vals(self, company):
rslt = [(5, 0, 0)]
for record in self:
tags_to_add = self.env['account.account.tag']
tags_to_add += record.plus_report_line_ids.mapped('tag_ids').filtered(lambda x: not x.tax_negate)
tags_to_add += record.minus_report_line_ids.mapped('tag_ids').filtered(lambda x: x.tax_negate)
tags_to_add += record.tag_ids
rslt.append((0, 0, {
'factor_percent': record.factor_percent,
'repartition_type': record.repartition_type,
'tag_ids': [(6, 0, tags_to_add.ids)],
'company_id': company.id,
'use_in_tax_closing': record.use_in_tax_closing
}))
return rslt
# Fiscal Position Templates
class AccountFiscalPositionTemplate(models.Model):
_name = 'account.fiscal.position.template'
_description = 'Template for Fiscal Position'
sequence = fields.Integer()
name = fields.Char(string='Fiscal Position Template', required=True)
chart_template_id = fields.Many2one('account.chart.template', string='Chart Template', required=True)
account_ids = fields.One2many('account.fiscal.position.account.template', 'position_id', string='Account Mapping')
tax_ids = fields.One2many('account.fiscal.position.tax.template', 'position_id', string='Tax Mapping')
note = fields.Text(string='Notes')
auto_apply = fields.Boolean(string='Detect Automatically', help="Apply automatically this fiscal position.")
vat_required = fields.Boolean(string='VAT required', help="Apply only if partner has a VAT number.")
country_id = fields.Many2one('res.country', string='Country',
help="Apply only if delivery country matches.")
country_group_id = fields.Many2one('res.country.group', string='Country Group',
help="Apply only if delivery country matches the group.")
state_ids = fields.Many2many('res.country.state', string='Federal States')
zip_from = fields.Char(string='Zip Range From')
zip_to = fields.Char(string='Zip Range To')
class AccountFiscalPositionTaxTemplate(models.Model):
_name = 'account.fiscal.position.tax.template'
_description = 'Tax Mapping Template of Fiscal Position'
_rec_name = 'position_id'
position_id = fields.Many2one('account.fiscal.position.template', string='Fiscal Position', required=True, ondelete='cascade')
tax_src_id = fields.Many2one('account.tax.template', string='Tax Source', required=True)
tax_dest_id = fields.Many2one('account.tax.template', string='Replacement Tax')
class AccountFiscalPositionAccountTemplate(models.Model):
_name = 'account.fiscal.position.account.template'
_description = 'Accounts Mapping Template of Fiscal Position'
_rec_name = 'position_id'
position_id = fields.Many2one('account.fiscal.position.template', string='Fiscal Mapping', required=True, ondelete='cascade')
account_src_id = fields.Many2one('account.account.template', string='Account Source', required=True)
account_dest_id = fields.Many2one('account.account.template', string='Account Destination', required=True)
class AccountReconcileModelTemplate(models.Model):
_name = "account.reconcile.model.template"
_description = 'Reconcile Model Template'
# Base fields.
chart_template_id = fields.Many2one('account.chart.template', string='Chart Template', required=True)
name = fields.Char(string='Button Label', required=True)
sequence = fields.Integer(required=True, default=10)
rule_type = fields.Selection(selection=[
('writeoff_button', 'Manually create a write-off on clicked button'),
('writeoff_suggestion', 'Suggest a write-off'),
('invoice_matching', 'Match existing invoices/bills')
], string='Type', default='writeoff_button', required=True)
auto_reconcile = fields.Boolean(string='Auto-validate',
help='Validate the statement line automatically (reconciliation based on your rule).')
to_check = fields.Boolean(string='To Check', default=False, help='This matching rule is used when the user is not certain of all the information of the counterpart.')
matching_order = fields.Selection(
selection=[
('old_first', 'Oldest first'),
('new_first', 'Newest first'),
]
)
# ===== Conditions =====
match_text_location_label = fields.Boolean(
default=True,
help="Search in the Statement's Label to find the Invoice/Payment's reference",
)
match_text_location_note = fields.Boolean(
default=False,
help="Search in the Statement's Note to find the Invoice/Payment's reference",
)
match_text_location_reference = fields.Boolean(
default=False,
help="Search in the Statement's Reference to find the Invoice/Payment's reference",
)
match_journal_ids = fields.Many2many('account.journal', string='Journals',
domain="[('type', 'in', ('bank', 'cash'))]",
help='The reconciliation model will only be available from the selected journals.')
match_nature = fields.Selection(selection=[
('amount_received', 'Amount Received'),
('amount_paid', 'Amount Paid'),
('both', 'Amount Paid/Received')
], string='Amount Nature', required=True, default='both',
help='''The reconciliation model will only be applied to the selected transaction type:
* Amount Received: Only applied when receiving an amount.
* Amount Paid: Only applied when paying an amount.
* Amount Paid/Received: Applied in both cases.''')
match_amount = fields.Selection(selection=[
('lower', 'Is Lower Than'),
('greater', 'Is Greater Than'),
('between', 'Is Between'),
], string='Amount',
help='The reconciliation model will only be applied when the amount being lower than, greater than or between specified amount(s).')
match_amount_min = fields.Float(string='Amount Min Parameter')
match_amount_max = fields.Float(string='Amount Max Parameter')
match_label = fields.Selection(selection=[
('contains', 'Contains'),
('not_contains', 'Not Contains'),
('match_regex', 'Match Regex'),
], string='Label', help='''The reconciliation model will only be applied when the label:
* Contains: The proposition label must contains this string (case insensitive).
* Not Contains: Negation of "Contains".
* Match Regex: Define your own regular expression.''')
match_label_param = fields.Char(string='Label Parameter')
match_note = fields.Selection(selection=[
('contains', 'Contains'),
('not_contains', 'Not Contains'),
('match_regex', 'Match Regex'),
], string='Note', help='''The reconciliation model will only be applied when the note:
* Contains: The proposition note must contains this string (case insensitive).
* Not Contains: Negation of "Contains".
* Match Regex: Define your own regular expression.''')
match_note_param = fields.Char(string='Note Parameter')
match_transaction_type = fields.Selection(selection=[
('contains', 'Contains'),
('not_contains', 'Not Contains'),
('match_regex', 'Match Regex'),
], string='Transaction Type', help='''The reconciliation model will only be applied when the transaction type:
* Contains: The proposition transaction type must contains this string (case insensitive).
* Not Contains: Negation of "Contains".
* Match Regex: Define your own regular expression.''')
match_transaction_type_param = fields.Char(string='Transaction Type Parameter')
match_same_currency = fields.Boolean(string='Same Currency Matching', default=True,
help='Restrict to propositions having the same currency as the statement line.')
match_total_amount = fields.Boolean(string='Amount Matching', default=True,
help='The sum of total residual amount propositions matches the statement line amount.')
match_total_amount_param = fields.Float(string='Amount Matching %', default=100,
help='The sum of total residual amount propositions matches the statement line amount under this percentage.')
match_partner = fields.Boolean(string='Partner Is Set',
help='The reconciliation model will only be applied when a customer/vendor is set.')
match_partner_ids = fields.Many2many('res.partner', string='Restrict Partners to',
help='The reconciliation model will only be applied to the selected customers/vendors.')
match_partner_category_ids = fields.Many2many('res.partner.category', string='Restrict Partner Categories to',
help='The reconciliation model will only be applied to the selected customer/vendor categories.')
line_ids = fields.One2many('account.reconcile.model.line.template', 'model_id')
decimal_separator = fields.Char(help="Every character that is nor a digit nor this separator will be removed from the matching string")
class AccountReconcileModelLineTemplate(models.Model):
_name = "account.reconcile.model.line.template"
_description = 'Reconcile Model Line Template'
model_id = fields.Many2one('account.reconcile.model.template')
sequence = fields.Integer(required=True, default=10)
account_id = fields.Many2one('account.account.template', string='Account', ondelete='cascade', domain=[('deprecated', '=', False)])
label = fields.Char(string='Journal Item Label')
amount_type = fields.Selection([
('fixed', 'Fixed'),
('percentage', 'Percentage of balance'),
('regex', 'From label'),
], required=True, default='percentage')
amount_string = fields.Char(string="Amount")
force_tax_included = fields.Boolean(string='Tax Included in Price', help='Force the tax to be managed as a price included tax.')
tax_ids = fields.Many2many('account.tax.template', string='Taxes', ondelete='restrict')
|