Suppliers as a managed list with per-spool assignments (#2996)

This commit is contained in:
Thomansky
2026-09-28 15:14:07 +02:00
committed by GitHub
parent 66379e97b5
commit 053cfa73ad
46 changed files with 4843 additions and 54 deletions
+85
View File
@@ -3661,6 +3661,61 @@ export interface InventorySpool {
k_profiles?: SpoolKProfile[];
storage_location?: string | null;
location_id?: number | null;
// Supplier assignments (#2988). Absent in Spoolman mode — Spoolman's
// vendor is the manufacturer, not the seller, so there is no mapping.
suppliers?: SpoolSupplierLink[];
}
// ── Suppliers (#2988) ──────────────────────────────────────────────────────
/** Where filament is bought — distinct from brand (who made it). */
export interface Supplier {
id: number;
name: string;
website: string | null;
customer_number: string | null;
note: string | null;
/** Spools referencing this supplier; a referenced supplier cannot be deleted. */
spool_count: number;
created_at: string;
updated_at: string;
}
export interface SupplierInput {
name: string;
website?: string | null;
customer_number?: string | null;
note?: string | null;
}
/** One spool-to-supplier assignment as written by the spool dialog. */
export interface SpoolSupplierLinkInput {
supplier_id: number;
/** The supplier's own article number — NOT the internal material number. */
supplier_article_number?: string | null;
/** Quoted price for comparison — never the cost basis (spool.cost_per_kg). */
quoted_price_per_kg?: number | null;
/** Marks where this concrete spool was actually bought. */
is_purchase_source?: boolean;
}
export interface SpoolSupplierLink {
id: number;
supplier_id: number;
supplier_name: string;
supplier_article_number: string | null;
quoted_price_per_kg: number | null;
is_purchase_source: boolean;
}
/** Per-supplier inventory aggregate (#2988), purchase-source spools only. */
export interface SupplierStats {
supplier_id: number;
supplier_name: string;
spool_count: number;
remaining_g: number;
consumed_g: number;
cost: number;
}
export interface SpoolmanBulkCreateResult {
@@ -6662,6 +6717,36 @@ export const api = {
request<{ deleted: number }>('/inventory/catalog/bulk-delete', { method: 'POST', body: JSON.stringify({ ids }) }),
resetSpoolCatalog: () =>
request<{ status: string }>('/inventory/catalog/reset', { method: 'POST' }),
// ── Suppliers (#2988) — inventory master data, Locations pattern ─────────
getSuppliers: () =>
request<Supplier[]>('/inventory/suppliers'),
createSupplier: (data: SupplierInput) =>
request<Supplier>('/inventory/suppliers', { method: 'POST', body: JSON.stringify(data) }),
updateSupplier: (id: number, data: Partial<SupplierInput>) =>
request<Supplier>(`/inventory/suppliers/${id}`, { method: 'PATCH', body: JSON.stringify(data) }),
deleteSupplier: (id: number) =>
request<{ status: string }>(`/inventory/suppliers/${id}`, { method: 'DELETE' }),
setSpoolSuppliers: (spoolId: number, links: SpoolSupplierLinkInput[]) =>
request<SpoolSupplierLink[]>(`/inventory/spools/${spoolId}/suppliers`, {
method: 'PUT',
body: JSON.stringify(links),
}),
// Spoolman parity: the assignment rows live Bambuddy-side, keyed by the
// remote spool id — same request/response shape as the built-in inventory.
setSpoolmanSpoolSuppliers: (spoolmanSpoolId: number, links: SpoolSupplierLinkInput[]) =>
request<SpoolSupplierLink[]>(`/spoolman/inventory/spools/${spoolmanSpoolId}/suppliers`, {
method: 'PUT',
body: JSON.stringify(links),
}),
// date_from/date_to scope the usage half only, so the widget can follow the
// dashboard timeframe; stock stays point-in-time.
getSupplierStats: (dateFrom?: string, dateTo?: string) => {
const params = new URLSearchParams();
if (dateFrom) params.set('date_from', dateFrom);
if (dateTo) params.set('date_to', dateTo);
const qs = params.toString();
return request<SupplierStats[]>(`/inventory/stats/suppliers${qs ? `?${qs}` : ''}`);
},
getLocations: () =>
request<StorageLocation[]>('/inventory/locations'),
createLocation: (data: { name: string; identifier?: string | null }) =>