coati-payroll 0.0.2__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 coati-payroll might be problematic. Click here for more details.
- coati_payroll/__init__.py +415 -0
- coati_payroll/app.py +95 -0
- coati_payroll/audit_helpers.py +904 -0
- coati_payroll/auth.py +123 -0
- coati_payroll/cli.py +1318 -0
- coati_payroll/config.py +219 -0
- coati_payroll/demo_data.py +813 -0
- coati_payroll/enums.py +278 -0
- coati_payroll/forms.py +1769 -0
- coati_payroll/formula_engine/__init__.py +81 -0
- coati_payroll/formula_engine/ast/__init__.py +110 -0
- coati_payroll/formula_engine/ast/ast_visitor.py +259 -0
- coati_payroll/formula_engine/ast/expression_evaluator.py +228 -0
- coati_payroll/formula_engine/ast/safe_operators.py +131 -0
- coati_payroll/formula_engine/ast/type_converter.py +172 -0
- coati_payroll/formula_engine/data_sources.py +752 -0
- coati_payroll/formula_engine/engine.py +247 -0
- coati_payroll/formula_engine/exceptions.py +52 -0
- coati_payroll/formula_engine/execution/__init__.py +24 -0
- coati_payroll/formula_engine/execution/execution_context.py +52 -0
- coati_payroll/formula_engine/execution/step_executor.py +62 -0
- coati_payroll/formula_engine/execution/variable_store.py +59 -0
- coati_payroll/formula_engine/novelty_codes.py +206 -0
- coati_payroll/formula_engine/results/__init__.py +20 -0
- coati_payroll/formula_engine/results/execution_result.py +59 -0
- coati_payroll/formula_engine/steps/__init__.py +30 -0
- coati_payroll/formula_engine/steps/assignment_step.py +71 -0
- coati_payroll/formula_engine/steps/base_step.py +48 -0
- coati_payroll/formula_engine/steps/calculation_step.py +42 -0
- coati_payroll/formula_engine/steps/conditional_step.py +122 -0
- coati_payroll/formula_engine/steps/step_factory.py +58 -0
- coati_payroll/formula_engine/steps/tax_lookup_step.py +45 -0
- coati_payroll/formula_engine/tables/__init__.py +24 -0
- coati_payroll/formula_engine/tables/bracket_calculator.py +51 -0
- coati_payroll/formula_engine/tables/table_lookup.py +161 -0
- coati_payroll/formula_engine/tables/tax_table.py +32 -0
- coati_payroll/formula_engine/validation/__init__.py +24 -0
- coati_payroll/formula_engine/validation/schema_validator.py +37 -0
- coati_payroll/formula_engine/validation/security_validator.py +52 -0
- coati_payroll/formula_engine/validation/tax_table_validator.py +205 -0
- coati_payroll/formula_engine_examples.py +153 -0
- coati_payroll/i18n.py +54 -0
- coati_payroll/initial_data.py +613 -0
- coati_payroll/interes_engine.py +450 -0
- coati_payroll/liquidacion_engine/__init__.py +25 -0
- coati_payroll/liquidacion_engine/engine.py +267 -0
- coati_payroll/locale_config.py +165 -0
- coati_payroll/log.py +138 -0
- coati_payroll/model.py +2410 -0
- coati_payroll/nomina_engine/__init__.py +87 -0
- coati_payroll/nomina_engine/calculators/__init__.py +30 -0
- coati_payroll/nomina_engine/calculators/benefit_calculator.py +79 -0
- coati_payroll/nomina_engine/calculators/concept_calculator.py +254 -0
- coati_payroll/nomina_engine/calculators/deduction_calculator.py +105 -0
- coati_payroll/nomina_engine/calculators/exchange_rate_calculator.py +51 -0
- coati_payroll/nomina_engine/calculators/perception_calculator.py +75 -0
- coati_payroll/nomina_engine/calculators/salary_calculator.py +86 -0
- coati_payroll/nomina_engine/domain/__init__.py +27 -0
- coati_payroll/nomina_engine/domain/calculation_items.py +52 -0
- coati_payroll/nomina_engine/domain/employee_calculation.py +53 -0
- coati_payroll/nomina_engine/domain/payroll_context.py +44 -0
- coati_payroll/nomina_engine/engine.py +188 -0
- coati_payroll/nomina_engine/processors/__init__.py +28 -0
- coati_payroll/nomina_engine/processors/accounting_processor.py +171 -0
- coati_payroll/nomina_engine/processors/accumulation_processor.py +90 -0
- coati_payroll/nomina_engine/processors/loan_processor.py +227 -0
- coati_payroll/nomina_engine/processors/novelty_processor.py +42 -0
- coati_payroll/nomina_engine/processors/vacation_processor.py +67 -0
- coati_payroll/nomina_engine/repositories/__init__.py +32 -0
- coati_payroll/nomina_engine/repositories/acumulado_repository.py +83 -0
- coati_payroll/nomina_engine/repositories/base_repository.py +40 -0
- coati_payroll/nomina_engine/repositories/config_repository.py +102 -0
- coati_payroll/nomina_engine/repositories/employee_repository.py +34 -0
- coati_payroll/nomina_engine/repositories/exchange_rate_repository.py +58 -0
- coati_payroll/nomina_engine/repositories/novelty_repository.py +54 -0
- coati_payroll/nomina_engine/repositories/planilla_repository.py +52 -0
- coati_payroll/nomina_engine/results/__init__.py +24 -0
- coati_payroll/nomina_engine/results/error_result.py +28 -0
- coati_payroll/nomina_engine/results/payroll_result.py +53 -0
- coati_payroll/nomina_engine/results/validation_result.py +39 -0
- coati_payroll/nomina_engine/services/__init__.py +22 -0
- coati_payroll/nomina_engine/services/accounting_voucher_service.py +708 -0
- coati_payroll/nomina_engine/services/employee_processing_service.py +173 -0
- coati_payroll/nomina_engine/services/payroll_execution_service.py +374 -0
- coati_payroll/nomina_engine/services/snapshot_service.py +295 -0
- coati_payroll/nomina_engine/validators/__init__.py +31 -0
- coati_payroll/nomina_engine/validators/base_validator.py +48 -0
- coati_payroll/nomina_engine/validators/currency_validator.py +50 -0
- coati_payroll/nomina_engine/validators/employee_validator.py +87 -0
- coati_payroll/nomina_engine/validators/period_validator.py +44 -0
- coati_payroll/nomina_engine/validators/planilla_validator.py +136 -0
- coati_payroll/plugin_manager.py +176 -0
- coati_payroll/queue/__init__.py +33 -0
- coati_payroll/queue/driver.py +127 -0
- coati_payroll/queue/drivers/__init__.py +22 -0
- coati_payroll/queue/drivers/dramatiq_driver.py +268 -0
- coati_payroll/queue/drivers/huey_driver.py +390 -0
- coati_payroll/queue/drivers/noop_driver.py +54 -0
- coati_payroll/queue/selector.py +121 -0
- coati_payroll/queue/tasks.py +764 -0
- coati_payroll/rate_limiting.py +83 -0
- coati_payroll/rbac.py +183 -0
- coati_payroll/report_engine.py +512 -0
- coati_payroll/report_export.py +208 -0
- coati_payroll/schema_validator.py +167 -0
- coati_payroll/security.py +77 -0
- coati_payroll/static/styles.css +1044 -0
- coati_payroll/system_reports.py +573 -0
- coati_payroll/templates/auth/login.html +189 -0
- coati_payroll/templates/base.html +283 -0
- coati_payroll/templates/index.html +227 -0
- coati_payroll/templates/macros.html +146 -0
- coati_payroll/templates/modules/calculation_rule/form.html +78 -0
- coati_payroll/templates/modules/calculation_rule/index.html +102 -0
- coati_payroll/templates/modules/calculation_rule/schema_editor.html +1159 -0
- coati_payroll/templates/modules/carga_inicial_prestacion/form.html +170 -0
- coati_payroll/templates/modules/carga_inicial_prestacion/index.html +170 -0
- coati_payroll/templates/modules/carga_inicial_prestacion/reporte.html +193 -0
- coati_payroll/templates/modules/config_calculos/index.html +44 -0
- coati_payroll/templates/modules/configuracion/index.html +90 -0
- coati_payroll/templates/modules/currency/form.html +47 -0
- coati_payroll/templates/modules/currency/index.html +64 -0
- coati_payroll/templates/modules/custom_field/form.html +62 -0
- coati_payroll/templates/modules/custom_field/index.html +78 -0
- coati_payroll/templates/modules/deduccion/form.html +1 -0
- coati_payroll/templates/modules/deduccion/index.html +1 -0
- coati_payroll/templates/modules/employee/form.html +254 -0
- coati_payroll/templates/modules/employee/index.html +76 -0
- coati_payroll/templates/modules/empresa/form.html +74 -0
- coati_payroll/templates/modules/empresa/index.html +71 -0
- coati_payroll/templates/modules/exchange_rate/form.html +47 -0
- coati_payroll/templates/modules/exchange_rate/import.html +93 -0
- coati_payroll/templates/modules/exchange_rate/index.html +114 -0
- coati_payroll/templates/modules/liquidacion/index.html +58 -0
- coati_payroll/templates/modules/liquidacion/nueva.html +51 -0
- coati_payroll/templates/modules/liquidacion/ver.html +91 -0
- coati_payroll/templates/modules/payroll_concepts/audit_log.html +146 -0
- coati_payroll/templates/modules/percepcion/form.html +1 -0
- coati_payroll/templates/modules/percepcion/index.html +1 -0
- coati_payroll/templates/modules/planilla/config.html +190 -0
- coati_payroll/templates/modules/planilla/config_deducciones.html +129 -0
- coati_payroll/templates/modules/planilla/config_empleados.html +116 -0
- coati_payroll/templates/modules/planilla/config_percepciones.html +113 -0
- coati_payroll/templates/modules/planilla/config_prestaciones.html +118 -0
- coati_payroll/templates/modules/planilla/config_reglas.html +120 -0
- coati_payroll/templates/modules/planilla/ejecutar_nomina.html +106 -0
- coati_payroll/templates/modules/planilla/form.html +197 -0
- coati_payroll/templates/modules/planilla/index.html +144 -0
- coati_payroll/templates/modules/planilla/listar_nominas.html +91 -0
- coati_payroll/templates/modules/planilla/log_nomina.html +135 -0
- coati_payroll/templates/modules/planilla/novedades/form.html +177 -0
- coati_payroll/templates/modules/planilla/novedades/index.html +170 -0
- coati_payroll/templates/modules/planilla/ver_nomina.html +477 -0
- coati_payroll/templates/modules/planilla/ver_nomina_empleado.html +231 -0
- coati_payroll/templates/modules/plugins/index.html +71 -0
- coati_payroll/templates/modules/prestacion/form.html +1 -0
- coati_payroll/templates/modules/prestacion/index.html +1 -0
- coati_payroll/templates/modules/prestacion_management/dashboard.html +150 -0
- coati_payroll/templates/modules/prestacion_management/initial_balance_bulk.html +195 -0
- coati_payroll/templates/modules/prestamo/approve.html +156 -0
- coati_payroll/templates/modules/prestamo/condonacion.html +249 -0
- coati_payroll/templates/modules/prestamo/detail.html +443 -0
- coati_payroll/templates/modules/prestamo/form.html +203 -0
- coati_payroll/templates/modules/prestamo/index.html +150 -0
- coati_payroll/templates/modules/prestamo/pago_extraordinario.html +211 -0
- coati_payroll/templates/modules/prestamo/tabla_pago_pdf.html +181 -0
- coati_payroll/templates/modules/report/admin_index.html +125 -0
- coati_payroll/templates/modules/report/detail.html +129 -0
- coati_payroll/templates/modules/report/execute.html +266 -0
- coati_payroll/templates/modules/report/index.html +95 -0
- coati_payroll/templates/modules/report/permissions.html +64 -0
- coati_payroll/templates/modules/settings/index.html +274 -0
- coati_payroll/templates/modules/shared/concept_form.html +201 -0
- coati_payroll/templates/modules/shared/concept_index.html +145 -0
- coati_payroll/templates/modules/tipo_planilla/form.html +70 -0
- coati_payroll/templates/modules/tipo_planilla/index.html +68 -0
- coati_payroll/templates/modules/user/form.html +65 -0
- coati_payroll/templates/modules/user/index.html +76 -0
- coati_payroll/templates/modules/user/profile.html +81 -0
- coati_payroll/templates/modules/vacation/account_detail.html +149 -0
- coati_payroll/templates/modules/vacation/account_form.html +52 -0
- coati_payroll/templates/modules/vacation/account_index.html +68 -0
- coati_payroll/templates/modules/vacation/dashboard.html +156 -0
- coati_payroll/templates/modules/vacation/initial_balance_bulk.html +149 -0
- coati_payroll/templates/modules/vacation/initial_balance_form.html +93 -0
- coati_payroll/templates/modules/vacation/leave_request_detail.html +158 -0
- coati_payroll/templates/modules/vacation/leave_request_form.html +61 -0
- coati_payroll/templates/modules/vacation/leave_request_index.html +98 -0
- coati_payroll/templates/modules/vacation/policy_detail.html +176 -0
- coati_payroll/templates/modules/vacation/policy_form.html +152 -0
- coati_payroll/templates/modules/vacation/policy_index.html +79 -0
- coati_payroll/templates/modules/vacation/register_taken_form.html +178 -0
- coati_payroll/translations/en/LC_MESSAGES/messages.mo +0 -0
- coati_payroll/translations/en/LC_MESSAGES/messages.po +7283 -0
- coati_payroll/translations/es/LC_MESSAGES/messages.mo +0 -0
- coati_payroll/translations/es/LC_MESSAGES/messages.po +7374 -0
- coati_payroll/vacation_service.py +451 -0
- coati_payroll/version.py +18 -0
- coati_payroll/vistas/__init__.py +64 -0
- coati_payroll/vistas/calculation_rule.py +307 -0
- coati_payroll/vistas/carga_inicial_prestacion.py +423 -0
- coati_payroll/vistas/config_calculos.py +72 -0
- coati_payroll/vistas/configuracion.py +87 -0
- coati_payroll/vistas/constants.py +17 -0
- coati_payroll/vistas/currency.py +112 -0
- coati_payroll/vistas/custom_field.py +120 -0
- coati_payroll/vistas/employee.py +305 -0
- coati_payroll/vistas/empresa.py +153 -0
- coati_payroll/vistas/exchange_rate.py +341 -0
- coati_payroll/vistas/liquidacion.py +205 -0
- coati_payroll/vistas/payroll_concepts.py +580 -0
- coati_payroll/vistas/planilla/__init__.py +38 -0
- coati_payroll/vistas/planilla/association_routes.py +238 -0
- coati_payroll/vistas/planilla/config_routes.py +158 -0
- coati_payroll/vistas/planilla/export_routes.py +175 -0
- coati_payroll/vistas/planilla/helpers/__init__.py +34 -0
- coati_payroll/vistas/planilla/helpers/association_helpers.py +161 -0
- coati_payroll/vistas/planilla/helpers/excel_helpers.py +29 -0
- coati_payroll/vistas/planilla/helpers/form_helpers.py +97 -0
- coati_payroll/vistas/planilla/nomina_routes.py +488 -0
- coati_payroll/vistas/planilla/novedad_routes.py +227 -0
- coati_payroll/vistas/planilla/routes.py +145 -0
- coati_payroll/vistas/planilla/services/__init__.py +26 -0
- coati_payroll/vistas/planilla/services/export_service.py +687 -0
- coati_payroll/vistas/planilla/services/nomina_service.py +233 -0
- coati_payroll/vistas/planilla/services/novedad_service.py +126 -0
- coati_payroll/vistas/planilla/services/planilla_service.py +34 -0
- coati_payroll/vistas/planilla/validators/__init__.py +18 -0
- coati_payroll/vistas/planilla/validators/planilla_validators.py +40 -0
- coati_payroll/vistas/plugins.py +45 -0
- coati_payroll/vistas/prestacion.py +272 -0
- coati_payroll/vistas/prestamo.py +808 -0
- coati_payroll/vistas/report.py +432 -0
- coati_payroll/vistas/settings.py +29 -0
- coati_payroll/vistas/tipo_planilla.py +134 -0
- coati_payroll/vistas/user.py +172 -0
- coati_payroll/vistas/vacation.py +1045 -0
- coati_payroll-0.0.2.dist-info/LICENSE +201 -0
- coati_payroll-0.0.2.dist-info/METADATA +581 -0
- coati_payroll-0.0.2.dist-info/RECORD +243 -0
- coati_payroll-0.0.2.dist-info/WHEEL +5 -0
- coati_payroll-0.0.2.dist-info/entry_points.txt +2 -0
- coati_payroll-0.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Copyright 2025 BMO Soluciones, S.A.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""Safe operators and functions for expression evaluation.
|
|
15
|
+
|
|
16
|
+
This module defines the ONLY operators and functions allowed in formula expressions.
|
|
17
|
+
It implements a whitelist-based security model to prevent arbitrary code execution.
|
|
18
|
+
|
|
19
|
+
Security Guarantees:
|
|
20
|
+
- Only mathematical operations are allowed (no I/O, no imports, no system calls)
|
|
21
|
+
- No access to Python builtins beyond explicitly whitelisted functions
|
|
22
|
+
- No attribute access or dynamic code execution
|
|
23
|
+
- All operations are deterministic and side-effect free
|
|
24
|
+
- Expression complexity is bounded to prevent DoS attacks
|
|
25
|
+
|
|
26
|
+
Allowed Operations:
|
|
27
|
+
- Arithmetic: +, -, *, /, //, %, **
|
|
28
|
+
- Functions: min, max, abs, round
|
|
29
|
+
- Variables: Only pre-defined variables from the execution context
|
|
30
|
+
- Constants: Numeric literals only
|
|
31
|
+
|
|
32
|
+
Prohibited Operations:
|
|
33
|
+
- File I/O, network access, system calls
|
|
34
|
+
- Import statements, eval, exec, compile
|
|
35
|
+
- Attribute access (__getattr__, __setattr__, etc.)
|
|
36
|
+
- Lambda functions, list comprehensions
|
|
37
|
+
- Class definitions, decorators
|
|
38
|
+
- Any Python builtin not explicitly whitelisted
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import ast
|
|
44
|
+
import operator
|
|
45
|
+
from typing import Any, Callable
|
|
46
|
+
|
|
47
|
+
MAX_EXPRESSION_LENGTH = 1000
|
|
48
|
+
MAX_AST_DEPTH = 50
|
|
49
|
+
MAX_FUNCTION_ARGS = 20
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def validate_safe_function_call(func_name: str, args: list[Any]) -> None:
|
|
53
|
+
"""Validate that a function call is safe.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
func_name: Name of the function being called
|
|
57
|
+
args: Arguments passed to the function
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
ValueError: If the function call is unsafe
|
|
61
|
+
"""
|
|
62
|
+
if func_name not in SAFE_FUNCTIONS:
|
|
63
|
+
raise ValueError(
|
|
64
|
+
f"Function '{func_name}' is not in the whitelist. " f"Allowed functions: {', '.join(SAFE_FUNCTIONS.keys())}"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if len(args) > MAX_FUNCTION_ARGS:
|
|
68
|
+
raise ValueError(
|
|
69
|
+
f"Too many arguments ({len(args)}) for function '{func_name}'. " f"Maximum allowed: {MAX_FUNCTION_ARGS}"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if func_name == "round" and len(args) > 2:
|
|
73
|
+
raise ValueError("round() accepts at most 2 arguments")
|
|
74
|
+
|
|
75
|
+
if func_name in ("min", "max") and len(args) < 1:
|
|
76
|
+
raise ValueError(f"{func_name}() requires at least 1 argument")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# Safe operators for expression evaluation
|
|
80
|
+
SAFE_OPERATORS = {
|
|
81
|
+
ast.Add: operator.add,
|
|
82
|
+
ast.Sub: operator.sub,
|
|
83
|
+
ast.Mult: operator.mul,
|
|
84
|
+
ast.Div: operator.truediv,
|
|
85
|
+
ast.FloorDiv: operator.floordiv,
|
|
86
|
+
ast.Mod: operator.mod,
|
|
87
|
+
ast.Pow: operator.pow,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Safe comparison operators for conditional evaluation
|
|
91
|
+
COMPARISON_OPERATORS: dict[str, Callable[[Any, Any], bool]] = {
|
|
92
|
+
">": operator.gt,
|
|
93
|
+
">=": operator.ge,
|
|
94
|
+
"<": operator.lt,
|
|
95
|
+
"<=": operator.le,
|
|
96
|
+
"==": operator.eq,
|
|
97
|
+
"!=": operator.ne,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Safe functions for calculations - WHITELIST ONLY
|
|
101
|
+
# These are the ONLY functions allowed in formula expressions.
|
|
102
|
+
# Adding functions here requires security review.
|
|
103
|
+
SAFE_FUNCTIONS: dict[str, Callable[..., Any]] = {
|
|
104
|
+
"min": min,
|
|
105
|
+
"max": max,
|
|
106
|
+
"abs": abs,
|
|
107
|
+
"round": round,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# Allowed AST node types for security validation - WHITELIST ONLY
|
|
111
|
+
# These are the ONLY AST node types allowed in parsed expressions.
|
|
112
|
+
# Any other node type will be rejected as a security violation.
|
|
113
|
+
# This prevents code injection, attribute access, imports, etc.
|
|
114
|
+
ALLOWED_AST_TYPES: tuple[type[ast.AST], ...] = (
|
|
115
|
+
ast.Expression,
|
|
116
|
+
ast.Constant,
|
|
117
|
+
ast.Name,
|
|
118
|
+
ast.Load,
|
|
119
|
+
ast.BinOp,
|
|
120
|
+
ast.UnaryOp,
|
|
121
|
+
ast.Call,
|
|
122
|
+
ast.Add,
|
|
123
|
+
ast.Sub,
|
|
124
|
+
ast.Mult,
|
|
125
|
+
ast.Div,
|
|
126
|
+
ast.FloorDiv,
|
|
127
|
+
ast.Mod,
|
|
128
|
+
ast.Pow,
|
|
129
|
+
ast.UAdd,
|
|
130
|
+
ast.USub,
|
|
131
|
+
)
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Copyright 2025 BMO Soluciones, S.A.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""Type conversion utilities for formula engine.
|
|
15
|
+
|
|
16
|
+
This module provides safe type conversion functions that maintain financial
|
|
17
|
+
precision and prevent common security issues:
|
|
18
|
+
|
|
19
|
+
1. Decimal Precision: All numeric values are converted to Decimal to avoid
|
|
20
|
+
floating-point precision errors in financial calculations.
|
|
21
|
+
|
|
22
|
+
2. Input Validation: Values are validated to prevent:
|
|
23
|
+
- Infinity and NaN values
|
|
24
|
+
- Extremely large numbers that could cause DoS
|
|
25
|
+
- Invalid type conversions
|
|
26
|
+
|
|
27
|
+
3. Safe Division: Division by zero is handled gracefully by returning 0
|
|
28
|
+
instead of raising an exception.
|
|
29
|
+
|
|
30
|
+
Security Considerations:
|
|
31
|
+
- All conversions are deterministic and side-effect free
|
|
32
|
+
- No dynamic type inspection or attribute access
|
|
33
|
+
- Bounded numeric ranges prevent resource exhaustion
|
|
34
|
+
- Explicit error messages aid in debugging and auditing
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
from decimal import Decimal, InvalidOperation
|
|
40
|
+
from typing import Any
|
|
41
|
+
import math
|
|
42
|
+
|
|
43
|
+
from ..exceptions import ValidationError
|
|
44
|
+
|
|
45
|
+
MAX_DECIMAL_DIGITS = 28
|
|
46
|
+
MAX_DECIMAL_VALUE = Decimal("9" * MAX_DECIMAL_DIGITS)
|
|
47
|
+
MIN_DECIMAL_VALUE = -MAX_DECIMAL_VALUE
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def to_decimal(value: Any) -> Decimal:
|
|
51
|
+
"""Safely convert a value to Decimal with validation.
|
|
52
|
+
|
|
53
|
+
This function converts various Python types to Decimal while enforcing
|
|
54
|
+
security constraints:
|
|
55
|
+
- Rejects infinity and NaN values
|
|
56
|
+
- Validates numeric ranges to prevent DoS
|
|
57
|
+
- Maintains financial precision
|
|
58
|
+
- Provides clear error messages
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
value: Value to convert. Supported types:
|
|
62
|
+
- None: converts to 0
|
|
63
|
+
- Decimal: returned as-is (after validation)
|
|
64
|
+
- bool: True -> 1, False -> 0
|
|
65
|
+
- int, float, str: converted to Decimal
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Decimal representation of the value
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
ValidationError: If value cannot be safely converted to Decimal
|
|
72
|
+
|
|
73
|
+
Examples:
|
|
74
|
+
>>> to_decimal(42)
|
|
75
|
+
Decimal('42')
|
|
76
|
+
>>> to_decimal('123.45')
|
|
77
|
+
Decimal('123.45')
|
|
78
|
+
>>> to_decimal(True)
|
|
79
|
+
Decimal('1')
|
|
80
|
+
>>> to_decimal(None)
|
|
81
|
+
Decimal('0')
|
|
82
|
+
"""
|
|
83
|
+
if value is None:
|
|
84
|
+
return Decimal("0")
|
|
85
|
+
|
|
86
|
+
if isinstance(value, Decimal):
|
|
87
|
+
_validate_decimal_range(value)
|
|
88
|
+
return value
|
|
89
|
+
|
|
90
|
+
if isinstance(value, bool):
|
|
91
|
+
return Decimal("1") if value else Decimal("0")
|
|
92
|
+
|
|
93
|
+
if isinstance(value, float):
|
|
94
|
+
if math.isnan(value):
|
|
95
|
+
raise ValidationError(
|
|
96
|
+
"Cannot convert NaN (Not a Number) to Decimal. "
|
|
97
|
+
"This may indicate a calculation error in the input data."
|
|
98
|
+
)
|
|
99
|
+
if math.isinf(value):
|
|
100
|
+
raise ValidationError(
|
|
101
|
+
"Cannot convert infinity to Decimal. " "Check for division by zero or overflow in input calculations."
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
result = Decimal(str(value))
|
|
106
|
+
_validate_decimal_range(result)
|
|
107
|
+
return result
|
|
108
|
+
except (InvalidOperation, ValueError) as e:
|
|
109
|
+
raise ValidationError(
|
|
110
|
+
f"Cannot convert value '{value}' (type: {type(value).__name__}) to Decimal: {e}. "
|
|
111
|
+
"Ensure the value is a valid number."
|
|
112
|
+
) from e
|
|
113
|
+
except Exception as e:
|
|
114
|
+
raise ValidationError(f"Unexpected error converting '{value}' to Decimal: {e}") from e
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _validate_decimal_range(value: Decimal) -> None:
|
|
118
|
+
"""Validate that a Decimal value is within acceptable range.
|
|
119
|
+
|
|
120
|
+
This prevents denial-of-service attacks via extremely large numbers
|
|
121
|
+
that could consume excessive memory or CPU during calculations.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
value: Decimal value to validate
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
ValidationError: If value is outside acceptable range
|
|
128
|
+
"""
|
|
129
|
+
if value.is_nan():
|
|
130
|
+
raise ValidationError("Decimal value is NaN (Not a Number). " "This indicates an invalid calculation result.")
|
|
131
|
+
|
|
132
|
+
if value.is_infinite():
|
|
133
|
+
raise ValidationError("Decimal value is infinite. " "This indicates an overflow or division by zero.")
|
|
134
|
+
|
|
135
|
+
if value > MAX_DECIMAL_VALUE or value < MIN_DECIMAL_VALUE:
|
|
136
|
+
raise ValidationError(
|
|
137
|
+
f"Decimal value {value} is outside acceptable range "
|
|
138
|
+
f"[{MIN_DECIMAL_VALUE}, {MAX_DECIMAL_VALUE}]. "
|
|
139
|
+
f"This limit ({MAX_DECIMAL_DIGITS} digits) prevents resource exhaustion attacks."
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def safe_divide(numerator: Decimal, denominator: Decimal) -> Decimal:
|
|
144
|
+
"""Safely divide two decimals, handling division by zero.
|
|
145
|
+
|
|
146
|
+
This function provides safe division for payroll calculations where
|
|
147
|
+
division by zero should return 0 rather than raising an exception.
|
|
148
|
+
This is common in formulas like: bonus = sales / days_worked
|
|
149
|
+
where days_worked might be 0 for new employees.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
numerator: The dividend (value to be divided)
|
|
153
|
+
denominator: The divisor (value to divide by)
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Result of division, or Decimal('0') if denominator is 0
|
|
157
|
+
|
|
158
|
+
Examples:
|
|
159
|
+
>>> safe_divide(Decimal('100'), Decimal('5'))
|
|
160
|
+
Decimal('20')
|
|
161
|
+
>>> safe_divide(Decimal('100'), Decimal('0'))
|
|
162
|
+
Decimal('0')
|
|
163
|
+
"""
|
|
164
|
+
if denominator == 0:
|
|
165
|
+
return Decimal("0")
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
result = numerator / denominator
|
|
169
|
+
_validate_decimal_range(result)
|
|
170
|
+
return result
|
|
171
|
+
except (InvalidOperation, ValueError) as e:
|
|
172
|
+
raise ValidationError(f"Error in division {numerator} / {denominator}: {e}") from e
|