Release v0.1.22
This commit is contained in:
@@ -101,6 +101,29 @@ def _coerce_bool(value: object) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _coerce_import_bool(value: object, *, default: bool = False) -> bool:
|
||||||
|
"""Conservative boolean parsing for ad-hoc imports.
|
||||||
|
|
||||||
|
Uploaded CSV/XLSX rows often leave destination columns blank, or use text
|
||||||
|
like "stock" / "order" elsewhere in the row. Those should not silently
|
||||||
|
become True. Only explicit truthy markers opt in.
|
||||||
|
"""
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return value
|
||||||
|
if value is None:
|
||||||
|
return default
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
return bool(value)
|
||||||
|
text = str(value).strip().lower()
|
||||||
|
if not text:
|
||||||
|
return default
|
||||||
|
if text in {"yes", "y", "true", "1", "pass", "ok", "x", "checked"}:
|
||||||
|
return True
|
||||||
|
if text in {"no", "n", "false", "0", "fail"}:
|
||||||
|
return False
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
def _coerce_float(value: object) -> float | None:
|
def _coerce_float(value: object) -> float | None:
|
||||||
if value is None or value == "":
|
if value is None or value == "":
|
||||||
return None
|
return None
|
||||||
@@ -631,8 +654,8 @@ def import_entries_from_file(
|
|||||||
by_item[item_id] = product
|
by_item[item_id] = product
|
||||||
by_name[product_name.lower()] = product
|
by_name[product_name.lower()] = product
|
||||||
|
|
||||||
for_order = _coerce_bool(cell(row, "for_order")) if field_index.get("for_order") is not None else False
|
for_order = _coerce_import_bool(cell(row, "for_order")) if field_index.get("for_order") is not None else False
|
||||||
for_stock = _coerce_bool(cell(row, "for_stock")) if field_index.get("for_stock") is not None else False
|
for_stock = _coerce_import_bool(cell(row, "for_stock")) if field_index.get("for_stock") is not None else False
|
||||||
stock_quantity = _coerce_float(cell(row, "stock_quantity")) if for_stock else None
|
stock_quantity = _coerce_float(cell(row, "stock_quantity")) if for_stock else None
|
||||||
|
|
||||||
calculated = calculate_kg(quantity, quantity_type, bag_size)
|
calculated = calculate_kg(quantity, quantity_type, bag_size)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from app.models.throughput import ProductionThroughput, ThroughputProduct
|
|||||||
from app.seed import seed_throughput_products_from_costing
|
from app.seed import seed_throughput_products_from_costing
|
||||||
from app.services.throughput_service import (
|
from app.services.throughput_service import (
|
||||||
calculate_kg,
|
calculate_kg,
|
||||||
|
import_entries_from_file,
|
||||||
import_names_sheet,
|
import_names_sheet,
|
||||||
import_production_sheet,
|
import_production_sheet,
|
||||||
normalise_staff_name,
|
normalise_staff_name,
|
||||||
@@ -240,3 +241,48 @@ def test_seed_throughput_products_from_costing_updates_existing_by_item_id():
|
|||||||
assert products[0].name == "Updated Wheat 25kg"
|
assert products[0].name == "Updated Wheat 25kg"
|
||||||
assert products[0].default_bag_size == 25
|
assert products[0].default_bag_size == 25
|
||||||
assert products[0].active is True
|
assert products[0].active is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_upload_import_keeps_blank_destination_flags_false():
|
||||||
|
db = _session()
|
||||||
|
csv_bytes = (
|
||||||
|
"Date,Product,Quantity,Type,Bag Size,For Order,For Stock,Job Number\n"
|
||||||
|
"2026-06-12,Specialty Pigeon Breeder,40,bags,20,,,\n"
|
||||||
|
).encode("utf-8")
|
||||||
|
|
||||||
|
result = import_entries_from_file(
|
||||||
|
db,
|
||||||
|
filename="throughput-import.csv",
|
||||||
|
content=csv_bytes,
|
||||||
|
tenant_id="test-tenant",
|
||||||
|
created_by="tester@example.com",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["entries_imported"] == 1
|
||||||
|
entry = db.scalar(select(ProductionThroughput))
|
||||||
|
assert entry is not None
|
||||||
|
assert entry.for_order is False
|
||||||
|
assert entry.for_stock is False
|
||||||
|
assert entry.job_number is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_upload_import_does_not_treat_unknown_destination_text_as_true():
|
||||||
|
db = _session()
|
||||||
|
csv_bytes = (
|
||||||
|
"Date,Product,Quantity,Type,Bag Size,For Order,For Stock\n"
|
||||||
|
"2026-06-12,Specialty Pigeon Breeder,40,bags,20,stock,\n"
|
||||||
|
).encode("utf-8")
|
||||||
|
|
||||||
|
result = import_entries_from_file(
|
||||||
|
db,
|
||||||
|
filename="throughput-import.csv",
|
||||||
|
content=csv_bytes,
|
||||||
|
tenant_id="test-tenant",
|
||||||
|
created_by="tester@example.com",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["entries_imported"] == 1
|
||||||
|
entry = db.scalar(select(ProductionThroughput))
|
||||||
|
assert entry is not None
|
||||||
|
assert entry.for_order is False
|
||||||
|
assert entry.for_stock is False
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "hunter-app",
|
"name": "hunter-app",
|
||||||
"version": "0.1.18",
|
"version": "0.1.22",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "hunter-app",
|
"name": "hunter-app",
|
||||||
"version": "0.1.18",
|
"version": "0.1.22",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/inter": "^5.2.8",
|
"@fontsource/inter": "^5.2.8",
|
||||||
"lucide-svelte": "^1.0.1"
|
"lucide-svelte": "^1.0.1"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hunter-app",
|
"name": "hunter-app",
|
||||||
"version": "0.1.21",
|
"version": "0.1.22",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -17,6 +17,13 @@ export type ChangelogEntry = {
|
|||||||
export const APP_VERSION: string = packageInfo.version;
|
export const APP_VERSION: string = packageInfo.version;
|
||||||
|
|
||||||
export const changelog: ChangelogEntry[] = [
|
export const changelog: ChangelogEntry[] = [
|
||||||
|
{
|
||||||
|
version: '0.1.22',
|
||||||
|
date: '2026-06-15',
|
||||||
|
highlights: [
|
||||||
|
'Web App - Throughput module is now live.'
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
version: '0.1.20',
|
version: '0.1.20',
|
||||||
date: '2026-06-13',
|
date: '2026-06-13',
|
||||||
|
|||||||
@@ -146,8 +146,7 @@ export const throughputItem: NavItem = {
|
|||||||
label: 'Throughput',
|
label: 'Throughput',
|
||||||
shortLabel: 'OT',
|
shortLabel: 'OT',
|
||||||
icon: Gauge,
|
icon: Gauge,
|
||||||
moduleKey: 'operations_throughput',
|
moduleKey: 'operations_throughput'
|
||||||
badge: 'test'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const orderingItem: NavItem = {
|
export const orderingItem: NavItem = {
|
||||||
|
|||||||
@@ -541,13 +541,19 @@
|
|||||||
return (a ?? '').localeCompare(b ?? '', undefined, { sensitivity: 'base' });
|
return (a ?? '').localeCompare(b ?? '', undefined, { sensitivity: 'base' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function compareDate(a: string | null | undefined, b: string | null | undefined) {
|
||||||
|
const aTime = a ? Date.parse(a) : Number.NEGATIVE_INFINITY;
|
||||||
|
const bTime = b ? Date.parse(b) : Number.NEGATIVE_INFINITY;
|
||||||
|
return aTime - bTime;
|
||||||
|
}
|
||||||
|
|
||||||
const sortedEntries = $derived.by(() => {
|
const sortedEntries = $derived.by(() => {
|
||||||
const direction = sortDirection === 'asc' ? 1 : -1;
|
const direction = sortDirection === 'asc' ? 1 : -1;
|
||||||
return [...entries].sort((a, b) => {
|
return [...entries].sort((a, b) => {
|
||||||
let result = 0;
|
let result = 0;
|
||||||
|
|
||||||
if (sortKey === 'date') {
|
if (sortKey === 'date') {
|
||||||
result = compareText(a.production_date, b.production_date);
|
result = compareDate(a.production_date, b.production_date);
|
||||||
} else if (sortKey === 'product') {
|
} else if (sortKey === 'product') {
|
||||||
result = compareText(a.product_name_snapshot, b.product_name_snapshot);
|
result = compareText(a.product_name_snapshot, b.product_name_snapshot);
|
||||||
} else if (sortKey === 'packed') {
|
} else if (sortKey === 'packed') {
|
||||||
@@ -565,6 +571,10 @@
|
|||||||
result = compareText(a.notes, b.notes);
|
result = compareText(a.notes, b.notes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (result === 0) {
|
||||||
|
result = (a.id ?? 0) - (b.id ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
return result * direction;
|
return result * direction;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user