odoo-addon-l10n-br-fiscal 16.0.8.0.2__py3-none-any.whl → 16.0.19.4.0__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 (115) hide show
  1. odoo/addons/l10n_br_fiscal/README.rst +1 -1
  2. odoo/addons/l10n_br_fiscal/__manifest__.py +10 -3
  3. odoo/addons/l10n_br_fiscal/constants/fiscal.py +64 -18
  4. odoo/addons/l10n_br_fiscal/data/l10n_br_fiscal.cest.csv +1043 -983
  5. odoo/addons/l10n_br_fiscal/data/l10n_br_fiscal.cst.csv +58 -0
  6. odoo/addons/l10n_br_fiscal/data/l10n_br_fiscal.document.type.csv +1 -0
  7. odoo/addons/l10n_br_fiscal/data/l10n_br_fiscal.operation.indicator.csv +27 -0
  8. odoo/addons/l10n_br_fiscal/data/l10n_br_fiscal.tax.classification.csv +163 -0
  9. odoo/addons/l10n_br_fiscal/data/l10n_br_fiscal.tax.csv +31 -0
  10. odoo/addons/l10n_br_fiscal/data/l10n_br_fiscal.tax.group.csv +3 -0
  11. odoo/addons/l10n_br_fiscal/data/operation_data.xml +1 -1
  12. odoo/addons/l10n_br_fiscal/data/uom_data.xml +186 -33
  13. odoo/addons/l10n_br_fiscal/demo/fiscal_document_demo.xml +3 -377
  14. odoo/addons/l10n_br_fiscal/demo/fiscal_document_nfse_demo.xml +0 -12
  15. odoo/addons/l10n_br_fiscal/demo/fiscal_operation_demo.xml +2 -2
  16. odoo/addons/l10n_br_fiscal/i18n/l10n_br_fiscal.pot +902 -304
  17. odoo/addons/l10n_br_fiscal/i18n/pt_BR.po +22 -22
  18. odoo/addons/l10n_br_fiscal/migrations/16.0.13.0.0/pre-migration.py +25 -0
  19. odoo/addons/l10n_br_fiscal/migrations/16.0.14.0.0/pre-migration.py +30 -0
  20. odoo/addons/l10n_br_fiscal/migrations/16.0.14.0.5/pre-migration.py +15 -0
  21. odoo/addons/l10n_br_fiscal/models/__init__.py +3 -2
  22. odoo/addons/l10n_br_fiscal/models/comment.py +2 -2
  23. odoo/addons/l10n_br_fiscal/models/data_ncm_nbs_abstract.py +1 -1
  24. odoo/addons/l10n_br_fiscal/models/document.py +83 -226
  25. odoo/addons/l10n_br_fiscal/models/document_line.py +67 -5
  26. odoo/addons/l10n_br_fiscal/models/document_line_mixin.py +1932 -136
  27. odoo/addons/l10n_br_fiscal/models/document_mixin.py +248 -17
  28. odoo/addons/l10n_br_fiscal/models/document_related.py +11 -8
  29. odoo/addons/l10n_br_fiscal/models/document_serie.py +33 -0
  30. odoo/addons/l10n_br_fiscal/models/ibpt.py +1 -1
  31. odoo/addons/l10n_br_fiscal/models/icms_regulation.py +1 -1
  32. odoo/addons/l10n_br_fiscal/models/invalidate_number.py +4 -5
  33. odoo/addons/l10n_br_fiscal/models/operation_dashboard.py +3 -2
  34. odoo/addons/l10n_br_fiscal/models/operation_indicator.py +58 -0
  35. odoo/addons/l10n_br_fiscal/models/operation_line.py +28 -0
  36. odoo/addons/l10n_br_fiscal/models/partner_profile.py +6 -0
  37. odoo/addons/l10n_br_fiscal/models/product_template.py +5 -1
  38. odoo/addons/l10n_br_fiscal/models/res_company.py +18 -0
  39. odoo/addons/l10n_br_fiscal/models/res_partner.py +27 -6
  40. odoo/addons/l10n_br_fiscal/models/simplified_tax_range.py +8 -0
  41. odoo/addons/l10n_br_fiscal/models/tax.py +7 -3
  42. odoo/addons/l10n_br_fiscal/models/tax_classification.py +81 -0
  43. odoo/addons/l10n_br_fiscal/models/tax_pis_cofins_base.py +1 -1
  44. odoo/addons/l10n_br_fiscal/models/tax_pis_cofins_credit.py +1 -1
  45. odoo/addons/l10n_br_fiscal/models/uom_uom.py +24 -0
  46. odoo/addons/l10n_br_fiscal/security/fiscal_security.xml +6 -16
  47. odoo/addons/l10n_br_fiscal/security/ir.model.access.csv +8 -2
  48. odoo/addons/l10n_br_fiscal/static/description/index.html +1 -1
  49. odoo/addons/l10n_br_fiscal/tests/__init__.py +3 -0
  50. odoo/addons/l10n_br_fiscal/tests/test_document_edition.py +308 -0
  51. odoo/addons/l10n_br_fiscal/tests/test_fiscal_document_generic.py +23 -111
  52. odoo/addons/l10n_br_fiscal/tests/test_fiscal_document_nfse.py +5 -13
  53. odoo/addons/l10n_br_fiscal/tests/test_fiscal_document_serie.py +60 -0
  54. odoo/addons/l10n_br_fiscal/tests/test_ibpt.py +2 -2
  55. odoo/addons/l10n_br_fiscal/tests/test_icms_regulation.py +2 -2
  56. odoo/addons/l10n_br_fiscal/tests/test_tax_benefit.py +14 -20
  57. odoo/addons/l10n_br_fiscal/tests/test_tax_classification.py +110 -0
  58. odoo/addons/l10n_br_fiscal/tools.py +1 -1
  59. odoo/addons/l10n_br_fiscal/views/cest_view.xml +2 -4
  60. odoo/addons/l10n_br_fiscal/views/cfop_view.xml +3 -5
  61. odoo/addons/l10n_br_fiscal/views/city_taxation_code.xml +1 -4
  62. odoo/addons/l10n_br_fiscal/views/cnae_view.xml +2 -4
  63. odoo/addons/l10n_br_fiscal/views/comment_view.xml +2 -4
  64. odoo/addons/l10n_br_fiscal/views/cst_view.xml +6 -8
  65. odoo/addons/l10n_br_fiscal/views/{document_fiscal_line_mixin_view.xml → document_line_mixin_view.xml} +525 -388
  66. odoo/addons/l10n_br_fiscal/views/document_line_view.xml +101 -82
  67. odoo/addons/l10n_br_fiscal/views/document_related_view.xml +44 -46
  68. odoo/addons/l10n_br_fiscal/views/document_serie_view.xml +2 -6
  69. odoo/addons/l10n_br_fiscal/views/document_type_view.xml +0 -2
  70. odoo/addons/l10n_br_fiscal/views/document_view.xml +304 -346
  71. odoo/addons/l10n_br_fiscal/views/icms_regulation_view.xml +14 -16
  72. odoo/addons/l10n_br_fiscal/views/icms_relief_view.xml +8 -10
  73. odoo/addons/l10n_br_fiscal/views/invalidate_number_view.xml +46 -48
  74. odoo/addons/l10n_br_fiscal/views/l10n_br_fiscal_action.xml +162 -244
  75. odoo/addons/l10n_br_fiscal/views/l10n_br_fiscal_menu.xml +16 -72
  76. odoo/addons/l10n_br_fiscal/views/legal_nature_view.xml +0 -2
  77. odoo/addons/l10n_br_fiscal/views/nbm_view.xml +5 -6
  78. odoo/addons/l10n_br_fiscal/views/nbs_view.xml +5 -6
  79. odoo/addons/l10n_br_fiscal/views/ncm_view.xml +12 -15
  80. odoo/addons/l10n_br_fiscal/views/operation_dashboard_view.xml +13 -12
  81. odoo/addons/l10n_br_fiscal/views/operation_indicator_view.xml +75 -0
  82. odoo/addons/l10n_br_fiscal/views/operation_line_view.xml +22 -21
  83. odoo/addons/l10n_br_fiscal/views/operation_view.xml +3 -6
  84. odoo/addons/l10n_br_fiscal/views/partner_profile_view.xml +3 -6
  85. odoo/addons/l10n_br_fiscal/views/product_genre_view.xml +7 -9
  86. odoo/addons/l10n_br_fiscal/views/product_product_view.xml +37 -14
  87. odoo/addons/l10n_br_fiscal/views/product_template_view.xml +34 -14
  88. odoo/addons/l10n_br_fiscal/views/res_company_view.xml +40 -38
  89. odoo/addons/l10n_br_fiscal/views/res_config_settings_view.xml +23 -28
  90. odoo/addons/l10n_br_fiscal/views/res_partner_view.xml +13 -2
  91. odoo/addons/l10n_br_fiscal/views/service_type_view.xml +7 -8
  92. odoo/addons/l10n_br_fiscal/views/simplified_tax_range_view.xml +0 -2
  93. odoo/addons/l10n_br_fiscal/views/simplified_tax_view.xml +0 -2
  94. odoo/addons/l10n_br_fiscal/views/tax_classification.xml +110 -0
  95. odoo/addons/l10n_br_fiscal/views/tax_definition_view.xml +157 -129
  96. odoo/addons/l10n_br_fiscal/views/tax_estimate_view.xml +0 -2
  97. odoo/addons/l10n_br_fiscal/views/tax_group_view.xml +3 -6
  98. odoo/addons/l10n_br_fiscal/views/tax_ipi_control_seal_view.xml +0 -2
  99. odoo/addons/l10n_br_fiscal/views/tax_ipi_guideline_class_view.xml +0 -2
  100. odoo/addons/l10n_br_fiscal/views/tax_ipi_guideline_view.xml +2 -4
  101. odoo/addons/l10n_br_fiscal/views/tax_pis_cofins_base_view.xml +2 -4
  102. odoo/addons/l10n_br_fiscal/views/tax_pis_cofins_credit_view.xml +2 -4
  103. odoo/addons/l10n_br_fiscal/views/tax_pis_cofins_view.xml +5 -7
  104. odoo/addons/l10n_br_fiscal/views/tax_view.xml +5 -7
  105. odoo/addons/l10n_br_fiscal/views/uom_uom.xml +52 -0
  106. odoo/addons/l10n_br_fiscal/wizards/__init__.py +1 -0
  107. odoo/addons/l10n_br_fiscal/wizards/base_wizard_mixin.py +1 -1
  108. odoo/addons/l10n_br_fiscal/wizards/document_import_wizard_mixin.py +129 -0
  109. odoo/addons/l10n_br_fiscal/wizards/document_import_wizard_mixin.xml +41 -0
  110. {odoo_addon_l10n_br_fiscal-16.0.8.0.2.dist-info → odoo_addon_l10n_br_fiscal-16.0.19.4.0.dist-info}/METADATA +3 -3
  111. {odoo_addon_l10n_br_fiscal-16.0.8.0.2.dist-info → odoo_addon_l10n_br_fiscal-16.0.19.4.0.dist-info}/RECORD +113 -99
  112. {odoo_addon_l10n_br_fiscal-16.0.8.0.2.dist-info → odoo_addon_l10n_br_fiscal-16.0.19.4.0.dist-info}/WHEEL +1 -1
  113. odoo/addons/l10n_br_fiscal/models/document_line_mixin_methods.py +0 -814
  114. odoo/addons/l10n_br_fiscal/models/document_mixin_methods.py +0 -363
  115. {odoo_addon_l10n_br_fiscal-16.0.8.0.2.dist-info → odoo_addon_l10n_br_fiscal-16.0.19.4.0.dist-info}/top_level.txt +0 -0
