- Add Supplier Map component to suppliers page and reorganize sidebar navigation - Implement picking slip PDF generation with supplier per line item and picked-by signature footer - Add invoice bulk print tracking API and view models to track printed invoice state - Extend company search and form to support email/telephone fields and improve searchability - Fix data table debounce race condition by using useRef for onSearch handler - Fix supplier page fetch race condition with debounced timeout - Reduce list page item counts to 10 per page for customers, companies, and suppliers - Update company search filter to include email and telephone in search queries - Add comprehensive test suite for new and existing components and APIs - Add pdf-lib dependency for PDF generation and update related tooling - Clean up unused code and fix minor linting issues
5 KiB
5 KiB
Invoice Print Bundle (2x Invoice + 1x Picking Slip) Design
Date: 2026-07-09 Area: Invoicing / Printing Status: Drafted for review
Goal
From one Print button, generate a single combined PDF containing:
- QuickBooks invoice PDF (all pages) copy #1
- QuickBooks invoice PDF (all pages) copy #2
- Picking slip PDF (1 copy)
The bundle is created on demand in Next.js, without changing the n8n “create invoice” flow.
Decisions (Approved)
- Bundle is generated on demand in the Next.js app (not in n8n).
- Bundle key is single invoice number:
order_item_details.invoice.number. - Duplicate all pages of the invoice PDF, twice.
- Picking slip is generated for invoiced items only, but must still show:
- what is outstanding per line (if ordered > invoiced qty),
- what goes to stock per line (if collected > invoiced qty).
- Picking slip header should show:
Order number + Company + Customer. pdf-libcan be added as a dependency.
System Behavior
Frontend (Invoicing page)
- When clicking
Print, the UI calls a new API route with aninvoiceNumber:GET /api/invoicing/print-bundle?invoiceNumber=INV28819
- The browser opens the returned PDF for printing (single print action).
Next.js API Route (Bundle Generator)
The API route performs:
- Validate request:
invoiceNumberis required. - Query PocketBase for all
order_itemswith:order_item_details.invoice.number == invoiceNumberitem_status == "invoiced"
- Find the invoice PDF source:
- pick the first record in that invoice group where
qb_invoice_pdfis set - error if none of the matching records have
qb_invoice_pdf
- pick the first record in that invoice group where
- Download the invoice PDF from PocketBase using the resolved file URL.
- Generate picking slip PDF from the same invoice group (see below).
- Use
pdf-libto:- copy all pages from the invoice PDF into a new output PDF
- copy all pages from the invoice PDF again (2 copies)
- append the picking slip pages at the end
- Return the combined PDF as
application/pdf.
Picking Slip Content
Header
- Order number
- Company
- Customer
Data source:
- Prefer to derive from invoice-group records (from PocketBase query):
order numberfrom the order-item’s mapped order numbercompanyfrom the order-item’s mapped company namecustomerfrom the order-item’s mapped customer name
If there is any inconsistency across items, the picking slip uses the first record as the header source and includes the invoice number prominently.
Line Item Rules
The picking slip includes only the items that are part of this invoice number.
Each line should show:
- Description (and optional supplier)
- Ordered qty
- Collected qty
- Pick / Invoiced qty
- Outstanding qty
- To stock qty
Quantities
Inputs:
ordered:order_qty(or equivalent “ordered quantity” field)collected:qty_collected(or the best available collected field)invoicedQty: derived from invoice payload rules, using:- if invoice overrides exist for that invoice run: use override qty
- else use
min(collected, ordered)for the “fulfilled now” quantity
Derived outputs:
outstanding = max(ordered - invoicedQty, 0)toStock = max(collected - invoicedQty, 0)
This matches the operational requirement:
- surplus collected due to min order qty goes to stock
- partial collection leaves an outstanding balance visible on the slip
Storage Strategy (Invoice PDF)
Short-term:
- The API route supports either:
qb_invoice_pdfstored on every invoiced item, orqb_invoice_pdfstored only once, on one item in the invoice group.
Preferred longer-term:
- Store the PDF on only one record per invoice number (or move invoices into a separate
invoicescollection and link items).
The API route will:
- query all items by invoice number,
- then pick the first record that has the PDF.
Error Handling
- If
invoiceNumbermissing: return400. - If no invoiced
order_itemsmatch that invoice number: return404. - If invoice group exists but none have
qb_invoice_pdf: return409with a clear error message. - If invoice PDF download fails: return
502. - If bundle generation fails: return
500with a request id.
Performance Notes
- The API route is server-side so the browser does not download and merge PDFs locally.
pdf-libperforms in-memory copy/append, and the output is streamed back as a single response.- Avoid storing or generating “double invoice copy” PDFs in PocketBase; generate the bundle on demand.
Testing / Validation
Manual
- Use known test invoice:
INV28819(from the successful example). - Confirm:
- the returned PDF contains invoice twice and then the picking slip
- printing produces 2 invoice copies and 1 picking slip
- outstanding and to-stock quantities render correctly
Automated
- Unit test: PDF bundler builds:
2 * invoicePages + slipPagesin the output
- Integration test: API route returns
application/pdffor a mocked invoice group with oneqb_invoice_pdfrecord.