← back to Filemaker Mcp
auto-save: 2026-06-29T12:49:09 (2 files) — lib/invoice.js src/fm-client.js
1894d579958dcc3f8e5b7e4c6cf7abdd383ae5ac · 2026-06-29 12:49:14 -0700 · Steve Abrams
Files touched
M lib/invoice.jsM src/fm-client.js
Diff
commit 1894d579958dcc3f8e5b7e4c6cf7abdd383ae5ac
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Jun 29 12:49:14 2026 -0700
auto-save: 2026-06-29T12:49:09 (2 files) — lib/invoice.js src/fm-client.js
---
lib/invoice.js | 32 +++++++++++++++++++++++++-------
src/fm-client.js | 7 +++++++
2 files changed, 32 insertions(+), 7 deletions(-)
diff --git a/lib/invoice.js b/lib/invoice.js
index 6ebfab7..60a678d 100644
--- a/lib/invoice.js
+++ b/lib/invoice.js
@@ -20,7 +20,9 @@ const WCPAT_LAYOUT = '1-order detail 1 Copy3'; // exposes "wc pattern"
const today = () => { const d = new Date(); return `${String(d.getMonth()+1).padStart(2,'0')}/${String(d.getDate()).padStart(2,'0')}/${d.getFullYear()}`; };
const stamp = () => new Date().toLocaleString('en-US', { month:'short', day:'numeric', year:'numeric', hour:'numeric', minute:'2-digit' });
const baseSku = (sku) => { const m = (sku||'').match(/^([A-Za-z]+-?\d+)/); return m ? m[1] : (sku||''); };
-const wcPattern = (sku) => (sku||'').replace(/-?\s*sample\s*/ig, '').trim();
+// WC PATTERN / SKU = the pattern's Shopify SKU with the -Sample/Sample variant removed.
+const wcPattern = (sku) => baseSku((sku||'').replace(/-?\s*sample\s*/ig, '').trim());
+const r2 = (n) => Math.round((Number(n)||0) * 100) / 100;
function uom(li) {
const t = `${li.variant_title||''} ${li.name||''} ${li.sku||''}`.toLowerCase();
if (/per yard|\/ ?yard|\byard/.test(t)) return 'yards';
@@ -47,8 +49,9 @@ async function nextInvoiceNumber() {
// safe single-field patch (skips calc / non-modifiable fields without aborting)
async function patch(id, field, value) {
try { await fm.updateRecord('invoice', HDR, id, { [field]: value }, { dryRun: false }); }
- catch (e) { if (!['102','201','509'].includes(e.fmCode)) throw e; }
+ catch (e) { if (!['102','201','509','111'].includes(e.fmCode)) throw e; }
}
+const MAX_LINES = 4; // DETAIL 1 / Q1 / NET 1 family caps at maxRepeat 4
export async function createInvoiceForOrder(order, accountNumber, { commit = true } = {}) {
const c = order.customer || {}, s = order.shipping_address || order.billing_address || {};
@@ -89,9 +92,12 @@ export async function createInvoiceForOrder(order, accountNumber, { commit = tru
await patch(id, 'SALES TAX YN', orderTax > 0 ? 'Y' : 'N');
await patch(id, 'check Number', today());
- // line items -> repetitions
- for (let i = 0; i < items.length; i++) {
- const li = items[i], r = i + 1, price = Number(li.price || 0);
+ // line items -> repetitions 1..MAX_LINES. If more line items than slots,
+ // write the first MAX_LINES-1 individually and CONSOLIDATE the rest into the
+ // last slot (qty 1 × summed $) so the merchandise total stays exact.
+ const individual = items.length <= MAX_LINES ? items.length : MAX_LINES - 1;
+ for (let i = 0; i < individual; i++) {
+ const li = items[i], r = i + 1, price = r2(li.price);
await patch(id, `DETAIL 1(${r})`, `${li.sku || ''} — ${li.name || ''}`.trim());
await patch(id, `Q1(${r})`, li.quantity);
await patch(id, `Unit 1(${r})`, uom(li));
@@ -99,13 +105,25 @@ export async function createInvoiceForOrder(order, accountNumber, { commit = tru
await patch(id, `NET 1(${r})`, price);
await patch(id, `Sidemark 1(${r})`, s.company || '');
}
+ if (items.length > MAX_LINES) {
+ const rest = items.slice(individual);
+ const restTotal = r2(rest.reduce((a, li) => a + (Number(li.price)||0) * (li.quantity||1), 0));
+ const skus = rest.map((li) => baseSku(li.sku)).join(', ');
+ const r = MAX_LINES;
+ await patch(id, `DETAIL 1(${r})`, `+${rest.length} more items: ${skus}`.slice(0, 250));
+ await patch(id, `Q1(${r})`, 1);
+ await patch(id, `Unit 1(${r})`, 'lot');
+ await patch(id, `RETAIL 1(${r})`, restTotal);
+ await patch(id, `NET 1(${r})`, restTotal);
+ await patch(id, `Sidemark 1(${r})`, s.company || '');
+ }
// cross-table fields (different base-table layouts; share the record id)
try { await fm.updateRecord('invoice', SKU_LAYOUT, id, { 'JS Easy SKU #': baseSku(primarySku) }, { dryRun: false }); } catch {}
try { await fm.updateRecord('invoice', WCPAT_LAYOUT, id, { 'wc pattern': wcPattern(primarySku) }, { dryRun: false }); } catch {}
- // paid-in-full = computed grand total -> Net Due 0
- const grand = (await fm.getRecord('invoice', HDR, id))?.fieldData?.['GRAND TOTAL'] || 0;
+ // paid-in-full = computed grand total (rounded to cents) -> Net Due 0
+ const grand = r2((await fm.getRecord('invoice', HDR, id))?.fieldData?.['GRAND TOTAL'] || 0);
await patch(id, 'PAID ON ACCOUNT', grand);
await patch(id, 'Notes', `Imported by Otto — ${stamp()}`);
diff --git a/src/fm-client.js b/src/fm-client.js
index a21cf28..deb7798 100644
--- a/src/fm-client.js
+++ b/src/fm-client.js
@@ -151,6 +151,13 @@ export async function updateRecord(db, layout, recordId, fieldData, { dryRun = t
return { committed: true, dryRun: false, database: db, layout: lay, recordId, changes: diff };
}
+export async function deleteRecord(db, layout, recordId) {
+ assertWritable(db);
+ const lay = resolveLayout(db, layout);
+ await fm(db, `/layouts/${encodeURIComponent(lay)}/records/${encodeURIComponent(recordId)}`, { method: 'DELETE' });
+ return { deleted: true, database: db, recordId };
+}
+
export async function createRecord(db, layout, fieldData, { dryRun = true } = {}) {
assertWritable(db);
const lay = resolveLayout(db, layout);
← c4dc440 sync: refresh existing client's contact info from the order
·
back to Filemaker Mcp
·
fix: consolidate multi-item orders to single line (exact tot 7525e58 →