fixes
This commit is contained in:
@@ -10,6 +10,7 @@ Inven is a single-container inventory management system for Unraid-style deploym
|
||||
- Sales orders can be created and shipped
|
||||
- Purchase orders can be created and received
|
||||
- Sales orders and purchase orders are created from relational inventory line selections
|
||||
- Shipping and receiving are posted from relational order-line quantity controls
|
||||
- Sales orders support partial shipments
|
||||
- Purchase orders support partial receipts
|
||||
- Invoices are generated from shipped sales orders
|
||||
@@ -75,6 +76,7 @@ Suggested Unraid mapping:
|
||||
- Add parts and assemblies in the Parts module.
|
||||
- Define assembly components in the Assemblies module.
|
||||
- Create SOs and POs by selecting actual inventory items from the item master instead of typing free-form SKUs.
|
||||
- Fulfill SOs and POs by entering quantities against existing order lines instead of typing SKU text.
|
||||
- Use purchase orders to restock and receive inventory.
|
||||
- Use build transactions to convert component stock into assembly stock.
|
||||
- Use sales orders and ship them fully or partially to reduce stock and generate invoices plus journal entries.
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { addKitComponent, buildAssembly } from "@/lib/actions";
|
||||
import { getAssembliesWithComponents, getParts } from "@/lib/repository";
|
||||
|
||||
export default function AssembliesPage() {
|
||||
export default async function AssembliesPage({
|
||||
searchParams
|
||||
}: {
|
||||
searchParams?: Promise<{ error?: string; success?: string }>;
|
||||
}) {
|
||||
const params = (await searchParams) ?? {};
|
||||
const parts = getParts();
|
||||
const assemblies = parts.filter((part) => part.kind === "assembly");
|
||||
const components = parts.filter((part) => part.kind === "part");
|
||||
@@ -9,6 +14,8 @@ export default function AssembliesPage() {
|
||||
|
||||
return (
|
||||
<div className="grid">
|
||||
{params.error ? <section className="panel"><p className="muted" style={{ color: "var(--danger)" }}>{params.error}</p></section> : null}
|
||||
{params.success ? <section className="panel"><p className="muted" style={{ color: "var(--success)" }}>{params.success}</p></section> : null}
|
||||
<section className="two-up">
|
||||
<article className="panel">
|
||||
<h2 className="section-title">Bill of Materials</h2>
|
||||
@@ -16,26 +23,26 @@ export default function AssembliesPage() {
|
||||
<form action={addKitComponent} className="form-grid">
|
||||
<div className="form-row">
|
||||
<label htmlFor="assemblySku">Assembly SKU</label>
|
||||
<select className="select" id="assemblySku" name="assemblySku">{assemblies.map((assembly) => <option key={assembly.id} value={assembly.sku}>{assembly.sku} - {assembly.name}</option>)}</select>
|
||||
<select className="select" id="assemblySku" name="assemblySku" disabled={assemblies.length === 0}>{assemblies.map((assembly) => <option key={assembly.id} value={assembly.sku}>{assembly.sku} - {assembly.name}</option>)}</select>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="componentSku">Component SKU</label>
|
||||
<select className="select" id="componentSku" name="componentSku">{components.map((component) => <option key={component.id} value={component.sku}>{component.sku} - {component.name}</option>)}</select>
|
||||
<select className="select" id="componentSku" name="componentSku" disabled={components.length === 0}>{components.map((component) => <option key={component.id} value={component.sku}>{component.sku} - {component.name}</option>)}</select>
|
||||
</div>
|
||||
<div className="form-row"><label htmlFor="component-qty">Quantity Per Assembly</label><input className="input" id="component-qty" name="quantity" type="number" min="0.01" step="0.01" defaultValue="1" /></div>
|
||||
<button className="button" type="submit">Save Component</button>
|
||||
<button className="button" type="submit" disabled={assemblies.length === 0 || components.length === 0}>Save Component</button>
|
||||
</form>
|
||||
</article>
|
||||
<article className="panel">
|
||||
<h2 className="section-title">Build Assembly</h2>
|
||||
<p className="section-copy">Consume component stock and create finished kit inventory in one transaction flow.</p>
|
||||
<p className="section-copy">Consume component stock and create finished kit inventory in one transaction flow. This only works after the assembly has a BOM and enough component stock exists.</p>
|
||||
<form action={buildAssembly} className="form-grid">
|
||||
<div className="form-row">
|
||||
<label htmlFor="build-assembly">Assembly SKU</label>
|
||||
<select className="select" id="build-assembly" name="assemblySku">{assemblies.map((assembly) => <option key={assembly.id} value={assembly.sku}>{assembly.sku} - {assembly.name}</option>)}</select>
|
||||
<select className="select" id="build-assembly" name="assemblySku" disabled={assemblies.length === 0}>{assemblies.map((assembly) => <option key={assembly.id} value={assembly.sku}>{assembly.sku} - {assembly.name}</option>)}</select>
|
||||
</div>
|
||||
<div className="form-row"><label htmlFor="build-qty">Build Quantity</label><input className="input" id="build-qty" name="quantity" type="number" min="1" step="1" defaultValue="1" /></div>
|
||||
<button className="button secondary" type="submit">Build Now</button>
|
||||
<button className="button secondary" type="submit" disabled={assemblies.length === 0}>Build Now</button>
|
||||
</form>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { receivePurchaseOrder } from "@/lib/actions";
|
||||
import { PurchaseOrderFulfillmentForm } from "@/components/purchase-order-fulfillment-form";
|
||||
import { PurchaseOrderForm } from "@/components/purchase-order-form";
|
||||
import { formatCurrency, formatDate } from "@/lib/format";
|
||||
import { getLowStockParts, getOrderItemOptions, getPurchaseOrders, getVendors } from "@/lib/repository";
|
||||
import { getLowStockParts, getOrderItemOptions, getPurchaseOrderLineDetails, getPurchaseOrders, getVendors } from "@/lib/repository";
|
||||
|
||||
export default function PurchaseOrdersPage() {
|
||||
const vendors = getVendors();
|
||||
const items = getOrderItemOptions();
|
||||
const orders = getPurchaseOrders();
|
||||
const orderLines = getPurchaseOrderLineDetails();
|
||||
const lowStockParts = getLowStockParts();
|
||||
|
||||
return (
|
||||
@@ -19,7 +20,7 @@ export default function PurchaseOrdersPage() {
|
||||
</article>
|
||||
<article className="panel">
|
||||
<h2 className="section-title">Receiving Flow</h2>
|
||||
<p className="section-copy">Leave line quantities blank to receive the remaining balance, or enter `SKU,quantity` rows for a partial receipt.</p>
|
||||
<p className="section-copy">Receive relational order lines directly by entering quantities against the remaining balance on each line.</p>
|
||||
<div className="table-wrap">
|
||||
<table className="table">
|
||||
<thead><tr><th>Order</th><th>Vendor</th><th>Status</th><th>Total</th><th>Qty Progress</th><th>Created</th><th>Action</th></tr></thead>
|
||||
@@ -39,11 +40,10 @@ export default function PurchaseOrdersPage() {
|
||||
{order.status === "received" ? (
|
||||
<span className="muted">Received</span>
|
||||
) : (
|
||||
<form action={receivePurchaseOrder} className="form-grid">
|
||||
<input type="hidden" name="orderId" value={order.id} />
|
||||
<textarea className="textarea" name="lines" placeholder={"PART-001,4\nPART-002,10"} />
|
||||
<button className="button secondary" type="submit">Receive</button>
|
||||
</form>
|
||||
<PurchaseOrderFulfillmentForm
|
||||
orderId={order.id}
|
||||
lines={orderLines.filter((line) => line.purchaseOrderId === order.id && line.remainingQuantity > 0)}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { shipSalesOrder } from "@/lib/actions";
|
||||
import { SalesOrderFulfillmentForm } from "@/components/sales-order-fulfillment-form";
|
||||
import { SalesOrderForm } from "@/components/sales-order-form";
|
||||
import { formatCurrency, formatDate } from "@/lib/format";
|
||||
import { getCustomers, getOrderItemOptions, getSalesOrders } from "@/lib/repository";
|
||||
import { getCustomers, getOrderItemOptions, getSalesOrderLineDetails, getSalesOrders } from "@/lib/repository";
|
||||
|
||||
export default function SalesOrdersPage() {
|
||||
const customers = getCustomers();
|
||||
const items = getOrderItemOptions();
|
||||
const orders = getSalesOrders();
|
||||
const orderLines = getSalesOrderLineDetails();
|
||||
|
||||
return (
|
||||
<div className="grid">
|
||||
@@ -18,7 +19,7 @@ export default function SalesOrdersPage() {
|
||||
</article>
|
||||
<article className="panel">
|
||||
<h2 className="section-title">Shipping Flow</h2>
|
||||
<p className="section-copy">Leave line quantities blank to ship the remaining balance, or enter `SKU,quantity` rows for a partial shipment.</p>
|
||||
<p className="section-copy">Ship relational order lines directly by choosing quantities from the remaining balance on each line.</p>
|
||||
<div className="table-wrap">
|
||||
<table className="table">
|
||||
<thead><tr><th>Order</th><th>Customer</th><th>Status</th><th>Total</th><th>Qty Progress</th><th>Created</th><th>Action</th></tr></thead>
|
||||
@@ -38,11 +39,10 @@ export default function SalesOrdersPage() {
|
||||
{order.status === "shipped" ? (
|
||||
<span className="muted">Shipped</span>
|
||||
) : (
|
||||
<form action={shipSalesOrder} className="form-grid">
|
||||
<input type="hidden" name="orderId" value={order.id} />
|
||||
<textarea className="textarea" name="lines" placeholder={"PART-001,1\nKIT-100,2"} />
|
||||
<button className="button secondary" type="submit">Ship</button>
|
||||
</form>
|
||||
<SalesOrderFulfillmentForm
|
||||
orderId={order.id}
|
||||
lines={orderLines.filter((line) => line.salesOrderId === order.id && line.remainingQuantity > 0)}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
74
components/purchase-order-fulfillment-form.tsx
Normal file
74
components/purchase-order-fulfillment-form.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { receivePurchaseOrder } from "@/lib/actions";
|
||||
import type { PurchaseOrderLineDetailRow } from "@/lib/types";
|
||||
|
||||
type Props = {
|
||||
orderId: number;
|
||||
lines: PurchaseOrderLineDetailRow[];
|
||||
};
|
||||
|
||||
export function PurchaseOrderFulfillmentForm({ orderId, lines }: Props) {
|
||||
const [quantities, setQuantities] = useState<Record<number, string>>({});
|
||||
|
||||
const payload = useMemo(
|
||||
() =>
|
||||
JSON.stringify(
|
||||
lines
|
||||
.map((line) => ({
|
||||
lineId: line.lineId,
|
||||
quantity: Number(quantities[line.lineId] || 0)
|
||||
}))
|
||||
.filter((line) => line.quantity > 0)
|
||||
),
|
||||
[lines, quantities]
|
||||
);
|
||||
|
||||
return (
|
||||
<form action={receivePurchaseOrder} className="form-grid">
|
||||
<input type="hidden" name="orderId" value={orderId} />
|
||||
<input type="hidden" name="fulfillmentLines" value={payload} />
|
||||
<div className="table-wrap">
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>SKU</th>
|
||||
<th>Item</th>
|
||||
<th>Remaining</th>
|
||||
<th>Receive Now</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{lines.map((line) => (
|
||||
<tr key={line.lineId}>
|
||||
<td>{line.sku}</td>
|
||||
<td>{line.partName}</td>
|
||||
<td>{line.remainingQuantity} {line.unitOfMeasure}</td>
|
||||
<td>
|
||||
<input
|
||||
className="input"
|
||||
type="number"
|
||||
min="0"
|
||||
max={line.remainingQuantity}
|
||||
step="0.01"
|
||||
value={quantities[line.lineId] ?? ""}
|
||||
onChange={(event) =>
|
||||
setQuantities((current) => ({
|
||||
...current,
|
||||
[line.lineId]: event.target.value
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<button className="button secondary" type="submit">
|
||||
Receive Selected Quantities
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
76
components/sales-order-fulfillment-form.tsx
Normal file
76
components/sales-order-fulfillment-form.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { shipSalesOrder } from "@/lib/actions";
|
||||
import type { SalesOrderLineDetailRow } from "@/lib/types";
|
||||
|
||||
type Props = {
|
||||
orderId: number;
|
||||
lines: SalesOrderLineDetailRow[];
|
||||
};
|
||||
|
||||
export function SalesOrderFulfillmentForm({ orderId, lines }: Props) {
|
||||
const [quantities, setQuantities] = useState<Record<number, string>>({});
|
||||
|
||||
const payload = useMemo(
|
||||
() =>
|
||||
JSON.stringify(
|
||||
lines
|
||||
.map((line) => ({
|
||||
lineId: line.lineId,
|
||||
quantity: Number(quantities[line.lineId] || 0)
|
||||
}))
|
||||
.filter((line) => line.quantity > 0)
|
||||
),
|
||||
[lines, quantities]
|
||||
);
|
||||
|
||||
return (
|
||||
<form action={shipSalesOrder} className="form-grid">
|
||||
<input type="hidden" name="orderId" value={orderId} />
|
||||
<input type="hidden" name="fulfillmentLines" value={payload} />
|
||||
<div className="table-wrap">
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>SKU</th>
|
||||
<th>Item</th>
|
||||
<th>Remaining</th>
|
||||
<th>On Hand</th>
|
||||
<th>Ship Now</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{lines.map((line) => (
|
||||
<tr key={line.lineId}>
|
||||
<td>{line.sku}</td>
|
||||
<td>{line.partName}</td>
|
||||
<td>{line.remainingQuantity} {line.unitOfMeasure}</td>
|
||||
<td>{line.quantityOnHand} {line.unitOfMeasure}</td>
|
||||
<td>
|
||||
<input
|
||||
className="input"
|
||||
type="number"
|
||||
min="0"
|
||||
max={Math.min(line.remainingQuantity, line.quantityOnHand)}
|
||||
step="0.01"
|
||||
value={quantities[line.lineId] ?? ""}
|
||||
onChange={(event) =>
|
||||
setQuantities((current) => ({
|
||||
...current,
|
||||
[line.lineId]: event.target.value
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<button className="button secondary" type="submit">
|
||||
Ship Selected Quantities
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
104
lib/actions.ts
104
lib/actions.ts
@@ -22,6 +22,11 @@ type ParsedFulfillmentLine = {
|
||||
quantity: number;
|
||||
};
|
||||
|
||||
type RelationalFulfillmentLine = {
|
||||
lineId: number;
|
||||
quantity: number;
|
||||
};
|
||||
|
||||
function db() {
|
||||
return getDb();
|
||||
}
|
||||
@@ -121,6 +126,32 @@ function parseFulfillmentLines(raw: string): ParsedFulfillmentLine[] {
|
||||
});
|
||||
}
|
||||
|
||||
function parseRelationalFulfillmentLines(raw: string): RelationalFulfillmentLine[] {
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as Array<Record<string, unknown>>;
|
||||
if (!Array.isArray(parsed) || parsed.length === 0) {
|
||||
throw new Error("No fulfillment lines selected.");
|
||||
}
|
||||
|
||||
return parsed.map((line) => {
|
||||
const lineId = Number(line.lineId);
|
||||
const quantity = Number(line.quantity);
|
||||
|
||||
if (!Number.isInteger(lineId) || lineId <= 0) {
|
||||
throw new Error("Invalid line selection.");
|
||||
}
|
||||
|
||||
if (!Number.isFinite(quantity) || quantity <= 0) {
|
||||
throw new Error("Invalid fulfillment quantity.");
|
||||
}
|
||||
|
||||
return { lineId, quantity };
|
||||
});
|
||||
} catch {
|
||||
throw new Error("Invalid relational fulfillment payload.");
|
||||
}
|
||||
}
|
||||
|
||||
function getPartIdBySku(sku: string) {
|
||||
const row = db().prepare(`SELECT id FROM parts WHERE sku = ?`).get(sku) as { id: number } | undefined;
|
||||
if (!row) {
|
||||
@@ -274,8 +305,12 @@ export async function buildAssembly(formData: FormData) {
|
||||
const assemblySku = getText(formData, "assemblySku");
|
||||
const buildQuantity = getNumber(formData, "quantity");
|
||||
|
||||
if (!assemblySku) {
|
||||
redirect("/assemblies?error=Select an assembly before building.");
|
||||
}
|
||||
|
||||
if (buildQuantity <= 0) {
|
||||
throw new Error("Build quantity must be greater than zero.");
|
||||
redirect("/assemblies?error=Build quantity must be greater than zero.");
|
||||
}
|
||||
|
||||
const assemblyId = getPartIdBySku(assemblySku);
|
||||
@@ -304,13 +339,13 @@ export async function buildAssembly(formData: FormData) {
|
||||
}>;
|
||||
|
||||
if (components.length === 0) {
|
||||
throw new Error("Assembly has no bill of materials defined.");
|
||||
redirect("/assemblies?error=Assembly has no bill of materials defined.");
|
||||
}
|
||||
|
||||
for (const component of components) {
|
||||
const needed = component.componentQuantity * buildQuantity;
|
||||
if (component.quantityOnHand < needed) {
|
||||
throw new Error(`Not enough stock for component ${component.sku}. Need ${needed}, have ${component.quantityOnHand}.`);
|
||||
redirect(`/assemblies?error=${encodeURIComponent(`Not enough stock for component ${component.sku}. Need ${needed}, have ${component.quantityOnHand}.`)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,6 +389,7 @@ export async function buildAssembly(formData: FormData) {
|
||||
revalidatePath("/");
|
||||
revalidatePath("/parts");
|
||||
revalidatePath("/assemblies");
|
||||
redirect(`/assemblies?success=${encodeURIComponent(`Built ${buildQuantity} of ${assemblySku}.`)}`);
|
||||
}
|
||||
|
||||
export async function recordAdjustment(formData: FormData) {
|
||||
@@ -548,18 +584,18 @@ export async function shipSalesOrder(formData: FormData) {
|
||||
throw new Error("Sales order has no lines.");
|
||||
}
|
||||
|
||||
const requestedLines = parseFulfillmentLines(getText(formData, "lines"));
|
||||
const fulfilledLines = requestedLines.length
|
||||
? requestedLines.map((request) => {
|
||||
const matchingLine = orderLines.find((line) => line.sku === request.sku);
|
||||
const relationalPayload = getText(formData, "fulfillmentLines");
|
||||
const fulfilledLines = relationalPayload
|
||||
? parseRelationalFulfillmentLines(relationalPayload).map((request) => {
|
||||
const matchingLine = orderLines.find((line) => line.lineId === request.lineId);
|
||||
|
||||
if (!matchingLine) {
|
||||
throw new Error(`SKU ${request.sku} is not on this sales order.`);
|
||||
throw new Error("Selected sales order line is invalid.");
|
||||
}
|
||||
|
||||
const remaining = matchingLine.quantity - matchingLine.shippedQuantity;
|
||||
if (request.quantity > remaining) {
|
||||
throw new Error(`Cannot ship ${request.quantity} of ${request.sku}; only ${remaining} remain.`);
|
||||
throw new Error(`Cannot ship ${request.quantity} of ${matchingLine.sku}; only ${remaining} remain.`);
|
||||
}
|
||||
|
||||
if (matchingLine.quantityOnHand < request.quantity) {
|
||||
@@ -568,7 +604,26 @@ export async function shipSalesOrder(formData: FormData) {
|
||||
|
||||
return { ...matchingLine, shipQuantity: request.quantity };
|
||||
})
|
||||
: orderLines
|
||||
: parseFulfillmentLines(getText(formData, "lines")).length
|
||||
? parseFulfillmentLines(getText(formData, "lines")).map((request) => {
|
||||
const matchingLine = orderLines.find((line) => line.sku === request.sku);
|
||||
|
||||
if (!matchingLine) {
|
||||
throw new Error(`SKU ${request.sku} is not on this sales order.`);
|
||||
}
|
||||
|
||||
const remaining = matchingLine.quantity - matchingLine.shippedQuantity;
|
||||
if (request.quantity > remaining) {
|
||||
throw new Error(`Cannot ship ${request.quantity} of ${request.sku}; only ${remaining} remain.`);
|
||||
}
|
||||
|
||||
if (matchingLine.quantityOnHand < request.quantity) {
|
||||
throw new Error(`Insufficient stock for ${matchingLine.sku}. Need ${request.quantity}, have ${matchingLine.quantityOnHand}.`);
|
||||
}
|
||||
|
||||
return { ...matchingLine, shipQuantity: request.quantity };
|
||||
})
|
||||
: orderLines
|
||||
.map((line) => {
|
||||
const remaining = line.quantity - line.shippedQuantity;
|
||||
return remaining > 0 ? { ...line, shipQuantity: remaining } : null;
|
||||
@@ -718,23 +773,38 @@ export async function receivePurchaseOrder(formData: FormData) {
|
||||
throw new Error("Purchase order has no lines.");
|
||||
}
|
||||
|
||||
const requestedLines = parseFulfillmentLines(getText(formData, "lines"));
|
||||
const fulfilledLines = requestedLines.length
|
||||
? requestedLines.map((request) => {
|
||||
const matchingLine = lines.find((line) => line.sku === request.sku);
|
||||
const relationalPayload = getText(formData, "fulfillmentLines");
|
||||
const fulfilledLines = relationalPayload
|
||||
? parseRelationalFulfillmentLines(relationalPayload).map((request) => {
|
||||
const matchingLine = lines.find((line) => line.lineId === request.lineId);
|
||||
|
||||
if (!matchingLine) {
|
||||
throw new Error(`SKU ${request.sku} is not on this purchase order.`);
|
||||
throw new Error("Selected purchase order line is invalid.");
|
||||
}
|
||||
|
||||
const remaining = matchingLine.quantity - matchingLine.receivedQuantity;
|
||||
if (request.quantity > remaining) {
|
||||
throw new Error(`Cannot receive ${request.quantity} of ${request.sku}; only ${remaining} remain.`);
|
||||
throw new Error(`Cannot receive ${request.quantity} of ${matchingLine.sku}; only ${remaining} remain.`);
|
||||
}
|
||||
|
||||
return { ...matchingLine, receiveQuantity: request.quantity };
|
||||
})
|
||||
: lines
|
||||
: parseFulfillmentLines(getText(formData, "lines")).length
|
||||
? parseFulfillmentLines(getText(formData, "lines")).map((request) => {
|
||||
const matchingLine = lines.find((line) => line.sku === request.sku);
|
||||
|
||||
if (!matchingLine) {
|
||||
throw new Error(`SKU ${request.sku} is not on this purchase order.`);
|
||||
}
|
||||
|
||||
const remaining = matchingLine.quantity - matchingLine.receivedQuantity;
|
||||
if (request.quantity > remaining) {
|
||||
throw new Error(`Cannot receive ${request.quantity} of ${request.sku}; only ${remaining} remain.`);
|
||||
}
|
||||
|
||||
return { ...matchingLine, receiveQuantity: request.quantity };
|
||||
})
|
||||
: lines
|
||||
.map((line) => {
|
||||
const remaining = line.quantity - line.receivedQuantity;
|
||||
return remaining > 0 ? { ...line, receiveQuantity: remaining } : null;
|
||||
|
||||
@@ -11,7 +11,9 @@ import type {
|
||||
OrderItemOption,
|
||||
PartRow,
|
||||
PurchaseOrderListRow,
|
||||
PurchaseOrderLineDetailRow,
|
||||
SalesOrderListRow,
|
||||
SalesOrderLineDetailRow,
|
||||
VendorBillRow
|
||||
} from "@/lib/types";
|
||||
|
||||
@@ -190,6 +192,54 @@ export function getPurchaseOrders(): PurchaseOrderListRow[] {
|
||||
.all() as PurchaseOrderListRow[];
|
||||
}
|
||||
|
||||
export function getSalesOrderLineDetails(): SalesOrderLineDetailRow[] {
|
||||
return db()
|
||||
.prepare(
|
||||
`
|
||||
SELECT
|
||||
sol.id AS lineId,
|
||||
sol.sales_order_id AS salesOrderId,
|
||||
sol.part_id AS partId,
|
||||
p.sku,
|
||||
p.name AS partName,
|
||||
sol.quantity,
|
||||
sol.shipped_quantity AS fulfilledQuantity,
|
||||
sol.quantity - sol.shipped_quantity AS remainingQuantity,
|
||||
sol.unit_price AS unitPrice,
|
||||
COALESCE(ib.quantity_on_hand, 0) AS quantityOnHand,
|
||||
p.unit_of_measure AS unitOfMeasure
|
||||
FROM sales_order_lines sol
|
||||
INNER JOIN parts p ON p.id = sol.part_id
|
||||
LEFT JOIN inventory_balances ib ON ib.part_id = p.id
|
||||
ORDER BY sol.sales_order_id DESC, sol.id ASC
|
||||
`
|
||||
)
|
||||
.all() as SalesOrderLineDetailRow[];
|
||||
}
|
||||
|
||||
export function getPurchaseOrderLineDetails(): PurchaseOrderLineDetailRow[] {
|
||||
return db()
|
||||
.prepare(
|
||||
`
|
||||
SELECT
|
||||
pol.id AS lineId,
|
||||
pol.purchase_order_id AS purchaseOrderId,
|
||||
pol.part_id AS partId,
|
||||
p.sku,
|
||||
p.name AS partName,
|
||||
pol.quantity,
|
||||
pol.received_quantity AS fulfilledQuantity,
|
||||
pol.quantity - pol.received_quantity AS remainingQuantity,
|
||||
pol.unit_cost AS unitCost,
|
||||
p.unit_of_measure AS unitOfMeasure
|
||||
FROM purchase_order_lines pol
|
||||
INNER JOIN parts p ON p.id = pol.part_id
|
||||
ORDER BY pol.purchase_order_id DESC, pol.id ASC
|
||||
`
|
||||
)
|
||||
.all() as PurchaseOrderLineDetailRow[];
|
||||
}
|
||||
|
||||
export function getJournalEntries(): JournalEntryRow[] {
|
||||
const entries = db()
|
||||
.prepare(
|
||||
|
||||
27
lib/types.ts
27
lib/types.ts
@@ -141,3 +141,30 @@ export type VendorBillRow = {
|
||||
paidAmount: number;
|
||||
balanceDue: number;
|
||||
};
|
||||
|
||||
export type SalesOrderLineDetailRow = {
|
||||
lineId: number;
|
||||
salesOrderId: number;
|
||||
partId: number;
|
||||
sku: string;
|
||||
partName: string;
|
||||
quantity: number;
|
||||
fulfilledQuantity: number;
|
||||
remainingQuantity: number;
|
||||
unitPrice: number;
|
||||
quantityOnHand: number;
|
||||
unitOfMeasure: string;
|
||||
};
|
||||
|
||||
export type PurchaseOrderLineDetailRow = {
|
||||
lineId: number;
|
||||
purchaseOrderId: number;
|
||||
partId: number;
|
||||
sku: string;
|
||||
partName: string;
|
||||
quantity: number;
|
||||
fulfilledQuantity: number;
|
||||
remainingQuantity: number;
|
||||
unitCost: number;
|
||||
unitOfMeasure: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user