v1.8.0: warehouse module (16 commits), docházka mzda model, leave_type=holiday removal, api.ts race fix

Highlights:
- Warehouse module: receipts, issues, reservations, inventory, reports, dashboard,
  master data (categories, suppliers, locations), FIFO service, integration tests
- Docházka: mzda PDF counting model (Odpracováno / Vč. svátků / Přesčas / Svátek /
  So/Ne / Noc) with Czech weekday names and decimal hours
- AttendanceAdmin/AttendanceHistory KPI cards unified to mzda formula with
  fund bar colored by delta, badges for Práce/Dov/Nem/Sv/Nep
- Remove leave_type=holiday entirely (auto-computed from Czech public holidays)
- Allow multiple work shifts per day (overlap detection only)
- Pre-flight refresh in api.ts eliminates spurious 401s on fresh page loads
- Prisma: company_settings gets 6 nullable columns for warehouse numbering
  (PRI/VYD/INV prefixes, default patterns); migration seeds defaults
This commit is contained in:
BOHA
2026-06-03 23:13:10 +02:00
parent dac45baaa8
commit 5233db2002
149 changed files with 11132 additions and 26950 deletions

View File

@@ -21,6 +21,7 @@ import {
cancelReservation,
confirmInventorySession,
getBelowMinimumItems,
validateIssueReservationRules,
} from "../../services/warehouse.service";
import {
CreateCategorySchema,
@@ -385,13 +386,22 @@ export default async function warehouseRoutes(
// LOCATIONS
// =============================================================
// GET /locations — list all active, ordered by code asc
fastify.get(
// GET /locations — list locations, ordered by code asc
// Query param `active`: "true" (default — only active, used by dropdowns),
// "false" (only inactive), or "all" (everything, used by the management page
// so deactivated rows remain visible — soft-delete, not a hard hide).
fastify.get<{ Querystring: { active?: string } }>(
"/locations",
{ preHandler: requirePermission("warehouse.manage") },
async (_request, reply) => {
async (request, reply) => {
const where: Record<string, unknown> = {};
const active = request.query.active ?? "true";
if (active === "true") where.is_active = true;
else if (active === "false") where.is_active = false;
// "all" or anything else: no filter
const locations = await prisma.sklad_locations.findMany({
where: { is_active: true },
where,
orderBy: { code: "asc" },
});
return success(reply, locations);
@@ -829,7 +839,7 @@ export default async function warehouseRoutes(
},
);
// DELETE /items/:id — soft delete (set is_active=false)
// DELETE /items/:id — hard delete, blocked if item has stock activity
fastify.delete<{ Params: { id: string } }>(
"/items/:id",
{ preHandler: requirePermission("warehouse.manage") },
@@ -842,23 +852,43 @@ export default async function warehouseRoutes(
});
if (!existing) return error(reply, "Položka nenalezena", 404);
await prisma.sklad_items.update({
where: { id },
data: { is_active: false },
// Check if item is referenced by any stock activity
const [receiptLines, issueLines, activeReservations] = await Promise.all([
prisma.sklad_receipt_lines.count({ where: { item_id: id } }),
prisma.sklad_issue_lines.count({ where: { item_id: id } }),
prisma.sklad_reservations.count({
where: { item_id: id, status: "ACTIVE" },
}),
]);
if (receiptLines > 0 || issueLines > 0 || activeReservations > 0) {
return error(
reply,
"Položku nelze smazat — má vazby na příjmy, výdaje nebo aktivní rezervace",
409,
);
}
// Safe to delete: clean up locations and consumed batches first
await prisma.sklad_item_locations.deleteMany({ where: { item_id: id } });
await prisma.sklad_batches.deleteMany({ where: { item_id: id } });
await prisma.sklad_reservations.deleteMany({
where: { item_id: id, status: "CANCELLED" },
});
await prisma.sklad_inventory_lines.deleteMany({ where: { item_id: id } });
await prisma.sklad_items.delete({ where: { id } });
await logAudit({
request,
authData: request.authData,
action: "deactivate",
action: "delete",
entityType: "warehouse_item",
entityId: id,
description: `Deaktivována položka ${existing.name}`,
oldValues: { is_active: existing.is_active },
newValues: { is_active: false },
description: `Smazána položka ${existing.name}`,
oldValues: { name: existing.name, item_number: existing.item_number },
});
return success(reply, null, 200, "Položka byla deaktivována");
return success(reply, null, 200, "Položka byla smazána");
},
);
@@ -908,7 +938,21 @@ export default async function warehouseRoutes(
orderBy: { received_at: "asc" },
});
return success(reply, batches);
// Include reservation-aware available quantity
const totalStock = await getItemTotalStock(id);
const reservedResult = await prisma.sklad_reservations.aggregate({
_sum: { remaining_qty: true },
where: { item_id: id, status: "ACTIVE" },
});
const reservedQty = Number(reservedResult._sum.remaining_qty ?? 0);
const availableQuantity = totalStock - reservedQty;
return success(reply, {
batches,
total_stock: totalStock,
reserved_quantity: reservedQty,
available_quantity: availableQuantity,
});
},
);
@@ -967,7 +1011,7 @@ export default async function warehouseRoutes(
received_by_user: {
select: { id: true, first_name: true, last_name: true },
},
_count: { select: { lines: true } },
_count: { select: { items: true } },
},
}),
prisma.sklad_receipts.count({ where: filter }),
@@ -981,7 +1025,7 @@ export default async function warehouseRoutes(
},
);
// GET /receipts/:id — detail with supplier, received_by_user, lines, attachments
// GET /receipts/:id — detail with supplier, received_by_user, items, attachments
fastify.get<{ Params: { id: string } }>(
"/receipts/:id",
{ preHandler: requirePermission("warehouse.view") },
@@ -996,7 +1040,7 @@ export default async function warehouseRoutes(
received_by_user: {
select: { id: true, first_name: true, last_name: true },
},
lines: {
items: {
include: {
item: { select: { id: true, name: true, unit: true } },
location: { select: { id: true, code: true, name: true } },
@@ -1011,7 +1055,7 @@ export default async function warehouseRoutes(
},
);
// POST /receipts — create DRAFT with lines
// POST /receipts — create DRAFT with items
fastify.post(
"/receipts",
{ preHandler: requirePermission("warehouse.operate") },
@@ -1031,19 +1075,19 @@ export default async function warehouseRoutes(
notes: body.notes ?? null,
status: "DRAFT",
received_by: authUserId,
lines: {
create: body.lines.map((line) => ({
item_id: line.item_id,
quantity: line.quantity,
unit_price: line.unit_price,
location_id: line.location_id ?? null,
notes: line.notes ?? null,
items: {
create: body.items.map((item) => ({
item_id: item.item_id,
quantity: item.quantity,
unit_price: item.unit_price,
location_id: item.location_id ?? null,
notes: item.notes ?? null,
})),
},
},
include: {
supplier: { select: { id: true, name: true } },
lines: {
items: {
include: {
item: { select: { id: true, name: true, unit: true } },
},
@@ -1061,7 +1105,7 @@ export default async function warehouseRoutes(
newValues: {
supplier_id: receipt.supplier_id,
status: receipt.status,
line_count: body.lines.length,
item_count: body.items.length,
},
});
@@ -1069,7 +1113,7 @@ export default async function warehouseRoutes(
},
);
// PUT /receipts/:id — update header + replace lines (DRAFT only)
// PUT /receipts/:id — update header + replace items (DRAFT only)
fastify.put<{ Params: { id: string } }>(
"/receipts/:id",
{ preHandler: requirePermission("warehouse.operate") },
@@ -1083,7 +1127,7 @@ export default async function warehouseRoutes(
const existing = await prisma.sklad_receipts.findUnique({
where: { id },
include: { lines: true },
include: { items: true },
});
if (!existing) return error(reply, "Příjem nenalezen", 404);
if (existing.status !== "DRAFT") {
@@ -1102,19 +1146,19 @@ export default async function warehouseRoutes(
if (body.notes !== undefined) updateData.notes = body.notes;
updateData.modified_at = new Date();
// Replace lines if provided
if (body.lines) {
// Replace items if provided
if (body.items) {
await prisma.sklad_receipt_lines.deleteMany({
where: { receipt_id: id },
});
await prisma.sklad_receipt_lines.createMany({
data: body.lines.map((line) => ({
data: body.items.map((item) => ({
receipt_id: id,
item_id: line.item_id,
quantity: line.quantity,
unit_price: line.unit_price,
location_id: line.location_id ?? null,
notes: line.notes ?? null,
item_id: item.item_id,
quantity: item.quantity,
unit_price: item.unit_price,
location_id: item.location_id ?? null,
notes: item.notes ?? null,
})),
});
}
@@ -1124,7 +1168,7 @@ export default async function warehouseRoutes(
data: updateData,
include: {
supplier: { select: { id: true, name: true } },
lines: {
items: {
include: {
item: { select: { id: true, name: true, unit: true } },
location: { select: { id: true, code: true, name: true } },
@@ -1144,13 +1188,13 @@ export default async function warehouseRoutes(
supplier_id: existing.supplier_id,
delivery_note_number: existing.delivery_note_number,
notes: existing.notes,
line_count: existing.lines.length,
item_count: existing.items.length,
},
newValues: {
supplier_id: updated.supplier_id,
delivery_note_number: updated.delivery_note_number,
notes: updated.notes,
line_count: updated.lines.length,
item_count: updated.items.length,
},
});
@@ -1362,11 +1406,11 @@ export default async function warehouseRoutes(
take: limit,
orderBy: { id: "desc" },
include: {
project: { select: { id: true, name: true } },
project: { select: { id: true, name: true, project_number: true } },
issued_by_user: {
select: { id: true, first_name: true, last_name: true },
},
_count: { select: { lines: true } },
_count: { select: { items: true } },
},
}),
prisma.sklad_issues.count({ where: filter }),
@@ -1376,7 +1420,7 @@ export default async function warehouseRoutes(
},
);
// GET /issues/:id — detail with project, issued_by_user, lines
// GET /issues/:id — detail with project, issued_by_user, items
fastify.get<{ Params: { id: string } }>(
"/issues/:id",
{ preHandler: requirePermission("warehouse.view") },
@@ -1387,11 +1431,11 @@ export default async function warehouseRoutes(
const issue = await prisma.sklad_issues.findUnique({
where: { id },
include: {
project: { select: { id: true, name: true } },
project: { select: { id: true, name: true, project_number: true } },
issued_by_user: {
select: { id: true, first_name: true, last_name: true },
},
lines: {
items: {
include: {
item: { select: { id: true, name: true, unit: true } },
batch: {
@@ -1416,7 +1460,7 @@ export default async function warehouseRoutes(
},
);
// POST /issues — create DRAFT with lines, auto-FIFO for lines without batch_id
// POST /issues — create DRAFT with items, auto-FIFO for items without batch_id
fastify.post(
"/issues",
{ preHandler: requirePermission("warehouse.operate") },
@@ -1426,8 +1470,8 @@ export default async function warehouseRoutes(
const body = parsed.data;
const authUserId = request.authData!.userId;
// Resolve FIFO batches for lines without batch_id
const resolvedLines: Array<{
// Resolve FIFO batches for items without batch_id
const resolvedItems: Array<{
item_id: number;
batch_id: number;
quantity: number;
@@ -1436,31 +1480,31 @@ export default async function warehouseRoutes(
notes: string | null;
}> = [];
for (const line of body.lines) {
if (line.batch_id != null) {
resolvedLines.push({
item_id: line.item_id,
batch_id: line.batch_id,
quantity: line.quantity,
location_id: line.location_id ?? null,
reservation_id: line.reservation_id ?? null,
notes: line.notes ?? null,
for (const item of body.items) {
if (item.batch_id != null) {
resolvedItems.push({
item_id: item.item_id,
batch_id: item.batch_id,
quantity: item.quantity,
location_id: item.location_id ?? null,
reservation_id: item.reservation_id ?? null,
notes: item.notes ?? null,
});
} else {
// Auto-select FIFO batches
try {
const fifoBatches = await selectFifoBatches(
line.item_id,
line.quantity,
item.item_id,
item.quantity,
);
for (const fb of fifoBatches) {
resolvedLines.push({
item_id: line.item_id,
resolvedItems.push({
item_id: item.item_id,
batch_id: fb.batchId,
quantity: fb.qty,
location_id: line.location_id ?? null,
reservation_id: line.reservation_id ?? null,
notes: line.notes ?? null,
location_id: item.location_id ?? null,
reservation_id: item.reservation_id ?? null,
notes: item.notes ?? null,
});
}
} catch (err) {
@@ -1473,19 +1517,32 @@ export default async function warehouseRoutes(
}
}
// Validate reservation rules
const reservationValidation = await validateIssueReservationRules(
resolvedItems,
body.project_id,
);
if ("error" in reservationValidation) {
return error(
reply,
reservationValidation.error,
reservationValidation.status,
);
}
const issue = await prisma.sklad_issues.create({
data: {
project_id: body.project_id,
notes: body.notes ?? null,
status: "DRAFT",
issued_by: authUserId,
lines: {
create: resolvedLines,
items: {
create: resolvedItems,
},
},
include: {
project: { select: { id: true, name: true } },
lines: {
project: { select: { id: true, name: true, project_number: true } },
items: {
include: {
item: { select: { id: true, name: true, unit: true } },
},
@@ -1503,7 +1560,7 @@ export default async function warehouseRoutes(
newValues: {
project_id: issue.project_id,
status: issue.status,
line_count: resolvedLines.length,
item_count: resolvedItems.length,
},
});
@@ -1511,7 +1568,7 @@ export default async function warehouseRoutes(
},
);
// PUT /issues/:id — update (DRAFT only), auto-FIFO for lines without batch_id
// PUT /issues/:id — update (DRAFT only), auto-FIFO for items without batch_id
fastify.put<{ Params: { id: string } }>(
"/issues/:id",
{ preHandler: requirePermission("warehouse.operate") },
@@ -1525,7 +1582,7 @@ export default async function warehouseRoutes(
const existing = await prisma.sklad_issues.findUnique({
where: { id },
include: { lines: true },
include: { items: true },
});
if (!existing) return error(reply, "Výdej nenalezen", 404);
if (existing.status !== "DRAFT") {
@@ -1538,10 +1595,10 @@ export default async function warehouseRoutes(
if (body.notes !== undefined) updateData.notes = body.notes;
updateData.modified_at = new Date();
// Replace lines if provided
if (body.lines) {
// Resolve FIFO batches for lines without batch_id
const resolvedLines: Array<{
// Replace items if provided
if (body.items) {
// Resolve FIFO batches for items without batch_id
const resolvedItems: Array<{
item_id: number;
batch_id: number;
quantity: number;
@@ -1550,30 +1607,30 @@ export default async function warehouseRoutes(
notes: string | null;
}> = [];
for (const line of body.lines) {
if (line.batch_id != null) {
resolvedLines.push({
item_id: line.item_id,
batch_id: line.batch_id,
quantity: line.quantity,
location_id: line.location_id ?? null,
reservation_id: line.reservation_id ?? null,
notes: line.notes ?? null,
for (const item of body.items) {
if (item.batch_id != null) {
resolvedItems.push({
item_id: item.item_id,
batch_id: item.batch_id,
quantity: item.quantity,
location_id: item.location_id ?? null,
reservation_id: item.reservation_id ?? null,
notes: item.notes ?? null,
});
} else {
try {
const fifoBatches = await selectFifoBatches(
line.item_id,
line.quantity,
item.item_id,
item.quantity,
);
for (const fb of fifoBatches) {
resolvedLines.push({
item_id: line.item_id,
resolvedItems.push({
item_id: item.item_id,
batch_id: fb.batchId,
quantity: fb.qty,
location_id: line.location_id ?? null,
reservation_id: line.reservation_id ?? null,
notes: line.notes ?? null,
location_id: item.location_id ?? null,
reservation_id: item.reservation_id ?? null,
notes: item.notes ?? null,
});
}
} catch (err) {
@@ -1586,18 +1643,32 @@ export default async function warehouseRoutes(
}
}
// Validate reservation rules
const issueProjectId = body.project_id ?? existing.project_id;
const reservationValidation = await validateIssueReservationRules(
resolvedItems,
issueProjectId,
);
if ("error" in reservationValidation) {
return error(
reply,
reservationValidation.error,
reservationValidation.status,
);
}
await prisma.sklad_issue_lines.deleteMany({
where: { issue_id: id },
});
await prisma.sklad_issue_lines.createMany({
data: resolvedLines.map((line) => ({
data: resolvedItems.map((item) => ({
issue_id: id,
item_id: line.item_id,
batch_id: line.batch_id,
quantity: line.quantity,
location_id: line.location_id,
reservation_id: line.reservation_id,
notes: line.notes,
item_id: item.item_id,
batch_id: item.batch_id,
quantity: item.quantity,
location_id: item.location_id,
reservation_id: item.reservation_id,
notes: item.notes,
})),
});
}
@@ -1606,8 +1677,8 @@ export default async function warehouseRoutes(
where: { id },
data: updateData,
include: {
project: { select: { id: true, name: true } },
lines: {
project: { select: { id: true, name: true, project_number: true } },
items: {
include: {
item: { select: { id: true, name: true, unit: true } },
},
@@ -1625,12 +1696,12 @@ export default async function warehouseRoutes(
oldValues: {
project_id: existing.project_id,
notes: existing.notes,
line_count: existing.lines.length,
item_count: existing.items.length,
},
newValues: {
project_id: updated.project_id,
notes: updated.notes,
line_count: updated.lines.length,
item_count: updated.items.length,
},
});
@@ -1730,7 +1801,7 @@ export default async function warehouseRoutes(
orderBy: { id: "desc" },
include: {
item: { select: { id: true, name: true, unit: true } },
project: { select: { id: true, name: true } },
project: { select: { id: true, name: true, project_number: true } },
reserved_by_user: {
select: { id: true, first_name: true, last_name: true },
},
@@ -1837,7 +1908,7 @@ export default async function warehouseRoutes(
take: limit,
orderBy: { id: "desc" },
include: {
_count: { select: { lines: true } },
_count: { select: { items: true } },
},
}),
prisma.sklad_inventory_sessions.count({ where }),
@@ -1851,7 +1922,7 @@ export default async function warehouseRoutes(
},
);
// GET /inventory-sessions/:id — detail with lines
// GET /inventory-sessions/:id — detail with items
fastify.get<{ Params: { id: string } }>(
"/inventory-sessions/:id",
{ preHandler: requirePermission("warehouse.inventory") },
@@ -1862,7 +1933,7 @@ export default async function warehouseRoutes(
const session = await prisma.sklad_inventory_sessions.findUnique({
where: { id },
include: {
lines: {
items: {
include: {
item: { select: { id: true, name: true, unit: true } },
location: { select: { id: true, code: true, name: true } },
@@ -1885,36 +1956,36 @@ export default async function warehouseRoutes(
if ("error" in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
// Build lines with auto-filled system_qty and calculated difference
const linesData = await Promise.all(
body.lines.map(async (line) => {
// Build items with auto-filled system_qty and calculated difference
const itemsData = await Promise.all(
body.items.map(async (item) => {
let systemQty = 0;
if (line.location_id != null) {
if (item.location_id != null) {
// Get stock at specific location
const loc = await prisma.sklad_item_locations.findUnique({
where: {
item_id_location_id: {
item_id: line.item_id,
location_id: line.location_id,
item_id: item.item_id,
location_id: item.location_id,
},
},
});
systemQty = loc ? Number(loc.quantity) : 0;
} else {
// Get total stock across all locations
systemQty = await getItemTotalStock(line.item_id);
systemQty = await getItemTotalStock(item.item_id);
}
const difference = Number(line.actual_qty) - systemQty;
const difference = Number(item.actual_qty) - systemQty;
return {
item_id: line.item_id,
location_id: line.location_id ?? null,
item_id: item.item_id,
location_id: item.location_id ?? null,
system_qty: systemQty,
actual_qty: Number(line.actual_qty),
actual_qty: Number(item.actual_qty),
difference,
notes: line.notes ?? null,
notes: item.notes ?? null,
};
}),
);
@@ -1923,12 +1994,12 @@ export default async function warehouseRoutes(
data: {
notes: body.notes ?? null,
status: "DRAFT",
lines: {
create: linesData,
items: {
create: itemsData,
},
},
include: {
lines: {
items: {
include: {
item: { select: { id: true, name: true, unit: true } },
location: { select: { id: true, code: true, name: true } },
@@ -1946,7 +2017,7 @@ export default async function warehouseRoutes(
description: `Vytvořena inventarizace #${session.id}`,
newValues: {
status: session.status,
line_count: linesData.length,
item_count: itemsData.length,
},
});
@@ -1954,7 +2025,7 @@ export default async function warehouseRoutes(
},
);
// PUT /inventory-sessions/:id — update lines (DRAFT only), recalculate system_qty and difference
// PUT /inventory-sessions/:id — update items (DRAFT only), recalculate system_qty and difference
fastify.put<{ Params: { id: string } }>(
"/inventory-sessions/:id",
{ preHandler: requirePermission("warehouse.inventory") },
@@ -1983,35 +2054,35 @@ export default async function warehouseRoutes(
};
if (body.notes !== undefined) updateData.notes = body.notes;
// Replace lines if provided, recalculating system_qty and difference
if (body.lines) {
const linesData = await Promise.all(
body.lines.map(async (line) => {
// Replace items if provided, recalculating system_qty and difference
if (body.items) {
const itemsData = await Promise.all(
body.items.map(async (item) => {
let systemQty = 0;
if (line.location_id != null) {
if (item.location_id != null) {
const loc = await prisma.sklad_item_locations.findUnique({
where: {
item_id_location_id: {
item_id: line.item_id,
location_id: line.location_id,
item_id: item.item_id,
location_id: item.location_id,
},
},
});
systemQty = loc ? Number(loc.quantity) : 0;
} else {
systemQty = await getItemTotalStock(line.item_id);
systemQty = await getItemTotalStock(item.item_id);
}
const difference = Number(line.actual_qty) - systemQty;
const difference = Number(item.actual_qty) - systemQty;
return {
item_id: line.item_id,
location_id: line.location_id ?? null,
item_id: item.item_id,
location_id: item.location_id ?? null,
system_qty: systemQty,
actual_qty: Number(line.actual_qty),
actual_qty: Number(item.actual_qty),
difference,
notes: line.notes ?? null,
notes: item.notes ?? null,
};
}),
);
@@ -2020,9 +2091,9 @@ export default async function warehouseRoutes(
where: { session_id: id },
});
await prisma.sklad_inventory_lines.createMany({
data: linesData.map((line) => ({
data: itemsData.map((item) => ({
session_id: id,
...line,
...item,
})),
});
}
@@ -2031,7 +2102,7 @@ export default async function warehouseRoutes(
where: { id },
data: updateData,
include: {
lines: {
items: {
include: {
item: { select: { id: true, name: true, unit: true } },
location: { select: { id: true, code: true, name: true } },
@@ -2160,8 +2231,8 @@ export default async function warehouseRoutes(
const issues = await prisma.sklad_issues.findMany({
where: { AND: issueWhere },
include: {
project: { select: { id: true, name: true } },
lines: {
project: { select: { id: true, name: true, project_number: true } },
items: {
include: {
item: { select: { id: true, name: true } },
batch: { select: { unit_price: true } },
@@ -2184,10 +2255,10 @@ export default async function warehouseRoutes(
>();
for (const issue of issues) {
for (const line of issue.lines) {
const key = `${line.item_id}-${issue.project_id}`;
const qty = Number(line.quantity);
const price = Number(line.batch?.unit_price ?? 0);
for (const item of issue.items) {
const key = `${item.item_id}-${issue.project_id}`;
const qty = Number(item.quantity);
const price = Number(item.batch?.unit_price ?? 0);
const value = qty * price;
const existing = consumptionMap.get(key);
@@ -2196,8 +2267,8 @@ export default async function warehouseRoutes(
existing.total_value += value;
} else {
consumptionMap.set(key, {
item_id: line.item_id,
item_name: line.item.name,
item_id: item.item_id,
item_name: item.item.name,
project_id: issue.project_id,
project_name: issue.project.name,
total_qty: qty,
@@ -2237,7 +2308,7 @@ export default async function warehouseRoutes(
where: { AND: receiptWhere },
include: {
supplier: { select: { id: true, name: true } },
lines: {
items: {
include: {
item: { select: { id: true, name: true } },
},
@@ -2248,8 +2319,8 @@ export default async function warehouseRoutes(
prisma.sklad_issues.findMany({
where: { AND: issueWhere },
include: {
project: { select: { id: true, name: true } },
lines: {
project: { select: { id: true, name: true, project_number: true } },
items: {
include: {
item: { select: { id: true, name: true } },
batch: { select: { unit_price: true } },
@@ -2274,32 +2345,32 @@ export default async function warehouseRoutes(
}> = [];
for (const receipt of receipts) {
for (const line of receipt.lines) {
for (const item of receipt.items) {
movements.push({
type: "receipt",
document_id: receipt.id,
document_number: receipt.receipt_number,
date: receipt.created_at,
item_id: line.item_id,
item_name: line.item.name,
quantity: Number(line.quantity),
unit_price: Number(line.unit_price),
item_id: item.item_id,
item_name: item.item.name,
quantity: Number(item.quantity),
unit_price: Number(item.unit_price),
supplier: receipt.supplier,
});
}
}
for (const issue of issues) {
for (const line of issue.lines) {
for (const item of issue.items) {
movements.push({
type: "issue",
document_id: issue.id,
document_number: issue.issue_number,
date: issue.created_at,
item_id: line.item_id,
item_name: line.item.name,
quantity: Number(line.quantity),
unit_price: Number(line.batch?.unit_price ?? 0),
item_id: item.item_id,
item_name: item.item.name,
quantity: Number(item.quantity),
unit_price: Number(item.batch?.unit_price ?? 0),
project: issue.project,
});
}