tributus-engine 0.2.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.
@@ -0,0 +1,9 @@
1
+ from .engine import (
2
+ BRAZIL_STATES,
3
+ BaseCalculator,
4
+ TaxEngine,
5
+ TaxContext,
6
+ TaxDependencyGraph,
7
+ TaxResult,
8
+ ValueContext,
9
+ )
@@ -0,0 +1,623 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from decimal import Decimal, ROUND_HALF_EVEN
5
+ from typing import Any
6
+
7
+ from .implementations.cofins import Cofins01_02, Cofins03
8
+ from .implementations.ibs_cbs import Cbs, IbsUf
9
+ from .implementations.icms.cst import (
10
+ Icms00,
11
+ Icms10,
12
+ Icms20,
13
+ Icms30,
14
+ Icms51,
15
+ Icms70,
16
+ Icms90,
17
+ Icms101,
18
+ Icms201,
19
+ Icms202_203,
20
+ Icms900,
21
+ )
22
+ from .implementations.icms.fcp import Fcp, FcpDiferido, FcpEfetivo, FcpST
23
+ from .implementations.ipi import Ipi50AdValorem, Ipi50Especifico
24
+ from .implementations.pis import Pis01_02, Pis03
25
+
26
+ BRAZIL_STATES = (
27
+ 'AC', 'AL', 'AP', 'AM', 'BA', 'CE', 'DF', 'ES', 'GO', 'MA', 'MT', 'MS',
28
+ 'MG', 'PA', 'PB', 'PR', 'PE', 'PI', 'RJ', 'RN', 'RS', 'RO', 'RR', 'SC',
29
+ 'SP', 'SE', 'TO',
30
+ )
31
+
32
+ ZERO = Decimal('0.00')
33
+ VALID_TAX_NAMES = {
34
+ 'icms',
35
+ 'icms_st',
36
+ 'icms_credito_sn',
37
+ 'fcp',
38
+ 'fcp_st',
39
+ 'fcp_diferido',
40
+ 'ipi',
41
+ 'pis',
42
+ 'cofins',
43
+ 'ibs',
44
+ 'cbs',
45
+ }
46
+ STATEFUL_TAX_NAMES = ('icms', 'fcp', 'pis', 'cofins', 'ibs', 'cbs')
47
+ ORCHESTRATED_TAX_NAMES = {'ipi', 'icms', 'fcp', 'pis', 'cofins', 'ibs', 'cbs'}
48
+
49
+
50
+ def _to_decimal(value: Any, default: Decimal = ZERO) -> Decimal:
51
+ if value is None or value == '':
52
+ return default
53
+ if isinstance(value, Decimal):
54
+ return value
55
+ return Decimal(str(value)).quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN)
56
+
57
+
58
+ def _sum_amounts(values: list[Decimal]) -> Decimal:
59
+ total = ZERO
60
+ for value in values:
61
+ total += value
62
+ return total.quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN)
63
+
64
+
65
+ @dataclass
66
+ class ValueContext:
67
+ quantity: Decimal = Decimal('0')
68
+ unit_price: Decimal = ZERO
69
+ gross_value: Decimal = ZERO
70
+ discount_value: Decimal = ZERO
71
+ freight_value: Decimal = ZERO
72
+ insurance_value: Decimal = ZERO
73
+ other_expenses: Decimal = ZERO
74
+
75
+
76
+ @dataclass
77
+ class TaxContext:
78
+ values: ValueContext
79
+ taxes: dict[str, Any] = field(default_factory=dict)
80
+
81
+ def validate(self) -> None:
82
+ monetary_values = (
83
+ self.values.quantity,
84
+ self.values.unit_price,
85
+ self.values.gross_value,
86
+ self.values.discount_value,
87
+ self.values.freight_value,
88
+ self.values.insurance_value,
89
+ self.values.other_expenses,
90
+ )
91
+ if any(value < ZERO for value in monetary_values):
92
+ raise ValueError('values must be non-negative')
93
+
94
+ if not isinstance(self.taxes, dict):
95
+ raise ValueError('taxes must be a dict with taxes and rates configuration')
96
+
97
+ rates = self.taxes.get('rates', {})
98
+ enabled = self.taxes.get('enabled', [])
99
+ if not isinstance(rates, dict):
100
+ raise ValueError('taxes.rates must be a dict')
101
+ if not isinstance(enabled, list):
102
+ raise ValueError('taxes.enabled must be a list')
103
+
104
+ invalid_enabled = set(enabled) - VALID_TAX_NAMES
105
+ if invalid_enabled:
106
+ raise ValueError(f'unknown tax names in taxes.enabled: {sorted(invalid_enabled)}')
107
+
108
+ invalid_rates = set(rates) - VALID_TAX_NAMES
109
+ if invalid_rates:
110
+ raise ValueError(f'unknown tax names in taxes.rates: {sorted(invalid_rates)}')
111
+
112
+ for tax_name, rate in rates.items():
113
+ if rate < ZERO:
114
+ raise ValueError(f'tax rate must be non-negative: {tax_name}')
115
+
116
+ if not enabled and not rates:
117
+ has_tax_detail = any(
118
+ name != 'rates' and isinstance(config, dict)
119
+ for name, config in self.taxes.items()
120
+ )
121
+ if not has_tax_detail:
122
+ raise ValueError('taxes must include enabled/rates or per-tax config entries')
123
+
124
+ def get_rate(self, tax_name: str) -> Decimal | None:
125
+ rates = self.taxes.get('rates', {})
126
+ if not isinstance(rates, dict):
127
+ return None
128
+ return rates.get(tax_name)
129
+
130
+ def get_tax_detail(self, tax_name: str) -> dict[str, Any]:
131
+ config = self.taxes.get(tax_name, {})
132
+ if isinstance(config, dict):
133
+ return config
134
+ return {}
135
+
136
+
137
+ @dataclass
138
+ class TaxResult:
139
+ bases: dict[str, Decimal] = field(default_factory=dict)
140
+ rates: dict[str, Decimal] = field(default_factory=dict)
141
+ amounts: dict[str, Decimal] = field(default_factory=dict)
142
+ calculation_order: list[str] = field(default_factory=list)
143
+ metadata: dict[str, dict[str, Any]] = field(default_factory=dict)
144
+ messages: list[str] = field(default_factory=list)
145
+
146
+ def register_tax(
147
+ self,
148
+ tax_name: str,
149
+ *,
150
+ base: Decimal,
151
+ rate: Decimal,
152
+ amount: Decimal,
153
+ metadata: dict[str, Any] | None = None,
154
+ ) -> None:
155
+ self.bases[tax_name] = base.quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN)
156
+ self.rates[tax_name] = rate.quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN)
157
+ self.amounts[tax_name] = amount.quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN)
158
+ if metadata:
159
+ self.metadata[tax_name] = metadata
160
+
161
+ @property
162
+ def total(self) -> Decimal:
163
+ return _sum_amounts(list(self.amounts.values()))
164
+
165
+ def to_dict(self) -> dict[str, Any]:
166
+ taxes = {}
167
+ for tax_name in self.amounts:
168
+ taxes[tax_name] = {
169
+ 'base': f'{self.bases[tax_name]:.2f}',
170
+ 'rate': f'{self.rates[tax_name]:.2f}',
171
+ 'amount': f'{self.amounts[tax_name]:.2f}',
172
+ }
173
+ if tax_name in self.metadata:
174
+ taxes[tax_name]['metadata'] = self.metadata[tax_name]
175
+ return {
176
+ 'taxes': taxes,
177
+ 'calculation_order': self.calculation_order,
178
+ 'messages': self.messages,
179
+ 'total': f'{self.total:.2f}',
180
+ }
181
+
182
+
183
+ @dataclass
184
+ class BaseCalculator:
185
+ gross_value: Decimal
186
+ discount_value: Decimal
187
+ freight_value: Decimal
188
+ insurance_value: Decimal
189
+ other_expenses: Decimal
190
+
191
+ @classmethod
192
+ def from_context(cls, context: TaxContext) -> 'BaseCalculator':
193
+ return cls(
194
+ gross_value=context.values.gross_value,
195
+ discount_value=context.values.discount_value,
196
+ freight_value=context.values.freight_value,
197
+ insurance_value=context.values.insurance_value,
198
+ other_expenses=context.values.other_expenses,
199
+ )
200
+
201
+ def base_padrao(self) -> Decimal:
202
+ base = (
203
+ self.gross_value
204
+ - self.discount_value
205
+ + self.freight_value
206
+ + self.insurance_value
207
+ + self.other_expenses
208
+ )
209
+ return base.quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN)
210
+
211
+
212
+ class TaxDependencyGraph:
213
+ def __init__(self) -> None:
214
+ self._dependencies: dict[str, set[str]] = {}
215
+
216
+ def add_dependency(self, tax_name: str, dependency_name: str) -> None:
217
+ self._dependencies.setdefault(tax_name, set()).add(dependency_name)
218
+ self._dependencies.setdefault(dependency_name, set())
219
+
220
+ def resolve_order(self, enabled_taxes: set[str], preferred_order: tuple[str, ...] | None = None) -> list[str]:
221
+ resolved: list[str] = []
222
+ temporary: set[str] = set()
223
+ permanent: set[str] = set()
224
+
225
+ def visit(tax_name: str) -> None:
226
+ if tax_name in permanent:
227
+ return
228
+ if tax_name in temporary:
229
+ raise ValueError('circular tax dependency detected')
230
+ temporary.add(tax_name)
231
+ dependency_names = self._ordered_tax_names(self._dependencies.get(tax_name, set()), preferred_order)
232
+ for dependency in dependency_names:
233
+ if dependency in enabled_taxes:
234
+ visit(dependency)
235
+ temporary.remove(tax_name)
236
+ permanent.add(tax_name)
237
+ if tax_name in enabled_taxes:
238
+ resolved.append(tax_name)
239
+
240
+ for tax_name in self._ordered_tax_names(enabled_taxes, preferred_order):
241
+ visit(tax_name)
242
+
243
+ return resolved
244
+
245
+ def _ordered_tax_names(self, tax_names: set[str], preferred_order: tuple[str, ...] | None) -> list[str]:
246
+ if preferred_order is None:
247
+ return sorted(tax_names)
248
+
249
+ priority = {tax_name: index for index, tax_name in enumerate(preferred_order)}
250
+ return sorted(tax_names, key=lambda tax_name: (priority.get(tax_name, len(priority)), tax_name))
251
+
252
+
253
+ class TaxEngine:
254
+ def calculate(self, context: TaxContext) -> TaxResult:
255
+ context.validate()
256
+ result = TaxResult()
257
+ order = self._resolve_order(context)
258
+ result.calculation_order = order
259
+
260
+ for tax_name in order:
261
+ self._calculate_tax(tax_name, context, result)
262
+
263
+ return result
264
+
265
+ def calculate_from_dict(self, payload: dict[str, Any]) -> TaxResult:
266
+ return self.calculate(self._context_from_dict(payload))
267
+
268
+ def list_taxes_for_sale_all_states_from_dict(self, payload: dict[str, Any]) -> dict[str, dict[str, str]]:
269
+ base_payload = dict(payload)
270
+ results: dict[str, dict[str, str]] = {}
271
+ for state in BRAZIL_STATES:
272
+ context = self._context_from_dict(base_payload)
273
+
274
+ state_rates: dict[str, str] = {}
275
+ for tax_name in STATEFUL_TAX_NAMES:
276
+ rate = self._resolve_rate(tax_name, context)
277
+ if rate > ZERO:
278
+ state_rates[tax_name] = f'{rate:.2f}'
279
+ results[state] = state_rates
280
+
281
+ return results
282
+
283
+ def _resolve_order(self, context: TaxContext) -> list[str]:
284
+ graph = TaxDependencyGraph()
285
+ enabled = self._enabled_taxes(context)
286
+ preferred_order = ('ipi', 'icms', 'fcp', 'pis', 'cofins', 'ibs', 'cbs')
287
+
288
+ if 'icms' in enabled and 'ipi' in enabled:
289
+ graph.add_dependency('icms', 'ipi')
290
+ if 'pis' in enabled and 'icms' in enabled:
291
+ graph.add_dependency('pis', 'icms')
292
+ if 'cofins' in enabled and 'icms' in enabled:
293
+ graph.add_dependency('cofins', 'icms')
294
+ if 'fcp' in enabled and 'icms' in enabled:
295
+ graph.add_dependency('fcp', 'icms')
296
+
297
+ return graph.resolve_order(enabled, preferred_order)
298
+
299
+ def _enabled_taxes(self, context: TaxContext) -> set[str]:
300
+ enabled = self._raw_enabled_taxes(context)
301
+ if 'icms_st' in enabled or 'icms_credito_sn' in enabled:
302
+ enabled.add('icms')
303
+ if 'fcp_st' in enabled or 'fcp_diferido' in enabled:
304
+ enabled.add('fcp')
305
+ return {tax_name for tax_name in enabled if tax_name in ORCHESTRATED_TAX_NAMES}
306
+
307
+ def _calculate_tax(self, tax_name: str, context: TaxContext, result: TaxResult) -> None:
308
+ if tax_name == 'ipi':
309
+ self._calculate_ipi(context, result)
310
+ return
311
+ if tax_name == 'icms':
312
+ self._calculate_icms(context, result)
313
+ return
314
+ if tax_name == 'fcp':
315
+ self._calculate_fcp(context, result)
316
+ return
317
+ if tax_name == 'pis':
318
+ self._calculate_pis(context, result)
319
+ return
320
+ if tax_name == 'cofins':
321
+ self._calculate_cofins(context, result)
322
+ return
323
+ if tax_name == 'ibs':
324
+ self._calculate_ibs(context, result)
325
+ return
326
+ if tax_name == 'cbs':
327
+ self._calculate_cbs(context, result)
328
+
329
+ def _calculate_ipi(self, context: TaxContext, result: TaxResult) -> None:
330
+ values = context.values
331
+ config = self._tax_details(context, 'ipi')
332
+ mode = self._mode(config, specific_key='aliquota_por_unidade')
333
+
334
+ if mode == 'specific':
335
+ base = _to_decimal(config.get('base_calculo'), values.quantity)
336
+ rate = _to_decimal(config.get('aliquota_por_unidade'), self._resolve_rate('ipi', context))
337
+ calculator = Ipi50Especifico(base_calculo=base, aliquota_por_unidade=rate)
338
+ amount = calculator.valor_ipi()
339
+ else:
340
+ rate = _to_decimal(config.get('aliquota_ipi'), self._resolve_rate('ipi', context))
341
+ calculator = Ipi50AdValorem(
342
+ valor_produto=values.gross_value,
343
+ valor_frete=values.freight_value,
344
+ valor_seguro=values.insurance_value,
345
+ despesas_acessorias=values.other_expenses,
346
+ aliquota_ipi=rate,
347
+ )
348
+ base = calculator.calcular_base_ipi()
349
+ amount = calculator.valor_ipi()
350
+
351
+ result.register_tax('ipi', base=base, rate=rate, amount=amount, metadata={'mode': mode})
352
+
353
+ def _calculate_icms(self, context: TaxContext, result: TaxResult) -> None:
354
+ values = context.values
355
+ config = self._tax_details(context, 'icms')
356
+ cst = str(config.get('cst', '00')).strip().upper().replace('-', '_')
357
+ valor_ipi = result.amounts.get('ipi', ZERO) if config.get('include_ipi_in_base', True) else ZERO
358
+ own_rate = _to_decimal(config.get('aliquota_icms_proprio'), self._resolve_rate('icms', context))
359
+ st_rate = _to_decimal(config.get('aliquota_icms_st'), self._resolve_rate('icms_st', context))
360
+ mva = _to_decimal(config.get('mva'))
361
+ reducao = _to_decimal(config.get('percentual_reducao'))
362
+ reducao_st = _to_decimal(config.get('percentual_reducao_st'))
363
+ credito_sn = _to_decimal(config.get('percentual_credito_sn'))
364
+ diferimento = _to_decimal(config.get('percentual_diferimento'))
365
+
366
+ common = {
367
+ 'valor_produto': values.gross_value,
368
+ 'valor_frete': values.freight_value,
369
+ 'valor_seguro': values.insurance_value,
370
+ 'despesas_acessorias': values.other_expenses,
371
+ 'valor_desconto': values.discount_value,
372
+ }
373
+
374
+ if cst == '00':
375
+ calculator = Icms00(valor_ipi=valor_ipi, aliquota_icms_proprio=own_rate, **common)
376
+ result.register_tax('icms', base=calculator.base_icms_proprio(), rate=own_rate, amount=calculator.valor_icms_proprio(), metadata={'type': 'Icms00'})
377
+ return
378
+
379
+ if cst == '10':
380
+ calculator = Icms10(valor_ipi=valor_ipi, aliquota_icms_proprio=own_rate, aliquota_icms_st=st_rate, mva=mva, percentual_reducao_st=reducao_st, **common)
381
+ result.register_tax('icms', base=calculator.base_icms_proprio(), rate=own_rate, amount=calculator.valor_icms_proprio(), metadata={'type': 'Icms10'})
382
+ result.register_tax('icms_st', base=calculator.base_icms_st(), rate=st_rate, amount=calculator.valor_icms_st(), metadata={'type': 'Icms10'})
383
+ return
384
+
385
+ if cst == '20':
386
+ calculator = Icms20(valor_ipi=valor_ipi, aliquota_icms_proprio=own_rate, percentual_reducao=reducao, **common)
387
+ result.register_tax('icms', base=calculator.base_reduzida_icms_proprio(), rate=own_rate, amount=calculator.valor_icms_proprio(), metadata={'type': 'Icms20'})
388
+ return
389
+
390
+ if cst == '30':
391
+ calculator = Icms30(valor_ipi=valor_ipi, aliq_icms_proprio=own_rate, aliq_icms_st=st_rate, mva=mva, percentual_reducao_st=reducao_st, **common)
392
+ result.register_tax('icms', base=calculator.base_icms_proprio(), rate=own_rate, amount=calculator.valor_icms_desonerado(), metadata={'type': 'Icms30', 'note': 'desonerado'})
393
+ result.register_tax('icms_st', base=calculator.base_icms_st(), rate=st_rate, amount=calculator.valor_icms_st(), metadata={'type': 'Icms30'})
394
+ return
395
+
396
+ if cst == '51':
397
+ calculator = Icms51(valor_ipi=valor_ipi, aliq_icms_proprio=own_rate, percentual_reducao=reducao, percentual_diferimento=diferimento, **common)
398
+ result.register_tax('icms', base=calculator.base_icms_proprio(), rate=own_rate, amount=calculator.valor_icms_proprio(), metadata={'type': 'Icms51'})
399
+ return
400
+
401
+ if cst == '70':
402
+ calculator = Icms70(valor_ipi=valor_ipi, aliquota_icms_proprio=own_rate, aliquota_icms_st=st_rate, mva=mva, percentual_reducao=reducao, percentual_reducao_st=reducao_st, **common)
403
+ result.register_tax('icms', base=calculator.base_icms_proprio(), rate=own_rate, amount=calculator.valor_icms_proprio(), metadata={'type': 'Icms70'})
404
+ result.register_tax('icms_st', base=calculator.base_icms_st(), rate=st_rate, amount=calculator.valor_icms_st(), metadata={'type': 'Icms70'})
405
+ return
406
+
407
+ if cst == '90':
408
+ calculator = Icms90(valor_ipi=valor_ipi, aliquota_icms_proprio=own_rate, aliquota_icms_st=st_rate, mva=mva, percentual_reducao=reducao, percentual_reducao_st=reducao_st, **common)
409
+ base_icms = calculator.calcular_base_reduzida_icms_proprio() if reducao > ZERO else calculator.calcular_base_icms_proprio()
410
+ amount_icms = calculator.valor_icms_proprio_base_reduzida() if reducao > ZERO else calculator.valor_icms_proprio()
411
+ base_st = calculator.calcular_base_reduzida_icms_st() if reducao_st > ZERO else calculator.calcular_base_icms_st()
412
+ amount_st = calculator.valor_icms_st_base_reduzida() if reducao_st > ZERO else calculator.valor_icms_st()
413
+ result.register_tax('icms', base=base_icms, rate=own_rate, amount=amount_icms, metadata={'type': 'Icms90'})
414
+ result.register_tax('icms_st', base=base_st, rate=st_rate, amount=amount_st, metadata={'type': 'Icms90'})
415
+ return
416
+
417
+ if cst == '101':
418
+ calculator = Icms101(percentual_credito_sn=credito_sn, percentual_reducao=reducao, **common)
419
+ result.register_tax('icms_credito_sn', base=calculator.calcular_base_icms_proprio(), rate=credito_sn, amount=calculator.valor_credito_sn(), metadata={'type': 'Icms101'})
420
+ return
421
+
422
+ if cst == '201':
423
+ calculator = Icms201(aliquota_icms_proprio=own_rate, aliquota_icms_st=st_rate, mva=mva, percentual_credito_sn=credito_sn, percentual_reducao=reducao, percentual_reducao_st=reducao_st, **common)
424
+ result.register_tax('icms', base=calculator.calcular_base_icms_proprio(), rate=own_rate, amount=calculator.valor_icms_proprio(), metadata={'type': 'Icms201'})
425
+ result.register_tax('icms_st', base=calculator.base_icms_st(), rate=st_rate, amount=calculator.valor_icms_st(), metadata={'type': 'Icms201'})
426
+ result.register_tax('icms_credito_sn', base=calculator.calcular_base_icms_proprio(), rate=credito_sn, amount=calculator.valor_credito_sn(), metadata={'type': 'Icms201'})
427
+ return
428
+
429
+ if cst in {'202', '203', '202_203'}:
430
+ calculator = Icms202_203(aliquota_icms_proprio=own_rate, aliquota_icms_st=st_rate, mva=mva, percentual_reducao=reducao, percentual_reducao_st=reducao_st, **common)
431
+ result.register_tax('icms', base=calculator.calcular_base_icms_proprio(), rate=own_rate, amount=calculator.valor_icms_proprio(), metadata={'type': 'Icms202_203'})
432
+ result.register_tax('icms_st', base=calculator.base_icms_st(), rate=st_rate, amount=calculator.valor_icms_st(), metadata={'type': 'Icms202_203'})
433
+ return
434
+
435
+ if cst == '900':
436
+ calculator = Icms900(valor_ipi=valor_ipi, aliquota_icms_proprio=own_rate, aliquota_icms_st=st_rate, mva=mva, percentual_credito_sn=credito_sn, percentual_reducao=reducao, percentual_reducao_st=reducao_st, **common)
437
+ base_icms = calculator.calcular_base_reduzida_icms_proprio() if reducao > ZERO else calculator.calcular_base_icms_proprio()
438
+ amount_icms = calculator.valor_icms_proprio_base_reduzida() if reducao > ZERO else calculator.valor_icms_proprio()
439
+ base_st = calculator.calcular_base_reduzida_icms_st() if reducao_st > ZERO else calculator.calcular_base_icms_st()
440
+ amount_st = calculator.valor_icms_st_base_reduzida() if reducao_st > ZERO else calculator.valor_icms_st()
441
+ result.register_tax('icms', base=base_icms, rate=own_rate, amount=amount_icms, metadata={'type': 'Icms900'})
442
+ result.register_tax('icms_st', base=base_st, rate=st_rate, amount=amount_st, metadata={'type': 'Icms900'})
443
+ if credito_sn > ZERO:
444
+ credit_base = base_icms
445
+ result.register_tax('icms_credito_sn', base=credit_base, rate=credito_sn, amount=calculator.valor_credito_sn(), metadata={'type': 'Icms900'})
446
+ return
447
+
448
+ raise ValueError(f'unsupported ICMS cst: {cst}')
449
+
450
+ def _calculate_fcp(self, context: TaxContext, result: TaxResult) -> None:
451
+ config = self._tax_details(context, 'fcp')
452
+ deferment_rate = _to_decimal(config.get('aliquota_diferimento_fcp'))
453
+
454
+ if 'icms_st' in result.bases and (config.get('use_st_base') or self._is_enabled(context, 'fcp_st')):
455
+ base = result.bases['icms_st']
456
+ rate = _to_decimal(config.get('aliquota_fcp_st'), self._resolve_rate('fcp_st', context))
457
+ calculator = FcpST(base_calculo_st=base, aliquota_fcp_st=rate)
458
+ amount = calculator.valor_fcp_st()
459
+ result.register_tax('fcp_st', base=base, rate=rate, amount=amount, metadata={'type': 'FcpST'})
460
+ return
461
+
462
+ base = result.bases.get('icms', BaseCalculator.from_context(context).base_padrao())
463
+ rate = _to_decimal(config.get('aliquota_fcp'), self._resolve_rate('fcp', context))
464
+ calculator = Fcp(base_calculo=base, aliquota_fcp=rate)
465
+ amount = calculator.valor_fcp()
466
+
467
+ if deferment_rate > ZERO:
468
+ deferred = FcpDiferido(valor_fcp=amount, aliquota_diferimento_fcp=deferment_rate)
469
+ effective = FcpEfetivo(valor_fcp=amount, valor_fcp_diferido=deferred.valor_fcp_diferido())
470
+ result.register_tax('fcp_diferido', base=base, rate=deferment_rate, amount=deferred.valor_fcp_diferido(), metadata={'type': 'FcpDiferido'})
471
+ amount = effective.valor_fcp_efetivo()
472
+
473
+ result.register_tax('fcp', base=base, rate=rate, amount=amount, metadata={'type': 'Fcp'})
474
+
475
+ def _calculate_pis(self, context: TaxContext, result: TaxResult) -> None:
476
+ values = context.values
477
+ config = self._tax_details(context, 'pis')
478
+ mode = self._mode(config, specific_key='aliquota_por_unidade')
479
+
480
+ if mode == 'specific':
481
+ base = _to_decimal(config.get('base_calculo'), values.quantity)
482
+ rate = _to_decimal(config.get('aliquota_por_unidade'), self._resolve_rate('pis', context))
483
+ calculator = Pis03(base_calculo=base, aliquota_por_unidade=rate)
484
+ amount = calculator.valor_pis()
485
+ else:
486
+ rate = _to_decimal(config.get('aliquota_pis'), self._resolve_rate('pis', context))
487
+ calculator = Pis01_02(
488
+ valor_produto=values.gross_value,
489
+ valor_frete=values.freight_value,
490
+ valor_seguro=values.insurance_value,
491
+ despesas_acessorias=values.other_expenses,
492
+ valor_desconto=values.discount_value,
493
+ aliquota_pis=rate,
494
+ valor_icms=result.amounts.get('icms', ZERO),
495
+ )
496
+ base = calculator.base_pis()
497
+ amount = calculator.valor_pis()
498
+
499
+ result.register_tax('pis', base=base, rate=rate, amount=amount, metadata={'mode': mode})
500
+
501
+ def _calculate_cofins(self, context: TaxContext, result: TaxResult) -> None:
502
+ values = context.values
503
+ config = self._tax_details(context, 'cofins')
504
+ mode = self._mode(config, specific_key='aliquota_por_unidade')
505
+
506
+ if mode == 'specific':
507
+ base = _to_decimal(config.get('base_calculo'), values.quantity)
508
+ rate = _to_decimal(config.get('aliquota_por_unidade'), self._resolve_rate('cofins', context))
509
+ calculator = Cofins03(base_calculo=base, aliquota_por_unidade=rate)
510
+ amount = calculator.valor_cofins()
511
+ else:
512
+ rate = _to_decimal(config.get('aliquota_cofins'), self._resolve_rate('cofins', context))
513
+ calculator = Cofins01_02(
514
+ valor_produto=values.gross_value,
515
+ valor_frete=values.freight_value,
516
+ valor_seguro=values.insurance_value,
517
+ despesas_acessorias=values.other_expenses,
518
+ valor_desconto=values.discount_value,
519
+ aliquota_cofins=rate,
520
+ valor_icms=result.amounts.get('icms', ZERO),
521
+ )
522
+ base = calculator.base_cofins()
523
+ amount = calculator.valor_cofins()
524
+
525
+ result.register_tax('cofins', base=base, rate=rate, amount=amount, metadata={'mode': mode})
526
+
527
+ def _calculate_ibs(self, context: TaxContext, result: TaxResult) -> None:
528
+ values = context.values
529
+ config = self._tax_details(context, 'ibs')
530
+ rate = _to_decimal(config.get('aliquota_efetiva_percentual'), self._resolve_rate('ibs', context))
531
+ deferment = _to_decimal(config.get('percentual_diferimento'))
532
+ calculator = IbsUf(
533
+ valor_produto=values.gross_value,
534
+ valor_frete=values.freight_value,
535
+ valor_seguro=values.insurance_value,
536
+ despesas_acessorias=values.other_expenses,
537
+ aliquota_efetiva_percentual=rate,
538
+ percentual_diferimento=deferment,
539
+ )
540
+ result.register_tax('ibs', base=calculator.valor_base_ibs_cbs(), rate=rate, amount=calculator.valor_ibs_uf(), metadata={'type': 'IbsUf'})
541
+
542
+ def _calculate_cbs(self, context: TaxContext, result: TaxResult) -> None:
543
+ values = context.values
544
+ config = self._tax_details(context, 'cbs')
545
+ rate = _to_decimal(config.get('aliquota_efetiva_percentual'), self._resolve_rate('cbs', context))
546
+ deferment = _to_decimal(config.get('percentual_diferimento'))
547
+ calculator = Cbs(
548
+ valor_produto=values.gross_value,
549
+ valor_frete=values.freight_value,
550
+ valor_seguro=values.insurance_value,
551
+ despesas_acessorias=values.other_expenses,
552
+ aliquota_efetiva_percentual=rate,
553
+ percentual_diferimento=deferment,
554
+ )
555
+ result.register_tax('cbs', base=calculator.valor_base_ibs_cbs(), rate=rate, amount=calculator.valor_cbs(), metadata={'type': 'Cbs'})
556
+
557
+ def _context_from_dict(self, payload: dict[str, Any]) -> TaxContext:
558
+ values_payload = payload.get('values', {})
559
+ taxes_payload = payload.get('taxes', {})
560
+
561
+ quantity = _to_decimal(values_payload.get('quantity'), Decimal('0'))
562
+ unit_price = _to_decimal(values_payload.get('unit_price'))
563
+ gross_value = values_payload.get('gross_value')
564
+ if gross_value is None and quantity > ZERO and unit_price > ZERO:
565
+ gross_value = (quantity * unit_price).quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN)
566
+
567
+ values = ValueContext(
568
+ quantity=quantity,
569
+ unit_price=unit_price,
570
+ gross_value=_to_decimal(gross_value),
571
+ discount_value=_to_decimal(values_payload.get('discount_value')),
572
+ freight_value=_to_decimal(values_payload.get('freight_value')),
573
+ insurance_value=_to_decimal(values_payload.get('insurance_value')),
574
+ other_expenses=_to_decimal(values_payload.get('other_expenses')),
575
+ )
576
+
577
+ rates = {
578
+ tax_name: _to_decimal(rate)
579
+ for tax_name, rate in taxes_payload.get('rates', {}).items()
580
+ }
581
+ taxes = dict(taxes_payload)
582
+ taxes['rates'] = rates
583
+
584
+ return TaxContext(
585
+ values=values,
586
+ taxes=taxes,
587
+ )
588
+
589
+ def _resolve_rate(self, tax_name: str, context: TaxContext) -> Decimal:
590
+ rate = context.get_rate(tax_name)
591
+ if rate is None:
592
+ return ZERO
593
+ return rate
594
+
595
+ def _tax_details(self, context: TaxContext, tax_name: str) -> dict[str, Any]:
596
+ return context.get_tax_detail(tax_name)
597
+
598
+ def _is_enabled(self, context: TaxContext, tax_name: str) -> bool:
599
+ return tax_name in self._raw_enabled_taxes(context)
600
+
601
+ def _raw_enabled_taxes(self, context: TaxContext) -> set[str]:
602
+ if not context.taxes:
603
+ return set()
604
+
605
+ enabled_raw = context.taxes.get('enabled', [])
606
+ enabled = set(enabled_raw if isinstance(enabled_raw, list) else [])
607
+ rates = context.taxes.get('rates', {})
608
+ if not enabled:
609
+ if isinstance(rates, dict):
610
+ enabled.update(rates)
611
+ enabled.update(
612
+ name
613
+ for name, config in context.taxes.items()
614
+ if isinstance(config, dict) and name != 'rates' and config.get('enabled', True)
615
+ )
616
+
617
+ return {tax_name for tax_name in enabled if tax_name in VALID_TAX_NAMES}
618
+
619
+ def _mode(self, config: dict[str, Any], *, specific_key: str) -> str:
620
+ raw_mode = str(config.get('mode', '')).strip().lower()
621
+ if raw_mode in {'specific', 'especifico', 'specifico'} or specific_key in config:
622
+ return 'specific'
623
+ return 'ad_valorem'
@@ -0,0 +1,20 @@
1
+ from .ipi import (
2
+ BaseIPI,
3
+ Ipi50AdValorem,
4
+ Ipi50Especifico,
5
+ )
6
+ from .pis import (
7
+ BasePIS,
8
+ Pis01_02,
9
+ Pis03,
10
+ )
11
+ from .cofins import (
12
+ BaseCofins,
13
+ Cofins01_02,
14
+ Cofins03,
15
+ )
16
+ from .ibs_cbs import (
17
+ BaseIbsCbs,
18
+ Cbs,
19
+ IbsUf,
20
+ )