odoo-addon-l10n-br-fiscal 16.0.19.4.0__py3-none-any.whl → 17.0.4.0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. odoo/addons/l10n_br_fiscal/README.rst +10 -10
  2. odoo/addons/l10n_br_fiscal/__init__.py +2 -3
  3. odoo/addons/l10n_br_fiscal/__manifest__.py +3 -7
  4. odoo/addons/l10n_br_fiscal/constants/fiscal.py +0 -66
  5. odoo/addons/l10n_br_fiscal/data/l10n_br_fiscal.cest.csv +983 -1043
  6. odoo/addons/l10n_br_fiscal/data/l10n_br_fiscal.cst.csv +0 -58
  7. odoo/addons/l10n_br_fiscal/data/l10n_br_fiscal.tax.csv +0 -31
  8. odoo/addons/l10n_br_fiscal/data/l10n_br_fiscal.tax.group.csv +0 -3
  9. odoo/addons/l10n_br_fiscal/data/product_data.xml +2 -2
  10. odoo/addons/l10n_br_fiscal/demo/fiscal_document_demo.xml +88 -0
  11. odoo/addons/l10n_br_fiscal/i18n/l10n_br_fiscal.pot +28 -1031
  12. odoo/addons/l10n_br_fiscal/i18n/pt_BR.po +1078 -1197
  13. odoo/addons/l10n_br_fiscal/models/__init__.py +2 -2
  14. odoo/addons/l10n_br_fiscal/models/comment.py +20 -18
  15. odoo/addons/l10n_br_fiscal/models/data_abstract.py +11 -12
  16. odoo/addons/l10n_br_fiscal/models/document.py +4 -30
  17. odoo/addons/l10n_br_fiscal/models/document_line.py +3 -16
  18. odoo/addons/l10n_br_fiscal/models/document_line_mixin.py +27 -1048
  19. odoo/addons/l10n_br_fiscal/models/document_line_mixin_methods.py +883 -0
  20. odoo/addons/l10n_br_fiscal/models/document_mixin.py +3 -241
  21. odoo/addons/l10n_br_fiscal/models/document_mixin_methods.py +239 -0
  22. odoo/addons/l10n_br_fiscal/models/document_serie.py +5 -3
  23. odoo/addons/l10n_br_fiscal/models/invalidate_number.py +0 -6
  24. odoo/addons/l10n_br_fiscal/models/operation.py +0 -11
  25. odoo/addons/l10n_br_fiscal/models/operation_dashboard.py +2 -3
  26. odoo/addons/l10n_br_fiscal/models/operation_line.py +0 -28
  27. odoo/addons/l10n_br_fiscal/models/partner_profile.py +0 -6
  28. odoo/addons/l10n_br_fiscal/models/product_template.py +0 -4
  29. odoo/addons/l10n_br_fiscal/models/res_company.py +0 -18
  30. odoo/addons/l10n_br_fiscal/models/res_partner.py +0 -17
  31. odoo/addons/l10n_br_fiscal/models/simplified_tax_range.py +0 -8
  32. odoo/addons/l10n_br_fiscal/models/tax_definition.py +2 -35
  33. odoo/addons/l10n_br_fiscal/security/ir.model.access.csv +0 -6
  34. odoo/addons/l10n_br_fiscal/static/description/index.html +8 -8
  35. odoo/addons/l10n_br_fiscal/tests/__init__.py +0 -1
  36. odoo/addons/l10n_br_fiscal/tests/test_cnae.py +2 -3
  37. odoo/addons/l10n_br_fiscal/tests/test_document_edition.py +10 -37
  38. odoo/addons/l10n_br_fiscal/tests/test_fiscal_document_generic.py +28 -4
  39. odoo/addons/l10n_br_fiscal/tests/test_fiscal_document_nfse.py +4 -0
  40. odoo/addons/l10n_br_fiscal/tests/test_service_type.py +2 -3
  41. odoo/addons/l10n_br_fiscal/tests/test_tax_benefit.py +4 -2
  42. odoo/addons/l10n_br_fiscal/views/cest_view.xml +2 -2
  43. odoo/addons/l10n_br_fiscal/views/cnae_view.xml +0 -1
  44. odoo/addons/l10n_br_fiscal/views/document_line_mixin_view.xml +170 -243
  45. odoo/addons/l10n_br_fiscal/views/document_line_view.xml +7 -11
  46. odoo/addons/l10n_br_fiscal/views/document_related_view.xml +15 -11
  47. odoo/addons/l10n_br_fiscal/views/document_serie_view.xml +1 -1
  48. odoo/addons/l10n_br_fiscal/views/document_view.xml +32 -44
  49. odoo/addons/l10n_br_fiscal/views/invalidate_number_view.xml +12 -12
  50. odoo/addons/l10n_br_fiscal/views/l10n_br_fiscal_action.xml +0 -31
  51. odoo/addons/l10n_br_fiscal/views/l10n_br_fiscal_menu.xml +2 -18
  52. odoo/addons/l10n_br_fiscal/views/nbm_view.xml +2 -2
  53. odoo/addons/l10n_br_fiscal/views/nbs_view.xml +1 -1
  54. odoo/addons/l10n_br_fiscal/views/ncm_view.xml +3 -3
  55. odoo/addons/l10n_br_fiscal/views/operation_dashboard_view.xml +27 -18
  56. odoo/addons/l10n_br_fiscal/views/operation_line_view.xml +2 -5
  57. odoo/addons/l10n_br_fiscal/views/operation_view.xml +39 -16
  58. odoo/addons/l10n_br_fiscal/views/partner_profile_view.xml +3 -6
  59. odoo/addons/l10n_br_fiscal/views/product_genre_view.xml +2 -2
  60. odoo/addons/l10n_br_fiscal/views/product_product_view.xml +17 -45
  61. odoo/addons/l10n_br_fiscal/views/product_template_view.xml +16 -35
  62. odoo/addons/l10n_br_fiscal/views/res_company_view.xml +17 -18
  63. odoo/addons/l10n_br_fiscal/views/res_config_settings_view.xml +37 -98
  64. odoo/addons/l10n_br_fiscal/views/res_partner_view.xml +3 -13
  65. odoo/addons/l10n_br_fiscal/views/service_type_view.xml +2 -3
  66. odoo/addons/l10n_br_fiscal/views/tax_definition_view.xml +60 -29
  67. odoo/addons/l10n_br_fiscal/views/tax_estimate_view.xml +2 -2
  68. odoo/addons/l10n_br_fiscal/views/tax_group_view.xml +4 -4
  69. odoo/addons/l10n_br_fiscal/views/tax_view.xml +14 -16
  70. odoo/addons/l10n_br_fiscal/wizards/document_import_wizard_mixin.py +1 -1
  71. odoo/addons/l10n_br_fiscal/wizards/document_import_wizard_mixin.xml +2 -5
  72. {odoo_addon_l10n_br_fiscal-16.0.19.4.0.dist-info → odoo_addon_l10n_br_fiscal-17.0.4.0.0.1.dist-info}/METADATA +16 -16
  73. {odoo_addon_l10n_br_fiscal-16.0.19.4.0.dist-info → odoo_addon_l10n_br_fiscal-17.0.4.0.0.1.dist-info}/RECORD +77 -88
  74. odoo/addons/l10n_br_fiscal/data/l10n_br_fiscal.operation.indicator.csv +0 -27
  75. odoo/addons/l10n_br_fiscal/data/l10n_br_fiscal.tax.classification.csv +0 -163
  76. odoo/addons/l10n_br_fiscal/migrations/16.0.14.0.5/pre-migration.py +0 -15
  77. odoo/addons/l10n_br_fiscal/migrations/16.0.2.0.0/pre-migration.py +0 -25
  78. odoo/addons/l10n_br_fiscal/migrations/16.0.2.15.0/pre-migration.py +0 -19
  79. odoo/addons/l10n_br_fiscal/migrations/16.0.4.0.0/pre-migration.py +0 -220
  80. odoo/addons/l10n_br_fiscal/migrations/16.0.5.0.0/pre-migration.py +0 -33
  81. odoo/addons/l10n_br_fiscal/migrations/16.0.5.2.0/pre-migration.py +0 -21
  82. odoo/addons/l10n_br_fiscal/models/operation_indicator.py +0 -58
  83. odoo/addons/l10n_br_fiscal/models/tax_classification.py +0 -81
  84. odoo/addons/l10n_br_fiscal/tests/test_tax_classification.py +0 -110
  85. odoo/addons/l10n_br_fiscal/views/operation_indicator_view.xml +0 -75
  86. odoo/addons/l10n_br_fiscal/views/tax_classification.xml +0 -110
  87. /odoo/addons/l10n_br_fiscal/migrations/{16.0.13.0.0 → 17.0.2.0.0}/pre-migration.py +0 -0
  88. /odoo/addons/l10n_br_fiscal/migrations/{16.0.14.0.0 → 17.0.3.0.0}/pre-migration.py +0 -0
  89. {odoo_addon_l10n_br_fiscal-16.0.19.4.0.dist-info → odoo_addon_l10n_br_fiscal-17.0.4.0.0.1.dist-info}/WHEEL +0 -0
  90. {odoo_addon_l10n_br_fiscal-16.0.19.4.0.dist-info → odoo_addon_l10n_br_fiscal-17.0.4.0.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,883 @@
1
+ # Copyright (C) 2019 Renato Lima - Akretion <renato.lima@akretion.com.br>
2
+ # License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
3
+
4
+ from copy import deepcopy
5
+
6
+ from lxml import etree
7
+ from lxml.builder import E
8
+
9
+ from odoo import Command, api, models
10
+
11
+ from ..constants.fiscal import CFOP_DESTINATION_EXPORT, FISCAL_IN, TAX_DOMAIN_ICMS
12
+ from ..constants.icms import (
13
+ ICMS_BASE_TYPE_DEFAULT,
14
+ ICMS_ORIGIN_DEFAULT,
15
+ ICMS_ST_BASE_TYPE_DEFAULT,
16
+ )
17
+
18
+ FISCAL_TAX_ID_FIELDS = [
19
+ "cofins_tax_id",
20
+ "cofins_wh_tax_id",
21
+ "cofinsst_tax_id",
22
+ "csll_tax_id",
23
+ "csll_wh_tax_id",
24
+ "icms_tax_id",
25
+ "icmsfcp_tax_id",
26
+ "icmssn_tax_id",
27
+ "icmsst_tax_id",
28
+ "icmsfcpst_tax_id",
29
+ "ii_tax_id",
30
+ "inss_tax_id",
31
+ "inss_wh_tax_id",
32
+ "ipi_tax_id",
33
+ "irpj_tax_id",
34
+ "irpj_wh_tax_id",
35
+ "issqn_tax_id",
36
+ "issqn_wh_tax_id",
37
+ "pis_tax_id",
38
+ "pis_wh_tax_id",
39
+ "pisst_tax_id",
40
+ ]
41
+
42
+ FISCAL_CST_ID_FIELDS = [
43
+ "icms_cst_id",
44
+ "ipi_cst_id",
45
+ "pis_cst_id",
46
+ "pisst_cst_id",
47
+ "cofins_cst_id",
48
+ "cofinsst_cst_id",
49
+ ]
50
+
51
+
52
+ class FiscalDocumentLineMixinMethods(models.AbstractModel):
53
+ """
54
+ Provides the method implementations for l10n_br_fiscal.document.line.mixin.
55
+
56
+ These methods are extracted into this separate mixin due to the way
57
+ l10n_br_fiscal.document.line is incorporated into account.move.line
58
+ by the l10n_br_account module (decorator pattern).
59
+
60
+ Specifically:
61
+ - In l10n_br_account, fields from l10n_br_fiscal.document.line
62
+ are added to account.move.line using Odoo's `_inherits` (composition)
63
+ mechanism.
64
+ - The methods in *this* mixin, however, are intended to be inherited
65
+ using the standard `_inherit` mechanism.
66
+
67
+ This separation is crucial because `_inherits` handles field composition
68
+ but does not inherit methods. Thus, `_inherit` is used to bring in
69
+ these methods. If these methods were defined in the same class as the
70
+ fields of l10n_br_fiscal.document.line.mixin (which are subject to
71
+ `_inherits`), and account.move.line also used `_inherit` on that
72
+ single class, the fields would be duplicated.
73
+ """
74
+
75
+ _name = "l10n_br_fiscal.document.line.mixin.methods"
76
+ _description = "Fiscal Document Mixin Methods"
77
+
78
+ @api.model
79
+ def inject_fiscal_fields(
80
+ self,
81
+ doc,
82
+ view_ref="l10n_br_fiscal.document_fiscal_line_mixin_form",
83
+ xpath_mappings=None,
84
+ ):
85
+ """
86
+ Inject common fiscal fields into view placeholder elements.
87
+ Used for invoice line, sale order line, purchase order line...
88
+ """
89
+
90
+ # the list of computed fields we will add to the view when missing
91
+ missing_line_fields = set(
92
+ [
93
+ fname
94
+ for fname, _field in filter(
95
+ lambda item: item[1].compute
96
+ in (
97
+ "_compute_tax_fields",
98
+ "_compute_fiscal_tax_ids",
99
+ "_compute_product_fiscal_fields",
100
+ ),
101
+ self.env["l10n_br_fiscal.document.line.mixin"]._fields.items(),
102
+ )
103
+ ]
104
+ )
105
+
106
+ fiscal_view = self.env.ref(
107
+ "l10n_br_fiscal.document_fiscal_line_mixin_form"
108
+ ).sudo()
109
+ fsc_doc = etree.fromstring(
110
+ fiscal_view.with_context(inherit_branding=True).get_combined_arch()
111
+ )
112
+
113
+ if xpath_mappings is None:
114
+ xpath_mappings = (
115
+ # (placeholder_xpath, fiscal_xpath)
116
+ (".//group[@name='fiscal_fields']", "//group[@name='fiscal_fields']"),
117
+ (".//page[@name='fiscal_taxes']", "//page[@name='fiscal_taxes']"),
118
+ (
119
+ ".//page[@name='fiscal_line_extra_info']",
120
+ "//page[@name='fiscal_line_extra_info']",
121
+ ),
122
+ # these will only collect (invisible) fields for onchanges:
123
+ (
124
+ ".//control[@name='fiscal_fields']...",
125
+ "//group[@name='fiscal_fields']//field",
126
+ ),
127
+ (
128
+ ".//control[@name='fiscal_taxes_fields']...",
129
+ "//page[@name='fiscal_taxes']//field",
130
+ ),
131
+ (
132
+ ".//control[@name='fiscal_line_extra_info_fields']...",
133
+ "//page[@name='fiscal_line_extra_info']//field",
134
+ ),
135
+ )
136
+ for placeholder_xpath, fiscal_xpath in xpath_mappings:
137
+ placeholder_nodes = doc.findall(placeholder_xpath)
138
+ if not placeholder_nodes:
139
+ continue
140
+ fiscal_nodes = fsc_doc.xpath(fiscal_xpath)
141
+ for target_node in placeholder_nodes:
142
+ if len(fiscal_nodes) == 1:
143
+ # replace unique placeholder
144
+ # (deepcopy is required to inject fiscal nodes in possible
145
+ # next places)
146
+ replace_node = deepcopy(fiscal_nodes[0])
147
+ target_node.getparent().replace(target_node, replace_node)
148
+ else:
149
+ # append multiple fields to placeholder container
150
+ existing_fields = [
151
+ e.attrib["name"] for e in target_node if e.tag == "field"
152
+ ]
153
+ for fiscal_node in fiscal_nodes:
154
+ if fiscal_node.attrib["name"] in missing_line_fields:
155
+ missing_line_fields.remove(fiscal_node.attrib["name"])
156
+ if fiscal_node.attrib["name"] in existing_fields:
157
+ continue
158
+ field = deepcopy(fiscal_node)
159
+ if not field.attrib.get("optional"):
160
+ field.attrib["optional"] = "hide"
161
+ target_node.append(field)
162
+ for fname in missing_line_fields:
163
+ if fname not in existing_fields:
164
+ target_node.append(
165
+ E.field(name=fname, string=fname, optional="hide")
166
+ )
167
+ return doc
168
+
169
+ @api.model
170
+ def _get_view(self, view_id=None, view_type="form", **options):
171
+ arch, view = super()._get_view(view_id, view_type, **options)
172
+ if view_type == "form":
173
+ arch = self.inject_fiscal_fields(arch)
174
+ return arch, view
175
+
176
+ @api.depends(
177
+ "fiscal_price",
178
+ "discount_value",
179
+ "insurance_value",
180
+ "other_value",
181
+ "freight_value",
182
+ "fiscal_quantity",
183
+ "amount_tax_not_included",
184
+ "amount_tax_included",
185
+ "amount_tax_withholding",
186
+ "uot_id",
187
+ "product_id",
188
+ "partner_id",
189
+ "company_id",
190
+ "price_unit",
191
+ "quantity",
192
+ "icms_relief_id",
193
+ "fiscal_operation_line_id",
194
+ )
195
+ def _compute_fiscal_amounts(self):
196
+ for record in self:
197
+ round_curr = record.currency_id or self.env.ref("base.BRL")
198
+
199
+ # Total value of products or services
200
+ record.price_gross = round_curr.round(record.price_unit * record.quantity)
201
+ record.amount_fiscal = record.price_gross - record.discount_value
202
+ record.fiscal_amount_tax = record.amount_tax_not_included
203
+
204
+ add_to_amount = sum(record[a] for a in record._add_fields_to_amount())
205
+ rm_to_amount = sum(record[r] for r in record._rm_fields_to_amount())
206
+ record.fiscal_amount_untaxed = (
207
+ record.price_gross
208
+ - record.discount_value
209
+ + add_to_amount
210
+ - rm_to_amount
211
+ )
212
+
213
+ # Valor do documento (NF)
214
+ record.fiscal_amount_total = (
215
+ record.fiscal_amount_untaxed + record.fiscal_amount_tax
216
+ )
217
+
218
+ # Valor Liquido (TOTAL + IMPOSTOS - RETENÇÕES)
219
+ record.amount_taxed = (
220
+ record.fiscal_amount_total - record.amount_tax_withholding
221
+ )
222
+
223
+ # Valor do documento (NF) - RETENÇÕES
224
+ record.fiscal_amount_total = record.amount_taxed
225
+
226
+ # Valor financeiro
227
+ if (
228
+ record.fiscal_operation_line_id
229
+ and record.fiscal_operation_line_id.add_to_amount
230
+ and (not record.cfop_id or record.cfop_id.finance_move)
231
+ ):
232
+ record.financial_total = record.amount_taxed
233
+ record.financial_total_gross = (
234
+ record.financial_total + record.discount_value
235
+ )
236
+ record.financial_discount_value = record.discount_value
237
+ else:
238
+ record.financial_total_gross = record.financial_total = 0.0
239
+ record.financial_discount_value = 0.0
240
+
241
+ @api.depends("tax_icms_or_issqn", "partner_is_public_entity")
242
+ def _compute_allow_csll_irpj(self):
243
+ """Calculates the possibility of 'CSLL' and 'IRPJ' tax charges."""
244
+ for line in self:
245
+ # Determine if 'CSLL' and 'IRPJ' taxes may apply:
246
+ # 1. When providing services (tax_icms_or_issqn == "issqn")
247
+ # 2. When supplying products to public entities (partner_is_public_entity
248
+ # is True)
249
+ if line.tax_icms_or_issqn == "issqn" or line.partner_is_public_entity:
250
+ line.allow_csll_irpj = True # Tax charges may apply
251
+ else:
252
+ line.allow_csll_irpj = False # No tax charges expected
253
+
254
+ def _prepare_br_fiscal_dict(self, default=False):
255
+ self.ensure_one()
256
+ fields = self.env["l10n_br_fiscal.document.line.mixin"]._fields.keys()
257
+
258
+ # we now read the record fiscal fields except the m2m tax:
259
+ vals = self._convert_to_write(self.read(fields)[0])
260
+
261
+ # remove id field to avoid conflicts
262
+ vals.pop("id", None)
263
+
264
+ if default: # in case you want to use new rather than write later
265
+ return {f"default_{k}": vals[k] for k in vals.keys()}
266
+ return vals
267
+
268
+ @api.onchange("fiscal_operation_id", "company_id", "partner_id", "product_id")
269
+ def _onchange_fiscal_operation_id(self):
270
+ if self.fiscal_operation_id:
271
+ self.fiscal_operation_line_id = self.fiscal_operation_id.line_definition(
272
+ company=self.company_id,
273
+ partner=self._get_fiscal_partner(),
274
+ product=self.product_id,
275
+ )
276
+
277
+ def _get_fiscal_tax_ids_dependencies(self):
278
+ """
279
+ Dynamically get the list of fields dependencies, overriden in l10n_br_purchase.
280
+ """
281
+ return [
282
+ "company_id",
283
+ "partner_id",
284
+ "fiscal_operation_line_id",
285
+ "product_id",
286
+ "ncm_id",
287
+ "nbs_id",
288
+ "nbm_id",
289
+ "cest_id",
290
+ "city_taxation_code_id",
291
+ "service_type_id",
292
+ "ind_final",
293
+ ]
294
+
295
+ @api.depends(lambda self: self._get_fiscal_tax_ids_dependencies())
296
+ def _compute_fiscal_tax_ids(self):
297
+ """
298
+ Use fiscal_operation_line_id to map and compute the applicable Brazilian taxes.
299
+
300
+ Among the dependencies, company_id, partner_id and ind_final are related
301
+ to the fiscal document/line container. When called from account.move.line
302
+ via _inherits on newID records, we read these values from the related aml
303
+ to work around and _inherits/precompute limitation.
304
+ """
305
+ if self._context.get("skip_compute_fiscal_tax_ids"):
306
+ return
307
+ for line in self:
308
+ if hasattr(line, "account_line_ids") and line.account_line_ids:
309
+ # it seems Odoo 16 ORM has a limitation when line is an
310
+ # l10n_br_fiscal.document.line that is edited via an account.move.line
311
+ # form and when both are a newID, then line relational field might be
312
+ # empty here. But in this case, we detect it and we wrap it back in the
313
+ wrapped_line = line.account_line_ids[0]
314
+ else:
315
+ wrapped_line = line
316
+
317
+ if wrapped_line.fiscal_operation_line_id:
318
+ mapping_result = wrapped_line.fiscal_operation_line_id.map_fiscal_taxes(
319
+ company=wrapped_line.company_id,
320
+ partner=wrapped_line._get_fiscal_partner(),
321
+ product=wrapped_line.product_id,
322
+ ncm=wrapped_line.ncm_id,
323
+ nbm=wrapped_line.nbm_id,
324
+ nbs=wrapped_line.nbs_id,
325
+ cest=wrapped_line.cest_id,
326
+ city_taxation_code=wrapped_line.city_taxation_code_id,
327
+ service_type=wrapped_line.service_type_id,
328
+ ind_final=wrapped_line.ind_final,
329
+ )
330
+ line.cfop_id = mapping_result["cfop"]
331
+ line.ipi_guideline_id = mapping_result["ipi_guideline"]
332
+ line.icms_tax_benefit_id = mapping_result["icms_tax_benefit_id"]
333
+ if wrapped_line._is_imported():
334
+ return
335
+
336
+ taxes = line.env["l10n_br_fiscal.tax"]
337
+ for tax in mapping_result["taxes"].values():
338
+ taxes |= tax
339
+ line.fiscal_tax_ids = taxes
340
+ else:
341
+ line.fiscal_tax_ids = [Command.clear()]
342
+
343
+ @api.depends("fiscal_operation_line_id")
344
+ def _compute_comment_ids(self):
345
+ for line in self:
346
+ line.comment_ids = [
347
+ Command.set(line.fiscal_operation_line_id.comment_ids.ids)
348
+ ]
349
+
350
+ @api.model
351
+ def _build_null_mask_dict(self) -> dict:
352
+ """
353
+ Build a null values mask dict to reset all fiscal fields.
354
+ """
355
+ mask_dict = {
356
+ f[0]: False
357
+ for f in filter(
358
+ lambda f: f[1].compute == "_compute_tax_fields",
359
+ self.env["l10n_br_fiscal.document.line.mixin"]._fields.items(),
360
+ )
361
+ }
362
+ for fiscal_tax_field in FISCAL_TAX_ID_FIELDS:
363
+ mask_dict[fiscal_tax_field] = False
364
+ return mask_dict
365
+
366
+ def _get_tax_fields_dependencies(self):
367
+ """
368
+ Dynamically get the list of fields dependencies, overriden in l10n_br_purchase.
369
+ """
370
+ # IMPORTANT NOTE: as _compute_fiscal_tax_ids triggers _compute_tax_fields,
371
+ # we don't put fields that trigger _compute_fiscal_tax_ids as dependencies here.
372
+ return [
373
+ "price_unit",
374
+ "quantity",
375
+ "uom_id",
376
+ "fiscal_price",
377
+ "fiscal_quantity",
378
+ "uot_id",
379
+ "discount_value",
380
+ "insurance_value",
381
+ "ii_customhouse_charges",
382
+ "ii_iof_value",
383
+ "other_value",
384
+ "freight_value",
385
+ "cfop_id",
386
+ "icmssn_range_id",
387
+ "icms_origin",
388
+ "icms_cst_id",
389
+ "icms_relief_id",
390
+ "fiscal_tax_ids",
391
+ ]
392
+
393
+ @api.depends(lambda self: self._get_tax_fields_dependencies())
394
+ def _compute_tax_fields(self):
395
+ """
396
+ Compute base, percent, value... tax fields for ICMS, IPI, PIS, COFINS... taxes.
397
+ """
398
+ if self._context.get("skip_compute_tax_fields"):
399
+ return
400
+
401
+ null_mask = None
402
+ for line in self.filtered(lambda line: not line._is_imported()):
403
+ if hasattr(line, "account_line_ids") and line.account_line_ids:
404
+ # it seems Odoo 16 ORM has a limitation when line is an
405
+ # l10n_br_fiscal.document.line that is edited via an account.move.line
406
+ # form and when both are a newID, then line relational field might be
407
+ # empty here. But in this case, we detect it and we wrap it back in the
408
+ wrapped_line = line.account_line_ids[0]
409
+ else:
410
+ wrapped_line = line
411
+
412
+ if null_mask is None:
413
+ null_mask = self._build_null_mask_dict()
414
+ to_update = null_mask.copy()
415
+ if wrapped_line.fiscal_operation_line_id:
416
+ compute_result = wrapped_line.fiscal_tax_ids.compute_taxes(
417
+ company=wrapped_line.company_id,
418
+ partner=wrapped_line._get_fiscal_partner(),
419
+ product=wrapped_line.product_id,
420
+ price_unit=wrapped_line.price_unit,
421
+ quantity=wrapped_line.quantity,
422
+ uom_id=wrapped_line.uom_id,
423
+ fiscal_price=wrapped_line.fiscal_price,
424
+ fiscal_quantity=wrapped_line.fiscal_quantity,
425
+ uot_id=wrapped_line.uot_id,
426
+ discount_value=wrapped_line.discount_value,
427
+ insurance_value=wrapped_line.insurance_value,
428
+ ii_customhouse_charges=wrapped_line.ii_customhouse_charges,
429
+ ii_iof_value=wrapped_line.ii_iof_value,
430
+ other_value=wrapped_line.other_value,
431
+ freight_value=wrapped_line.freight_value,
432
+ ncm=wrapped_line.ncm_id,
433
+ nbs=wrapped_line.nbs_id,
434
+ nbm=wrapped_line.nbm_id,
435
+ cest=wrapped_line.cest_id,
436
+ operation_line=wrapped_line.fiscal_operation_line_id,
437
+ cfop=wrapped_line.cfop_id,
438
+ icmssn_range=wrapped_line.icmssn_range_id,
439
+ icms_origin=wrapped_line.icms_origin,
440
+ icms_cst_id=wrapped_line.icms_cst_id,
441
+ ind_final=wrapped_line.ind_final,
442
+ icms_relief_id=wrapped_line.icms_relief_id,
443
+ )
444
+ to_update.update(wrapped_line._prepare_tax_fields(compute_result))
445
+ else:
446
+ compute_result = {}
447
+ to_update.update(
448
+ {
449
+ "amount_tax_included": compute_result.get("amount_included", 0.0),
450
+ "amount_tax_not_included": compute_result.get(
451
+ "amount_not_included", 0.0
452
+ ),
453
+ "amount_tax_withholding": compute_result.get(
454
+ "amount_withholding", 0.0
455
+ ),
456
+ "estimate_tax": compute_result.get("estimate_tax", 0.0),
457
+ }
458
+ )
459
+ in_draft_mode = wrapped_line != wrapped_line._origin
460
+ if in_draft_mode:
461
+ wrapped_line.update(to_update)
462
+ else:
463
+ wrapped_line.write(to_update)
464
+
465
+ def _prepare_tax_fields(self, compute_result):
466
+ self.ensure_one()
467
+ tax_values = {}
468
+ if self._is_imported():
469
+ return tax_values
470
+ computed_taxes = compute_result.get("taxes", {})
471
+ for tax in self.fiscal_tax_ids:
472
+ computed_tax = computed_taxes.get(tax.tax_domain, {})
473
+ tax_field_name = f"{tax.tax_domain}_tax_id"
474
+ if hasattr(self, tax_field_name):
475
+ tax_values[tax_field_name] = tax.ids[0]
476
+ method = getattr(self, f"_prepare_fields_{tax.tax_domain}", None)
477
+ if method and computed_tax:
478
+ prepared_fields = method(computed_tax)
479
+ if prepared_fields:
480
+ tax_values.update(prepared_fields)
481
+ return tax_values
482
+
483
+ @api.depends(
484
+ "product_id",
485
+ "fiscal_operation_id",
486
+ )
487
+ def _compute_price_unit_fiscal(self): # OK when edited from aml?? c-> check
488
+ for line in self:
489
+ line.price_unit = {
490
+ "sale_price": line.product_id.list_price,
491
+ "cost_price": line.product_id.standard_price,
492
+ }.get(line.fiscal_operation_id.default_price_unit, 0)
493
+
494
+ def _get_fiscal_partner(self):
495
+ """
496
+ Meant to be overriden when the l10n_br_fiscal.document partner_id should not
497
+ be the same as the sale.order, purchase.order, account.move (...) partner_id.
498
+
499
+ (In the case of invoicing, the invoicing partner set by the user should
500
+ get priority over any invoicing contact returned by address_get.)
501
+ """
502
+ self.ensure_one()
503
+ return self.partner_id
504
+
505
+ @api.depends("product_id")
506
+ def _compute_product_fiscal_fields(self):
507
+ if self._context.get("skip_compute_product_fiscal_fields"):
508
+ return
509
+ for line in self:
510
+ if not line.product_id:
511
+ # reset to default values:
512
+ line.fiscal_type = False
513
+ line.ncm_id = False
514
+ line.nbm_id = False
515
+ line.tax_icms_or_issqn = TAX_DOMAIN_ICMS
516
+ line.icms_origin = ICMS_ORIGIN_DEFAULT
517
+ line.cest_id = False
518
+ line.nbs_id = False
519
+ line.fiscal_genre_id = False
520
+ line.service_type_id = False
521
+ line.city_taxation_code_id = False
522
+ line.issqn_fg_city_id = False
523
+ continue
524
+
525
+ product = line.product_id
526
+ line.fiscal_type = product.fiscal_type
527
+ line.ncm_id = product.ncm_id
528
+ line.nbm_id = product.nbm_id
529
+ line.tax_icms_or_issqn = product.tax_icms_or_issqn
530
+ line.icms_origin = product.icms_origin
531
+ line.cest_id = product.cest_id
532
+ line.nbs_id = product.nbs_id
533
+ line.fiscal_genre_id = product.fiscal_genre_id
534
+ line.service_type_id = product.service_type_id
535
+ if product.city_taxation_code_ids and line.company_id:
536
+ city = product.city_taxation_code_ids.filtered(
537
+ lambda r, current_line=line: r.city_id
538
+ == current_line.company_id.city_id
539
+ )
540
+ if city:
541
+ line.city_taxation_code_id = city
542
+ line.issqn_fg_city_id = line.company_id.city_id
543
+ else:
544
+ line.city_taxation_code_id = False
545
+ line.issqn_fg_city_id = False
546
+ else:
547
+ line.city_taxation_code_id = False
548
+ line.issqn_fg_city_id = False
549
+
550
+ def _prepare_fields_issqn(self, tax_dict):
551
+ self.ensure_one()
552
+ return {
553
+ "issqn_base": tax_dict.get("base"),
554
+ "issqn_percent": tax_dict.get("percent_amount"),
555
+ "issqn_reduction": tax_dict.get("percent_reduction"),
556
+ "issqn_value": tax_dict.get("tax_value"),
557
+ }
558
+
559
+ def _prepare_fields_issqn_wh(self, tax_dict):
560
+ self.ensure_one()
561
+ return {
562
+ "issqn_wh_base": tax_dict.get("base"),
563
+ "issqn_wh_percent": tax_dict.get("percent_amount"),
564
+ "issqn_wh_reduction": tax_dict.get("percent_reduction"),
565
+ "issqn_wh_value": tax_dict.get("tax_value"),
566
+ }
567
+
568
+ def _prepare_fields_csll(self, tax_dict):
569
+ self.ensure_one()
570
+ return {
571
+ "csll_base": tax_dict.get("base"),
572
+ "csll_percent": tax_dict.get("percent_amount"),
573
+ "csll_reduction": tax_dict.get("percent_reduction"),
574
+ "csll_value": tax_dict.get("tax_value"),
575
+ }
576
+
577
+ def _prepare_fields_csll_wh(self, tax_dict):
578
+ self.ensure_one()
579
+ return {
580
+ "csll_wh_base": tax_dict.get("base"),
581
+ "csll_wh_percent": tax_dict.get("percent_amount"),
582
+ "csll_wh_reduction": tax_dict.get("percent_reduction"),
583
+ "csll_wh_value": tax_dict.get("tax_value"),
584
+ }
585
+
586
+ def _prepare_fields_irpj(self, tax_dict):
587
+ self.ensure_one()
588
+ return {
589
+ "irpj_base": tax_dict.get("base"),
590
+ "irpj_percent": tax_dict.get("percent_amount"),
591
+ "irpj_reduction": tax_dict.get("percent_reduction"),
592
+ "irpj_value": tax_dict.get("tax_value"),
593
+ }
594
+
595
+ def _prepare_fields_irpj_wh(self, tax_dict):
596
+ self.ensure_one()
597
+ return {
598
+ "irpj_wh_base": tax_dict.get("base"),
599
+ "irpj_wh_percent": tax_dict.get("percent_amount"),
600
+ "irpj_wh_reduction": tax_dict.get("percent_reduction"),
601
+ "irpj_wh_value": tax_dict.get("tax_value"),
602
+ }
603
+
604
+ def _prepare_fields_inss(self, tax_dict):
605
+ self.ensure_one()
606
+ return {
607
+ "inss_base": tax_dict.get("base"),
608
+ "inss_percent": tax_dict.get("percent_amount"),
609
+ "inss_reduction": tax_dict.get("percent_reduction"),
610
+ "inss_value": tax_dict.get("tax_value"),
611
+ }
612
+
613
+ def _prepare_fields_inss_wh(self, tax_dict):
614
+ self.ensure_one()
615
+ return {
616
+ "inss_wh_base": tax_dict.get("base"),
617
+ "inss_wh_percent": tax_dict.get("percent_amount"),
618
+ "inss_wh_reduction": tax_dict.get("percent_reduction"),
619
+ "inss_wh_value": tax_dict.get("tax_value"),
620
+ }
621
+
622
+ def _prepare_fields_icms(self, tax_dict):
623
+ self.ensure_one()
624
+ cst_id = tax_dict.get("cst_id").id if tax_dict.get("cst_id") else False
625
+ return {
626
+ "icms_cst_id": cst_id,
627
+ "icms_base_type": tax_dict.get("icms_base_type", ICMS_BASE_TYPE_DEFAULT),
628
+ "icms_base": tax_dict.get("base", 0.0),
629
+ "icms_percent": tax_dict.get("percent_amount", 0.0),
630
+ "icms_reduction": tax_dict.get("percent_reduction", 0.0),
631
+ "icms_value": tax_dict.get("tax_value", 0.0),
632
+ "icms_origin_percent": tax_dict.get("icms_origin_perc", 0.0),
633
+ "icms_destination_percent": tax_dict.get("icms_dest_perc", 0.0),
634
+ "icms_sharing_percent": tax_dict.get("icms_sharing_percent", 0.0),
635
+ "icms_destination_base": tax_dict.get("icms_dest_base", 0.0),
636
+ "icms_origin_value": tax_dict.get("icms_origin_value", 0.0),
637
+ "icms_destination_value": tax_dict.get("icms_dest_value", 0.0),
638
+ "icms_relief_value": tax_dict.get("icms_relief", 0.0),
639
+ }
640
+
641
+ @api.onchange(
642
+ "icms_base",
643
+ "icms_percent",
644
+ "icms_reduction",
645
+ "icms_value",
646
+ "icms_destination_base",
647
+ "icms_origin_percent",
648
+ "icms_destination_percent",
649
+ "icms_sharing_percent",
650
+ "icms_origin_value",
651
+ "icms_tax_benefit_id",
652
+ )
653
+ def _onchange_icms_fields(self):
654
+ if self.icms_tax_benefit_id:
655
+ self.icms_tax_id = self.icms_tax_benefit_id.tax_id
656
+
657
+ def _prepare_fields_icmssn(self, tax_dict):
658
+ self.ensure_one()
659
+ cst_id = tax_dict.get("cst_id").id if tax_dict.get("cst_id") else False
660
+ icmssn_base = tax_dict.get("base", 0.0)
661
+ icmssn_credit_value = tax_dict.get("tax_value", 0.0)
662
+ simple_value = icmssn_base * self.icmssn_range_id.total_tax_percent
663
+ simple_without_icms_value = simple_value - icmssn_credit_value
664
+ return {
665
+ "icms_cst_id": cst_id,
666
+ "icmssn_base": icmssn_base,
667
+ "icmssn_percent": tax_dict.get("percent_amount"),
668
+ "icmssn_reduction": tax_dict.get("percent_reduction"),
669
+ "icmssn_credit_value": icmssn_credit_value,
670
+ "simple_value": simple_value,
671
+ "simple_without_icms_value": simple_without_icms_value,
672
+ }
673
+
674
+ def _prepare_fields_icmsst(self, tax_dict):
675
+ self.ensure_one()
676
+ return {
677
+ "icmsst_base_type": tax_dict.get(
678
+ "icmsst_base_type", ICMS_ST_BASE_TYPE_DEFAULT
679
+ ),
680
+ "icmsst_mva_percent": tax_dict.get("icmsst_mva_percent"),
681
+ "icmsst_percent": tax_dict.get("percent_amount"),
682
+ "icmsst_reduction": tax_dict.get("percent_reduction"),
683
+ "icmsst_base": tax_dict.get("base"),
684
+ "icmsst_value": tax_dict.get("tax_value"),
685
+ }
686
+
687
+ def _prepare_fields_icmsfcp(self, tax_dict):
688
+ self.ensure_one()
689
+ return {
690
+ "icmsfcp_base": tax_dict.get("base", 0.0),
691
+ "icmsfcp_percent": tax_dict.get("percent_amount", 0.0),
692
+ "icmsfcp_value": tax_dict.get("tax_value", 0.0),
693
+ }
694
+
695
+ def _prepare_fields_icmsfcpst(self, tax_dict):
696
+ self.ensure_one()
697
+ return {
698
+ "icmsfcpst_base": self.icmsst_base,
699
+ "icmsfcpst_percent": tax_dict.get("percent_amount", 0.0),
700
+ "icmsfcpst_value": tax_dict.get("tax_value", 0.0),
701
+ }
702
+
703
+ def _prepare_fields_ipi(self, tax_dict):
704
+ self.ensure_one()
705
+ cst_id = tax_dict.get("cst_id").id if tax_dict.get("cst_id") else False
706
+ return {
707
+ "ipi_cst_id": cst_id,
708
+ "ipi_base_type": tax_dict.get("base_type", False),
709
+ "ipi_base": tax_dict.get("base", 0.00),
710
+ "ipi_percent": tax_dict.get("percent_amount", 0.00),
711
+ "ipi_reduction": tax_dict.get("percent_reduction", 0.00),
712
+ "ipi_value": tax_dict.get("tax_value", 0.00),
713
+ }
714
+
715
+ def _prepare_fields_ii(self, tax_dict):
716
+ self.ensure_one()
717
+ return {
718
+ "ii_base": tax_dict.get("base", 0.00),
719
+ "ii_percent": tax_dict.get("percent_amount", 0.00),
720
+ "ii_value": tax_dict.get("tax_value", 0.00),
721
+ }
722
+
723
+ def _prepare_fields_pis(self, tax_dict):
724
+ self.ensure_one()
725
+ cst_id = tax_dict.get("cst_id").id if tax_dict.get("cst_id") else False
726
+ return {
727
+ "pis_cst_id": cst_id,
728
+ "pis_base_type": tax_dict.get("base_type"),
729
+ "pis_base": tax_dict.get("base", 0.00),
730
+ "pis_percent": tax_dict.get("percent_amount", 0.00),
731
+ "pis_reduction": tax_dict.get("percent_reduction", 0.00),
732
+ "pis_value": tax_dict.get("tax_value", 0.00),
733
+ }
734
+
735
+ def _prepare_fields_pis_wh(self, tax_dict):
736
+ self.ensure_one()
737
+ return {
738
+ "pis_wh_base_type": tax_dict.get("base_type"),
739
+ "pis_wh_base": tax_dict.get("base", 0.00),
740
+ "pis_wh_percent": tax_dict.get("percent_amount", 0.00),
741
+ "pis_wh_reduction": tax_dict.get("percent_reduction", 0.00),
742
+ "pis_wh_value": tax_dict.get("tax_value", 0.00),
743
+ }
744
+
745
+ def _prepare_fields_pisst(self, tax_dict):
746
+ self.ensure_one()
747
+ cst_id = tax_dict.get("cst_id").id if tax_dict.get("cst_id") else False
748
+ return {
749
+ "pisst_cst_id": cst_id,
750
+ "pisst_base_type": tax_dict.get("base_type"),
751
+ "pisst_base": tax_dict.get("base", 0.00),
752
+ "pisst_percent": tax_dict.get("percent_amount", 0.00),
753
+ "pisst_reduction": tax_dict.get("percent_reduction", 0.00),
754
+ "pisst_value": tax_dict.get("tax_value", 0.00),
755
+ }
756
+
757
+ def _prepare_fields_cofins(self, tax_dict):
758
+ self.ensure_one()
759
+ cst_id = tax_dict.get("cst_id").id if tax_dict.get("cst_id") else False
760
+ return {
761
+ "cofins_cst_id": cst_id,
762
+ "cofins_base_type": tax_dict.get("base_type"),
763
+ "cofins_base": tax_dict.get("base", 0.00),
764
+ "cofins_percent": tax_dict.get("percent_amount", 0.00),
765
+ "cofins_reduction": tax_dict.get("percent_reduction", 0.00),
766
+ "cofins_value": tax_dict.get("tax_value", 0.00),
767
+ }
768
+
769
+ def _prepare_fields_cofins_wh(self, tax_dict):
770
+ self.ensure_one()
771
+ return {
772
+ "cofins_wh_base_type": tax_dict.get("base_type"),
773
+ "cofins_wh_base": tax_dict.get("base", 0.00),
774
+ "cofins_wh_percent": tax_dict.get("percent_amount", 0.00),
775
+ "cofins_wh_reduction": tax_dict.get("percent_reduction", 0.00),
776
+ "cofins_wh_value": tax_dict.get("tax_value", 0.00),
777
+ }
778
+
779
+ def _prepare_fields_cofinsst(self, tax_dict):
780
+ self.ensure_one()
781
+ cst_id = tax_dict.get("cst_id").id if tax_dict.get("cst_id") else False
782
+ return {
783
+ "cofinsst_cst_id": cst_id,
784
+ "cofinsst_base_type": tax_dict.get("base_type"),
785
+ "cofinsst_base": tax_dict.get("base", 0.00),
786
+ "cofinsst_percent": tax_dict.get("percent_amount", 0.00),
787
+ "cofinsst_reduction": tax_dict.get("percent_reduction", 0.00),
788
+ "cofinsst_value": tax_dict.get("tax_value", 0.00),
789
+ }
790
+
791
+ @api.onchange(*FISCAL_TAX_ID_FIELDS)
792
+ def _onchange_fiscal_taxes(self):
793
+ taxes = self.env["l10n_br_fiscal.tax"]
794
+ for fiscal_tax_field in FISCAL_TAX_ID_FIELDS:
795
+ taxes |= self[fiscal_tax_field]
796
+
797
+ for line in self:
798
+ taxes_groups = line.fiscal_tax_ids.mapped("tax_domain")
799
+ fiscal_taxes = line.fiscal_tax_ids.filtered(
800
+ lambda ft, taxes_groups=taxes_groups: ft.tax_domain not in taxes_groups
801
+ )
802
+ line.fiscal_tax_ids = fiscal_taxes + taxes
803
+
804
+ @api.depends("product_id", "uom_id")
805
+ def _compute_uot_id(self):
806
+ for line in self:
807
+ if line.uom_id:
808
+ line.uot_id = line.uom_id.id
809
+ elif line.product_id:
810
+ if line.product_id.uot_id:
811
+ line.uot_id = line.product_id.uot_id.id
812
+ else:
813
+ line.uot_id = line.product_id.uom_id.id
814
+ else:
815
+ line.uot_id = False
816
+
817
+ @api.onchange("price_unit")
818
+ def _onchange_price_unit_fiscal(self):
819
+ self.fiscal_price = 0
820
+ self._compute_fiscal_price()
821
+
822
+ @api.depends("price_unit")
823
+ def _compute_fiscal_price(self):
824
+ for line in self:
825
+ # this test and the onchange are required to avoid
826
+ # resetting manual changes in fiscal_price
827
+ if not line.fiscal_price:
828
+ if line.product_id and line.price_unit:
829
+ line.fiscal_price = line.price_unit / (
830
+ line.product_id.uot_factor or 1.0
831
+ )
832
+ else:
833
+ line.fiscal_price = line.price_unit
834
+
835
+ @api.onchange("quantity")
836
+ def _onchange_quantity_fiscal(self):
837
+ self.fiscal_quantity = 0
838
+ self._compute_fiscal_quantity()
839
+
840
+ @api.depends("quantity")
841
+ def _compute_fiscal_quantity(self):
842
+ for line in self:
843
+ # this test and the onchange are required to avoid
844
+ # resetting manual changes in fiscal_quantity
845
+ if not line.fiscal_quantity:
846
+ if line.product_id and line.quantity:
847
+ line.fiscal_quantity = line.quantity * (
848
+ line.product_id.uot_factor or 1.0
849
+ )
850
+ else:
851
+ line.fiscal_quantity = line.quantity
852
+
853
+ @api.onchange("city_taxation_code_id")
854
+ def _onchange_city_taxation_code_id(self):
855
+ if self.city_taxation_code_id:
856
+ self.cnae_id = self.city_taxation_code_id.cnae_id
857
+ self._onchange_fiscal_operation_id()
858
+ if self.city_taxation_code_id.city_id:
859
+ self.update({"issqn_fg_city_id": self.city_taxation_code_id.city_id})
860
+
861
+ @api.model
862
+ def _add_fields_to_amount(self):
863
+ fields_to_amount = ["insurance_value", "other_value", "freight_value"]
864
+ if (
865
+ self.cfop_id.destination == CFOP_DESTINATION_EXPORT
866
+ and self.fiscal_operation_id.fiscal_operation_type == FISCAL_IN
867
+ ):
868
+ fields_to_amount.append("pis_value")
869
+ fields_to_amount.append("cofins_value")
870
+ fields_to_amount.append("icms_value")
871
+ fields_to_amount.append("ii_value")
872
+ fields_to_amount.append("ii_customhouse_charges")
873
+ return fields_to_amount
874
+
875
+ @api.model
876
+ def _rm_fields_to_amount(self):
877
+ return ["icms_relief_value"]
878
+
879
+ def _is_imported(self):
880
+ # When the mixin is used for instance
881
+ # in a PO line or SO line, there is no document_id
882
+ # and we consider the document is not imported
883
+ return hasattr(self, "document_id") and self.document_id.imported_document