Dockerfile updates

This commit is contained in:
2026-06-02 15:41:53 +12:00
parent 84792c0947
commit f5a588d631
18 changed files with 742 additions and 220 deletions
+4
View File
@@ -161,6 +161,10 @@ def create_entry(
label_correct=payload.label_correct,
bag_sealed=payload.bag_sealed,
pallet_good_condition=payload.pallet_good_condition,
for_order=payload.for_order,
for_stock=payload.for_stock,
job_number=payload.job_number,
stock_quantity=payload.stock_quantity if payload.for_stock else None,
sample_box_no=payload.sample_box_no,
test_weight_1=payload.test_weight_1,
test_weight_2=payload.test_weight_2,
+5
View File
@@ -103,6 +103,11 @@ _LEGACY_COLUMN_PATCHES: tuple[tuple[str, str, str], ...] = (
("users", "password_hash", "VARCHAR(255)"),
("products", "visible", "BOOLEAN NOT NULL DEFAULT TRUE"),
("throughput_products", "is_stock_item", "BOOLEAN NOT NULL DEFAULT TRUE"),
("throughput_products", "client_name", "VARCHAR(255)"),
("production_throughput_entries", "for_order", "BOOLEAN NOT NULL DEFAULT FALSE"),
("production_throughput_entries", "for_stock", "BOOLEAN NOT NULL DEFAULT FALSE"),
("production_throughput_entries", "job_number", "VARCHAR(64)"),
("production_throughput_entries", "stock_quantity", "FLOAT"),
)
+10
View File
@@ -19,6 +19,7 @@ class ThroughputProduct(Base):
tenant_id: Mapped[str] = mapped_column(String(64), default="default", index=True)
item_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
name: Mapped[str] = mapped_column(String(255))
client_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
default_bag_size: Mapped[float | None] = mapped_column(Float, nullable=True)
is_bulka_default: Mapped[bool] = mapped_column(Boolean, default=False)
active: Mapped[bool] = mapped_column(Boolean, default=True)
@@ -48,6 +49,15 @@ class ProductionThroughput(Base):
bag_sealed: Mapped[bool] = mapped_column(Boolean, default=True)
pallet_good_condition: Mapped[bool] = mapped_column(Boolean, default=True)
# Where the run is destined. A run can be for a client order, for stock, or
# split across both. job_number records the Order Circle job for the order
# portion; stock_quantity records how much of a split goes into stock (in the
# same unit as `quantity`).
for_order: Mapped[bool] = mapped_column(Boolean, default=False)
for_stock: Mapped[bool] = mapped_column(Boolean, default=False)
job_number: Mapped[str | None] = mapped_column(String(64), nullable=True)
stock_quantity: Mapped[float | None] = mapped_column(Float, nullable=True)
sample_box_no: Mapped[str | None] = mapped_column(String(64), nullable=True)
test_weight_1: Mapped[float | None] = mapped_column(Float, nullable=True)
+22
View File
@@ -13,6 +13,7 @@ class ThroughputProductBase(BaseModel):
model_config = ConfigDict(extra="forbid")
item_id: str | None = Field(default=None, max_length=64)
name: str = Field(min_length=1, max_length=255)
client_name: str | None = Field(default=None, max_length=255)
default_bag_size: float | None = Field(default=None, ge=0)
is_bulka_default: bool = False
active: bool = True
@@ -28,6 +29,7 @@ class ThroughputProductUpdate(BaseModel):
model_config = ConfigDict(extra="forbid")
item_id: str | None = Field(default=None, max_length=64)
name: str | None = Field(default=None, min_length=1, max_length=255)
client_name: str | None = Field(default=None, max_length=255)
default_bag_size: float | None = Field(default=None, ge=0)
is_bulka_default: bool | None = None
active: bool | None = None
@@ -53,6 +55,10 @@ class ThroughputEntryBase(BaseModel):
label_correct: bool = True
bag_sealed: bool = True
pallet_good_condition: bool = True
for_order: bool = False
for_stock: bool = False
job_number: str | None = Field(default=None, max_length=64)
stock_quantity: float | None = Field(default=None, ge=0)
sample_box_no: str | None = Field(default=None, max_length=64)
test_weight_1: float | None = Field(default=None, ge=0)
test_weight_2: float | None = Field(default=None, ge=0)
@@ -64,6 +70,14 @@ class ThroughputEntryBase(BaseModel):
staff_name: str | None = Field(default=None, max_length=255)
notes: str | None = Field(default=None, max_length=2000)
@field_validator("job_number")
@classmethod
def _normalize_job_number(cls, value: str | None) -> str | None:
if value is None:
return None
stripped = value.strip()
return stripped or None
@field_validator("staff_name")
@classmethod
def _normalize_staff(cls, value: str | None) -> str | None:
@@ -87,6 +101,10 @@ class ThroughputEntryUpdate(BaseModel):
label_correct: bool | None = None
bag_sealed: bool | None = None
pallet_good_condition: bool | None = None
for_order: bool | None = None
for_stock: bool | None = None
job_number: str | None = Field(default=None, max_length=64)
stock_quantity: float | None = Field(default=None, ge=0)
sample_box_no: str | None = Field(default=None, max_length=64)
test_weight_1: float | None = Field(default=None, ge=0)
test_weight_2: float | None = Field(default=None, ge=0)
@@ -110,6 +128,10 @@ class ThroughputEntryRead(BaseModel):
label_correct: bool
bag_sealed: bool
pallet_good_condition: bool
for_order: bool
for_stock: bool
job_number: str | None
stock_quantity: float | None
sample_box_no: str | None
test_weight_1: float | None
test_weight_2: float | None
+10 -3
View File
@@ -794,15 +794,19 @@ def seed_throughput_products_from_costing(db) -> dict[str, int]:
default_bag_size = _infer_throughput_bag_size(costing_product)
is_bulka_default = _infer_throughput_bulka_default(costing_product)
client_name = (costing_product.client_name or "").strip() or None
product = (by_item.get(item_id) if item_id else None) or by_name.get(name_key)
if product is None:
product = ThroughputProduct(
tenant_id=TENANT_ID,
item_id=item_id,
name=name,
client_name=client_name,
default_bag_size=default_bag_size,
is_bulka_default=is_bulka_default,
active=costing_product.visible,
# Every costing SKU should be selectable in the throughput picker
# (the Client filter + search keep the long list manageable).
active=True,
is_stock_item=True,
notes="Seeded from costing products",
)
@@ -830,8 +834,11 @@ def seed_throughput_products_from_costing(db) -> dict[str, int]:
if product.is_bulka_default != is_bulka_default:
product.is_bulka_default = is_bulka_default
changed = True
if product.active != costing_product.visible:
product.active = costing_product.visible
if product.client_name != client_name:
product.client_name = client_name
changed = True
if product.active is not True:
product.active = True
changed = True
if product.is_stock_item is not True:
product.is_stock_item = True
+1
View File
@@ -51,6 +51,7 @@ ROLE_DEFINITIONS: dict[str, dict] = {
"edit_raw_materials",
"view_products",
"view_mixes",
"edit_mixes",
"view_throughput",
"edit_throughput",
"view_users",
+28 -5
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import logging
import os
from datetime import date, datetime
from pathlib import Path
from typing import Iterable
@@ -16,6 +17,10 @@ logger = logging.getLogger("data_entry_app.throughput")
PRODUCTION_SHEET = "Production"
NAMES_SHEET = "Names"
# The historical throughput export. Bundled into the image under input_data/ so
# the seed can import it on a fresh deployment (e.g. a new Postgres volume).
WORKBOOK_FILENAME = "Operations Throughput.xlsx"
# Anything at or above this kg/bag is treated as a bulka batch, not a per-bag count.
_BULKA_BAG_SIZE_THRESHOLD = 100.0
@@ -57,6 +62,10 @@ def serialize_entry(entry: ProductionThroughput) -> dict:
"label_correct": entry.label_correct,
"bag_sealed": entry.bag_sealed,
"pallet_good_condition": entry.pallet_good_condition,
"for_order": entry.for_order,
"for_stock": entry.for_stock,
"job_number": entry.job_number,
"stock_quantity": entry.stock_quantity,
"sample_box_no": entry.sample_box_no,
"test_weight_1": entry.test_weight_1,
"test_weight_2": entry.test_weight_2,
@@ -323,16 +332,30 @@ def import_workbook(db: Session, workbook_path: Path, tenant_id: str) -> dict:
def workbook_candidates() -> Iterable[Path]:
repo_root = Path(__file__).resolve().parents[3]
cwd = Path.cwd()
env_value = os.getenv("THROUGHPUT_WORKBOOK_PATH")
env_path = Path(env_value.strip()) if isinstance(env_value, str) and env_value.strip() else None
# input_data/ is where the workbook is bundled in the image; in the
# container the working directory is /app, so cwd/input_data resolves it.
candidates = [
repo_root / "Operations Throughput.xlsx",
repo_root.parent / "Operations Throughput.xlsx",
Path.cwd() / "Operations Throughput.xlsx",
Path("/srv/lean101-clients") / "Operations Throughput.xlsx",
Path("/app") / "Operations Throughput.xlsx",
env_path,
repo_root / "input_data" / WORKBOOK_FILENAME,
cwd / "input_data" / WORKBOOK_FILENAME,
Path("/app") / "input_data" / WORKBOOK_FILENAME,
Path("/srv/lean101-clients") / "input_data" / WORKBOOK_FILENAME,
repo_root / WORKBOOK_FILENAME,
repo_root.parent / WORKBOOK_FILENAME,
cwd / WORKBOOK_FILENAME,
Path("/srv/lean101-clients") / WORKBOOK_FILENAME,
Path("/app") / WORKBOOK_FILENAME,
]
seen: set[str] = set()
ordered: list[Path] = []
for candidate in candidates:
if candidate is None:
continue
key = str(candidate)
if key in seen:
continue