feat(delivery-notes): standardize delivery-note letterhead across PDFs
- Introduced a centralized `deliveryNoteLetterheadCode` function to ensure all delivery-note PDFs consistently use the Westcott Group letterhead, regardless of the subsidiary. - Updated relevant API routes and services to utilize the new function, removing previous hardcoded fallbacks. - Enhanced the `DriverStop` interface to include timestamps for better reporting capabilities. - Added new fields to track creation timestamps for driver stops, improving data accuracy for outstanding reporting.
This commit is contained in:
parent
bfcddb9d82
commit
2f0e911563
10 changed files with 272 additions and 16 deletions
BIN
.audit-screenshots/fix-09-new-quote-qty-22.png
Normal file
BIN
.audit-screenshots/fix-09-new-quote-qty-22.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 266 KiB |
|
|
@ -25,6 +25,14 @@
|
|||
"LOG_LEVEL": "error",
|
||||
"DISABLE_CONSOLE_OUTPUT": "true"
|
||||
}
|
||||
},
|
||||
"n8n-wfdash": {
|
||||
"type": "http",
|
||||
"url": "https://wfdash.n8n.apphub.co.za/mcp-server/http",
|
||||
"headers": {
|
||||
"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0Zjc3MGRmMi1iZDJmLTQ0MzYtYjE1My1iMjcwMDRlZmM3YjYiLCJpc3MiOiJuOG4iLCJhdWQiOiJtY3Atc2VydmVyLWFwaSIsImp0aSI6ImI1OWFlYTdjLTRhZGEtNGVlZS04NDFiLTI0YzI3OTAzYjZmZCIsImlhdCI6MTc4NzAzNzMxMX0.qWf1lMeatXnHr3ZDxxpT5L4PuG_5orWjDaMqBVEN5eI"
|
||||
},
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
76
.kilo/plans/wfdash-driver-live-alerter.json
Normal file
76
.kilo/plans/wfdash-driver-live-alerter.json
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
{
|
||||
"name": "WFDASH — Driver Live Stop Alerter",
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1",
|
||||
"timezone": "Africa/Johannesburg",
|
||||
"saveDataSuccessExecution": "all",
|
||||
"saveDataErrorExecution": "all",
|
||||
"callerPolicy": "workflowsFromSameOwner"
|
||||
},
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"rule": { "interval": [{ "secondsInterval": 90 }] }
|
||||
},
|
||||
"id": "sched-poll",
|
||||
"name": "Every 90s",
|
||||
"type": "n8n-nodes-base.scheduleTrigger",
|
||||
"typeVersion": 1.2,
|
||||
"position": [-640, 0]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "GET",
|
||||
"url": "=https://wfdash.apphub.co.za/api/v1/driver/stops?drv_id={{ $env.WFDASH_DRIVER_ID }}",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{ "name": "Authorization", "value": "=Bearer {{ $env.WFDASH_AGENT_API_KEY }}" }
|
||||
]
|
||||
},
|
||||
"options": { "timeout": 15000 }
|
||||
},
|
||||
"id": "fetch-stops",
|
||||
"name": "Fetch Driver Stops",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [-380, 0]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "runOnceForAllItems",
|
||||
"language": "javaScript",
|
||||
"jsCode": "const resp = $input.first().json;\nconst items = (resp && Array.isArray(resp.items)) ? resp.items : [];\n\nconst staticData = $getWorkflowStaticData('global');\nconst seen = new Set(Array.isArray(staticData.seenIds) ? staticData.seenIds : []);\nconst fresh = items.filter(i => i.id && !seen.has(i.id));\n\nstaticData.seenIds = items.map(i => i.id);\nstaticData.lastRunAt = new Date().toISOString();\n\nif (fresh.length === 0) return [];\n\nreturn fresh.map(i => ({\n json: {\n id: i.id,\n supplier: (i.supplier && i.supplier.name) || 'Unknown',\n description: i.description || '',\n quantity: i.quantity_ordered || 0,\n po: i.po_number || ''\n }\n}));"
|
||||
},
|
||||
"id": "diff-stops",
|
||||
"name": "Detect New Stops",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [-100, 0]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "sms",
|
||||
"operation": "send",
|
||||
"from": "=whatsapp:{{ $env.TWILIO_WHATSAPP_FROM }}",
|
||||
"to": "=whatsapp:{{ $env.MANAGER_WHATSAPP_NUMBER }}",
|
||||
"message": "=🚚 {{ $json.supplier }} — {{ $json.description }} ({{ $json.quantity }}) {{ $json.po }}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "send-wa",
|
||||
"name": "Send WhatsApp",
|
||||
"type": "n8n-nodes-base.twilio",
|
||||
"typeVersion": 1,
|
||||
"position": [180, 0],
|
||||
"credentials": {
|
||||
"twilioApi": { "id": "REPLACE_WITH_TWILIO_CREDENTIAL_ID", "name": "Twilio account" }
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Every 90s": { "main": [[{ "node": "Fetch Driver Stops", "type": "main", "index": 0 }]] },
|
||||
"Fetch Driver Stops": { "main": [[{ "node": "Detect New Stops", "type": "main", "index": 0 }]] },
|
||||
"Detect New Stops": { "main": [[{ "node": "Send WhatsApp", "type": "main", "index": 0 }]] }
|
||||
}
|
||||
}
|
||||
76
.kilo/plans/wfdash-driver-morning-summary.json
Normal file
76
.kilo/plans/wfdash-driver-morning-summary.json
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
{
|
||||
"name": "WFDASH — Driver Morning WhatsApp Summary",
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1",
|
||||
"timezone": "Africa/Johannesburg",
|
||||
"saveDataSuccessExecution": "all",
|
||||
"saveDataErrorExecution": "all",
|
||||
"callerPolicy": "workflowsFromSameOwner"
|
||||
},
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"rule": { "interval": [{ "hours": 7, "triggerAtMinute": 0 }] }
|
||||
},
|
||||
"id": "sched-morning",
|
||||
"name": "Daily 07:00",
|
||||
"type": "n8n-nodes-base.scheduleTrigger",
|
||||
"typeVersion": 1.2,
|
||||
"position": [-560, 0]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "GET",
|
||||
"url": "=https://wfdash.apphub.co.za/api/v1/driver/stops?drv_id={{ $env.WFDASH_DRIVER_ID }}",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{ "name": "Authorization", "value": "=Bearer {{ $env.WFDASH_AGENT_API_KEY }}" }
|
||||
]
|
||||
},
|
||||
"options": { "timeout": 15000 }
|
||||
},
|
||||
"id": "fetch-stops",
|
||||
"name": "Fetch Driver Stops",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [-280, 0]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "runOnceForAllItems",
|
||||
"language": "javaScript",
|
||||
"jsCode": "const resp = $input.first().json;\nconst items = (resp && Array.isArray(resp.items)) ? resp.items : [];\nconst live = items.filter(i => i.supplier && i.supplier.name);\n\nconst groups = {};\nfor (const i of live) {\n const k = i.supplier.name.trim();\n groups[k] = (groups[k] || 0) + 1;\n}\n\nconst today = new Date().toLocaleDateString('en-ZA', { weekday: 'long', day: 'numeric', month: 'short' });\nlet body;\nconst supplierCount = Object.keys(groups).length;\nif (live.length === 0) {\n body = '*WFDASH driver briefing* — ' + today + '\\n\\nNo stops today.\\nOpen app: https://wfdash.apphub.co.za/driver-app';\n} else {\n const sorted = Object.entries(groups).sort((a, b) => b[1] - a[1]);\n const lines = sorted.map(([name, n]) => '• ' + name + ' — ' + n);\n body = '*WFDASH driver briefing* — ' + today + '\\n\\n' + live.length + ' stop' + (live.length === 1 ? '' : 's') + ' across ' + supplierCount + ' supplier' + (supplierCount === 1 ? '' : 's') + ':\\n\\n' + lines.join('\\n') + '\\n\\nOpen app: https://wfdash.apphub.co.za/driver-app';\n}\n\nreturn [{ json: { message: body } }];"
|
||||
},
|
||||
"id": "build-msg",
|
||||
"name": "Group by Supplier",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [0, 0]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "sms",
|
||||
"operation": "send",
|
||||
"from": "=whatsapp:{{ $env.TWILIO_WHATSAPP_FROM }}",
|
||||
"to": "=whatsapp:{{ $env.MANAGER_WHATSAPP_NUMBER }}",
|
||||
"message": "={{ $json.message }}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "send-wa",
|
||||
"name": "Send WhatsApp (Twilio)",
|
||||
"type": "n8n-nodes-base.twilio",
|
||||
"typeVersion": 1,
|
||||
"position": [280, 0],
|
||||
"credentials": {
|
||||
"twilioApi": { "id": "REPLACE_WITH_TWILIO_CREDENTIAL_ID", "name": "Twilio account" }
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Daily 07:00": { "main": [[{ "node": "Fetch Driver Stops", "type": "main", "index": 0 }]] },
|
||||
"Fetch Driver Stops": { "main": [[{ "node": "Group by Supplier", "type": "main", "index": 0 }]] },
|
||||
"Group by Supplier": { "main": [[{ "node": "Send WhatsApp (Twilio)", "type": "main", "index": 0 }]] }
|
||||
}
|
||||
}
|
||||
|
|
@ -11,8 +11,8 @@ import {
|
|||
import { resolveDeliveryNoteLineQty } from "@/lib/pdf/delivery-note-document";
|
||||
import { renderDeliveryNotePdf } from "@/lib/pdf/render";
|
||||
import {
|
||||
deliveryNoteLetterheadCode,
|
||||
fetchLetterhead,
|
||||
letterheadForCompany,
|
||||
loadLetterheadLogoDataUri,
|
||||
} from "@/lib/letterhead";
|
||||
|
||||
|
|
@ -144,7 +144,9 @@ async function handlePrint(input: { deliveryNoteNumber: string; origin: string }
|
|||
// current items. This keeps older records printable. Same
|
||||
// letterhead + logo lookup as the create route so the fallback
|
||||
// rendering matches the saved version byte-for-byte.
|
||||
const letterheadCode = letterheadForCompany(header.companyName) ?? "CNL";
|
||||
// Same canonical letterhead as the create route — see
|
||||
// `deliveryNoteLetterheadCode()` for the business rule.
|
||||
const letterheadCode = deliveryNoteLetterheadCode();
|
||||
const [letterhead, logoDataUri] = await Promise.all([
|
||||
fetchLetterhead(letterheadCode),
|
||||
loadLetterheadLogoDataUri(letterheadCode),
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import { getAdminPocketBase } from "@/lib/server/pocketbase-admin";
|
|||
import { renderDeliveryNotePdf } from "@/lib/pdf/render";
|
||||
import { resolveDeliveryNoteLineQty } from "@/lib/pdf/delivery-note-document";
|
||||
import {
|
||||
deliveryNoteLetterheadCode,
|
||||
fetchLetterhead,
|
||||
letterheadForCompany,
|
||||
loadLetterheadLogoDataUri,
|
||||
} from "@/lib/letterhead";
|
||||
|
||||
|
|
@ -340,10 +340,13 @@ export async function POST(request: NextRequest) {
|
|||
const first = items[0];
|
||||
const order = first.expand?.order;
|
||||
const companyName = order?.expand?.company?.company ?? "";
|
||||
// Derive the letterhead from the company name (CNL vs
|
||||
// ISHERWOOD). Falls back to CNL when the name doesn't match
|
||||
// either — same default the rest of the invoicing flow uses.
|
||||
const letterheadCode = letterheadForCompany(companyName) ?? "CNL";
|
||||
// Every delivery-note PDF renders under the Westcott Group
|
||||
// letterhead, regardless of which subsidiary the customer
|
||||
// belongs to. The subsidiary name still appears in the PDF
|
||||
// meta block so the printed document tells the driver which
|
||||
// subsidiary the delivery is for — only the letterhead is
|
||||
// always Westcott. See `deliveryNoteLetterheadCode()`.
|
||||
const letterheadCode = deliveryNoteLetterheadCode();
|
||||
const letterhead = await fetchLetterhead(letterheadCode);
|
||||
const logoDataUri = await loadLetterheadLogoDataUri(letterheadCode);
|
||||
const pdfBuffer = await renderDeliveryNotePdf({
|
||||
|
|
|
|||
49
src/lib/letterhead.test.ts
Normal file
49
src/lib/letterhead.test.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
DELIVERY_NOTE_LETTERHEAD_CODE,
|
||||
deliveryNoteLetterheadCode,
|
||||
letterheadForCompany,
|
||||
} from "./letterhead";
|
||||
|
||||
/**
|
||||
* Locks down the contract that delivery-note PDFs always render under
|
||||
* the Westcott Group letterhead, regardless of which subsidiary the
|
||||
* customer belongs to. If a future change makes the rule
|
||||
* subsidiary-driven, this test is the guardrail that will fail first
|
||||
* and force the discussion.
|
||||
*
|
||||
* `letterheadForCompany` is intentionally kept narrow (ISHERWOOD / CNL
|
||||
* only) because that helper is also used by the quote / PO flows
|
||||
* which have different brand rules. Delivery notes don't.
|
||||
*/
|
||||
|
||||
describe("deliveryNoteLetterheadCode", () => {
|
||||
it("is the Westcott Group letterhead", () => {
|
||||
expect(deliveryNoteLetterheadCode()).toBe("WESTSCOTT GROUP OF COMPANIES");
|
||||
expect(DELIVERY_NOTE_LETTERHEAD_CODE).toBe("WESTSCOTT GROUP OF COMPANIES");
|
||||
});
|
||||
|
||||
it("returns the canonical code regardless of subsidiary name", () => {
|
||||
// Every subsidiary the Westcott Group owns should still render
|
||||
// under the Westcott letterhead. If anyone ever wires
|
||||
// `letterheadForCompany` into the delivery-note flow, this test
|
||||
// will catch it because `letterheadForCompany` returns CNL /
|
||||
// ISHERWOOD for these inputs.
|
||||
for (const subsidiary of [
|
||||
"CNL Fasteners (Pty) Ltd",
|
||||
"Isherwood Mining South Africa (Pty) Ltd",
|
||||
"Westcott Test Engineering (Pty) Ltd",
|
||||
"Merafong Crushers (Pty) Ltd.",
|
||||
"",
|
||||
]) {
|
||||
const fromCompany = letterheadForCompany(subsidiary);
|
||||
const fromHelper = deliveryNoteLetterheadCode();
|
||||
// The helper is the source of truth for delivery notes — it
|
||||
// must never equal whatever `letterheadForCompany` returns for
|
||||
// a subsidiary name (CNL / ISHERWOOD are the only two it
|
||||
// could match; the helper must always be the Westcott code).
|
||||
expect(fromHelper).toBe("WESTSCOTT GROUP OF COMPANIES");
|
||||
expect(fromHelper).not.toBe(fromCompany ?? "");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -19,6 +19,31 @@ export const LETTERHEAD_CODES: readonly LetterheadCode[] = [
|
|||
"WESTSCOTT GROUP OF COMPANIES",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Canonical letterhead for ALL delivery-note PDFs, regardless of
|
||||
* which subsidiary the customer belongs to. The subsidiary name
|
||||
* still appears in the PDF meta block (so the printed document
|
||||
* tells the driver which subsidiary the delivery is for) but the
|
||||
* letterhead/header is always the Westcott Group one.
|
||||
*
|
||||
* Centralised here so the delivery-notes service and the invoicing
|
||||
* API (which both render delivery-note PDFs) can't drift apart
|
||||
* again. If the brand ever needs to change, this is the only
|
||||
* constant to touch.
|
||||
*/
|
||||
export const DELIVERY_NOTE_LETTERHEAD_CODE: LetterheadCode =
|
||||
"WESTSCOTT GROUP OF COMPANIES";
|
||||
|
||||
/**
|
||||
* Helper that returns the canonical delivery-note letterhead code.
|
||||
* Use this anywhere a delivery-note PDF is being rendered so the
|
||||
* three-letter `?? "CNL"` fallbacks that used to live in the
|
||||
* invoicing API routes stay gone.
|
||||
*/
|
||||
export function deliveryNoteLetterheadCode(): LetterheadCode {
|
||||
return DELIVERY_NOTE_LETTERHEAD_CODE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick the letterhead code that matches a given company name. The
|
||||
* invoice / delivery-note flows don't always carry an explicit
|
||||
|
|
|
|||
|
|
@ -37,9 +37,16 @@ import { generateQuoteNumberServer } from "./quotes-service";
|
|||
import { renderDeliveryNotePdf } from "@/lib/pdf/render";
|
||||
import { resolveDeliveryNoteLineQty } from "@/lib/pdf/delivery-note-document";
|
||||
import {
|
||||
DELIVERY_NOTE_LETTERHEAD_CODE,
|
||||
deliveryNoteLetterheadCode,
|
||||
fetchLetterhead,
|
||||
loadLetterheadLogoDataUri,
|
||||
} from "@/lib/letterhead";
|
||||
|
||||
// Re-export so existing callers that imported the constant from this
|
||||
// module (`import { DELIVERY_NOTE_LETTERHEAD_CODE } from "@/lib/server/delivery-notes-service"`)
|
||||
// keep working after the canonical home moved to `@/lib/letterhead`.
|
||||
export { DELIVERY_NOTE_LETTERHEAD_CODE };
|
||||
import type {
|
||||
DeliveryNote,
|
||||
DeliveryNoteStatus,
|
||||
|
|
@ -80,10 +87,6 @@ const ORDERS = "orders";
|
|||
*
|
||||
* If you want to swap the brand (e.g. rebrand to a new parent
|
||||
* company), update the PB row and change this string in
|
||||
* lockstep. The PDF will pick up the new content from PB
|
||||
* automatically without any code change beyond the constant.
|
||||
*/
|
||||
const DELIVERY_NOTE_LETTERHEAD_CODE = "WESTSCOTT GROUP OF COMPANIES";
|
||||
|
||||
/**
|
||||
* Lifecycle transitions for the delivery_notes.status field. The
|
||||
|
|
@ -374,11 +377,11 @@ async function attachDeliveryNoteToQuote(
|
|||
const companyName = dn.expand?.company?.company ?? "";
|
||||
// All delivery-note PDFs render under the Westcott
|
||||
// Group letterhead, regardless of which subsidiary the
|
||||
// customer belongs to — see DELIVERY_NOTE_LETTERHEAD_CODE.
|
||||
// customer belongs to — see deliveryNoteLetterheadCode().
|
||||
// The `companyName` above still goes into the PDF meta
|
||||
// block so the printed document tells the driver which
|
||||
// subsidiary the delivery is for.
|
||||
const letterheadCode = DELIVERY_NOTE_LETTERHEAD_CODE;
|
||||
const letterheadCode = deliveryNoteLetterheadCode();
|
||||
const letterhead = await fetchLetterhead(letterheadCode);
|
||||
const logoDataUri = await loadLetterheadLogoDataUri(letterheadCode);
|
||||
const customerName = dn.expand?.customer?.customer ?? "";
|
||||
|
|
@ -945,7 +948,7 @@ export async function createAdHocDeliveryNote(
|
|||
? (companyRow as unknown as { company: string }).company
|
||||
: "";
|
||||
|
||||
const letterheadCode = DELIVERY_NOTE_LETTERHEAD_CODE;
|
||||
const letterheadCode = deliveryNoteLetterheadCode();
|
||||
const letterhead = await fetchLetterhead(letterheadCode);
|
||||
const logoDataUri = await loadLetterheadLogoDataUri(letterheadCode);
|
||||
|
||||
|
|
@ -1066,11 +1069,11 @@ export async function regenerateDeliveryNotePdf(
|
|||
|
||||
// All delivery-note PDFs render under the Westcott
|
||||
// Group letterhead, regardless of which subsidiary the
|
||||
// customer belongs to — see DELIVERY_NOTE_LETTERHEAD_CODE.
|
||||
// customer belongs to — see deliveryNoteLetterheadCode().
|
||||
// The `companyName` above still goes into the PDF meta
|
||||
// block so the printed document tells the driver which
|
||||
// subsidiary the delivery is for.
|
||||
const letterheadCode = DELIVERY_NOTE_LETTERHEAD_CODE;
|
||||
const letterheadCode = deliveryNoteLetterheadCode();
|
||||
const [letterhead, logoDataUri] = await Promise.all([
|
||||
fetchLetterhead(letterheadCode),
|
||||
loadLetterheadLogoDataUri(letterheadCode),
|
||||
|
|
|
|||
|
|
@ -83,6 +83,16 @@ export interface DriverStop {
|
|||
drv_id: string | null;
|
||||
/** ISO timestamp of the most recent assignment (date-only on stock side). */
|
||||
assigned_at: string | null;
|
||||
/** ISO timestamp of when the underlying PB row was created. Useful
|
||||
* for "outstanding N days" reporting from the driver app / n8n. */
|
||||
created_at: string | null;
|
||||
/** ISO timestamp of when the parent order / purchase-order was
|
||||
* created in WFDASH. This is the canonical "this thing came into
|
||||
* the system on day X" timestamp — distinct from `assigned_at`
|
||||
* (when a driver was put on it) and `created_at` (when the line
|
||||
* item was added, which can differ if the line was split). Use
|
||||
* this for "outstanding N days" reporting. */
|
||||
outstanding_since: string | null;
|
||||
/** Parent PO number, formatted for display. */
|
||||
po_number: string;
|
||||
/** Parent order id (delivery only — `null` for pickups). */
|
||||
|
|
@ -305,6 +315,8 @@ function normaliseDelivery(r: DeliveryRow): DriverStop {
|
|||
status: (r.item_status ?? "collection_pool") as DriverStopStatus,
|
||||
drv_id: r.drv_id || null,
|
||||
assigned_at: r.assigned_at || null,
|
||||
created_at: r.created || null,
|
||||
outstanding_since: order?.created || null,
|
||||
po_number: r.expand?.purchase_order?.po_number || "",
|
||||
order_id: order?.id || null,
|
||||
customer_name: customer?.customer || null,
|
||||
|
|
@ -340,6 +352,8 @@ function normalisePickup(r: PickupRow): DriverStop {
|
|||
status: (r.status ?? "collection_pool") as DriverStopStatus,
|
||||
drv_id: r.drv_id || null,
|
||||
assigned_at: r.assigned_at || null,
|
||||
created_at: r.created || null,
|
||||
outstanding_since: r.expand?.purchase?.created || null,
|
||||
po_number: r.expand?.purchase?.purchase_number || "",
|
||||
order_id: null,
|
||||
customer_name: null,
|
||||
|
|
|
|||
Loading…
Reference in a new issue