dodil/erp-brick-fixed-assets
DODIL ERP brick: fixed assets
src/brick_fixed_assets/domain/odoo.py
"""Mapping an Odoo account.asset to this register (module.yaml migration.odoo.maps) — pure functions over the rows the reader
port hands over. Exact Decimals; Odoo's `False` for an empty field is treated as empty."""
from dataclasses import dataclass
from datetime import date
from decimal import Decimal
from typing import Optional
from brick_fixed_assets.domain.errors import RuleBroken
from brick_fixed_assets.domain.values import ZERO, money, period_of
def num(v) -> Decimal:
return ZERO if not v else Decimal(str(v))
def iso(v) -> Optional[date]:
if not v:
return None
return v if isinstance(v, date) else date.fromisoformat(str(v)[:10])
def ref_id(v) -> Optional[int]:
"""A many2one as Odoo returns it: [id, name], an id, or False."""
if not v:
return None
return int(v[0]) if isinstance(v, (list, tuple)) else int(v)
def method_map(method: str, progress_factor) -> tuple:
"""(our method, rate_pct): linear -> straight_line; degressive -> declining_balance at method_progress_factor x 100;
degressive_then_linear -> declining_then_straight."""
m = (method or "linear").lower()
if m == "linear":
return "straight_line", None
rate = money(num(progress_factor) * 100) if progress_factor else None
if m == "degressive":
return "declining_balance", rate
if m == "degressive_then_linear":
return "declining_then_straight", rate
raise RuleBroken(f"Odoo method {method!r} has no key here: map it by hand")
def life_periods(method_number, method_period) -> int:
"""method_number x method_period months (Odoo's method_period is 1 (monthly) or 12 (yearly))."""
n = int(method_number or 0)
p = int(method_period or 1)
return max(n * p, 1)
def convention(prorata_computation_type) -> str:
"""none -> full_month; constant_periods -> full_month from prorata_date's month; daily_computation -> daily."""
return {"daily_computation": "daily"}.get(prorata_computation_type or "none", "full_month")
def class_code(name: str) -> str:
code = "".join(ch for ch in (name or "").upper() if ch.isalnum())[:24]
return code or "ODOO"
def valid_moves(moves: list) -> list:
"""Posted depreciation moves that stand: a move Odoo reversed, and the reversal itself, cancel out."""
reversed_ids = {ref_id(m.get("reversed_entry_id")) for m in moves if m.get("reversed_entry_id")}
return [m for m in moves if m["id"] not in reversed_ids and not m.get("reversed_entry_id") and
num(m.get("depreciation_value")) != 0 and not m.get("asset_value_change")]
def split(moves: list, fy_start: date, cutover: date) -> tuple:
"""(depreciation before the fiscal year start, depreciation in the year to the cutover, periods depreciated)."""
before = in_year = ZERO
n = 0
for m in valid_moves(moves):
d = iso(m["date"])
if d is None or d > cutover:
continue
v = num(m.get("depreciation_value"))
n += 1
if d < fy_start:
before += v
else:
in_year += v
return before, in_year, n
def value_changes(moves: list, cutover: date) -> Decimal:
"""asset_value_change moves (increases / decreases) as Odoo stored them: folded into cost, listed for the door."""
return sum((num(m.get("depreciation_value")) for m in moves if m.get("asset_value_change") and
(iso(m["date"]) or cutover) <= cutover), ZERO)
def fy_start_of(cutover: date, fy_end_month: int) -> date:
"""The first day of the fiscal year the cutover date is in (the year ends on the last day of fy_end_month)."""
if fy_end_month == 12:
return date(cutover.year, 1, 1)
first_month = fy_end_month + 1
year = cutover.year if cutover.month > fy_end_month else cutover.year - 1
return date(year, first_month, 1)
@dataclass(frozen=True)
class Carried:
"""What one running asset brings: cost, accumulated depreciation at the start of the fiscal year, the year's charge, the
life left in periods, and the first period depreciated here."""
cost: Decimal
acc_start: Decimal
in_year: Decimal
remaining: int
first_period: str
nbv: Decimal
scrap: Decimal
def carry(asset: dict, moves: list, fy_start: date, cutover: date, life: int) -> Carried:
before, in_year, n = split(moves, fy_start, cutover)
cost = num(asset.get("original_value")) + value_changes(moves, cutover)
acc_start = before + num(asset.get("already_depreciated_amount_import"))
nbv = cost - acc_start - in_year
from brick_fixed_assets.domain.values import period_add
return Carried(cost, acc_start, in_year, max(life - n, 0), period_add(period_of(cutover), 1), nbv,
num(asset.get("salvage_value")))