odoo-addon-l10n-br-fiscal 18.0.5.0.0.1__py3-none-any.whl → 18.0.7.1.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.

Potentially problematic release.


This version of odoo-addon-l10n-br-fiscal might be problematic. Click here for more details.

Files changed (54) hide show
  1. odoo/addons/l10n_br_fiscal/README.rst +1 -1
  2. odoo/addons/l10n_br_fiscal/__manifest__.py +6 -2
  3. odoo/addons/l10n_br_fiscal/constants/fiscal.py +18 -0
  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.operation.indicator.csv +27 -0
  7. odoo/addons/l10n_br_fiscal/data/l10n_br_fiscal.tax.classification.csv +163 -0
  8. odoo/addons/l10n_br_fiscal/data/l10n_br_fiscal.tax.csv +31 -0
  9. odoo/addons/l10n_br_fiscal/data/l10n_br_fiscal.tax.group.csv +3 -0
  10. odoo/addons/l10n_br_fiscal/i18n/l10n_br_fiscal.pot +523 -43
  11. odoo/addons/l10n_br_fiscal/i18n/pt_BR.po +523 -44
  12. odoo/addons/l10n_br_fiscal/models/__init__.py +2 -2
  13. odoo/addons/l10n_br_fiscal/models/data_abstract.py +9 -6
  14. odoo/addons/l10n_br_fiscal/models/document_line_mixin.py +993 -8
  15. odoo/addons/l10n_br_fiscal/models/document_mixin.py +236 -1
  16. odoo/addons/l10n_br_fiscal/models/operation_indicator.py +58 -0
  17. odoo/addons/l10n_br_fiscal/models/operation_line.py +28 -0
  18. odoo/addons/l10n_br_fiscal/models/product_template.py +4 -0
  19. odoo/addons/l10n_br_fiscal/models/res_company.py +17 -0
  20. odoo/addons/l10n_br_fiscal/models/res_partner.py +10 -0
  21. odoo/addons/l10n_br_fiscal/models/simplified_tax_range.py +8 -0
  22. odoo/addons/l10n_br_fiscal/models/tax_classification.py +81 -0
  23. odoo/addons/l10n_br_fiscal/security/ir.model.access.csv +7 -1
  24. odoo/addons/l10n_br_fiscal/static/description/index.html +1 -1
  25. odoo/addons/l10n_br_fiscal/tests/__init__.py +1 -0
  26. odoo/addons/l10n_br_fiscal/tests/test_tax_classification.py +110 -0
  27. odoo/addons/l10n_br_fiscal/views/document_line_mixin_view.xml +106 -4
  28. odoo/addons/l10n_br_fiscal/views/document_line_view.xml +4 -0
  29. odoo/addons/l10n_br_fiscal/views/document_view.xml +14 -0
  30. odoo/addons/l10n_br_fiscal/views/icms_regulation_view.xml +1 -5
  31. odoo/addons/l10n_br_fiscal/views/l10n_br_fiscal_action.xml +30 -0
  32. odoo/addons/l10n_br_fiscal/views/l10n_br_fiscal_menu.xml +16 -0
  33. odoo/addons/l10n_br_fiscal/views/nbs_view.xml +1 -5
  34. odoo/addons/l10n_br_fiscal/views/ncm_view.xml +1 -5
  35. odoo/addons/l10n_br_fiscal/views/operation_indicator_view.xml +75 -0
  36. odoo/addons/l10n_br_fiscal/views/operation_line_view.xml +4 -5
  37. odoo/addons/l10n_br_fiscal/views/operation_view.xml +1 -5
  38. odoo/addons/l10n_br_fiscal/views/product_template_view.xml +5 -0
  39. odoo/addons/l10n_br_fiscal/views/res_company_view.xml +6 -0
  40. odoo/addons/l10n_br_fiscal/views/res_partner_view.xml +4 -0
  41. odoo/addons/l10n_br_fiscal/views/service_type_view.xml +1 -5
  42. odoo/addons/l10n_br_fiscal/views/tax_classification.xml +108 -0
  43. odoo/addons/l10n_br_fiscal/views/tax_definition_view.xml +1 -5
  44. odoo/addons/l10n_br_fiscal/views/tax_view.xml +2 -2
  45. odoo/addons/l10n_br_fiscal/wizards/__init__.py +1 -1
  46. odoo/addons/l10n_br_fiscal/wizards/document_import_wizard.py +234 -0
  47. odoo/addons/l10n_br_fiscal/wizards/{document_import_wizard_mixin.xml → document_import_wizard.xml} +26 -7
  48. {odoo_addon_l10n_br_fiscal-18.0.5.0.0.1.dist-info → odoo_addon_l10n_br_fiscal-18.0.7.1.0.dist-info}/METADATA +2 -2
  49. {odoo_addon_l10n_br_fiscal-18.0.5.0.0.1.dist-info → odoo_addon_l10n_br_fiscal-18.0.7.1.0.dist-info}/RECORD +51 -46
  50. odoo/addons/l10n_br_fiscal/models/document_line_mixin_methods.py +0 -818
  51. odoo/addons/l10n_br_fiscal/models/document_mixin_methods.py +0 -247
  52. odoo/addons/l10n_br_fiscal/wizards/document_import_wizard_mixin.py +0 -129
  53. {odoo_addon_l10n_br_fiscal-18.0.5.0.0.1.dist-info → odoo_addon_l10n_br_fiscal-18.0.7.1.0.dist-info}/WHEEL +0 -0
  54. {odoo_addon_l10n_br_fiscal-18.0.5.0.0.1.dist-info → odoo_addon_l10n_br_fiscal-18.0.7.1.0.dist-info}/top_level.txt +0 -0
@@ -1,18 +1,29 @@
1
1
  # Copyright (C) 2019 Renato Lima - Akretion <renato.lima@akretion.com.br>
2
2
  # License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
3
3
 
4
- from odoo import api, fields, models
4
+ from copy import deepcopy
5
+
6
+ from lxml import etree
7
+ from lxml.builder import E
8
+
9
+ from odoo import Command, api, fields, models
5
10
 
