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.
- tributus_engine/__init__.py +9 -0
- tributus_engine/engine.py +623 -0
- tributus_engine/implementations/__init__.py +20 -0
- tributus_engine/implementations/cofins.py +56 -0
- tributus_engine/implementations/ibs_cbs.py +103 -0
- tributus_engine/implementations/icms/__init__.py +27 -0
- tributus_engine/implementations/icms/base.py +84 -0
- tributus_engine/implementations/icms/cst.py +515 -0
- tributus_engine/implementations/icms/fcp.py +36 -0
- tributus_engine/implementations/ipi.py +49 -0
- tributus_engine/implementations/pis.py +56 -0
- tributus_engine/interfaces/__init__.py +25 -0
- tributus_engine/interfaces/protocols.py +124 -0
- tributus_engine-0.2.0.dist-info/METADATA +186 -0
- tributus_engine-0.2.0.dist-info/RECORD +17 -0
- tributus_engine-0.2.0.dist-info/WHEEL +5 -0
- tributus_engine-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -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
|
+
)
|