import copy
import datetime
import decimal
from typing import MutableSequence, Optional, Type
from typing_extensions import Self
from .generated import cost_spec
from .cost_component import CostComponent
from . import internal
from .date import Date
from .punctuation import Asterisk
from .escaped_string import EscapedString
from .currency import Currency
from .number_expr import NumberExpr
from .amount import Amount
from .compound_amount import CompoundAmount
from .cost import UnitCost, TotalCost
[docs]@internal.tree_model
class CostSpec(cost_spec.CostSpec):
@internal.custom_property
def raw_cost_components(self) -> MutableSequence[CostComponent]:
return self.raw_cost.raw_components
raw_compound_amount_comp = internal.unordered_node_property(raw_cost_components, CompoundAmount, prepend=True)
raw_amount_comp = internal.unordered_node_property(raw_cost_components, Amount, prepend=True)
raw_number_comp = internal.unordered_node_property(raw_cost_components, NumberExpr, prepend=True)
raw_currency_comp = internal.unordered_node_property(raw_cost_components, Currency, prepend=True)
raw_date_comp = internal.unordered_node_property(raw_cost_components, Date)
raw_label_comp = internal.unordered_node_property(raw_cost_components, EscapedString)
raw_asterisk_comp = internal.unordered_node_property(raw_cost_components, Asterisk)
@internal.custom_property
def raw_number_per(self) -> Optional[NumberExpr]:
if compound_amount := self.raw_compound_amount_comp:
return compound_amount.raw_number_per
if not isinstance(self.raw_cost, UnitCost):
return None
if amount := self.raw_amount_comp:
return amount.raw_number
return self.raw_number_comp
@raw_number_per.setter
def __raw_number_per(self, value: Optional[NumberExpr]) -> None:
if compound_amount := self.raw_compound_amount_comp: # CompoundAmount
compound_amount.raw_number_per = value
elif isinstance(self.raw_cost, UnitCost):
if amount := self.raw_amount_comp:
if value: # Amount(per)
amount.raw_number = value
else: # Amount(per) - Number(per) -> Currency
self.raw_currency_comp = copy.deepcopy(amount.raw_currency)
self.raw_amount_comp = None
elif (currency := self.raw_currency_comp) and value: # Currency + Number(per) -> Amount(per)
self.raw_amount_comp = Amount.from_children(value, copy.deepcopy(currency))
self.raw_currency_comp = None
else: # Number(per)
self.raw_number_comp = value
elif isinstance(self.raw_cost, TotalCost) and value:
if amount := self.raw_amount_comp: # Amount(total) + Number(per) -> CompoundAmount
self._into_unit_cost(self.raw_cost)
compound_amount = CompoundAmount.from_children(
value,
copy.deepcopy(amount.raw_number),
copy.deepcopy(amount.raw_currency))
self.raw_compound_amount_comp = compound_amount
self.raw_amount_comp = None
elif currency := self.raw_currency_comp: # Currency(total) + Number(per) -> Amount(per)
self._into_unit_cost(self.raw_cost)
amount = Amount.from_children(value, copy.deepcopy(currency))
self.raw_amount_comp = amount
self.raw_currency_comp = None
elif self.raw_number_comp: # Number(total) + Number(per) -> error
raise ValueError('Cannot set both number_per and number_total without a currency.')
else: # /(total) + Number(per) -> Number(per)
self._into_unit_cost(self.raw_cost)
self.raw_number_comp = value
@internal.custom_property
def raw_number_total(self) -> Optional[NumberExpr]:
if compound_amount := self.raw_compound_amount_comp:
return compound_amount.raw_number_total
if not isinstance(self.raw_cost, TotalCost):
return None
if amount := self.raw_amount_comp:
return amount.raw_number
return self.raw_number_comp
@raw_number_total.setter
def __raw_number_total(self, value: Optional[NumberExpr]) -> None:
if compound_amount := self.raw_compound_amount_comp: # CompoundAmount
compound_amount.raw_number_total = value
elif isinstance(self.raw_cost, TotalCost):
if amount := self.raw_amount_comp:
if value: # Amount(total):
amount.raw_number = value
else: # Amount(total) - Number(total) -> Currency
self.raw_currency_comp = copy.deepcopy(amount.raw_currency)
self.raw_amount_comp = None
elif (currency := self.raw_currency_comp) and value: # Currency + Number(total) -> Amount(total)
self.raw_amount_comp = Amount.from_children(value, copy.deepcopy(currency))
self.raw_currency_comp = None
else: # Number(total)
self.raw_number_comp = value
elif isinstance(self.raw_cost, UnitCost) and value:
if amount := self.raw_amount_comp: # Amount(per) + Number(total) -> CompoundAmount
compound_amount = CompoundAmount.from_children(
copy.deepcopy(amount.raw_number),
value,
copy.deepcopy(amount.raw_currency))
self.raw_compound_amount_comp = compound_amount
self.raw_amount_comp = None
elif currency := self.raw_currency_comp: # Currency(per) + Number(total) -> Amount(total)
self._into_total_cost(self.raw_cost)
amount = Amount.from_children(value, copy.deepcopy(currency))
self.raw_amount_comp = amount
self.raw_currency_comp = None
elif self.raw_number_comp: # Number(per) + Number(total) -> error
raise ValueError('Cannot set both number_per and number_total without a currency.')
else: # /(per) + Number(total) -> Number(total)
self._into_total_cost(self.raw_cost)
self.raw_number_comp = value
@internal.custom_property
def raw_currency(self) -> Optional[Currency]:
if compound_amount := self.raw_compound_amount_comp:
return compound_amount.raw_currency
if amount := self.raw_amount_comp:
return amount.raw_currency
return self.raw_currency_comp
@raw_currency.setter
def __raw_currency(self, value: Optional[Currency]) -> None:
if compound_amount := self.raw_compound_amount_comp:
if value: # CompoundAmount
compound_amount.raw_currency = value
else: # CompoundAmount - Currency
match compound_amount.raw_number_per, compound_amount.raw_number_total, self.raw_cost:
case NumberExpr(), None, UnitCost():
self.raw_number_comp = copy.deepcopy(compound_amount.raw_number_per)
case NumberExpr(), None, TotalCost() as c:
self._into_unit_cost(c)
self.raw_number_comp = copy.deepcopy(compound_amount.raw_number_per)
case None, NumberExpr(), TotalCost():
self.raw_number_comp = copy.deepcopy(compound_amount.raw_number_total)
case None, NumberExpr(), UnitCost() as c:
self._into_total_cost(c)
self.raw_number_comp = copy.deepcopy(compound_amount.raw_number_total)
case NumberExpr(), NumberExpr(), _:
raise ValueError('Cannot remove currency from compound amount with both numbers.')
self.raw_compound_amount_comp = None
elif amount := self.raw_amount_comp:
if value: # Amount
amount.raw_currency = value
else: # Amount - Currency -> Number
self.raw_number_comp = copy.deepcopy(amount.raw_number)
self.raw_amount_comp = None
else: # Currency
self.raw_currency_comp = value
raw_date = raw_date_comp
raw_label = raw_label_comp
raw_asterisk = raw_asterisk_comp
number_per = internal.optional_decimal_property(raw_number_per, NumberExpr)
number_total = internal.optional_decimal_property(raw_number_total, NumberExpr)
currency = internal.optional_string_property(raw_currency, Currency)
date = internal.optional_date_property(raw_date, Date)
label = internal.optional_string_property(raw_label, EscapedString)
@property
def merge(self) -> bool:
return self.raw_asterisk is not None
@merge.setter
def merge(self, value: bool) -> None:
current = self.merge
if current and not value:
self.raw_asterisk = None
elif not current and value:
self.raw_asterisk = Asterisk.from_default()
[docs] @classmethod
def from_value(
cls,
number_per: Optional[decimal.Decimal],
number_total: Optional[decimal.Decimal],
currency: Optional[str],
date: Optional[datetime.date] = None,
label: Optional[str] = None,
merge: bool = False,
) -> Self:
type_: Type[UnitCost | TotalCost]
comps: list[CostComponent] = []
if number_per is not None and number_total is not None: # CompoundAmount
if currency is None:
raise ValueError('Cannot set both number_per and number_total without a currency.')
type_ = UnitCost
comps.append(CompoundAmount.from_children(
NumberExpr.from_value(number_per),
NumberExpr.from_value(number_total),
Currency.from_value(currency)))
elif number_per is not None:
type_ = UnitCost
if currency is None:
comps.append(NumberExpr.from_value(number_per))
else:
comps.append(Amount.from_children(
NumberExpr.from_value(number_per),
Currency.from_value(currency)))
elif number_total is not None:
type_ = TotalCost
if currency is None:
comps.append(NumberExpr.from_value(number_total))
else:
comps.append(Amount.from_children(
NumberExpr.from_value(number_total),
Currency.from_value(currency)))
elif currency is not None:
type_ = UnitCost
comps.append(Currency.from_value(currency))
else:
type_ = UnitCost
if date is not None:
comps.append(Date.from_value(date))
if label is not None:
comps.append(EscapedString.from_value(label))
if merge:
comps.append(Asterisk.from_default())
return cls.from_children(type_.from_children(comps))
def _into_unit_cost(self, total_cost: TotalCost) -> None:
self._cost = total_cost.into_unit_cost()
def _into_total_cost(self, unit_cost: UnitCost) -> None:
self._cost = unit_cost.into_total_cost()