dodil/erp-brick-pos

DODIL ERP brick: point of sale

src/brick_pos/domain/values.py

"""Value objects: the states each lifecycle may be in, and the attributes with a fixed set of values.

Stored as their plain strings; naming them here makes a typo a NameError, not a new state.
"""
from enum import Enum


class SessionState(str, Enum):
    """session: its lifecycle states (module.yaml workflows). A session only moves forward."""
    OPEN = "open"
    COUNTED = "counted"
    CLOSED = "closed"
    FORCE_CLOSED = "force_closed"
    POSTED = "posted"


class ReceiptState(str, Enum):
    """receipt: its lifecycle states (module.yaml workflows). A finalised receipt never reopens."""
    OPEN = "open"
    FINALISED = "finalised"
    QUARANTINED = "quarantined"
    VOIDED = "voided"                      # void_open_receipt: kept as a void record with its lines (the workflow's "to: none")


class ReceiptKind(str, Enum):
    SALE = "sale"
    REFUND = "refund"


class PaymentMethod(str, Enum):
    CASH = "cash"
    CARD = "card"
    VOUCHER = "voucher"


class TerminalStatus(str, Enum):
    PENDING = "pending"
    AUTHORISED = "authorised"
    DECLINED = "declined"
    REVERSED = "reversed"


class CashMoveKind(str, Enum):
    SAFE_DROP = "safe_drop"
    PAID_IN = "paid_in"
    PAID_OUT = "paid_out"
    FLOAT_TOP_UP = "float_top_up"


class VoucherOperation(str, Enum):
    """voucher_claim: what a claim row did to the balance (the operation is part of what is claimed)."""
    ISSUE = "issue"
    REDEEM = "redeem"
    REFUND_TO_VOUCHER = "refund_to_voucher"
    EXPIRE = "expire"


class DepositState(str, Enum):
    """cash_deposit: sealed -> handed_over -> matched (payments reports the bank credit: a later slice). A bag never goes back."""
    SEALED = "sealed"
    HANDED_OVER = "handed_over"
    MATCHED = "matched"


class OutboxStatus(str, Enum):
    """outbox: pending -> sent | failed, and failed -> sent. A sent row never goes back to pending."""
    PENDING = "pending"
    SENT = "sent"
    FAILED = "failed"


# Fiscal profiles that need a certified device before a register may sell (POS-01, gate fiscal_device_ready).
DEVICE_PROFILES = ("de_kassensichv", "at_rksv")
FISCAL_PROFILES = ("none", "de_kassensichv", "fr_nf525", "at_rksv")

# module.yaml config_rows: pos_settings. key -> (kind, default, controls, what it changes). `controls` = held by the
# owner (pos:settings:controls), never by the people the control binds. business_timezone is the bucket's ONE zone
# (shared.yaml conventions): kept here until the platform owns it, read through SettingsRepository.timezone() only.
SETTING_DEFS = {
    "difference_threshold": ("decimal", "5.00", True, "a cash difference above this needs a second person"),
    "blind_close": ("bool", True, True, "the cashier counts before seeing the expected amount"),
    "paid_out_limit": ("decimal", "50.00", True, "a paid-out above this needs a shift lead"),
    "day_cutoff_time": ("time", "04:00", False, "sales before this time belong to the previous business date"),
    "tips_treatment": ("choice:liability|revenue", "liability", False,
                       "liability (tips_payable, passed to staff) | revenue; the UK Employment (Allocation of Tips) Act 2023 says liability"),
    "voucher_validity_months": ("int", 24, False, "months a gift voucher is valid from the day it is sold (UK policy: 24)"),
    "recipe_source": ("choice:pos|manufacturing", "pos", False, "pos: the recipes kept here; manufacturing: its usage-sale bills of materials"),
    "cash_refund_limit": ("decimal", "20.00", True, "a cash refund above this needs a shift lead"),
    "business_timezone": ("zone", "Europe/London", True, "the IANA time zone in which 'today' is counted (one per bucket)"),
}