@@ -8,7 +8,6 @@ from ..constants.fiscal import (
8
8
  DOCUMENT_ISSUER,
9
9
  DOCUMENT_ISSUER_COMPANY,
10
10
  FINAL_CUSTOMER,
11
- FINAL_CUSTOMER_YES,
12
11
  FISCAL_COMMENT_DOCUMENT,
13
12
  NFE_IND_PRES,
14
13
  NFE_IND_PRES_DEFAULT,
@@ -33,7 +32,6 @@ class FiscalDocumentMixin(models.AbstractModel):
33
32
  - Inverse methods for distributing header-level costs (freight, insurance)
34
33
  to lines.
35
34
  - Hooks for customizing data retrieval (e.g., lines, fiscal partner).
36
- - Onchange helpers for common fiscal fields.
37
35
 
38
36
  Models using this mixin are often expected to also include fields defined
39
37
  in `l10n_br_fiscal.document.mixin` for methods like
@@ -43,16 +41,11 @@ class FiscalDocumentMixin(models.AbstractModel):
43
41
  """
44
42
 
45
43
  _name = "l10n_br_fiscal.document.mixin"
46
- _inherit = "l10n_br_fiscal.document.mixin.methods"
47
44
  _description = "Document Fiscal Mixin Fields"
48
45
 
49
46
  def _date_server_format(self):
50
47
  return fields.Datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT)
51
48
 
52
- @api.model
53
- def _default_operation(self):
54
- return False
55
-
56
49
  @api.model
57
50
  def _operation_domain(self):
58
51
  domain = (
@@ -63,11 +56,222 @@ class FiscalDocumentMixin(models.AbstractModel):
63
56
  )
64
57
  return domain
65
58
 
59
+ def _prepare_br_fiscal_dict(self, default=False):
60
+ self.ensure_one()
61
+ fields = self.env["l10n_br_fiscal.document.mixin"]._fields.keys()
62
+
63
+ # we now read the record fiscal fields except the m2m tax:
64
+ vals = self._convert_to_write(self.read(fields)[0])
65
+
66
+ # remove id field to avoid conflicts
67
+ vals.pop("id", None)
68
+
69
+ if default: # in case you want to use new rather than write later
70
+ return {f"default_{k}": vals[k] for k in vals.keys()}
71
+ return vals
72
+
73
+ @api.onchange("document_type_id")
74
+ def _onchange_document_type_id(self):
75
+ if self.document_type_id and self.issuer == DOCUMENT_ISSUER_COMPANY:
76
+ self.document_serie_id = self.document_type_id.get_document_serie(
77
+ self.company_id, self.fiscal_operation_id
78
+ )
79
+
80
+ @api.depends("fiscal_operation_id")
81
+ def _compute_document_type_id(self):
82
+ for doc in self.filtered(lambda doc: doc.fiscal_operation_id):
83
+ if doc.issuer == DOCUMENT_ISSUER_COMPANY and not doc.document_type_id:
84
+ doc.document_type_id = doc.company_id.document_type_id
85
+
86
+ def _get_amount_lines(self):
87
+ """Get object lines instances used to compute fiscal fields"""
88
+ return self.mapped(self._get_fiscal_lines_field_name())
89
+
90
+ def _get_product_amount_lines(self):
91
+ fiscal_line_ids = self._get_amount_lines()
92
+ return fiscal_line_ids.filtered(lambda line: line.product_id.type != "service")
93
+
94
+ @api.model
95
+ def _get_amount_fields(self):
96
+ """Get all fields with 'amount_' prefix"""
97
+ fields = self.env["l10n_br_fiscal.document.mixin"]._fields.keys()
98
+ prefixes = ("amount_", "fiscal_amount_")
99
+ amount_fields = [f for f in fields if f.startswith(prefixes)]
100
+ return amount_fields
101
+
102
+ @api.depends("document_serie_id", "issuer")
103
+ def _compute_document_serie(self):
104
+ for doc in self:
105
+ if doc.document_serie_id and doc.issuer == DOCUMENT_ISSUER_COMPANY:
106
+ doc.document_serie = doc.document_serie_id.code
107
+ elif doc.document_serie is None:
108
+ doc.document_serie = False
109
+
110
+ @api.depends("document_type_id", "issuer")
111
+ def _compute_document_serie_id(self):
112
+ for doc in self:
113
+ if (
114
+ not doc.document_serie_id
115
+ and doc.document_type_id
116
+ and doc.issuer == DOCUMENT_ISSUER_COMPANY
117
+ ):
118
+ doc.document_serie_id = doc.document_type_id.get_document_serie(
119
+ doc.company_id, doc.fiscal_operation_id
120
+ )
121
+ elif doc.document_serie_id is None:
122
+ doc.document_serie_id = False
123
+
124
+ @api.model
125
+ def _get_fiscal_lines_field_name(self):
126
+ return "fiscal_line_ids"
127
+
128
+ def _get_fiscal_amount_field_dependencies(self):
129
+ """
130
+ Dynamically get the list of field dependencies.
131
+ """
132
+ if self._abstract:
133
+ return []
134
+ o2m_field_name = self._get_fiscal_lines_field_name()
135
+ target_fields = []
136
+ for field in self._get_amount_fields():
137
+ if (
138
+ field.replace("amount_", "")
139
+ in getattr(self, o2m_field_name)._fields.keys()
140
+ ):
141
+ target_fields.append(field.replace("amount_", ""))
142
+
143
+ return [o2m_field_name] + [
144
+ f"{o2m_field_name}.{target_field}" for target_field in target_fields
145
+ ]
146
+
147
+ @api.depends(lambda self: self._get_fiscal_amount_field_dependencies())
148
+ def _compute_fiscal_amount(self):
149
+ """
150
+ Compute and sum various fiscal amounts from the document lines.
151
+
152
+ This method iterates over fields prefixed with 'amount_' (as determined
153
+ by `_get_amount_fields`) and sums corresponding values from the lines
154
+ retrieved by `_get_amount_lines`.
155
+
156
+ It handles cases where delivery costs (freight, insurance, other) are
157
+ defined at the document total level rather than per line.
158
+ """
159
+
160
+ fields = self._get_amount_fields()
161
+ for doc in self.filtered(lambda m: m.fiscal_operation_id):
162
+ values = {key: 0.0 for key in fields}
163
+ for line in doc._get_amount_lines():
164
+ for field in fields:
165
+ if field in line._fields.keys():
166
+ values[field] += line[field]
167
+ if field.replace("amount_", "") in line._fields.keys():
168
+ # FIXME this field creates an error in invoice form
169
+ if field == "amount_financial_discount_value":
170
+ values["amount_financial_discount_value"] += (
171
+ 0 # line.financial_discount_value
172
+ )
173
+ else:
174
+ values[field] += line[field.replace("amount_", "")]
175
+
176
+ # Valores definidos pelo Total e não pela Linha
177
+ if (
178
+ doc.company_id.delivery_costs == "total"
179
+ or doc.force_compute_delivery_costs_by_total
180
+ ):
181
+ values["amount_freight_value"] = doc.amount_freight_value
182
+ values["amount_insurance_value"] = doc.amount_insurance_value
183
+ values["amount_other_value"] = doc.amount_other_value
184
+
185
+ doc.update(values)
186
+
187
+ def _get_fiscal_partner(self):
188
+ """
189
+ Hook method to determine the fiscal partner for the document.
190
+
191
+ This method is designed to be overridden in implementing models if the
192
+ partner relevant for fiscal purposes (e.g., for tax calculations,
193
+ final consumer status) is different from the main `partner_id`
194
+ of the document record. For instance, an invoice might use a specific
195
+ invoicing contact derived from the main partner.
196
+
197
+ :return: A `res.partner` recordset representing the fiscal partner.
198
+ """
199
+
200
+ self.ensure_one()
201
+ return self.partner_id
202
+
203
+ @api.depends("partner_id")
204
+ def _compute_ind_final(self):
205
+ for doc in self:
206
+ partner = doc._get_fiscal_partner()
207
+ if partner:
208
+ doc.ind_final = partner.ind_final
209
+ else:
210
+ # Default Value
211
+ doc.ind_final = "1" # Yes
212
+
213
+ @api.onchange("ind_final")
214
+ def _inverse_ind_final(self):
215
+ for doc in self:
216
+ for line in doc._get_amount_lines():
217
+ if line.ind_final != doc.ind_final:
218
+ line.ind_final = doc.ind_final
219
+
220
+ @api.depends("fiscal_operation_id")
221
+ def _compute_operation_name(self):
222
+ for doc in self:
223
+ if doc.fiscal_operation_id:
224
+ doc.operation_name = doc.fiscal_operation_id.name
225
+ else:
226
+ doc.operation_name = False
227
+
228
+ @api.depends("fiscal_operation_id")
229
+ def _compute_comment_ids(self):
230
+ for doc in self:
231
+ if doc.fiscal_operation_id:
232
+ doc.comment_ids = doc.fiscal_operation_id.comment_ids
233
+ elif doc.comment_ids is None:
234
+ doc.comment_ids = []
235
+
236
+ def _distribute_amount_to_lines(self, amount_field_name, line_field_name):
237
+ for record in self:
238
+ if not (
239
+ record.delivery_costs == "total"
240
+ or record.force_compute_delivery_costs_by_total
241
+ ):
242
+ continue
243
+ lines = record._get_product_amount_lines()
244
+ if not lines:
245
+ continue
246
+ amount_to_distribute = record[amount_field_name]
247
+ total_gross = sum(lines.mapped("price_gross"))
248
+ if total_gross > 0:
249
+ distributed_amount = 0
250
+ for line in lines[:-1]:
251
+ proportional_amount = record.currency_id.round(
252
+ amount_to_distribute * (line.price_gross / total_gross)
253
+ )
254
+ line[line_field_name] = proportional_amount
255
+ distributed_amount += proportional_amount
256
+ lines[-1][line_field_name] = amount_to_distribute - distributed_amount
257
+ else:
258
+ lines.write({line_field_name: 0.0})
259
+ if lines:
260
+ lines[0][line_field_name] = amount_to_distribute
261
+
262
+ def _inverse_amount_freight(self):
263
+ self._distribute_amount_to_lines("amount_freight_value", "freight_value")
264
+
265
+ def _inverse_amount_insurance(self):
266
+ self._distribute_amount_to_lines("amount_insurance_value", "insurance_value")
267
+
268
+ def _inverse_amount_other(self):
269
+ self._distribute_amount_to_lines("amount_other_value", "other_value")
270
+
66
271
  fiscal_operation_id = fields.Many2one(
67
272
  comodel_name="l10n_br_fiscal.operation",
68
273
  string="Operation",
69
274
  domain=lambda self: self._operation_domain(),
70
- default=_default_operation,
71
275
  )
72
276
 
73
277
  operation_name = fields.Char(
@@ -90,7 +294,6 @@ class FiscalDocumentMixin(models.AbstractModel):
90
294
 
91
295
  fiscal_operation_type = fields.Selection(
92
296
  related="fiscal_operation_id.fiscal_operation_type",
93
- readonly=True,
94
297
  )
95
298
 
96
299
  ind_pres = fields.Selection(
@@ -107,14 +310,10 @@ class FiscalDocumentMixin(models.AbstractModel):
107
310
  store=True,
108
311
  )
109
312
 
110
- fiscal_additional_data = fields.Text()
111
-
112
313
  manual_fiscal_additional_data = fields.Text(
113
314
  help="Fiscal Additional data manually entered by user",
114
315
  )
115
316
 
116
- customer_additional_data = fields.Text()
117
-
118
317
  manual_customer_additional_data = fields.Text(
119
318
  help="Customer Additional data manually entered by user",
120
319
  )
@@ -122,7 +321,11 @@ class FiscalDocumentMixin(models.AbstractModel):
122
321
  ind_final = fields.Selection(
123
322
  selection=FINAL_CUSTOMER,
124
323
  string="Final Consumption Operation",
125
- default=FINAL_CUSTOMER_YES,
324
+ compute="_compute_ind_final",
325
+ inverse="_inverse_ind_final",
326
+ store=True,
327
+ precompute=True,
328
+ readonly=False,
126
329
  )
127
330
 
128
331
  currency_id = fields.Many2one(
@@ -137,7 +340,31 @@ class FiscalDocumentMixin(models.AbstractModel):
137
340
  help="Amount without discount.",
138
341
  )
139
342
 
140
- amount_untaxed = fields.Monetary(
343
+ fiscal_amount_untaxed = fields.Monetary(
344
+ compute="_compute_fiscal_amount",
345
+ store=True,
346
+ )
347
+
348
+ amount_ibs_base = fields.Monetary(
349
+ string="IBS Base",
350
+ compute="_compute_fiscal_amount",
351
+ store=True,
352
+ )
353
+
354
+ amount_ibs_value = fields.Monetary(
355
+ string="IBS Value",
356
+ compute="_compute_fiscal_amount",
357
+ store=True,
358
+ )
359
+
360
+ amount_cbs_base = fields.Monetary(
361
+ string="CBS Base",
362
+ compute="_compute_fiscal_amount",
363
+ store=True,
364
+ )
365
+
366
+ amount_cbs_value = fields.Monetary(
367
+ string="CBS Value",
141
368
  compute="_compute_fiscal_amount",
142
369
  store=True,
143
370
  )
@@ -405,12 +632,12 @@ class FiscalDocumentMixin(models.AbstractModel):
405
632
  store=True,
406
633
  )
407
634
 
408
- amount_tax = fields.Monetary(
635
+ fiscal_amount_tax = fields.Monetary(
409
636
  compute="_compute_fiscal_amount",
410
637
  store=True,
411
638
  )
412
639
 
413
- amount_total = fields.Monetary(
640
+ fiscal_amount_total = fields.Monetary(
414
641
  compute="_compute_fiscal_amount",
415
642
  store=True,
416
643
  )
@@ -481,6 +708,10 @@ class FiscalDocumentMixin(models.AbstractModel):
481
708
 
482
709
  document_type_id = fields.Many2one(
483
710
  comodel_name="l10n_br_fiscal.document.type",
711
+ compute="_compute_document_type_id",
712
+ store=True,
713
+ precompute=True,
714
+ readonly=False,
484
715
  )
485
716
 
486
717
  document_serie_id = fields.Many2one(
@@ -56,7 +56,7 @@ class DocumentRelated(models.Model):
56
56
  default="cnpj",
57
57
  )
58
58
 
59
- inscr_est = fields.Char(string="Inscr. Estadual/RG", size=16)
59
+ l10n_br_ie_code = fields.Char(string="Inscr. Estadual/RG", size=16)
60
60
 
61
61
  document_date = fields.Date(string="Data")
62
62
 
@@ -87,11 +87,14 @@ class DocumentRelated(models.Model):
87
87
  for record in self:
88
88
  check_cnpj_cpf(record.env, record.cnpj_cpf, self.env.ref("base.br"))
89
89
 
90
- @api.constrains("inscr_est", "state_id")
90
+ @api.constrains("l10n_br_ie_code", "state_id")
91
91
  def _check_ie(self):
92
92
  for record in self:
93
93
  check_ie(
94
- record.env, record.inscr_est, record.state_id, self.env.ref("base.br")
94
+ record.env,
95
+ record.l10n_br_ie_code,
96
+ record.state_id,
97
+ self.env.ref("base.br"),
95
98
  )
96
99
 
97
100
  @api.onchange("document_related_id")
@@ -101,7 +104,7 @@ class DocumentRelated(models.Model):
101
104
  return False
102
105
 
103
106
  self.document_type_id = related.document_type_id
104
- self.document_total_amount = related.amount_total
107
+ self.document_total_amount = related.fiscal_amount_total
105
108
  self.document_total_weight = related.total_weight
106
109
 
107
110
  if related.document_type_id.electronic:
@@ -112,7 +115,7 @@ class DocumentRelated(models.Model):
112
115
  self.cnpj_cpf = False
113
116
  self.cpfcnpj_type = False
114
117
  self.document_date = False
115
- self.inscr_est = False
118
+ self.l10n_br_ie_code = False
116
119
 
117
120
  if related.document_type_id.code in ("01", "04"):
118
121
  self.document_key = False
@@ -125,7 +128,7 @@ class DocumentRelated(models.Model):
125
128
  or False
126
129
  )
127
130
 
128
- self.cnpj_cpf = related.partner_id and related.partner_id.cnpj_cpf or False
131
+ self.cnpj_cpf = related.partner_id and related.partner_id.vat or False
129
132
 
130
133
  if related.partner_id.is_company:
131
134
  self.cpfcnpj_type = "cnpj"
@@ -135,8 +138,8 @@ class DocumentRelated(models.Model):
135
138
  self.document_date = related.document_date
136
139
 
137
140
  if related.document_type_id.code == "04":
138
- self.inscr_est = (
139
- related.partner_id and related.partner_id.inscr_est or False
141
+ self.l10n_br_ie_code = (
142
+ related.partner_id and related.partner_id.l10n_br_ie_code or False
140
143
  )
141
144
 
142
145
  @api.onchange("cnpj_cpf", "cpfcnpj_type")
@@ -3,11 +3,13 @@
3
3
  # License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
4
4
 
5
5
  from odoo import _, api, fields, models
6
+ from odoo.exceptions import ValidationError
6
7
 
7
8
  from ..constants.fiscal import (
8
9
  DOCUMENT_ISSUER_COMPANY,
9
10
  FISCAL_IN_OUT,
10
11
  FISCAL_IN_OUT_DEFAULT,
12
+ SITUACAO_EDOC_EM_DIGITACAO,
11
13
  )
12
14
 
13
15
 
@@ -55,6 +57,14 @@ class DocumentSerie(models.Model):
55
57
  string="Invalidate Number Range",
56
58
  )
57
59
 
60
+ _sql_constraints = [
61
+ (
62
+ "document_serie_unique",
63
+ "unique(code, document_type_id, company_id)",
64
+ "A Fiscal Document Serie already exists for this document type.",
65
+ )
66
+ ]
67
+
58
68
  @api.model
59
69
  def _create_sequence(self, values):
60
70
  """Create new no_gap entry sequence for every
@@ -78,6 +88,29 @@ class DocumentSerie(models.Model):
78
88
  vals.update({"internal_sequence_id": self._create_sequence(vals)})
79
89
  return super().create(vals_list)
80
90
 
91
+ def write(self, vals):
92
+ if "internal_sequence_id" in vals:
93
+ raise ValidationError(_("You cannot change the internal sequence."))
94
+ if "code" in vals:
95
+ for serie in self:
96
+ if serie.code == vals["code"]:
97
+ continue
98
+ if self.env["l10n_br_fiscal.document"].search_count(
99
+ [
100
+ ("document_serie_id", "=", serie.id),
101
+ ("state_edoc", "not in", [SITUACAO_EDOC_EM_DIGITACAO]),
102
+ ],
103
+ limit=1,
104
+ ):
105
+ raise ValidationError(
106
+ _(
107
+ "You cannot change the code of a document "
108
+ "serie %(name)s that is already in use.",
109
+ name=serie.name,
110
+ )
111
+ )
112
+ return super().write(vals)
113
+
81
114
  def name_get(self):
82
115
  return [(r.id, f"{r.name}") for r in self]
83
116
 
@@ -45,7 +45,7 @@ def _request(ws_url, params, ibpt_request_timeout=30):
45
45
  elif response.status_code == requests.codes.service_unavailable:
46
46
  raise UserError(_("IBPT Service Unavailable - {!r}").format(ws_url))
47
47
  except Exception as e:
48
- raise UserError(_("Error in the request: {}").format(e)) from e
48
+ raise UserError(f"Error in the request: {e}") from e
49
49
 
50
50
 
51
51
  def get_ibpt_product(
@@ -2101,7 +2101,7 @@ class ICMSRegulation(models.Model):
2101
2101
  company.state_id != partner.state_id
2102
2102
  and partner.ind_ie_dest == NFE_IND_IE_DEST_9
2103
2103
  and operation_line.fiscal_operation_type == FISCAL_OUT
2104
- or operation_line.fiscal_operation_id.fiscal_type == "return_in"
2104
+ or operation_line.fiscal_operation_id.fiscal_type != "return_in"
2105
2105
  and operation_line.fiscal_operation_type == FISCAL_IN
2106
2106
  ):
2107
2107
  domain = self._build_map_tax_def_domain(
@@ -105,11 +105,10 @@ class InvalidateNumber(models.Model):
105
105
  @api.depends("document_type_id", "document_serie_id", "number_start", "number_end")
106
106
  def _compute_name(self):
107
107
  for record in self:
108
- record.name = "{type}/({serie}): {start} - {end}".format(
109
- type=record.document_type_id.type,
110
- serie=record.document_serie_id.name,
111
- start=record.number_start,
112
- end=record.number_end,
108
+ record.name = (
109
+ f"{record.document_type_id.type}/"
110
+ f"({record.document_serie_id.name}): "
111
+ f"{record.number_start} - {record.number_end}"
113
112
  )
114
113
 
115
114
  def unlink(self):
@@ -127,7 +127,7 @@ class Operation(models.Model):
127
127
  }
128
128
 
129
129
  def open_action(self):
130
- """return action based on type for related journals"""
130
+ """Return action based on type for related journals"""
131
131
 
132
132
  _fiscal_type_map = {
133
133
  "purchase": "in",
@@ -157,7 +157,8 @@ class Operation(models.Model):
157
157
  }
158
158
  )
159
159
 
160
- [action] = self.env.ref("l10n_br_fiscal.%s" % action_name).read()
160
+ xmlid = f"l10n_br_fiscal.{action_name}"
161
+ [action] = self.env.ref(xmlid).read()
161
162
  action["context"] = ctx
162
163
  action["domain"] = self._context.get("use_domain", [])
163
164
  action["domain"] += [
@@ -0,0 +1,58 @@
1
+ # Copyright (C) 2025 Marcel Savegnago <https://escodoo.com.br>
2
+ # License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
3
+
4
+ from odoo import fields, models
5
+
6
+
7
+ class OperationIndicator(models.Model):
8
+ """Operation Indicator
9
+
10
+ This model stores the Operation Indicators (cIndOp) table according to
11
+ Annex VII of Technical Note No. 004/2025 from NFS-e Nacional, which is
12
+ part of the Brazilian Tax Reform (Reforma Tributária do Consumo).
13
+
14
+ The cIndOp field is used in the Service Provision Declaration (DPS)
15
+ to categorize consumption operations, as required by Art. 11 of
16
+ Complementary Law No. 214/2025.
17
+
18
+ This table will become mandatory from January 1, 2026.
19
+ """
20
+
21
+ _name = "l10n_br_fiscal.operation.indicator"
22
+ _inherit = "l10n_br_fiscal.data.abstract"
23
+ _description = "Operation Indicator"
24
+ _order = "code"
25
+
26
+ code = fields.Char(
27
+ string="Operation Indicator Code",
28
+ required=True,
29
+ index=True,
30
+ size=6,
31
+ help="Operation indicator code according to Annex VII of NT 004/2025 "
32
+ "(e.g., 020101, 030101)",
33
+ )
34
+
35
+ operation_type = fields.Text(
36
+ required=True,
37
+ help="Type of operation according to Art. 11 of Complementary Law "
38
+ "No. 214/2025",
39
+ )
40
+
41
+ operation_location = fields.Text(
42
+ string="Operation Location Consideration",
43
+ help="Where the operation is considered to take place according to "
44
+ "the legislation",
45
+ )
46
+
47
+ supply_characteristic = fields.Text(
48
+ help="Specific characteristic of the supply execution that determines "
49
+ "the place of supply",
50
+ )
51
+
52
+ supply_location = fields.Text(
53
+ string="DFe Supply Location",
54
+ required=True,
55
+ help="Place of supply to be identified in the Digital Fiscal Document "
56
+ "(DFe), such as supplier establishment, acquirer address, "
57
+ "recipient address, or other locations depending on the operation",
58
+ )
@@ -37,6 +37,11 @@ class OperationLine(models.Model):
37
37
 
38
38
  document_type_id = fields.Many2one(comodel_name="l10n_br_fiscal.document.type")
39
39
 
40
+ tax_classification_id = fields.Many2one(
41
+ comodel_name="l10n_br_fiscal.tax.classification",
42
+ string="Tax Classification",
43
+ )
44
+
40
45
  cfop_internal_id = fields.Many2one(
41
46
  comodel_name="l10n_br_fiscal.cfop",
42
47
  string="CFOP Internal",
@@ -167,6 +172,13 @@ class OperationLine(models.Model):
167
172
  cfop = self.cfop_export_id
168
173
  return cfop
169
174
 
175
+ def _get_tax_classification(self, company):
176
+ if self.tax_classification_id:
177
+ return self.tax_classification_id
178
+ elif company.tax_classification_id:
179
+ return company.tax_classification_id
180
+ return self.env["l10n_br_fiscal.tax.classification"]
181
+
170
182
  def _build_mapping_result_ipi(self, mapping_result, tax_definition):
171
183
  if tax_definition and tax_definition.ipi_guideline_id:
172
184
  mapping_result["ipi_guideline"] = tax_definition.ipi_guideline_id
@@ -246,12 +258,15 @@ class OperationLine(models.Model):
246
258
  (l10n_br_fiscal.tax.ipi.guideline).
247
259
  - 'icms_tax_benefit_id': The determined ICMS tax benefit record
248
260
  ID (l10n_br_fiscal.tax.definition) or False.
261
+ - 'tax_classification': The determined Tax Classification record
262
+ (l10n_br_fiscal.tax.classification).
249
263
  """
250
264
  mapping_result = {
251
265
  "taxes": {},
252
266
  "cfop": False,
253
267
  "ipi_guideline": self.env.ref("l10n_br_fiscal.tax_guideline_999"),
254
268
  "icms_tax_benefit_id": False,
269
+ "tax_classification": False,
255
270
  }
256
271
 
257
272
  self.ensure_one()
@@ -259,6 +274,9 @@ class OperationLine(models.Model):
259
274
  # Define CFOP
260
275
  mapping_result["cfop"] = self._get_cfop(company, partner)
261
276
 
277
+ # Define Tax Classification
278
+ mapping_result["tax_classification"] = self._get_tax_classification(company)
279
+
262
280
  # 1 Get Tax Defs from Company
263
281
  for tax_definition in company.tax_definition_ids.map_tax_definition(
264
282
  company,
@@ -273,6 +291,16 @@ class OperationLine(models.Model):
273
291
  ):
274
292
  self._build_mapping_result(mapping_result, tax_definition)
275
293
 
294
+ # 1_5 From Tax Classification
295
+ if mapping_result["tax_classification"]:
296
+ mapping_result["taxes"][
297
+ mapping_result["tax_classification"].tax_cbs_id.tax_domain
298
+ ] = mapping_result["tax_classification"].tax_cbs_id
299
+
300
+ mapping_result["taxes"][
301
+ mapping_result["tax_classification"].tax_ibs_id.tax_domain
302
+ ] = mapping_result["tax_classification"].tax_ibs_id
303
+
276
304
  # 2 From NCM
277
305
  if not ncm and product:
278
306
  ncm = product.ncm_id
@@ -8,6 +8,7 @@ from odoo.exceptions import ValidationError
8
8
  from ..constants.fiscal import (
9
9
  NFE_IND_IE_DEST,
10
10
  NFE_IND_IE_DEST_DEFAULT,
11
+ PUBLIC_ENTIRY_TYPE,
11
12
  TAX_FRAMEWORK,
12
13
  TAX_FRAMEWORK_NORMAL,
13
14
  )
@@ -32,6 +33,11 @@ class PartnerProfile(models.Model):
32
33
  "other government-controlled organizations.",
33
34
  )
34
35
 
36
+ public_entity_type = fields.Selection(
37
+ selection=PUBLIC_ENTIRY_TYPE,
38
+ string="Tipo de Entidade Governamental",
39
+ )
40
+
35
41
  default = fields.Boolean(string="Default Profile", default=True)
36
42
 
37
43
  ind_ie_dest = fields.Selection(