aeat.core.decimal._coerce module

Canonical Decimal coercion helpers for the AEAT domain.

Consolidates three independent _coerce_decimal copies that previously lived in _calc_sheets_pull, _row_set_assembly, and invoices._models. All call-sites use coerce_decimal(), coerce_decimal_strict(), or normalize_decimal_separators() from this module rather than open-coding decimal parsing.

Variant analysis

  • _calc_sheets_pull — returned Decimal | None with no default. Callers checked for None explicitly to detect empty cells.

  • _row_set_assembly — took a required default: Decimal keyword argument and always returned Decimal. Callers passed Decimal("0").

  • invoices._models — raised TypeError on unparseable input (strict pydantic-validator context).

Canonical resolution

coerce_decimal() uses coerce_decimal(value, *, default=None) -> Decimal | None.

A single default keyword argument covers all three patterns:

  • Pass default=None (or omit it) for the nullable-cell pattern — callers check the return value and skip or handle None themselves.

  • Pass default=Decimal("0") for the aggregation pattern — guaranteed Decimal return, no None check needed.

  • Raise on None for the strict-validator pattern: pass no default and raise TypeError when the return is None — the validator itself turns that into a pydantic.ValidationError.

The helper treats int inputs as valid (the _models.py variant did; the other two would have produced Decimal(str(int)) anyway), allowing callers that pass mixed int / str / Decimal worksheet values to work without pre-conversion.

coerce_decimal(value, *, default=None)[source]

Coerce value to a Decimal, falling back to default.

Parameters:
  • value (object) – Raw input. Accepts Decimal, int, float, str, or None. Empty strings ("") are treated as absent.

  • default (Decimal | None) – Value returned when value is None, an empty string, or cannot be parsed. Defaults to None.

Return type:

Decimal | None

Returns:

Parsed decimal, or default when coercion fails.

Examples

>>> from decimal import Decimal
>>> coerce_decimal("12.34")
Decimal('12.34')
>>> coerce_decimal(None) is None
True
>>> coerce_decimal(None, default=Decimal("0"))
Decimal('0')
>>> coerce_decimal("bad", default=Decimal("0"))
Decimal('0')
>>> coerce_decimal(42)
Decimal('42')
coerce_decimal_strict(value)[source]

Coerce value to Decimal, raising on unparseable input.

Unlike coerce_decimal() — which swallows the parse failure and returns a default — this variant lets the underlying InvalidOperation (or ValueError) propagate, so callers that need to record which parse error occurred (e.g. a redaction-safe diagnostic that logs type(exc).__name__) can catch it themselves. The caller is responsible for the empty/None case.

Parameters:

value (object) – Raw input. Accepts Decimal, int, float, or str. Leading/trailing whitespace in a string is stripped by the Decimal constructor.

Return type:

Decimal

Returns:

The parsed decimal.

Raises:
normalize_decimal_separators(text, *, strip_thousands)[source]

Normalise a European-formatted numeric string to a dot-decimal form.

Maps the decimal comma to a dot so the result is parsable by Decimal. When strip_thousands is True the thousands dot is removed first (Spanish "1.234,56" -> "1234.56"); when False only the comma is converted ("1234,56" -> "1234.56"), for inputs already free of thousands separators.

Single canonical home for the comma/dot separator normalisation that the sede, registry-export, renta-web-oracle, and PDF-label parsers previously open-coded inline. Each caller keeps its own surrounding validation, symbol-stripping, locale-detection, and error handling; only the separator transform is shared.

Return type:

str

Parameters:
  • text (str)

  • strip_thousands (bool)