6
11
  from ..constants.fiscal import (
12
+ CFOP_DESTINATION_EXPORT,
7
13
  FINAL_CUSTOMER,
8
14
  FISCAL_COMMENT_LINE,
15
+ FISCAL_IN,
16
+ FISCAL_TAX_ID_FIELDS,
9
17
  PRODUCT_FISCAL_TYPE,
10
18
  TAX_BASE_TYPE,
19
+ TAX_BASE_TYPE_PERCENT,
20
+ TAX_DOMAIN_CBS,
11
21
  TAX_DOMAIN_COFINS,
12
22
  TAX_DOMAIN_COFINS_ST,
13
23
  TAX_DOMAIN_COFINS_WH,
14
24
  TAX_DOMAIN_CSLL,
15
25
  TAX_DOMAIN_CSLL_WH,
26
+ TAX_DOMAIN_IBS,
16
27
  TAX_DOMAIN_ICMS,
17
28
  TAX_DOMAIN_ICMS_FCP,
18
29
  TAX_DOMAIN_ICMS_FCP_ST,
@@ -34,8 +45,11 @@ from ..constants.fiscal import (
34
45
  )
35
46
  from ..constants.icms import (
36
47
  ICMS_BASE_TYPE,
48
+ ICMS_BASE_TYPE_DEFAULT,
37
49
  ICMS_ORIGIN,
50
+ ICMS_ORIGIN_DEFAULT,
38
51
  ICMS_ST_BASE_TYPE,
52
+ ICMS_ST_BASE_TYPE_DEFAULT,
39
53
  )
40
54
  from ..constants.issqn import (
41
55
  ISSQN_ELIGIBILITY,
@@ -63,16 +77,9 @@ class FiscalDocumentLineMixin(models.AbstractModel):
63
77
  ISSQN, etc.), covering their respective bases, rates, and
64
78
  calculated values.
65
79
  - Line-level totals and cost components.
66
-
67
- It inherits computational logic, onchange handlers, and other complex
68
- methods from `l10n_br_fiscal.document.line.mixin.methods`. Models
69
- that represent actual document lines (e.g.,
70
- `l10n_br_fiscal.document.line`) should inherit this mixin to
71
- acquire the necessary fiscal field definitions and associated behaviors.
72
80
  """
73
81
 
74
82
  _name = "l10n_br_fiscal.document.line.mixin"
75
- _inherit = "l10n_br_fiscal.document.line.mixin.methods"
76
83
  _description = "Document Fiscal Mixin"
77
84
 
78
85
  @api.model
@@ -95,6 +102,810 @@ class FiscalDocumentLineMixin(models.AbstractModel):
95
102
  domain = [("state", "=", "approved")]
96
103
  return domain
97
104
 
105
+ @api.model
106
+ def inject_fiscal_fields(
107
+ self,
108
+ doc,
109
+ view_ref="l10n_br_fiscal.document_fiscal_line_mixin_form",
110
+ xpath_mappings=None,
111
+ ):
112
+ """
113
+ Inject common fiscal fields into view placeholder elements.
114
+ Used for invoice line, sale order line, purchase order line...
115
+ """
116
+
117
+ # the list of computed fields we will add to the view when missing
118
+ missing_line_fields = set(
119
+ [
120
+ fname
121
+ for fname, _field in filter(
122
+ lambda item: item[1].compute
123
+ in (
124
+ "_compute_tax_fields",
125
+ "_compute_fiscal_tax_ids",
126
+ "_compute_product_fiscal_fields",
127
+ ),
128
+ self.env["l10n_br_fiscal.document.line.mixin"]._fields.items(),
129
+ )
130
+ ]
131
+ )
132
+
133
+ fiscal_view = self.env.ref(
134
+ "l10n_br_fiscal.document_fiscal_line_mixin_form"
135
+ ).sudo()
136
+ fsc_doc = etree.fromstring(
137
+ fiscal_view.with_context(inherit_branding=True).get_combined_arch()
138
+ )
139
+
140
+ if xpath_mappings is None:
141
+ xpath_mappings = (
142
+ # (placeholder_xpath, fiscal_xpath)
143
+ (".//group[@name='fiscal_fields']", "//group[@name='fiscal_fields']"),
144
+ (".//page[@name='fiscal_taxes']", "//page[@name='fiscal_taxes']"),
145
+ (
146
+ ".//page[@name='fiscal_line_extra_info']",
147
+ "//page[@name='fiscal_line_extra_info']",
148
+ ),
149
+ # these will only collect (invisible) fields for onchanges:
150
+ (
151
+ ".//control[@name='fiscal_fields']...",
152
+ "//group[@name='fiscal_fields']//field",
153
+ ),
154
+ (
155
+ ".//control[@name='fiscal_taxes_fields']...",
156
+ "//page[@name='fiscal_taxes']//field",
157
+ ),
158
+ (
159
+ ".//control[@name='fiscal_line_extra_info_fields']...",
160
+ "//page[@name='fiscal_line_extra_info']//field",
161
+ ),
162
+ )
163
+ for placeholder_xpath, fiscal_xpath in xpath_mappings:
164
+ placeholder_nodes = doc.findall(placeholder_xpath)
165
+ if not placeholder_nodes:
166
+ continue
167
+ fiscal_nodes = fsc_doc.xpath(fiscal_xpath)
168
+ for target_node in placeholder_nodes:
169
+ if len(fiscal_nodes) == 1:
170
+ # replace unique placeholder
171
+ # (deepcopy is required to inject fiscal nodes in possible
172
+ # next places)
173
+ replace_node = deepcopy(fiscal_nodes[0])
174
+ target_node.getparent().replace(target_node, replace_node)
175
+ else:
176
+ # append multiple fields to placeholder container
177
+ existing_fields = [
178
+ e.attrib["name"] for e in target_node if e.tag == "field"
179
+ ]
180
+ for fiscal_node in fiscal_nodes:
181
+ if fiscal_node.attrib["name"] in missing_line_fields:
182
+ missing_line_fields.remove(fiscal_node.attrib["name"])
183
+ if fiscal_node.attrib["name"] in existing_fields:
184
+ continue
185
+ field = deepcopy(fiscal_node)
186
+ if not field.attrib.get("optional"):
187
+ field.attrib["optional"] = "hide"
188
+ target_node.append(field)
189
+ for fname in missing_line_fields:
190
+ if fname not in existing_fields:
191
+ target_node.append(
192
+ E.field(name=fname, string=fname, optional="hide")
193
+ )
194
+ return doc
195
+
196
+ @api.model
197
+ def _get_view(self, view_id=None, view_type="form", **options):
198
+ arch, view = super()._get_view(view_id, view_type, **options)
199
+ if view_type == "form":
200
+ arch = self.inject_fiscal_fields(arch)
201
+ return arch, view
202
+
203
+ @api.depends(
204
+ "discount_value",
205
+ "amount_tax_not_included",
206
+ "amount_tax_withholding",
207
+ "price_unit",
208
+ "quantity",
209
+ "fiscal_operation_line_id",
210
+ "cfop_id",
211
+ "icms_relief_value",
212
+ "insurance_value",
213
+ "other_value",
214
+ "freight_value",
215
+ "pis_value",
216
+ "cofins_value",
217
+ "icms_value",
218
+ "ii_value",
219
+ "ii_customhouse_charges",
220
+ )
221
+ def _compute_fiscal_amounts(self):
222
+ for record in self:
223
+ round_curr = record.currency_id or self.env.ref("base.BRL")
224
+
225
+ # Total value of products or services
226
+ record.price_gross = round_curr.round(record.price_unit * record.quantity)
227
+ record.amount_fiscal = record.price_gross - record.discount_value
228
+ record.fiscal_amount_tax = record.amount_tax_not_included
229
+
230
+ add_to_amount = sum(record[a] for a in record._add_fields_to_amount())
231
+ rm_to_amount = sum(record[r] for r in record._rm_fields_to_amount())
232
+ record.fiscal_amount_untaxed = (
233
+ record.price_gross
234
+ - record.discount_value
235
+ + add_to_amount
236
+ - rm_to_amount
237
+ )
238
+
239
+ # Valor do documento (NF)
240
+ record.fiscal_amount_total = (
241
+ record.fiscal_amount_untaxed + record.fiscal_amount_tax
242
+ )
243
+
244
+ # Valor Liquido (TOTAL + IMPOSTOS - RETENÇÕES)
245
+ record.amount_taxed = (
246
+ record.fiscal_amount_total - record.amount_tax_withholding
247
+ )
248
+
249
+ # Valor do documento (NF) - RETENÇÕES
250
+ record.fiscal_amount_total = record.amount_taxed
251
+
252
+ # Valor financeiro
253
+ if (
254
+ record.fiscal_operation_line_id
255
+ and record.fiscal_operation_line_id.add_to_amount
256
+ and (not record.cfop_id or record.cfop_id.finance_move)
257
+ ):
258
+ record.financial_total = record.amount_taxed
259
+ record.financial_total_gross = (
260
+ record.financial_total + record.discount_value
261
+ )
262
+ record.financial_discount_value = record.discount_value
263
+ else:
264
+ record.financial_total_gross = record.financial_total = 0.0
265
+ record.financial_discount_value = 0.0
266
+
267
+ @api.depends("tax_icms_or_issqn", "partner_id")
268
+ def _compute_allow_csll_irpj(self):
269
+ """Calculates the possibility of 'CSLL' and 'IRPJ' tax charges."""
270
+ for line in self:
271
+ # Determine if 'CSLL' and 'IRPJ' taxes may apply:
272
+ # 1. When providing services (tax_icms_or_issqn == "issqn")
273
+ # 2. When supplying products to public entities (partner_is_public_entity
274
+ # is True)
275
+ if line.tax_icms_or_issqn == "issqn" or line.partner_is_public_entity:
276
+ line.allow_csll_irpj = True # Tax charges may apply
277
+ else:
278
+ line.allow_csll_irpj = False # No tax charges expected
279
+
280
+ def _prepare_br_fiscal_dict(self, default=False):
281
+ self.ensure_one()
282
+ fields = self.env["l10n_br_fiscal.document.line.mixin"]._fields.keys()
283
+
284
+ # we now read the record fiscal fields except the m2m tax:
285
+ vals = self._convert_to_write(self.read(fields)[0])
286
+
287
+ # remove id field to avoid conflicts
288
+ vals.pop("id", None)
289
+
290
+ if default: # in case you want to use new rather than write later
291
+ return {f"default_{k}": vals[k] for k in vals.keys()}
292
+ return vals
293
+
294
+ @api.depends("fiscal_operation_id", "partner_id", "product_id")
295
+ def _compute_fiscal_operation_line_id(self):
296
+ for line in self:
297
+ if line.fiscal_operation_id:
298
+ line.fiscal_operation_line_id = (
299
+ line.fiscal_operation_id.line_definition(
300
+ company=line.company_id,
301
+ partner=line.partner_id,
302
+ product=line.product_id,
303
+ )
304
+ )
305
+
306
+ @api.depends(
307
+ "partner_id",
308
+ "fiscal_operation_line_id",
309
+ "product_id",
310
+ "ncm_id",
311
+ "nbs_id",
312
+ "nbm_id",
313
+ "cest_id",
314
+ "city_taxation_code_id",
315
+ "service_type_id",
316
+ "ind_final",
317
+ )
318
+ def _compute_fiscal_tax_ids(self):
319
+ for line in self:
320
+ if line.fiscal_operation_line_id:
321
+ mapping_result = line.fiscal_operation_line_id.map_fiscal_taxes(
322
+ company=line.company_id,
323
+ partner=line._get_fiscal_partner(),
324
+ product=line.product_id,
325
+ ncm=line.ncm_id,
326
+ nbm=line.nbm_id,
327
+ nbs=line.nbs_id,
328
+ cest=line.cest_id,
329
+ city_taxation_code=line.city_taxation_code_id,
330
+ service_type=line.service_type_id,
331
+ ind_final=line.ind_final,
332
+ )
333
+ line.cfop_id = mapping_result["cfop"]
334
+ line.ipi_guideline_id = mapping_result["ipi_guideline"]
335
+ line.tax_classification_id = mapping_result["tax_classification"]
336
+ line.icms_tax_benefit_id = mapping_result["icms_tax_benefit_id"]
337
+
338
+ if line._is_imported():
339
+ continue
340
+
341
+ taxes = line.env["l10n_br_fiscal.tax"]
342
+ for tax in mapping_result["taxes"].values():
343
+ taxes |= tax
344
+ line.fiscal_tax_ids = taxes
345
+
346
+ @api.depends("fiscal_operation_line_id")
347
+ def _compute_comment_ids(self):
348
+ for line in self:
349
+ line.comment_ids = [
350
+ Command.set(line.fiscal_operation_line_id.comment_ids.ids)
351
+ ]
352
+
353
+ @api.model
354
+ def _build_null_mask_dict(self) -> dict:
355
+ """
356
+ Build a null values mask dict to reset all fiscal fields.
357
+ """
358
+ mask_dict = {
359
+ f[0]: False
360
+ for f in filter(
361
+ lambda f: f[1].compute == "_compute_tax_fields",
362
+ self.env["l10n_br_fiscal.document.line.mixin"]._fields.items(),
363
+ )
364
+ }
365
+ for fiscal_tax_field in FISCAL_TAX_ID_FIELDS:
366
+ mask_dict[fiscal_tax_field] = False
367
+ return mask_dict
368
+
369
+ def write(self, vals):
370
+ res = super().write(vals)
371
+
372
+ # Verifica se algum campo de imposto relevante foi alterado no 'write'
373
+ tax_fields_in_vals = [fld for fld in vals if fld in FISCAL_TAX_ID_FIELDS]
374
+
375
+ if tax_fields_in_vals:
376
+ # Por segurança, sempre recalcula se um campo relevante mudou.
377
+ self._update_fiscal_tax_ids()
378
+
379
+ return res
380
+
381
+ def _update_fiscal_tax_ids(self):
382
+ taxes = self.env["l10n_br_fiscal.tax"]
383
+ for fiscal_tax_field in FISCAL_TAX_ID_FIELDS:
384
+ taxes |= self[fiscal_tax_field]
385
+
386
+ for line in self:
387
+ taxes_groups = line.fiscal_tax_ids.mapped("tax_domain")
388
+ fiscal_taxes = line.fiscal_tax_ids.filtered(
389
+ lambda ft, taxes_groups=taxes_groups: ft.tax_domain not in taxes_groups
390
+ )
391
+ line.fiscal_tax_ids = fiscal_taxes + taxes
392
+
393
+ @api.onchange(*FISCAL_TAX_ID_FIELDS)
394
+ def _onchange_fiscal_taxes(self):
395
+ self._update_fiscal_tax_ids()
396
+
397
+ @api.depends(
398
+ "partner_id",
399
+ "fiscal_tax_ids",
400
+ "product_id",
401
+ "price_unit",
402
+ "quantity",
403
+ "uom_id",
404
+ "fiscal_price",
405
+ "fiscal_quantity",
406
+ "uot_id",
407
+ "discount_value",
408
+ "insurance_value",
409
+ "ii_customhouse_charges",
410
+ "ii_iof_value",
411
+ "other_value",
412
+ "freight_value",
413
+ "ncm_id",
414
+ "nbs_id",
415
+ "nbm_id",
416
+ "cest_id",
417
+ "fiscal_operation_line_id",
418
+ "cfop_id",
419
+ "icmssn_range_id",
420
+ "icms_origin",
421
+ "icms_cst_id",
422
+ "ind_final",
423
+ "icms_relief_id",
424
+ )
425
+ def _compute_tax_fields(self):
426
+ """
427
+ Compute base, percent, value... tax fields for ICMS, IPI, PIS, COFINS... taxes.
428
+ """
429
+ null_mask = None
430
+ for line in self.filtered(lambda line: not line._is_imported()):
431
+ if null_mask is None:
432
+ null_mask = self._build_null_mask_dict()
433
+ to_update = null_mask.copy()
434
+ # prepare with default values
435
+ to_update.update(
436
+ {
437
+ "icms_base_type": ICMS_BASE_TYPE_DEFAULT,
438
+ "icmsst_base_type": ICMS_ST_BASE_TYPE_DEFAULT,
439
+ "ipi_base_type": TAX_BASE_TYPE_PERCENT,
440
+ "cofins_base_type": TAX_BASE_TYPE_PERCENT,
441
+ "cofinsst_base_type": TAX_BASE_TYPE_PERCENT,
442
+ "cofins_wh_base_type": TAX_BASE_TYPE_PERCENT,
443
+ "pis_base_type": TAX_BASE_TYPE_PERCENT,
444
+ "pisst_base_type": TAX_BASE_TYPE_PERCENT,
445
+ "pis_wh_base_type": TAX_BASE_TYPE_PERCENT,
446
+ "cbs_base_type": TAX_BASE_TYPE_PERCENT,
447
+ "ibs_base_type": TAX_BASE_TYPE_PERCENT,
448
+ }
449
+ )
450
+ if line.fiscal_operation_line_id:
451
+ compute_result = line.fiscal_tax_ids.compute_taxes(
452
+ company=line.company_id,
453
+ partner=line._get_fiscal_partner(),
454
+ product=line.product_id,
455
+ price_unit=line.price_unit,
456
+ quantity=line.quantity,
457
+ uom_id=line.uom_id,
458
+ fiscal_price=line.fiscal_price,
459
+ fiscal_quantity=line.fiscal_quantity,
460
+ uot_id=line.uot_id,
461
+ discount_value=line.discount_value,
462
+ insurance_value=line.insurance_value,
463
+ ii_customhouse_charges=line.ii_customhouse_charges,
464
+ ii_iof_value=line.ii_iof_value,
465
+ other_value=line.other_value,
466
+ freight_value=line.freight_value,
467
+ ncm=line.ncm_id,
468
+ nbs=line.nbs_id,
469
+ nbm=line.nbm_id,
470
+ cest=line.cest_id,
471
+ operation_line=line.fiscal_operation_line_id,
472
+ cfop=line.cfop_id,
473
+ icmssn_range=line.icmssn_range_id,
474
+ icms_origin=line.icms_origin,
475
+ icms_cst_id=line.icms_cst_id,
476
+ ind_final=line.ind_final,
477
+ icms_relief_id=line.icms_relief_id,
478
+ )
479
+ to_update.update(line._prepare_tax_fields(compute_result))
480
+ else:
481
+ compute_result = {}
482
+ to_update.update(
483
+ {
484
+ "amount_tax_included": compute_result.get("amount_included", 0.0),
485
+ "amount_tax_not_included": compute_result.get(
486
+ "amount_not_included", 0.0
487
+ ),
488
+ "amount_tax_withholding": compute_result.get(
489
+ "amount_withholding", 0.0
490
+ ),
491
+ "estimate_tax": compute_result.get("estimate_tax", 0.0),
492
+ }
493
+ )
494
+ in_draft_mode = line != line._origin
495
+ if in_draft_mode:
496
+ line.update(to_update)
497
+ else:
498
+ line.write(to_update)
499
+
500
+ def _prepare_tax_fields(self, compute_result):
501
+ self.ensure_one()
502
+ tax_values = {}
503
+ if self._is_imported():
504
+ return tax_values
505
+ computed_taxes = compute_result.get("taxes", {})
506
+ for tax in self.fiscal_tax_ids:
507
+ computed_tax = computed_taxes.get(tax.tax_domain, {})
508
+ tax_field_name = f"{tax.tax_domain}_tax_id"
509
+ if hasattr(self, tax_field_name):
510
+ tax_values[tax_field_name] = tax.ids[0]
511
+ method = getattr(self, f"_prepare_fields_{tax.tax_domain}", None)
512
+ if method and computed_tax:
513
+ prepared_fields = method(computed_tax)
514
+ if prepared_fields:
515
+ tax_values.update(prepared_fields)
516
+ return tax_values
517
+
518
+ @api.depends(
519
+ "product_id",
520
+ "fiscal_operation_id",
521
+ )
522
+ def _compute_price_unit_fiscal(self): # OK when edited from aml?? c-> check
523
+ for line in self:
524
+ line.price_unit = {
525
+ "sale_price": line.product_id.list_price,
526
+ "cost_price": line.product_id.standard_price,
527
+ }.get(line.fiscal_operation_id.default_price_unit, 0)
528
+
529
+ def _get_document(self):
530
+ self.ensure_one()
531
+ return self.document_id
532
+
533
+ def _get_fiscal_partner(self):
534
+ """
535
+ Meant to be overriden when the l10n_br_fiscal.document partner_id should not
536
+ be the same as the sale.order, purchase.order, account.move (...) partner_id.
537
+
538
+ (In the case of invoicing, the invoicing partner set by the user should
539
+ get priority over any invoicing contact returned by address_get.)
540
+ """
541
+ self.ensure_one()
542
+ return self.partner_id
543
+
544
+ @api.depends("product_id")
545
+ def _compute_product_fiscal_fields(self):
546
+ for line in self:
547
+ if not line.product_id:
548
+ # reset to default values:
549
+ line.fiscal_type = False
550
+ line.ncm_id = False
551
+ line.nbm_id = False
552
+ line.tax_icms_or_issqn = TAX_DOMAIN_ICMS
553
+ line.icms_origin = ICMS_ORIGIN_DEFAULT
554
+ line.cest_id = False
555
+ line.nbs_id = False
556
+ line.fiscal_genre_id = False
557
+ line.service_type_id = False
558
+ line.operation_indicator_id = False
559
+ continue
560
+ p = line.product_id
561
+ line.fiscal_type = p.fiscal_type
562
+ line.ncm_id = p.ncm_id
563
+ line.nbm_id = p.nbm_id
564
+ line.tax_icms_or_issqn = p.tax_icms_or_issqn
565
+ line.icms_origin = p.icms_origin
566
+ line.cest_id = p.cest_id
567
+ line.nbs_id = p.nbs_id
568
+ line.fiscal_genre_id = p.fiscal_genre_id
569
+ line.service_type_id = p.service_type_id
570
+ line.operation_indicator_id = p.operation_indicator_id
571
+
572
+ @api.depends("product_id")
573
+ def _compute_city_taxation_code_id(self):
574
+ for line in self:
575
+ if not line.product_id:
576
+ line.city_taxation_code_id = False
577
+ continue
578
+ company_city = line.company_id.city_id
579
+ city_tax_codes = line.product_id.city_taxation_code_ids
580
+ city_tax_code = city_tax_codes.filtered(
581
+ lambda r, _city_id=company_city: r.city_id == _city_id
582
+ )
583
+ if city_tax_code:
584
+ line.city_taxation_code_id = city_tax_code
585
+ else:
586
+ line.city_taxation_code_id = False
587
+
588
+ def _prepare_fields_issqn(self, tax_dict):
589
+ self.ensure_one()
590
+ return {
591
+ "issqn_base": tax_dict.get("base"),
592
+ "issqn_percent": tax_dict.get("percent_amount"),
593
+ "issqn_reduction": tax_dict.get("percent_reduction"),
594
+ "issqn_value": tax_dict.get("tax_value"),
595
+ }
596
+
597
+ def _prepare_fields_issqn_wh(self, tax_dict):
598
+ self.ensure_one()
599
+ return {
600
+ "issqn_wh_base": tax_dict.get("base"),
601
+ "issqn_wh_percent": tax_dict.get("percent_amount"),
602
+ "issqn_wh_reduction": tax_dict.get("percent_reduction"),
603
+ "issqn_wh_value": tax_dict.get("tax_value"),
604
+ }
605
+
606
+ def _prepare_fields_csll(self, tax_dict):
607
+ self.ensure_one()
608
+ return {
609
+ "csll_base": tax_dict.get("base"),
610
+ "csll_percent": tax_dict.get("percent_amount"),
611
+ "csll_reduction": tax_dict.get("percent_reduction"),
612
+ "csll_value": tax_dict.get("tax_value"),
613
+ }
614
+
615
+ def _prepare_fields_csll_wh(self, tax_dict):
616
+ self.ensure_one()
617
+ return {
618
+ "csll_wh_base": tax_dict.get("base"),
619
+ "csll_wh_percent": tax_dict.get("percent_amount"),
620
+ "csll_wh_reduction": tax_dict.get("percent_reduction"),
621
+ "csll_wh_value": tax_dict.get("tax_value"),
622
+ }
623
+
624
+ def _prepare_fields_irpj(self, tax_dict):
625
+ self.ensure_one()
626
+ return {
627
+ "irpj_base": tax_dict.get("base"),
628
+ "irpj_percent": tax_dict.get("percent_amount"),
629
+ "irpj_reduction": tax_dict.get("percent_reduction"),
630
+ "irpj_value": tax_dict.get("tax_value"),
631
+ }
632
+
633
+ def _prepare_fields_irpj_wh(self, tax_dict):
634
+ self.ensure_one()
635
+ return {
636
+ "irpj_wh_base": tax_dict.get("base"),
637
+ "irpj_wh_percent": tax_dict.get("percent_amount"),
638
+ "irpj_wh_reduction": tax_dict.get("percent_reduction"),
639
+ "irpj_wh_value": tax_dict.get("tax_value"),
640
+ }
641
+
642
+ def _prepare_fields_inss(self, tax_dict):
643
+ self.ensure_one()
644
+ return {
645
+ "inss_base": tax_dict.get("base"),
646
+ "inss_percent": tax_dict.get("percent_amount"),
647
+ "inss_reduction": tax_dict.get("percent_reduction"),
648
+ "inss_value": tax_dict.get("tax_value"),
649
+ }
650
+
651
+ def _prepare_fields_inss_wh(self, tax_dict):
652
+ self.ensure_one()
653
+ return {
654
+ "inss_wh_base": tax_dict.get("base"),
655
+ "inss_wh_percent": tax_dict.get("percent_amount"),
656
+ "inss_wh_reduction": tax_dict.get("percent_reduction"),
657
+ "inss_wh_value": tax_dict.get("tax_value"),
658
+ }
659
+
660
+ def _prepare_fields_icms(self, tax_dict):
661
+ self.ensure_one()
662
+ cst_id = tax_dict.get("cst_id").id if tax_dict.get("cst_id") else False
663
+ return {
664
+ "icms_cst_id": cst_id,
665
+ "icms_base_type": tax_dict.get("icms_base_type", ICMS_BASE_TYPE_DEFAULT),
666
+ "icms_base": tax_dict.get("base", 0.0),
667
+ "icms_percent": tax_dict.get("percent_amount", 0.0),
668
+ "icms_reduction": tax_dict.get("percent_reduction", 0.0),
669
+ "icms_value": tax_dict.get("tax_value", 0.0),
670
+ "icms_origin_percent": tax_dict.get("icms_origin_perc", 0.0),
671
+ "icms_destination_percent": tax_dict.get("icms_dest_perc", 0.0),
672
+ "icms_sharing_percent": tax_dict.get("icms_sharing_percent", 0.0),
673
+ "icms_destination_base": tax_dict.get("icms_dest_base", 0.0),
674
+ "icms_origin_value": tax_dict.get("icms_origin_value", 0.0),
675
+ "icms_destination_value": tax_dict.get("icms_dest_value", 0.0),
676
+ "icms_relief_value": tax_dict.get("icms_relief", 0.0),
677
+ }
678
+
679
+ @api.onchange(
680
+ "icms_base",
681
+ "icms_percent",
682
+ "icms_reduction",
683
+ "icms_value",
684
+ "icms_destination_base",
685
+ "icms_origin_percent",
686
+ "icms_destination_percent",
687
+ "icms_sharing_percent",
688
+ "icms_origin_value",
689
+ "icms_tax_benefit_id",
690
+ )
691
+ def _onchange_icms_fields(self):
692
+ if self.icms_tax_benefit_id:
693
+ self.icms_tax_id = self.icms_tax_benefit_id.tax_id
694
+
695
+ @api.onchange("tax_classification_id")
696
+ def _onchange_tax_classification_id(self):
697
+ if self.tax_classification_id:
698
+ self.ibs_tax_id = self.tax_classification_id.tax_ibs_id
699
+ self.cbs_tax_id = self.tax_classification_id.tax_cbs_id
700
+
701
+ def _prepare_fields_icmssn(self, tax_dict):
702
+ self.ensure_one()
703
+ cst_id = tax_dict.get("cst_id").id if tax_dict.get("cst_id") else False
704
+ icmssn_base = tax_dict.get("base", 0.0)
705
+ icmssn_credit_value = tax_dict.get("tax_value", 0.0)
706
+ simple_value = icmssn_base * self.icmssn_range_id.total_tax_percent
707
+ simple_without_icms_value = simple_value - icmssn_credit_value
708
+ return {
709
+ "icms_cst_id": cst_id,
710
+ "icmssn_base": icmssn_base,
711
+ "icmssn_percent": tax_dict.get("percent_amount"),
712
+ "icmssn_reduction": tax_dict.get("percent_reduction"),
713
+ "icmssn_credit_value": icmssn_credit_value,
714
+ "simple_value": simple_value,
715
+ "simple_without_icms_value": simple_without_icms_value,
716
+ }
717
+
718
+ def _prepare_fields_icmsst(self, tax_dict):
719
+ self.ensure_one()
720
+ return {
721
+ "icmsst_base_type": tax_dict.get(
722
+ "icmsst_base_type", ICMS_ST_BASE_TYPE_DEFAULT
723
+ ),
724
+ "icmsst_mva_percent": tax_dict.get("icmsst_mva_percent"),
725
+ "icmsst_percent": tax_dict.get("percent_amount"),
726
+ "icmsst_reduction": tax_dict.get("percent_reduction"),
727
+ "icmsst_base": tax_dict.get("base"),
728
+ "icmsst_value": tax_dict.get("tax_value"),
729
+ }
730
+
731
+ def _prepare_fields_icmsfcp(self, tax_dict):
732
+ self.ensure_one()
733
+ return {
734
+ "icmsfcp_base": tax_dict.get("base", 0.0),
735
+ "icmsfcp_percent": tax_dict.get("percent_amount", 0.0),
736
+ "icmsfcp_value": tax_dict.get("tax_value", 0.0),
737
+ }
738
+
739
+ def _prepare_fields_icmsfcpst(self, tax_dict):
740
+ self.ensure_one()
741
+ return {
742
+ "icmsfcpst_base": self.icmsst_base,
743
+ "icmsfcpst_percent": tax_dict.get("percent_amount", 0.0),
744
+ "icmsfcpst_value": tax_dict.get("tax_value", 0.0),
745
+ }
746
+
747
+ def _prepare_fields_ipi(self, tax_dict):
748
+ self.ensure_one()
749
+ cst_id = tax_dict.get("cst_id").id if tax_dict.get("cst_id") else False
750
+ return {
751
+ "ipi_cst_id": cst_id,
752
+ "ipi_base_type": tax_dict.get("base_type", False),
753
+ "ipi_base": tax_dict.get("base", 0.00),
754
+ "ipi_percent": tax_dict.get("percent_amount", 0.00),
755
+ "ipi_reduction": tax_dict.get("percent_reduction", 0.00),
756
+ "ipi_value": tax_dict.get("tax_value", 0.00),
757
+ }
758
+
759
+ def _prepare_fields_ii(self, tax_dict):
760
+ self.ensure_one()
761
+ return {
762
+ "ii_base": tax_dict.get("base", 0.00),
763
+ "ii_percent": tax_dict.get("percent_amount", 0.00),
764
+ "ii_value": tax_dict.get("tax_value", 0.00),
765
+ }
766
+
767
+ def _prepare_fields_cbs(self, tax_dict):
768
+ self.ensure_one()
769
+ cst_id = tax_dict.get("cst_id").id if tax_dict.get("cst_id") else False
770
+ return {
771
+ "cbs_cst_id": cst_id,
772
+ "cbs_base_type": tax_dict.get("base_type", False),
773
+ "cbs_base": tax_dict.get("base", 0.00),
774
+ "cbs_percent": tax_dict.get("percent_amount", 0.00),
775
+ "cbs_reduction": tax_dict.get("percent_reduction", 0.00),
776
+ "cbs_value": tax_dict.get("tax_value", 0.00),
777
+ }
778
+
779
+ def _prepare_fields_ibs(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
+ "ibs_cst_id": cst_id,
784
+ "ibs_base_type": tax_dict.get("base_type", False),
785
+ "ibs_base": tax_dict.get("base", 0.00),
786
+ "ibs_percent": tax_dict.get("percent_amount", 0.00),
787
+ "ibs_reduction": tax_dict.get("percent_reduction", 0.00),
788
+ "ibs_value": tax_dict.get("tax_value", 0.00),
789
+ }
790
+
791
+ def _prepare_fields_pis(self, tax_dict):
792
+ self.ensure_one()
793
+ cst_id = tax_dict.get("cst_id").id if tax_dict.get("cst_id") else False
794
+ return {
795
+ "pis_cst_id": cst_id,
796
+ "pis_base_type": tax_dict.get("base_type"),
797
+ "pis_base": tax_dict.get("base", 0.00),
798
+ "pis_percent": tax_dict.get("percent_amount", 0.00),
799
+ "pis_reduction": tax_dict.get("percent_reduction", 0.00),
800
+ "pis_value": tax_dict.get("tax_value", 0.00),
801
+ }
802
+
803
+ def _prepare_fields_pis_wh(self, tax_dict):
804
+ self.ensure_one()
805
+ return {
806
+ "pis_wh_base_type": tax_dict.get("base_type"),
807
+ "pis_wh_base": tax_dict.get("base", 0.00),
808
+ "pis_wh_percent": tax_dict.get("percent_amount", 0.00),
809
+ "pis_wh_reduction": tax_dict.get("percent_reduction", 0.00),
810
+ "pis_wh_value": tax_dict.get("tax_value", 0.00),
811
+ }
812
+
813
+ def _prepare_fields_pisst(self, tax_dict):
814
+ self.ensure_one()
815
+ cst_id = tax_dict.get("cst_id").id if tax_dict.get("cst_id") else False
816
+ return {
817
+ "pisst_cst_id": cst_id,
818
+ "pisst_base_type": tax_dict.get("base_type"),
819
+ "pisst_base": tax_dict.get("base", 0.00),
820
+ "pisst_percent": tax_dict.get("percent_amount", 0.00),
821
+ "pisst_reduction": tax_dict.get("percent_reduction", 0.00),
822
+ "pisst_value": tax_dict.get("tax_value", 0.00),
823
+ }
824
+
825
+ def _prepare_fields_cofins(self, tax_dict):
826
+ self.ensure_one()
827
+ cst_id = tax_dict.get("cst_id").id if tax_dict.get("cst_id") else False
828
+ return {
829
+ "cofins_cst_id": cst_id,
830
+ "cofins_base_type": tax_dict.get("base_type"),
831
+ "cofins_base": tax_dict.get("base", 0.00),
832
+ "cofins_percent": tax_dict.get("percent_amount", 0.00),
833
+ "cofins_reduction": tax_dict.get("percent_reduction", 0.00),
834
+ "cofins_value": tax_dict.get("tax_value", 0.00),
835
+ }
836
+
837
+ def _prepare_fields_cofins_wh(self, tax_dict):
838
+ self.ensure_one()
839
+ return {
840
+ "cofins_wh_base_type": tax_dict.get("base_type"),
841
+ "cofins_wh_base": tax_dict.get("base", 0.00),
842
+ "cofins_wh_percent": tax_dict.get("percent_amount", 0.00),
843
+ "cofins_wh_reduction": tax_dict.get("percent_reduction", 0.00),
844
+ "cofins_wh_value": tax_dict.get("tax_value", 0.00),
845
+ }
846
+
847
+ def _prepare_fields_cofinsst(self, tax_dict):
848
+ self.ensure_one()
849
+ cst_id = tax_dict.get("cst_id").id if tax_dict.get("cst_id") else False
850
+ return {
851
+ "cofinsst_cst_id": cst_id,
852
+ "cofinsst_base_type": tax_dict.get("base_type"),
853
+ "cofinsst_base": tax_dict.get("base", 0.00),
854
+ "cofinsst_percent": tax_dict.get("percent_amount", 0.00),
855
+ "cofinsst_reduction": tax_dict.get("percent_reduction", 0.00),
856
+ "cofinsst_value": tax_dict.get("tax_value", 0.00),
857
+ }
858
+
859
+ @api.depends("product_id", "uom_id")
860
+ def _compute_uot_id(self):
861
+ for line in self:
862
+ p = line.product_id
863
+ line.uot_id = (p.uot_id if p else False) or line.uom_id
864
+
865
+ @api.depends("price_unit")
866
+ def _compute_fiscal_price(self):
867
+ for line in self:
868
+ if line.product_id and line.price_unit:
869
+ line.fiscal_price = line.price_unit / (
870
+ line.product_id.uot_factor or 1.0
871
+ )
872
+ else:
873
+ line.fiscal_price = line.price_unit
874
+
875
+ @api.depends("quantity")
876
+ def _compute_fiscal_quantity(self):
877
+ for line in self:
878
+ if line.product_id and line.quantity:
879
+ line.fiscal_quantity = line.quantity * (
880
+ line.product_id.uot_factor or 1.0
881
+ )
882
+ else:
883
+ line.fiscal_quantity = line.quantity
884
+
885
+ @api.model
886
+ def _add_fields_to_amount(self):
887
+ fields_to_amount = ["insurance_value", "other_value", "freight_value"]
888
+ if (
889
+ self.cfop_id.destination == CFOP_DESTINATION_EXPORT
890
+ and self.fiscal_operation_id.fiscal_operation_type == FISCAL_IN
891
+ ):
892
+ fields_to_amount.append("pis_value")
893
+ fields_to_amount.append("cofins_value")
894
+ fields_to_amount.append("icms_value")
895
+ fields_to_amount.append("ii_value")
896
+ fields_to_amount.append("ii_customhouse_charges")
897
+ return fields_to_amount
898
+
899
+ @api.model
900
+ def _rm_fields_to_amount(self):
901
+ return ["icms_relief_value"]
902
+
903
+ def _is_imported(self):
904
+ # When the mixin is used for instance
905
+ # in a PO line or SO line, there is no document_id
906
+ # and we consider the document is not imported
907
+ return hasattr(self, "document_id") and self.document_id.imported_document
908
+
98
909
  currency_id = fields.Many2one(
99
910
  comodel_name="res.currency",
100
911
  string="Currency",
@@ -392,6 +1203,15 @@ class FiscalDocumentLineMixin(models.AbstractModel):
392
1203
  precompute=True,
393
1204
  )
394
1205
 
1206
+ operation_indicator_id = fields.Many2one(
1207
+ comodel_name="l10n_br_fiscal.operation.indicator",
1208
+ string="Operation Indicator",
1209
+ compute="_compute_product_fiscal_fields",
1210
+ store=True,
1211
+ readonly=False,
1212
+ precompute=True,
1213
+ )
1214
+
395
1215
  partner_order = fields.Char(string="Partner Order (xPed)", size=15)
396
1216
 
397
1217
  partner_order_line = fields.Char(string="Partner Order Line (nItemPed)", size=6)
@@ -995,6 +1815,171 @@ class FiscalDocumentLineMixin(models.AbstractModel):
995
1815
 
996
1816
  ipi_devol_value = fields.Monetary(string="Valor do IPI devolvido")
997
1817
 
1818
+ # CBS Fields
1819
+ cbs_tax_id = fields.Many2one(
1820
+ comodel_name="l10n_br_fiscal.tax",
1821
+ string="Tax CBS",
1822
+ domain=(
1823
+ f"[('tax_domain', '=', '{TAX_DOMAIN_CBS}'), '|', "
1824
+ "('cst_in_id.code', 'like', cst_code_prefix_like), "
1825
+ "('cst_out_id.code', 'like', cst_code_prefix_like)]"
1826
+ ),
1827
+ compute="_compute_tax_fields",
1828
+ store=True,
1829
+ precompute=True,
1830
+ readonly=False,
1831
+ )
1832
+
1833
+ cbs_cst_id = fields.Many2one(
1834
+ comodel_name="l10n_br_fiscal.cst",
1835
+ string="CST CBS",
1836
+ domain="[('cst_type', '=', fiscal_operation_type),('tax_domain', '=', 'cbs')]",
1837
+ compute="_compute_tax_fields",
1838
+ store=True,
1839
+ precompute=True,
1840
+ readonly=False,
1841
+ )
1842
+
1843
+ cbs_cst_code = fields.Char(
1844
+ related="cbs_cst_id.code", string="CBS CST Code", store=True
1845
+ )
1846
+
1847
+ cbs_base_type = fields.Selection(
1848
+ selection=TAX_BASE_TYPE,
1849
+ string="CBS Base Type",
1850
+ compute="_compute_tax_fields",
1851
+ store=True,
1852
+ precompute=True,
1853
+ readonly=False,
1854
+ )
1855
+
1856
+ cbs_base = fields.Monetary(
1857
+ string="CBS Base",
1858
+ compute="_compute_tax_fields",
1859
+ store=True,
1860
+ precompute=True,
1861
+ readonly=False,
1862
+ )
1863
+
1864
+ cbs_percent = fields.Float(
1865
+ string="CBS %",
1866
+ compute="_compute_tax_fields",
1867
+ store=True,
1868
+ precompute=True,
1869
+ readonly=False,
1870
+ )
1871
+
1872
+ cbs_reduction = fields.Float(
1873
+ string="CBS % Reduction",
1874
+ compute="_compute_tax_fields",
1875
+ store=True,
1876
+ precompute=True,
1877
+ readonly=False,
1878
+ )
1879
+
1880
+ cbs_value = fields.Monetary(
1881
+ string="CBS Value",
1882
+ compute="_compute_tax_fields",
1883
+ store=True,
1884
+ precompute=True,
1885
+ readonly=False,
1886
+ )
1887
+
1888
+ # IBS Fields
1889
+ ibs_tax_id = fields.Many2one(
1890
+ comodel_name="l10n_br_fiscal.tax",
1891
+ string="Tax IBS",
1892
+ domain=(
1893
+ f"[('tax_domain', '=', '{TAX_DOMAIN_IBS}'), '|', "
1894
+ "('cst_in_id.code', 'like', cst_code_prefix_like), "
1895
+ "('cst_out_id.code', 'like', cst_code_prefix_like)]"
1896
+ ),
1897
+ compute="_compute_tax_fields",
1898
+ store=True,
1899
+ precompute=True,
1900
+ readonly=False,
1901
+ )
1902
+
1903
+ ibs_cst_id = fields.Many2one(
1904
+ comodel_name="l10n_br_fiscal.cst",
1905
+ string="CST IBS",
1906
+ domain="[('cst_type', '=', fiscal_operation_type),('tax_domain', '=', 'ibs')]",
1907
+ compute="_compute_tax_fields",
1908
+ store=True,
1909
+ precompute=True,
1910
+ readonly=False,
1911
+ )
1912
+
1913
+ ibs_cst_code = fields.Char(
1914
+ related="ibs_cst_id.code", string="IBS CST Code", store=True
1915
+ )
1916
+
1917
+ ibs_base_type = fields.Selection(
1918
+ selection=TAX_BASE_TYPE,
1919
+ string="IBS Base Type",
1920
+ compute="_compute_tax_fields",
1921
+ store=True,
1922
+ precompute=True,
1923
+ readonly=False,
1924
+ )
1925
+
1926
+ ibs_base = fields.Monetary(
1927
+ string="IBS Base",
1928
+ compute="_compute_tax_fields",
1929
+ store=True,
1930
+ precompute=True,
1931
+ readonly=False,
1932
+ )
1933
+
1934
+ ibs_percent = fields.Float(
1935
+ string="IBS %",
1936
+ compute="_compute_tax_fields",
1937
+ store=True,
1938
+ precompute=True,
1939
+ readonly=False,
1940
+ )
1941
+
1942
+ ibs_reduction = fields.Float(
1943
+ string="IBS % Reduction",
1944
+ compute="_compute_tax_fields",
1945
+ store=True,
1946
+ precompute=True,
1947
+ readonly=False,
1948
+ )
1949
+
1950
+ ibs_value = fields.Monetary(
1951
+ string="IBS Value",
1952
+ compute="_compute_tax_fields",
1953
+ store=True,
1954
+ precompute=True,
1955
+ readonly=False,
1956
+ )
1957
+
1958
+ # CBS/IBS Tax Classification
1959
+ tax_classification_id = fields.Many2one(
1960
+ comodel_name="l10n_br_fiscal.tax.classification",
1961
+ string="Tax Classification",
1962
+ compute="_compute_fiscal_tax_ids",
1963
+ store=True,
1964
+ precompute=True,
1965
+ readonly=False,
1966
+ )
1967
+
1968
+ cst_code_prefix_like = fields.Char(
1969
+ compute="_compute_cst_code_prefix_like",
1970
+ help="Helper field to filter taxes by CST code prefix (3 chars) using LIKE.",
1971
+ )
1972
+
1973
+ @api.depends("tax_classification_id")
1974
+ def _compute_cst_code_prefix_like(self):
1975
+ for rec in self:
1976
+ code = rec.tax_classification_id.code if rec.tax_classification_id else ""
1977
+ prefix = (code or "")[:3]
1978
+ # Avoid matching all records when the prefix is not available yet.
1979
+ rec.cst_code_prefix_like = (
1980
+ f"{prefix}%" if len(prefix) == 3 else "__no_match__%"
1981
+ )
1982
+
998
1983
  # II Fields
999
1984
  ii_tax_id = fields.Many2one(
1000
1985
  comodel_name="l10n_br_fiscal.tax",