Watch
1
0
Fork
You've already forked Seyyed_arc
0
forked from hesabix/arc
Seyyed_arc/hesabixAPI/app/services/payroll_statutory.py

166 lines
5.1 KiB
Python

"""
قوانین بیمه و مالیات حقوق (فاز ۴).
تنظیمات در payroll_settings.extra_settings.statutory_rules ذخیره می‌شود.
"""
from __future__ import annotations
from decimal import Decimal
from typing import Any, Dict, List, Optional, Tuple
from adapters.db.models.payroll import PayrollItemDefinition, PayrollSettings
DEFAULT_STATUTORY_RULES: Dict[str, Any] = {
"enabled": False,
"insurance": {
"employee_rate_percent": 7,
"employer_rate_percent": 20,
"unemployment_employer_rate_percent": 3,
"ceiling_amount": None,
},
"tax": {
"mode": "flat_percent",
"flat_rate_percent": 10,
"exemption_amount": 0,
"brackets": [
{"up_to": 50000000, "rate_percent": 0},
{"up_to": 100000000, "rate_percent": 10},
{"up_to": 200000000, "rate_percent": 15},
{"up_to": None, "rate_percent": 20},
],
},
}
_STATUTORY_ITEM_CODES = frozenset({"insurance_employee", "insurance_employer", "tax"})
def get_statutory_rules(settings: PayrollSettings) -> Dict[str, Any]:
extra = settings.extra_settings if isinstance(settings.extra_settings, dict) else {}
raw = extra.get("statutory_rules") if isinstance(extra.get("statutory_rules"), dict) else {}
rules = {**DEFAULT_STATUTORY_RULES, **raw}
ins = {**DEFAULT_STATUTORY_RULES["insurance"], **(raw.get("insurance") or {})}
tax = {**DEFAULT_STATUTORY_RULES["tax"], **(raw.get("tax") or {})}
rules["insurance"] = ins
rules["tax"] = tax
return rules
def _quantize(amount: Decimal, decimals: int) -> Decimal:
if decimals <= 0:
return amount.quantize(Decimal("1"))
step = Decimal(10) ** (-int(decimals))
return amount.quantize(step)
def _insurable_base(
items: List[Tuple[PayrollItemDefinition, Decimal, bool]],
) -> Decimal:
total = Decimal("0")
for item_def, amount, _ in items:
if item_def.item_kind == "earning" and item_def.affects_insurance:
total += amount
return total
def _taxable_base(
items: List[Tuple[PayrollItemDefinition, Decimal, bool]],
) -> Decimal:
total = Decimal("0")
for item_def, amount, _ in items:
if item_def.item_kind == "earning" and (item_def.is_taxable or item_def.affects_taxable):
total += amount
return total
def _apply_ceiling(base: Decimal, ceiling: Any) -> Decimal:
if ceiling is None or ceiling == "":
return base
try:
cap = Decimal(str(ceiling))
except Exception:
return base
return min(base, cap) if cap > 0 else base
def compute_employee_insurance(base: Decimal, rules: Dict[str, Any]) -> Decimal:
ins = rules.get("insurance") or {}
rate = Decimal(str(ins.get("employee_rate_percent") or 0))
return base * rate / Decimal("100")
def compute_employer_insurance(base: Decimal, rules: Dict[str, Any]) -> Decimal:
ins = rules.get("insurance") or {}
er = Decimal(str(ins.get("employer_rate_percent") or 0))
ur = Decimal(str(ins.get("unemployment_employer_rate_percent") or 0))
return base * (er + ur) / Decimal("100")
def compute_income_tax(taxable_base: Decimal, rules: Dict[str, Any]) -> Decimal:
tax_rules = rules.get("tax") or {}
exemption = Decimal(str(tax_rules.get("exemption_amount") or 0))
base = max(Decimal("0"), taxable_base - exemption)
if base <= 0:
return Decimal("0")
mode = str(tax_rules.get("mode") or "flat_percent")
if mode == "flat_percent":
rate = Decimal(str(tax_rules.get("flat_rate_percent") or 0))
return base * rate / Decimal("100")
brackets = tax_rules.get("brackets") or DEFAULT_STATUTORY_RULES["tax"]["brackets"]
remaining = base
tax_total = Decimal("0")
prev_limit = Decimal("0")
for bracket in brackets:
up_to_raw = bracket.get("up_to")
rate = Decimal(str(bracket.get("rate_percent") or 0))
if up_to_raw is None:
tax_total += remaining * rate / Decimal("100")
break
limit = Decimal(str(up_to_raw))
chunk = min(remaining, limit - prev_limit)
if chunk <= 0:
prev_limit = limit
continue
tax_total += chunk * rate / Decimal("100")
remaining -= chunk
prev_limit = limit
if remaining <= 0:
break
return tax_total
def apply_statutory_rules(
pending: List[Tuple[PayrollItemDefinition, Decimal, bool]],
explicit_by_id: Dict[int, Any],
settings: PayrollSettings,
) -> List[Tuple[PayrollItemDefinition, Decimal, bool]]:
rules = get_statutory_rules(settings)
if not rules.get("enabled"):
return pending
rt = int(settings.round_amounts_to or 0)
ins_base = _apply_ceiling(
_insurable_base(pending),
(rules.get("insurance") or {}).get("ceiling_amount"),
)
tax_base = _taxable_base(pending)
statutory_amounts = {
"insurance_employee": compute_employee_insurance(ins_base, rules),
"insurance_employer": compute_employer_insurance(ins_base, rules),
"tax": compute_income_tax(tax_base, rules),
}
out: List[Tuple[PayrollItemDefinition, Decimal, bool]] = []
for item_def, amount, computed in pending:
if item_def.id in explicit_by_id and explicit_by_id[item_def.id] not in (None, ""):
out.append((item_def, amount, computed))
continue
if item_def.code in _STATUTORY_ITEM_CODES or item_def.calculation_type == "statutory":
override = statutory_amounts.get(item_def.code)
if override is not None:
out.append((item_def, _quantize(override, rt), True))
continue
out.append((item_def, amount, computed))
return out