20 Commits

Author SHA1 Message Date
7155e199e8 Merge branch 'master' into staging 2026-07-29 10:25:04 -07:00
2b5fbaca00 Add template for new Reel Produce statement layout
The statement no longer prints "Reel Produce" as text (only
orders@reelproduce.com), switched to MM/DD/YYYY dates, and moved the
invoice number into an "INV #..." transaction description, so no
template matched and the file fell through to the glimpse2 fallback.

Adds a QuickBooks-statement-style template (same shape as Suncrest /
Ocean Queen) keyed on reelproduce.com + Statement, placed after the
existing Reel Produce statement template so the old layout still wins.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 10:24:31 -07:00
473556a45d Backfill script for olo 2026-07-29 09:59:02 -07:00
604d1ee1cf changes 2026-07-25 21:13:44 -07:00
e095cb94e4 fix(config): propagate rotated Plaid secret to worker configs
The rotation in 111eca41 updated the Plaid secret-key only in prod.edn,
leaving prod-background-worker.edn, prod-cloud-background-worker.edn, and
prod-cloud.edn on the old (now-invalidated) secret. Background worker jobs
loading those files failed with INVALID_API_KEYS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 20:29:15 -07:00
9d007ee8e2 Merge branch 'master' into staging 2026-07-22 21:50:12 -07:00
fa25620b7a olo fixes 2026-07-22 21:49:24 -07:00
d012283362 olo fixes 2026-07-22 21:43:31 -07:00
14726d0208 re-synced secrets. 2026-07-22 21:17:31 -07:00
6429cf823d fixes 2026-07-12 20:45:36 -07:00
111eca413e rotates passwords 2026-07-12 20:44:05 -07:00
80e45c026e feat(ssr): add sales summary csv download
Exports one row per sales summary item so each category is individually
auditable, rather than one row per daily summary. Uses the grid helper's
page->csv-entities hook, the same seam the ledger export uses to fan a
journal entry out into its line items.

Includes the GL account code alongside the name so rows tie back to the
chart of accounts, and formats amounts to cents to keep float noise
(36.900000000000006) out of the export. Accounts are resolved in a single
batched pull and clientized per summary so name overrides are respected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 23:51:46 -07:00
9cd3d9c962 fixed. 2026-07-08 23:33:37 -07:00
5f5686c39c fix(ssr): link to the real transaction route from paperclip menus
::transaction-routes/all-page does not exist -- the transactions index
is registered as ::transaction-routes/page ("/transaction2"). bidi's
path-for returns nil for an unknown handler, so (hu/url nil {...})
produced the relative "?exact-match-id=123". Clicking a Transaction
link in the ledger paperclip tooltip therefore stayed on the ledger and
re-filtered it by a transaction entity id, matching nothing.

Repoint all five call sites (ledger, payments and invoice paperclip
menus, the insights breadcrumb, and the expected-deposit row button) at
::transaction-routes/page.

Also add the missing conj in the ledger :invoice/source-url cond->
branch. The bare map made cond-> invoke it as a function against the
accumulated vector, yielding nil, so any journal entry whose original
entity was an invoice with a file rendered no paperclip at all.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 23:15:17 -07:00
d0846f91aa fix(ssr): use form-validation-error for transaction edit validations
Untyped (ex-info ... {:validation-error ...}) throws escaped the
wrap-form-4xx-2 middleware, surfacing as a 500 / unexpectedError box
instead of an inline form error. Route them through
utils/form-validation-error, which throws with :type :form-validation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 23:12:00 -07:00
56addf8c70 fix(ssr): order coerced vector form params by numeric index
Indexed form fields like periods[0]..periods[13] parse into a
string-keyed map; coerce-vector rebuilt the vector with a plain
lexicographic sort, so "10".."13" sorted before "2". Reports with
10+ periods (e.g. the 13/14-period option) rendered columns in the
order 0,1,10,11,12,13,2..9. Sort index keys numerically instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 05:22:52 -07:00
ebb48a0de4 feat(ssr): previous calendar year period + external register filters
- P&L period dropdown: replace "Calendar year (YYYY)" with
  "Previous Calendar Year (YYYY-1)" pulling the full prior year
- External register: title now reads "External Register"; add
  Source, External Id, and Location filters

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 07:22:31 -07:00
fc54b92ddb fixes 2026-06-04 22:59:51 -07:00
019a1b4cd8 fixes 2026-06-04 22:56:01 -07:00
38575aa5bd data(sysco): add missing line-item GL mappings for paper & other items
The Sysco importer codes line items by exact-matching the item description
against resources/sysco_line_item_mapping.csv, falling back to GL 50000
(Food Costs) when no entry exists. On master, 8 of the paper-product
descriptions on recent invoices (e.g. BAG PAPER 250 CT, NAPKIN 2PLY INTR
FOLD 6.3X8.26, CONTAINER PAPER 4/110OZ NTG) were missing, so they
defaulted to 50000 instead of 55000 (Paper Costs).

Append the 34 curated mappings (Ids 1762-1795) covering these paper items
(-> 55000) plus the other new items from the same invoices, so they code
correctly on re-import.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 09:28:23 -07:00
27 changed files with 902 additions and 57 deletions

View File

@@ -173,4 +173,4 @@
:token "EAAAEO2xSqesDutZz71hz3eulKmrlKTiEqG3uZ4j25x5GYlOluQ2cj2JxNUXqXD7"}}
:plaid {:base-url "https://production.plaid.com"
:client-id "61bfab05f7e762001b323f79"
:secret-key "2be026ca5e7f7e9f23f2fb4d7c914d"}}
:secret-key "44a05fbe9f33a2975b3b3ac06b0b62"}}

View File

@@ -31,5 +31,5 @@
:yodlee2-proxy-port 8888
:plaid {:base-url "https://production.plaid.com"
:client-id "61bfab05f7e762001b323f79"
:secret-key "2be026ca5e7f7e9f23f2fb4d7c914d"}
:secret-key "44a05fbe9f33a2975b3b3ac06b0b62"}
}

View File

@@ -34,5 +34,5 @@
:yodlee2-proxy-port 8888
:plaid {:base-url "https://production.plaid.com"
:client-id "61bfab05f7e762001b323f79"
:secret-key "2be026ca5e7f7e9f23f2fb4d7c914d"}
:secret-key "44a05fbe9f33a2975b3b3ac06b0b62"}
}

View File

@@ -6,7 +6,7 @@
:scheme "https"
:dd-env "prod"
:dd-service "integreat-app"
:jwt-secret "auto ap invoices are awesome"
:jwt-secret "rotated secrets are the best"
:invoice-import-queue-url "https://sqs.us-east-1.amazonaws.com/679918342773/integreat-mail-prod"
:requests-queue-url "https://sqs.us-east-1.amazonaws.com/679918342773/integreat-background-request-prod"
:invoice-email "invoices@mail.app.integreatconsult.com"
@@ -25,12 +25,12 @@
:run-background? false
:run-web? true
:yodlee2-integreat-user "integreat-main"
:yodlee2-client-id "3AATcwfPsWP1rP9oDoo4HvZhtaroGVcA"
:yodlee2-client-secret "cXTBmKbGfkaBFIpM"
:yodlee2-client-id "lNgzSdjkVXSkXyOBlResXUsCpCpBBDlG"
:yodlee2-client-secret "U3qjZP2gErZfbuTPud+LNJF9jHbNRzCWCZbEi6dDiHsziCNwI5yNNNrBAUsnjcu7VFsUVNmkxNKSW85Qf1YMOITC7q0kdv7MGv/ZqRLfQV5odkiPbLHgOrE7UE6//MtjU0jTznGA70WTPS+wwmugg8ArNnx+4QCHrrBrkRfFVOE="
:yodlee2-base-url "https://production.api.yodlee.com/ysl"
:yodlee2-fastlink "https://fl4.prod.yodlee.com/authenticate/USDevexProd2-319/fastlink/?channelAppName=usdevexprod2"
:yodlee2-proxy-host "172.31.10.83"
:yodlee2-proxy-port 8888
:plaid {:base-url "https://production.plaid.com"
:client-id "61bfab05f7e762001b323f79"
:secret-key "2be026ca5e7f7e9f23f2fb4d7c914d"}}
:secret-key "44a05fbe9f33a2975b3b3ac06b0b62"}}

View File

@@ -6,7 +6,7 @@
:scheme "https"
:dd-env "prod"
:dd-service "integreat-app"
:jwt-secret "auto ap invoices are awesome"
:jwt-secret "rotated secrets are the best"
:invoice-import-queue-url "https://sqs.us-east-1.amazonaws.com/679918342773/integreat-mail-prod"
:requests-queue-url "https://sqs.us-east-1.amazonaws.com/679918342773/integreat-background-request-prod"
:invoice-email "invoices@mail.app.integreatconsult.com"
@@ -25,13 +25,13 @@
:run-background? false
:run-web? true
:yodlee2-integreat-user "integreat-main"
:yodlee2-client-id "3AATcwfPsWP1rP9oDoo4HvZhtaroGVcA"
:yodlee2-client-secret "cXTBmKbGfkaBFIpM"
:yodlee2-client-id "lNgzSdjkVXSkXyOBlResXUsCpCpBBDlG"
:yodlee2-client-secret "U3qjZP2gErZfbuTPud+LNJF9jHbNRzCWCZbEi6dDiHsziCNwI5yNNNrBAUsnjcu7VFsUVNmkxNKSW85Qf1YMOITC7q0kdv7MGv/ZqRLfQV5odkiPbLHgOrE7UE6//MtjU0jTznGA70WTPS+wwmugg8ArNnx+4QCHrrBrkRfFVOE="
:yodlee2-base-url "https://production.api.yodlee.com/ysl"
:yodlee2-fastlink "https://fl4.prod.yodlee.com/authenticate/USDevexProd2-319/fastlink/?channelAppName=usdevexprod2"
:yodlee2-proxy-host "172.31.10.83"
:yodlee2-proxy-port 8888
:plaid {:base-url "https://production.plaid.com"
:client-id "61bfab05f7e762001b323f79"
:secret-key "2be026ca5e7f7e9f23f2fb4d7c914d"}
:secret-key "44a05fbe9f33a2975b3b3ac06b0b62"}
}

View File

@@ -1 +1 @@
1`

View File

@@ -0,0 +1 @@
,noti,pop-os,01.06.2026 21:02,file:///home/noti/.config/libreoffice/4;

View File

@@ -268,6 +268,21 @@ const calendarYearPeriod = (date) => {
return {end: formatDateMMDDYYYY(end), start: formatDateMMDDYYYY(start)};
}
const previousCalendarYearPeriod = (date) => {
if (!date) {
date= new Date()
} else {
date = parseMMDDYYYY(date)
}
const priorYear = date.getFullYear() - 1;
// Jan 1 - Dec 31 of the previous calendar year
const start = new Date(priorYear, 0, 1);
const end = new Date(priorYear, 11, 31);
return {end: formatDateMMDDYYYY(end), start: formatDateMMDDYYYY(start)};
}
const getLastMonthPeriods = (date) => {
if (!date) {
date = new Date();

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,43 @@
,,,,,,,,,,,,,,,,,,,,,,,,,d,,,,,,,,,,,,,,,,,,,,,,,,,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,20.56,CA,20.56,Y,0,0,0,1.2,07,48,02,01,CANNED AND DRY,24,20OZ,AQUAFIN,WATER PURIFIED BTL PET LSE DW,30,33,0.75,24,,000000,,0000,,,,29115,47,,8492330,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,14.84,CA,14.84,Y,0,0,0,0.6,07,16,05,01,CANNED AND DRY,12,11.2OZ,LOUX,SODA CHERRY VISSINADA GRK PLAS,9.5,10.5,0.26,12,,000000,,0000,,,3000P,808959,01,,7189422,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,14.93,CA,14.93,Y,0,0,0,0.6,07,16,05,01,CANNED AND DRY,12,8 OZ,LOUX,SODA LEMON LEMONADA GREEK,9,11.5,0.26,12,,000000,,0000,,,3200,808959,01,,9910355,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,79.83,CA,79.83,Y,0,0,0,0,07,35,03,99,CANNED AND DRY,4,5 LB,OTHRYS,SPICE OREGANO LEAF RUBBED,20,22,2.51,4,,000000,,0000,,,62760,808959,01,,9911236,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,31.07,CA,31.07,Y,0,0,0,0,07,35,99,99,CANNED AND DRY,22,4.68OZ,HI WEST,RICE MIX NICKS,6.43,7,0.16,22,,000000,,0000,,,30-5729,345717,03,,7301949,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,2,2,0,54.84,CA,109.68,Y,0,0,0,0,07,33,01,99,CANNED AND DRY,2,20 LB,ROYAL,RICE BASMATI PABROIL SELA CS,40,40.6,1.21,2,,000000,,0000,,,91000244,26992,43,,7053293,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,75.49,CA,75.49,Y,0,0,0,0,07,34,04,99,CANNED AND DRY,4,1 GAL,NICKGRK,DRESSING VINAIGRETTE LOGO,33,35,0.87,4,,000000,,0000,,,1654,853,01,,7108399,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,29.4,CA,29.4,Y,0,0,0,0,07,36,99,99,CANNED AND DRY,8,15 OZ,HAIG'S,DIP GARLIC TOUM,7.25,8.25,0.34,8,,000000,,0000,,,8PGD16,691816,01,,7360056,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,38.85,CA,38.85,Y,0,0,0,0,07,37,02,02,CANNED AND DRY,1,35 LB,BEOCO,OIL CORN,35,36.55,0.85,1,,000000,,0000,,,,9846,02,,4823761,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,2,2,0,56.72,CA,113.44,Y,0,0,0,0,07,36,99,99,CANNED AND DRY,4,4 LB,GRECDEL,SPREAD HUMMUS TRADITIONAL,16,17,0.62,4,,000000,,0000,,,HU000083,1533,19,,7278619,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,-7.22,CA,-7.32,Y,0,0,0,0,07,86,01,99,CANNED AND DRY,1,EA,NONPROD,ALLOWANCE FOR DROP SIZE,0.01,0.01,0.01,1,,000000,,0000,,,,,01,,9477498,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,4.17,CA,4.17,Y,0,0,0,0,07,86,01,99,CANNED AND DRY,1,EA,NONPROD,CHGS FOR FUEL SURCHARGE,1,1,0,1,,000000,,0000,,,,,01,,6592893,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,2,2,0,95.98,CA,191.96,Y,0,0,0,0,02,04,99,99,DAIRY PRODUCTS,1,5 GAL,NICKGRK,SAUCE TZATZIKI,42,43.5,1.2,1,,000000,,0000,,,SA000084,1533,19,,7213639,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,2,2,0,54.82,CA,109.64,Y,0,0,0,0,02,10,01,99,DAIRY PRODUCTS,4,1 GAL,NICKGRK,YOGURT FRZN NF NICK THE GREEK,39.9,39.9,0.97,4,,000000,,0000,,,13101,379887,05,,7302646,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,172.37,CA,172.37,Y,0,0,0,0,12,08,02,03,DISPENSER BEVRG,12,32 OZ,TRACTOR,JUICE CONC STRAWB DRAGON,24,25.5,0.58,12,,000000,,0000,,,6555,693956,01,,7206974,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,62.48,CA,62.48,Y,0,0,0,0,12,08,02,03,DISPENSER BEVRG,1,2.5GAL,DR PEPR,SYRUP DR PPR DIET BIB,20.93,21.82,0.47,1,,000000,,0000,,,12115,376510,09,,7459969,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,117.3,CA,117.3,Y,0,0,0,0,12,08,02,03,DISPENSER BEVRG,1,5GAL,DR PEPR,SYRUP DR PEPPER BIB,40,54.4,0.83,1,,000000,,0000,,,12109,9562,14,,4273553,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,8,8,0,26.36,CA,210.88,Y,0,0,0,0,06,02,45,99,FROZEN,12,10 CT,KONTOS,BREAD PITA GYRO PRE-OILED 7,21,24,1.65,12,,000000,,0000,,,10005,25370,01,,5223334,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,2,2,0,69.37,CA,138.74,Y,0,0,0,0,06,01,70,99,FROZEN,36,6 OZ,HELLAS,SPANAKOPITA SPINACH COOKED,12.4,13.4,0.62,36,,000000,,0000,,,216312,32248,01,,7455027,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,2,2,0,49.53,CA,99.06,Y,0,0,0,0,06,01,60,99,FROZEN,2,24 CT,HELLAS,BAKLAVA CLASSIC 2X24,9.6,10.6,0.46,2,,000000,,0000,,,100224,32248,01,,7187055,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,57.66,CA,57.66,Y,0,0,0,0,06,01,65,99,FROZEN,140,0.7 OZ,CHICPAT,DESSERT MINI PLAIN BEIGNET,6.17,7.5,0.88,140,,000000,,0000,,,540061,1188,53,,7212299,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,97.94,CA,97.94,Y,0,0,0,0,06,02,01,99,FROZEN,4,10 LB,NICKGRK,APTZR VEG FALAFEL PUCK HALAL,40,42,2.03,4,,000000,,0000,,,FA000090,1533,05,,7274591,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,1,53.5,1,53.5,7.556,LB,404.25,Y,0,0,0,0,03,02,01,13,MEATS,5,10.5#,TWORVRS,BEEF SHLDR TERES MAJOR SEL,53,55,1.99,5,,000000,,0000,,,B83003,527004,03,,0932867,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,87.76,CA,87.76,Y,0,0,0,0,03,04,99,99,MEATS,1,20 LB,GRECDEL,PORK SLI GYRO CONE,20,21,0.77,1,,000000,,0000,,,ME000215,1533,05,,7211838,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,7,7,0,92.53,CA,647.71,Y,0,0,0,0,03,02,04,99,MEATS,1,30 LB,NICKGRK,MEAT GYRO BEEF CONE NTG,30,31,0.97,1,,000000,,0000,,,ME000071,1533,05,,9906087,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,25.41,CA,25.41,Y,0,0,0,0,08,42,64,43,PAPER & DISP,20,50 CT,KARAT,LID PLAS FLAT F/12-22 OZ,5.75,7,1.94,20,,000000,,0000,,,C-KCL90,461672,05,,7661388,00000000000000,,,260402,04671945,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,22.32,CA,22.32,Y,0,0,0,0,08,60,99,99,PAPER & DISP,24,250 CT,ELEMEN,NAPKIN 2PLY INTR FOLD 6.3X8.26,16.1,16.8,1.55,24,,000000,,0000,,,11904,613310,01,,7452585,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,26.15,CA,26.15,Y,0,0,0,0,08,21,64,62,PAPER & DISP,50,50CT,KARAT,CUP PORTION PLAS CLR 1.50 OZ,10,10,1.36,50,,000000,,0000,,,FP-P150-PP,461672,05,,4613026,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,43.99,CA,43.99,Y,0,0,0,0,08,75,03,04,PAPER & DISP,6,50 EA,NATZWAY,BOWL PLASTIC COATING 42 OZ,14.55,17.19,3.56,6,,000000,,0000,,,10205,773772,01,,7408008,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,41.25,CA,41.25,Y,0,0,0,0,08,42,99,99,PAPER & DISP,6,50CT,NATZWAY,LID CLEAR PET 42 OZ,8.59,10.47,2.01,6,,000000,,0000,,,10206,773772,01,,7408215,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,47.6,CA,47.6,Y,0,0,0,0,08,21,56,99,PAPER & DISP,1000,22 OZ,NICKGRK,CUP PAPER COLD 22 OZ LOGO NTG,31.96,34.62,3.78,1000,,000000,,0000,,,810161542703,461672,05,,7354127,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,30.6,CA,30.6,Y,0,0,0,0,08,18,56,99,PAPER & DISP,1,450 CT,NICKGRK,CONTAINER PAPER 1/30 OZ NTG,23,25,3.25,1,,000000,,0000,,,810161542673,461672,05,,7354120,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,27.6,CA,27.6,Y,0,0,0,0,08,18,56,99,PAPER & DISP,1,160 CT,NICKGRK,CONTAINER PAPER 4/110OZ NTG,19.6,21.4,3.59,1,,000000,,0000,,,810161542680,461672,05,,7354119,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,3,3,0,37.6,CA,112.8,Y,0,0,0,0,08,18,02,12,PAPER & DISP,2,100CT,NATZWAY,CONTAINER PAPER MLD FBR 9X6,18,18,1.28,2,,000000,,0000,,,10042,773772,01,,7250678,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,2,2,0,46.07,CA,92.14,Y,0,0,0,0,08,09,56,99,PAPER & DISP,1,250BAG,NICKGRK,BAG PAPER 250 CT,14.5,15,2.18,1,,000000,,0000,,,,773772,01,,7417242,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,1,,1,1,0,24.46,EA,24.46,Y,0,0,0,0,08,36,56,79,PAPER & DISP,5,1000,BAGCRFT,WRAP DELI WHT 12X12 GRS RESIST,37,37,1.07,5,,000000,,0000,,,P057012,276,01,,5723808,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,1,1,0,39.41,CA,39.41,Y,3.85,0,0,0,08,06,25,10,PAPER & DISP,10,100CT,DHGPROF,GLOVE NITRILE BLK PEDRFREE LRG,12.21,12.21,0.66,10,,000000,,0000,,,DNGB-L,613310,01,,7296407,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,3,3,0,89.32,CA,267.96,Y,0,0,0,0,05,01,01,07,POULTRY,4,10 LB,SYS CLS,CHICKEN CVP THIGH BNLS SKLS,40,42,1.04,4,,000000,,0000,,,14301,3254,21,,7792187,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,9,9,0,86.97,CA,782.73,Y,0,0,0,0,05,02,01,99,POULTRY,1,20LB,GRECDEL,GYRO CHICKEN SHAWARMA CONE,20,21,0.77,1,,000000,,0000,,,ME000102,1533,05,,7124188,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,DET,,,,,5,5,0,23.65,CA,118.25,Y,0,0,0,0,11,02,23,01,PRODUCE,1,50 LB,PACKER,POTATO KENNEBEC FRESH,50,52,2,1,,000000,,0000,,,,696760,01,,2039220,00000000000000,,,260402,04672959,
EEK,,050,00175469,850081745,HDR,,,CKC CONCORD INC,,260402,,,,Rolling 8,,NICK THE GREEK CONCORD,2075 DIAMOND BLVD,STE H-103,CONCORD,CA,94520-582,408593,000000000,,,,,BBNKG,0,050,SYSCO SAN FRANCISCO,5900 STEWART AVENU,,FREMONT,CA,94538,,,,,,,1372486,4024,004,0000000,00000000,20260529,6.25,CRO8
EEK,,050,00175469,850081745,SUM,,,40,0,0,74,0,4625.36,6.25,00000463161,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
1 d
2 EEK 050 00175469 850081745 DET 1 1 0 20.56 CA 20.56 Y 0 0 0 1.2 07 48 02 01 CANNED AND DRY 24 20OZ AQUAFIN WATER PURIFIED BTL PET LSE DW 30 33 0.75 24 000000 0000 29115 47 8492330 00000000000000 260402 04672959
3 EEK 050 00175469 850081745 DET 1 1 0 14.84 CA 14.84 Y 0 0 0 0.6 07 16 05 01 CANNED AND DRY 12 11.2OZ LOUX SODA CHERRY VISSINADA GRK PLAS 9.5 10.5 0.26 12 000000 0000 3000P 808959 01 7189422 00000000000000 260402 04672959
4 EEK 050 00175469 850081745 DET 1 1 0 14.93 CA 14.93 Y 0 0 0 0.6 07 16 05 01 CANNED AND DRY 12 8 OZ LOUX SODA LEMON LEMONADA GREEK 9 11.5 0.26 12 000000 0000 3200 808959 01 9910355 00000000000000 260402 04672959
5 EEK 050 00175469 850081745 DET 1 1 0 79.83 CA 79.83 Y 0 0 0 0 07 35 03 99 CANNED AND DRY 4 5 LB OTHRYS SPICE OREGANO LEAF RUBBED 20 22 2.51 4 000000 0000 62760 808959 01 9911236 00000000000000 260402 04672959
6 EEK 050 00175469 850081745 DET 1 1 0 31.07 CA 31.07 Y 0 0 0 0 07 35 99 99 CANNED AND DRY 22 4.68OZ HI WEST RICE MIX NICKS 6.43 7 0.16 22 000000 0000 30-5729 345717 03 7301949 00000000000000 260402 04672959
7 EEK 050 00175469 850081745 DET 2 2 0 54.84 CA 109.68 Y 0 0 0 0 07 33 01 99 CANNED AND DRY 2 20 LB ROYAL RICE BASMATI PABROIL SELA CS 40 40.6 1.21 2 000000 0000 91000244 26992 43 7053293 00000000000000 260402 04672959
8 EEK 050 00175469 850081745 DET 1 1 0 75.49 CA 75.49 Y 0 0 0 0 07 34 04 99 CANNED AND DRY 4 1 GAL NICKGRK DRESSING VINAIGRETTE LOGO 33 35 0.87 4 000000 0000 1654 853 01 7108399 00000000000000 260402 04672959
9 EEK 050 00175469 850081745 DET 1 1 0 29.4 CA 29.4 Y 0 0 0 0 07 36 99 99 CANNED AND DRY 8 15 OZ HAIG'S DIP GARLIC TOUM 7.25 8.25 0.34 8 000000 0000 8PGD16 691816 01 7360056 00000000000000 260402 04672959
10 EEK 050 00175469 850081745 DET 1 1 0 38.85 CA 38.85 Y 0 0 0 0 07 37 02 02 CANNED AND DRY 1 35 LB BEOCO OIL CORN 35 36.55 0.85 1 000000 0000 9846 02 4823761 00000000000000 260402 04672959
11 EEK 050 00175469 850081745 DET 2 2 0 56.72 CA 113.44 Y 0 0 0 0 07 36 99 99 CANNED AND DRY 4 4 LB GRECDEL SPREAD HUMMUS TRADITIONAL 16 17 0.62 4 000000 0000 HU000083 1533 19 7278619 00000000000000 260402 04672959
12 EEK 050 00175469 850081745 DET 1 1 0 -7.22 CA -7.32 Y 0 0 0 0 07 86 01 99 CANNED AND DRY 1 EA NONPROD ALLOWANCE FOR DROP SIZE 0.01 0.01 0.01 1 000000 0000 01 9477498 00000000000000 260402 04672959
13 EEK 050 00175469 850081745 DET 1 1 0 4.17 CA 4.17 Y 0 0 0 0 07 86 01 99 CANNED AND DRY 1 EA NONPROD CHGS FOR FUEL SURCHARGE 1 1 0 1 000000 0000 01 6592893 00000000000000 260402 04672959
14 EEK 050 00175469 850081745 DET 2 2 0 95.98 CA 191.96 Y 0 0 0 0 02 04 99 99 DAIRY PRODUCTS 1 5 GAL NICKGRK SAUCE TZATZIKI 42 43.5 1.2 1 000000 0000 SA000084 1533 19 7213639 00000000000000 260402 04672959
15 EEK 050 00175469 850081745 DET 2 2 0 54.82 CA 109.64 Y 0 0 0 0 02 10 01 99 DAIRY PRODUCTS 4 1 GAL NICKGRK YOGURT FRZN NF NICK THE GREEK 39.9 39.9 0.97 4 000000 0000 13101 379887 05 7302646 00000000000000 260402 04672959
16 EEK 050 00175469 850081745 DET 1 1 0 172.37 CA 172.37 Y 0 0 0 0 12 08 02 03 DISPENSER BEVRG 12 32 OZ TRACTOR JUICE CONC STRAWB DRAGON 24 25.5 0.58 12 000000 0000 6555 693956 01 7206974 00000000000000 260402 04672959
17 EEK 050 00175469 850081745 DET 1 1 0 62.48 CA 62.48 Y 0 0 0 0 12 08 02 03 DISPENSER BEVRG 1 2.5GAL DR PEPR SYRUP DR PPR DIET BIB 20.93 21.82 0.47 1 000000 0000 12115 376510 09 7459969 00000000000000 260402 04672959
18 EEK 050 00175469 850081745 DET 1 1 0 117.3 CA 117.3 Y 0 0 0 0 12 08 02 03 DISPENSER BEVRG 1 5GAL DR PEPR SYRUP DR PEPPER BIB 40 54.4 0.83 1 000000 0000 12109 9562 14 4273553 00000000000000 260402 04672959
19 EEK 050 00175469 850081745 DET 8 8 0 26.36 CA 210.88 Y 0 0 0 0 06 02 45 99 FROZEN 12 10 CT KONTOS BREAD PITA GYRO PRE-OILED 7 21 24 1.65 12 000000 0000 10005 25370 01 5223334 00000000000000 260402 04672959
20 EEK 050 00175469 850081745 DET 2 2 0 69.37 CA 138.74 Y 0 0 0 0 06 01 70 99 FROZEN 36 6 OZ HELLAS SPANAKOPITA SPINACH COOKED 12.4 13.4 0.62 36 000000 0000 216312 32248 01 7455027 00000000000000 260402 04672959
21 EEK 050 00175469 850081745 DET 2 2 0 49.53 CA 99.06 Y 0 0 0 0 06 01 60 99 FROZEN 2 24 CT HELLAS BAKLAVA CLASSIC 2X24 9.6 10.6 0.46 2 000000 0000 100224 32248 01 7187055 00000000000000 260402 04672959
22 EEK 050 00175469 850081745 DET 1 1 0 57.66 CA 57.66 Y 0 0 0 0 06 01 65 99 FROZEN 140 0.7 OZ CHICPAT DESSERT MINI PLAIN BEIGNET 6.17 7.5 0.88 140 000000 0000 540061 1188 53 7212299 00000000000000 260402 04672959
23 EEK 050 00175469 850081745 DET 1 1 0 97.94 CA 97.94 Y 0 0 0 0 06 02 01 99 FROZEN 4 10 LB NICKGRK APTZR VEG FALAFEL PUCK HALAL 40 42 2.03 4 000000 0000 FA000090 1533 05 7274591 00000000000000 260402 04672959
24 EEK 050 00175469 850081745 DET 1 53.5 1 53.5 7.556 LB 404.25 Y 0 0 0 0 03 02 01 13 MEATS 5 10.5# TWORVRS BEEF SHLDR TERES MAJOR SEL 53 55 1.99 5 000000 0000 B83003 527004 03 0932867 00000000000000 260402 04672959
25 EEK 050 00175469 850081745 DET 1 1 0 87.76 CA 87.76 Y 0 0 0 0 03 04 99 99 MEATS 1 20 LB GRECDEL PORK SLI GYRO CONE 20 21 0.77 1 000000 0000 ME000215 1533 05 7211838 00000000000000 260402 04672959
26 EEK 050 00175469 850081745 DET 7 7 0 92.53 CA 647.71 Y 0 0 0 0 03 02 04 99 MEATS 1 30 LB NICKGRK MEAT GYRO BEEF CONE NTG 30 31 0.97 1 000000 0000 ME000071 1533 05 9906087 00000000000000 260402 04672959
27 EEK 050 00175469 850081745 DET 1 1 0 25.41 CA 25.41 Y 0 0 0 0 08 42 64 43 PAPER & DISP 20 50 CT KARAT LID PLAS FLAT F/12-22 OZ 5.75 7 1.94 20 000000 0000 C-KCL90 461672 05 7661388 00000000000000 260402 04671945
28 EEK 050 00175469 850081745 DET 1 1 0 22.32 CA 22.32 Y 0 0 0 0 08 60 99 99 PAPER & DISP 24 250 CT ELEMEN NAPKIN 2PLY INTR FOLD 6.3X8.26 16.1 16.8 1.55 24 000000 0000 11904 613310 01 7452585 00000000000000 260402 04672959
29 EEK 050 00175469 850081745 DET 1 1 0 26.15 CA 26.15 Y 0 0 0 0 08 21 64 62 PAPER & DISP 50 50CT KARAT CUP PORTION PLAS CLR 1.50 OZ 10 10 1.36 50 000000 0000 FP-P150-PP 461672 05 4613026 00000000000000 260402 04672959
30 EEK 050 00175469 850081745 DET 1 1 0 43.99 CA 43.99 Y 0 0 0 0 08 75 03 04 PAPER & DISP 6 50 EA NATZWAY BOWL PLASTIC COATING 42 OZ 14.55 17.19 3.56 6 000000 0000 10205 773772 01 7408008 00000000000000 260402 04672959
31 EEK 050 00175469 850081745 DET 1 1 0 41.25 CA 41.25 Y 0 0 0 0 08 42 99 99 PAPER & DISP 6 50CT NATZWAY LID CLEAR PET 42 OZ 8.59 10.47 2.01 6 000000 0000 10206 773772 01 7408215 00000000000000 260402 04672959
32 EEK 050 00175469 850081745 DET 1 1 0 47.6 CA 47.6 Y 0 0 0 0 08 21 56 99 PAPER & DISP 1000 22 OZ NICKGRK CUP PAPER COLD 22 OZ LOGO NTG 31.96 34.62 3.78 1000 000000 0000 810161542703 461672 05 7354127 00000000000000 260402 04672959
33 EEK 050 00175469 850081745 DET 1 1 0 30.6 CA 30.6 Y 0 0 0 0 08 18 56 99 PAPER & DISP 1 450 CT NICKGRK CONTAINER PAPER 1/30 OZ NTG 23 25 3.25 1 000000 0000 810161542673 461672 05 7354120 00000000000000 260402 04672959
34 EEK 050 00175469 850081745 DET 1 1 0 27.6 CA 27.6 Y 0 0 0 0 08 18 56 99 PAPER & DISP 1 160 CT NICKGRK CONTAINER PAPER 4/110OZ NTG 19.6 21.4 3.59 1 000000 0000 810161542680 461672 05 7354119 00000000000000 260402 04672959
35 EEK 050 00175469 850081745 DET 3 3 0 37.6 CA 112.8 Y 0 0 0 0 08 18 02 12 PAPER & DISP 2 100CT NATZWAY CONTAINER PAPER MLD FBR 9X6 18 18 1.28 2 000000 0000 10042 773772 01 7250678 00000000000000 260402 04672959
36 EEK 050 00175469 850081745 DET 2 2 0 46.07 CA 92.14 Y 0 0 0 0 08 09 56 99 PAPER & DISP 1 250BAG NICKGRK BAG PAPER 250 CT 14.5 15 2.18 1 000000 0000 773772 01 7417242 00000000000000 260402 04672959
37 EEK 050 00175469 850081745 DET 1 1 1 0 24.46 EA 24.46 Y 0 0 0 0 08 36 56 79 PAPER & DISP 5 1000 BAGCRFT WRAP DELI WHT 12X12 GRS RESIST 37 37 1.07 5 000000 0000 P057012 276 01 5723808 00000000000000 260402 04672959
38 EEK 050 00175469 850081745 DET 1 1 0 39.41 CA 39.41 Y 3.85 0 0 0 08 06 25 10 PAPER & DISP 10 100CT DHGPROF GLOVE NITRILE BLK PEDRFREE LRG 12.21 12.21 0.66 10 000000 0000 DNGB-L 613310 01 7296407 00000000000000 260402 04672959
39 EEK 050 00175469 850081745 DET 3 3 0 89.32 CA 267.96 Y 0 0 0 0 05 01 01 07 POULTRY 4 10 LB SYS CLS CHICKEN CVP THIGH BNLS SKLS 40 42 1.04 4 000000 0000 14301 3254 21 7792187 00000000000000 260402 04672959
40 EEK 050 00175469 850081745 DET 9 9 0 86.97 CA 782.73 Y 0 0 0 0 05 02 01 99 POULTRY 1 20LB GRECDEL GYRO CHICKEN SHAWARMA CONE 20 21 0.77 1 000000 0000 ME000102 1533 05 7124188 00000000000000 260402 04672959
41 EEK 050 00175469 850081745 DET 5 5 0 23.65 CA 118.25 Y 0 0 0 0 11 02 23 01 PRODUCE 1 50 LB PACKER POTATO KENNEBEC FRESH 50 52 2 1 000000 0000 696760 01 2039220 00000000000000 260402 04672959
42 EEK 050 00175469 850081745 HDR CKC CONCORD INC 260402 Rolling 8 NICK THE GREEK CONCORD 2075 DIAMOND BLVD STE H-103 CONCORD CA 94520-582 408593 000000000 BBNKG 0 050 SYSCO SAN FRANCISCO 5900 STEWART AVENU FREMONT CA 94538 1372486 4024 004 0000000 00000000 20260529 6.25 CRO8
43 EEK 050 00175469 850081745 SUM 40 0 0 74 0 4625.36 6.25 00000463161

View File

@@ -0,0 +1,271 @@
;; =====================================================================
;; ONE-OFF SCRATCH — re-code already-imported Sysco invoices after fixing
;; resources/sysco_line_item_mapping.csv.
;;
;; Context: the Sysco importer codes each line item by EXACT description
;; match against sysco_line_item_mapping.csv, defaulting to GL 50000 when a
;; description is missing (auto-ap.jobs.sysco/get-line-account). Missing
;; PAPER & DISP (and other) descriptions landed in 50000 (Food Costs)
;; instead of their real accounts (e.g. 55000 Paper Costs). The mapping is
;; now fixed; this re-derives the correct split from each invoice's source
;; CSV and rewrites :invoice/expense-accounts.
;;
;; Design:
;; - Recode EVERY invoice found in the CSV resource (a Sysco file may batch
;; several invoices; they're grouped by InvoiceNumber).
;; - Build ONE transaction covering every invoice, emitting only the datoms
;; that actually change:
;; * reuse an existing invoice-expense-account when its account (and
;; location) already match, updating just :amount when it differs;
;; * add a child for an account that has no row yet;
;; * retract a child whose account is no longer in the corrected split;
;; * emit nothing for rows already correct.
;; - Validate with (dc/with db changes): apply the tx to an in-memory db
;; value and assert every affected invoice's expense-account amounts sum
;; to its :invoice/total BEFORE committing for real.
;; - After committing, touch the ledger for every affected invoice. This is
;; a SEPARATE transaction on purpose: :upsert-invoice rebuilds the journal
;; entry from the invoice's expense-accounts as seen in db-before, so it
;; must run after the recode is committed.
;;
;; DO NOT load/evaluate this whole file. Step through the (comment ...) forms
;; one at a time in a connected REPL; the commit + ledger steps are gated #_.
;;
;; PRECONDITIONS
;; - The deployed artifact ships the fixed sysco_line_item_mapping.csv AND
;; the invoice CSV at resources/sysco_recode/<file>.csv (io/resource).
;; - You are connected to the DB you intend to mutate (prod conn!).
;; =====================================================================
(comment
(require '[auto-ap.jobs.sysco :as sysco]
'[auto-ap.datomic :refer [conn audit-transact random-tempid]]
'[auto-ap.utils :refer [dollars=]]
'[auto-ap.time :as t]
'[clj-time.coerce :as coerce]
'[clojure.data.csv :as csv]
'[clojure.java.io :as io]
'[datomic.api :as dc])
;; ------------------------------------------------------------------
;; STEP 0 — reload the mapping cache so the corrected CSV is in effect.
;; ------------------------------------------------------------------
(reset! sysco/sysco-name->line nil)
(count (sysco/get-sysco->line))
;; sanity: a previously-missing paper description now resolves to 55000.
(dc/pull (dc/db conn) [:account/numeric-code :account/name]
(sysco/get-line-account "BAG PAPER 250 CT")) ; => 55000
;; ------------------------------------------------------------------
;; Helpers
;; ------------------------------------------------------------------
(defn read-csv-rows
"Reads the invoice CSV from the classpath (so it ships with the deploy).
`resource-path` is relative to a resources/ root, e.g. \"sysco_recode/bad.csv\"."
[resource-path]
(with-open [r (io/reader (or (io/resource resource-path)
(throw (ex-info "CSV not found on classpath"
{:resource-path resource-path}))))]
(doall (csv/read-csv r))))
(defn parse-date
"Sysco yyMMdd string -> java.util.Date, the same way the importer stores
:invoice/date (auto-ap.jobs.sysco/extract-invoice-details)."
[yymmdd]
(coerce/to-date (t/parse yymmdd "yyMMdd")))
(defn group-invoices
"Split a (possibly multi-invoice) Sysco CSV into one entry per invoice.
Groups DET/HDR/SUM rows by InvoiceNumber (index 4); date comes from the
group's HDR row InvoiceDate (index 10)."
[rows]
(->> rows
(filter #(contains? #{"DET" "HDR" "SUM"} (nth % 5 nil)))
(group-by #(nth % 4))
(mapv (fn [[number grp]]
(let [hdr (first (filter #(= "HDR" (nth % 5)) grp))]
{:invoice-number number
:date-str (some-> hdr (nth 10))
:rows grp})))))
(defn desired-split
"Rows of one Sysco invoice -> {account-eid -> amount-double}, using the
CURRENT (fixed) mapping. DET rows only (record-type at index 5); tax
(SUM row, TotalTaxAmount index 14) added to the same account the
importer uses for \"TAX\". Mirrors auto-ap.jobs.sysco/code-individual-items."
[rows]
(let [det (filter #(= "DET" (nth % 5)) rows)
sum-row (first (filter #(= "SUM" (nth % 5)) rows))
tax (some-> sum-row (nth 14) Double/parseDouble)
by-acct (reduce
(fn [acc row]
(update acc
(sysco/get-line-account (nth row sysco/item-name-index))
(fnil + 0.0)
(Double/parseDouble (nth row sysco/item-price-index))))
{}
det)]
(cond-> by-acct
(and tax (not (zero? tax)))
(update (sysco/get-line-account "TAX") (fnil + 0.0) tax))))
(defn resolve-eid
"Match on invoice-number AND date (belt-and-suspenders). Asserts a unique
hit so we never recode the wrong invoice."
[invoice-number date]
(let [ids (mapv first (dc/q '[:find ?i :in $ ?n ?d
:where
[?i :invoice/invoice-number ?n]
[?i :invoice/date ?d]]
(dc/db conn) invoice-number date))]
(assert (>= 1 (count ids))
(str "multiple invoices match " invoice-number " / " date ": " ids))
(first ids)))
(defn invoice-change-datoms
"Minimal tx-data to make invoice `eid`'s expense-account split equal
`desired` ({account-eid -> amount}). Returns [] when already correct."
[db eid desired]
(let [existing (:invoice/expense-accounts
(dc/pull db [{:invoice/expense-accounts
[:db/id :invoice-expense-account/amount
:invoice-expense-account/location
{:invoice-expense-account/account [:db/id]}]}]
eid))
loc (or (some :invoice-expense-account/location existing) "HQ")
;; one child per account expected; index by account, retract any dupes
by-acct (group-by #(get-in % [:invoice-expense-account/account :db/id]) existing)
one (into {} (map (fn [[a cs]] [a (first cs)])) by-acct)
dupes (mapcat (fn [[_ cs]] (map :db/id (rest cs))) by-acct)
wanted (set (keys desired))
upserts (keep (fn [[acct amt]]
(let [child (get one acct)]
(cond
;; new account -> accrete a child under the invoice
(nil? child)
{:db/id eid
:invoice/expense-accounts
[#:invoice-expense-account{:db/id (random-tempid)
:account acct
:location loc
:amount amt}]}
;; right account, wrong value -> reuse, set amount
;; (and fix location if it drifted)
(or (not (dollars= (:invoice-expense-account/amount child) amt))
(not= (:invoice-expense-account/location child) loc))
(cond-> {:db/id (:db/id child)
:invoice-expense-account/amount amt}
(not= (:invoice-expense-account/location child) loc)
(assoc :invoice-expense-account/location loc))
;; already correct -> nothing
:else nil)))
desired)
retracts (for [[acct child] one :when (not (wanted acct))]
[:db/retractEntity (:db/id child)])]
(vec (concat upserts
retracts
(map (fn [id] [:db/retractEntity id]) dupes)))))
;; ------------------------------------------------------------------
;; STEP 1 — point at the CSV. EVERY invoice in this file gets recoded.
;; Place the file under resources/ (e.g. resources/sysco_recode/bad.csv)
;; and commit it so it's on the classpath of the deployed artifact.
;; ------------------------------------------------------------------
(def csv-path "sysco_recode/bad.csv")
(def rows (read-csv-rows csv-path))
(def invoices (group-invoices rows))
(mapv (juxt :invoice-number :date-str) invoices) ;; what we found in the file
;; ------------------------------------------------------------------
;; STEP 2 — resolve each invoice (number + date) and compute its split.
;; ------------------------------------------------------------------
(def plan
(mapv (fn [{:keys [invoice-number date-str rows]}]
(let [date (parse-date date-str)]
{:invoice-number invoice-number
:date date
:eid (resolve-eid invoice-number date)
:desired (desired-split rows)}))
invoices))
;; bail if any invoice number didn't resolve
(assert (every? :eid plan)
(str "unresolved invoices: "
(mapv (juxt :invoice-number :date) (remove :eid plan))))
;; ------------------------------------------------------------------
;; STEP 3 — build the SINGLE changes-only transaction across all invoices.
;; ------------------------------------------------------------------
(def changes
(let [db (dc/db conn)]
(vec (mapcat (fn [{:keys [eid desired]}] (invoice-change-datoms db eid desired))
plan))))
(count changes) ;; how many datoms we're actually changing
changes ;; inspect the full minimal tx
;; ------------------------------------------------------------------
;; STEP 4 — validate with dc/with: apply the tx to an in-memory db value
;; and confirm every affected invoice still balances (sum of expense
;; account amounts == :invoice/total).
;; ------------------------------------------------------------------
(def preview (dc/with (dc/db conn) changes))
(def balance-report
(let [db-after (:db-after preview)]
(mapv (fn [{:keys [eid invoice-number]}]
(let [inv (dc/pull db-after
[:invoice/total
{:invoice/expense-accounts [:invoice-expense-account/amount]}]
eid)
s (reduce + 0.0 (map :invoice-expense-account/amount
(:invoice/expense-accounts inv)))]
{:invoice-number invoice-number
:total (:invoice/total inv)
:ea-sum s
:ok? (dollars= s (:invoice/total inv))}))
plan)))
balance-report
;; HARD GATE — do not continue unless every invoice balances post-change.
(assert (every? :ok? balance-report)
(str "unbalanced after change: "
(filterv (complement :ok?) balance-report)))
;; ------------------------------------------------------------------
;; STEP 5 — COMMIT the recode (gated). One transaction, changes only.
;; ------------------------------------------------------------------
#_(audit-transact changes
{:user/name "sysco recode (missing GL mappings fix)"
:user/role "admin"})
;; ------------------------------------------------------------------
;; STEP 6 — touch the ledger for every affected invoice (separate tx;
;; :upsert-invoice rebuilds the journal entry from the now-committed
;; expense-accounts). Gated.
;; ------------------------------------------------------------------
#_(audit-transact (mapv (fn [{:keys [eid]}] [:upsert-invoice {:db/id eid}]) plan)
{:user/name "sysco recode ledger touch"
:user/role "admin"})
;; ------------------------------------------------------------------
;; STEP 7 — verify committed result.
;; ------------------------------------------------------------------
#_(let [db (dc/db conn)]
(mapv (fn [{:keys [eid invoice-number]}]
{:invoice-number invoice-number
:accounts
(->> (dc/pull db
[{:invoice/expense-accounts
[:invoice-expense-account/amount
{:invoice-expense-account/account [:account/numeric-code]}]}]
eid)
:invoice/expense-accounts
(map (juxt #(get-in % [:invoice-expense-account/account :account/numeric-code])
:invoice-expense-account/amount))
(sort-by first)
vec)})
plan)))

View File

@@ -0,0 +1,375 @@
(ns auto-ap.jobs.backfill-olo-processors
"One-off backfill for the Olo third-party-source fix (commit fa25620b, \"olo fixes\").
That commit replaced the exact-match `condp` on `(:name (:source order))` in
`square3/tender->charge` with substring matching, so Olo-brokered orders that
arrive from Square with source names like \"Olo - DoorDash\" or
\"olo-ubereats\" now classify as the real delivery processor instead of
falling through to `:ccp-processor/na`.
Orders imported *before* the fix shipped still carry the old
`:charge/processor`, which means `sales-summaries` bucketed them as
\"Unknown\" instead of \"Food App Payments\". This job recomputes
`:charge/processor` for already-imported Square charges over a date range
(default: 2026-07-07 -> today) and marks the affected sales summaries dirty
so `auto-ap.jobs.sales-summaries` recalculates them.
IMPORTANT: this backfill never re-implements the classifier. It feeds the
stored shape of each charge back through the real production function
(`square3/tender->charge`), so it cannot drift from the importer.
No Square API calls are made -- every input the classifier needs
(`:sales-order/source`, `:charge/note`, `:charge/type-name`) is already in
Datomic. See the `(comment ...)` block at the bottom for the API-based
fallback if you need to repair charges that have no parent sales order.
Dry run by default. Pass `:apply? true` to write."
(:require
[auto-ap.datomic :refer [conn]]
[auto-ap.jobs.core :refer [execute]]
[auto-ap.jobs.sales-summaries :as summaries]
[auto-ap.logging :as alog]
[auto-ap.square.core3 :as square3]
[auto-ap.time :as atime]
[clj-time.coerce :as coerce]
[clj-time.core :as time]
[clj-time.format :as f]
[clj-time.periodic :as per]
[clojure.string :as str]
[config.core :refer [env]]
[datomic.api :as dc]
;; the datalog below calls iol-ion.query/scan-charges by fully-qualified
;; symbol, so the namespace has to be loaded.
[iol-ion.query]))
;; ---------------------------------------------------------------------------
;; Date handling
;; ---------------------------------------------------------------------------
(def pacific (time/time-zone-for-id "America/Los_Angeles"))
(def default-start-date
"The Olo fix landed 2026-07-22; the user-visible bad data starts 7/7."
"2026-07-07")
(defn parse-day
"\"2026-07-07\" -> local (Pacific) midnight DateTime. Passes DateTimes through."
[d]
(cond
(nil? d) nil
(string? d) (f/parse (f/with-zone (f/formatter "yyyy-MM-dd") pacific) d)
:else (coerce/to-date-time d)))
(defn local-midnight [dt]
(.toDateMidnight (atime/localize dt)))
;; ---------------------------------------------------------------------------
;; Classification -- delegates to the production importer
;; ---------------------------------------------------------------------------
(defn recompute-processor
"Runs the *live* classifier over a charge's stored inputs.
`tender->charge` only reads `(:name (:source order))` from the order and
`:type`/`:note` from the tender when deciding `:charge/processor`, so a
synthetic order/tender carrying the stored values yields exactly what a
fresh import would produce today. `:created_at` and `:id` are placeholders
purely to keep the unrelated `:charge/date` and `:charge/reference-link`
branches from blowing up; we only read `:charge/processor` back out.
Note on nil sources: the importer stores `(or (:name (:source order))
\"Square\")`, so a source that was originally nil comes back as \"Square\".
Both nil and \"Square\" classify to `:ccp-processor/na`, so the round trip
is faithful."
[{:keys [source note type-name]}]
(:charge/processor
(square3/tender->charge {:source {:name source}
:created_at "1970-01-01T00:00:00Z"}
{} ; client -> only :charge/client
{} ; location -> only :charge/location
{:id "backfill"
:type type-name
:note note})))
;; ---------------------------------------------------------------------------
;; Scanning
;; ---------------------------------------------------------------------------
(def charge-pull
[:db/id
:charge/external-id
:charge/type-name
:charge/note
:charge/date
:charge/total
{:charge/processor [:db/ident]}
{:sales-order/_charges [:db/id
:sales-order/external-id
:sales-order/source
{:sales-order/vendor [:db/ident]}]}])
(defn client-charges
"All charges for `client-eid` between `start-inst` and `end-inst` (both
inclusive by day), via the :charge/client+date index."
[db client-eid start-inst end-inst]
(->> (dc/q '[:find (pull ?c pull-pattern)
:in $ pull-pattern [?clients ?start ?end]
:where
[(iol-ion.query/scan-charges $ ?clients ?start ?end) [[?c _ _] ...]]]
db
charge-pull
[[client-eid] start-inst end-inst])
(map first)))
(defn parent-order
"Reverse refs on component attributes come back as a single map from pull,
but tolerate a collection in case that ever changes."
[charge]
(let [o (:sales-order/_charges charge)]
(if (sequential? o) (first o) o)))
(defn square-charge? [charge]
(= :vendor/ccp-square (get-in (parent-order charge) [:sales-order/vendor :db/ident])))
;; ---------------------------------------------------------------------------
;; Planning
;; ---------------------------------------------------------------------------
(defn charge->change
"nil when the charge is already correct (or isn't ours to touch)."
[charge]
(when (square-charge? charge)
(let [order (parent-order charge)
current (get-in charge [:charge/processor :db/ident])
next (recompute-processor {:source (:sales-order/source order)
:note (:charge/note charge)
:type-name (:charge/type-name charge)})]
(when (not= current next)
{:charge (:db/id charge)
:external-id (:charge/external-id charge)
:order (:sales-order/external-id order)
:date (:charge/date charge)
:source (:sales-order/source order)
:note (:charge/note charge)
:type-name (:charge/type-name charge)
:total (:charge/total charge)
:from current
:to next}))))
(defn downgrade?
"A change that *loses* processor information. The Olo fix can only ever widen
matching, so these should not exist -- if they do, something else changed
and we'd rather report than clobber."
[{:keys [from to]}]
(and (= :ccp-processor/na to)
(not= :ccp-processor/na from)
(some? from)))
(defn plan-for-client
[db {:keys [db/id client/code]} start-inst end-inst allow-downgrade?]
(let [charges (client-charges db id start-inst end-inst)
square (filter square-charge? charges)
orphans (->> charges
(remove (comp some? parent-order))
(filter #(some-> (:charge/external-id %)
(str/starts-with? "square/charge/"))))
all (keep charge->change square)
[skipped changes] (if allow-downgrade?
[[] all]
[(filter downgrade? all) (remove downgrade? all)])]
{:client id
:code code
:scanned (count square)
:orphan-charges (count orphans)
:changes (vec changes)
:skipped (vec skipped)}))
(defn build-plan
[db clients start-inst end-inst allow-downgrade?]
(->> clients
(map #(plan-for-client db % start-inst end-inst allow-downgrade?))
(remove #(and (zero? (count (:changes %)))
(zero? (count (:skipped %)))
(zero? (:orphan-charges %))))
vec))
;; ---------------------------------------------------------------------------
;; Reporting
;; ---------------------------------------------------------------------------
(defn- transitions [changes]
(->> changes
(group-by (juxt :from :to))
(map (fn [[[from to] cs]]
[from to (count cs) (reduce + 0.0 (keep :total cs))]))
(sort-by #(- (nth % 2)))))
(defn print-report [plan]
(println)
(println "=== Olo processor backfill ===")
(doseq [{:keys [code scanned changes skipped orphan-charges]} plan]
(println)
(printf "%-8s scanned %d square charges, %d to update%n"
(str code) scanned (count changes))
(doseq [[from to n total] (transitions changes)]
(printf " %-24s -> %-24s %5d $%.2f%n"
(str from) (str to) n total))
(when (seq skipped)
(printf " !! %d downgrade(s) to :ccp-processor/na SKIPPED (pass :allow-downgrade? true to force)%n"
(count skipped))
(doseq [{:keys [external-id order source note from]} (take 10 skipped)]
(printf " %s (order %s, source %s, note %s) was %s%n"
external-id order (pr-str source) (pr-str note) from)))
(when (pos? orphan-charges)
(printf " note: %d square charge(s) have no parent sales order (custom-amount tenders).%n"
orphan-charges)
(println " Their order source is not stored, so they cannot be reclassified")
(println " locally -- use the re-import fallback in the comment block.")))
(let [total (reduce + 0 (map (comp count :changes) plan))]
(println)
(printf "TOTAL: %d charge(s) across %d client(s)%n" total (count plan))
(println)
total))
;; ---------------------------------------------------------------------------
;; Applying
;; ---------------------------------------------------------------------------
(defn apply-plan!
"Transacts the processor corrections in batches. Returns the number written."
[plan]
(let [tx (for [{:keys [changes]} plan
{:keys [charge to]} changes]
{:db/id charge :charge/processor to})]
(doseq [batch (partition-all 100 tx)]
(alog/info ::updating-charges :count (count batch))
@(dc/transact-async conn batch))
(count tx)))
(defn mark-summaries-dirty!
"Processor drives the Card / Food App / Unknown split in
`sales-summaries/get-payment-items`, so every day we touched has to be
recalculated. `periodic-seq` is end-exclusive, hence the +1 day."
[plan start end]
(doseq [{:keys [client code changes]} plan
:when (seq changes)]
(alog/info ::marking-summaries-dirty :client code)
(summaries/mark-dirty client
(local-midnight start)
(time/plus (local-midnight end) (time/days 1)))))
;; ---------------------------------------------------------------------------
;; Entry point
;; ---------------------------------------------------------------------------
(defn backfill!
"Recompute :charge/processor for imported Square charges.
Options:
:start \"2026-07-07\" (default) or a DateTime -- inclusive
:end \"2026-07-29\" or a DateTime -- inclusive, default today
:codes seq of client codes; default every Square client
:apply? false (default) = dry run, print only
:allow-downgrade? false (default) = refuse changes that drop a known
processor back to :ccp-processor/na
:mark-dirty? true (default when applying) = flag sales summaries for
recalculation
Returns the plan so you can inspect individual charges in the REPL."
[& {:keys [start end codes apply? allow-downgrade? mark-dirty?]
:or {start default-start-date
apply? false
allow-downgrade? false}}]
(let [start-dt (parse-day start)
end-dt (or (parse-day end) (atime/localize (time/now)))
start-inst (coerce/to-date (local-midnight start-dt))
end-inst (coerce/to-date (local-midnight end-dt))
db (dc/db conn)
clients (if (seq codes)
(apply square3/get-square-clients codes)
(square3/get-square-clients))
_ (alog/info ::scanning
:start start-inst
:end end-inst
:clients (count clients)
:dry-run? (not apply?))
plan (build-plan db clients start-inst end-inst allow-downgrade?)
total (print-report plan)]
(if-not apply?
(do (println "DRY RUN -- nothing written. Re-run with :apply? true to commit.")
(alog/info ::dry-run-complete :change-count total))
(do
(alog/info ::applying :change-count total)
(apply-plan! plan)
(when (not= false mark-dirty?)
(mark-summaries-dirty! plan start-dt end-dt)
(println "Sales summaries marked dirty. Run auto-ap.jobs.sales-summaries")
(println "(or `(auto-ap.jobs.sales-summaries/sales-summaries-v2)`) to rebuild them."))
(alog/info ::done :change-count total)))
plan))
(defn -main [& _]
(execute "backfill-olo-processors"
(fn []
(let [{:keys [start end codes apply allow-downgrade mark-dirty]} (:args env)]
(backfill! :start (or start default-start-date)
:end end
:codes (cond-> codes (string? codes)
(str/split #","))
:apply? (boolean apply)
:allow-downgrade? (boolean allow-downgrade)
:mark-dirty? (if (nil? mark-dirty) true (boolean mark-dirty)))))))
(comment
;; ---------------------------------------------------------------------
;; 1. Dry run everything from 7/7 -- read only, prints what would change.
;; ---------------------------------------------------------------------
(def plan (backfill!))
;; One client at a time while you sanity check.
(backfill! :codes ["NGCL"])
;; Eyeball the actual charges behind a transition.
(->> plan
(mapcat :changes)
(filter #(= :ccp-processor/doordash (:to %)))
(map (juxt :date :source :note :total :from :to))
(take 20))
;; Every distinct source string we saw reclassified -- confirms the Olo
;; variants ("Olo - DoorDash", "olo-ubereats", ...) are what moved.
(->> plan (mapcat :changes) (map :source) frequencies)
;; ---------------------------------------------------------------------
;; 2. Commit, then rebuild the summaries.
;; ---------------------------------------------------------------------
(backfill! :apply? true)
(auto-ap.jobs.sales-summaries/sales-summaries-v2)
;; ---------------------------------------------------------------------
;; 3. Verify: no square charge in the window disagrees with the classifier.
;; ---------------------------------------------------------------------
(->> (backfill!) (mapcat :changes) count) ; => 0
;; ---------------------------------------------------------------------
;; Fallback: charges with no parent sales order (custom-amount tenders,
;; see `is-order-only-for-charge?` in square3/order->sales-order) do not
;; store the order source, so they cannot be fixed locally. Re-import them
;; through the real pipeline instead -- `upsert` is idempotent on
;; :charge/external-id / :sales-order/external-id, so re-running a day is
;; safe and rewrites the processor from live Square data.
;;
;; This hits the Square API for every day x location; the existing
;; auto-ap.jobs.load-historical-sales job does the same thing by day count.
;; ---------------------------------------------------------------------
(doseq [client (square3/get-square-clients)
location (:client/square-locations client)
:when (:square-location/client-location location)
d (per/periodic-seq (parse-day "2026-07-07")
(time/plus (atime/localize (time/now)) (time/days 1))
(time/days 1))]
(println (:client/code client) (:square-location/client-location location) (str d))
@(square3/upsert client location d (time/plus d (time/days 1))))
;; ...then mark dirty + rebuild summaries as in step 2.
)

View File

@@ -742,6 +742,19 @@
:total [:trim-commas-and-negate nil]}
:multi #"\n"
:multi-match? #"^\d+"}
;; REEL PRODUCE STATEMENT (QuickBooks layout -- no "Reel Produce" text on the page)
{:vendor "Reel Produce"
:keywords [#"reelproduce\.com" #"Statement"]
:extract {:date #"^\s*([0-9]+/[0-9]+/[0-9]+)"
:customer-identifier #"To:(?:.*?)\n\s*(.*?)\s{2,}"
:invoice-number #"INV #(\d+)"
:total #"Orig\. Amount \$([\d\-,]+\.\d{2,2})"}
:parser {:date [:clj-time "MM/dd/yyyy"]
:total [:trim-commas-and-negate nil]}
:multi #"\n"
:multi-match? #"^\s*[0-9]+/[0-9]+/[0-9]+\s+INV #"}
{:vendor "Paulino's Bakery"
:keywords [#"paulinosbakery"]
:extract {:date #"\s*([0-9]+/[0-9]+/[0-9]+)"

View File

@@ -281,15 +281,15 @@
"grubhub" :ccp-processor/grubhub
"grub" :ccp-processor/grubhub
"gh" :ccp-processor/grubhub
(condp = (:name (:source order))
"GRUBHUB" :ccp-processor/grubhub
"UBEREATS" :ccp-processor/uber-eats
"Uber Eats" :ccp-processor/uber-eats
"DOORDASH" :ccp-processor/doordash
"DoorDash" :ccp-processor/doordash
"Koala" :ccp-processor/koala
"koala-production" :ccp-processor/koala
:ccp-processor/na))
(let [src (some-> (:name (:source order)) str/lower-case)]
(cond
(nil? src) :ccp-processor/na
(str/includes? src "doordash") :ccp-processor/doordash
(str/includes? src "uber") :ccp-processor/uber-eats
(str/includes? src "grubhub") :ccp-processor/grubhub
(str/includes? src "postmates") :ccp-processor/uber-eats
(str/includes? src "koala") :ccp-processor/koala
:else :ccp-processor/na)))
(= (:type t) "CARD")
:ccp-processor/square

View File

@@ -55,8 +55,8 @@
[:div.flex.flex-col.gap-2
(buttons/a-button- {"@click" "periods=getFourWeekPeriodsPeriods(source_date)"} [:span "13 periods, ending "
[:span {:x-text "source_date"}]])
(buttons/a-button- {"@click" "periods=[calendarYearPeriod(source_date)]"} [:span "Calendar year ("
[:span {:x-text "parseMMDDYYYY(source_date).getFullYear()"}]
(buttons/a-button- {"@click" "periods=[previousCalendarYearPeriod(source_date)]"} [:span "Previous Calendar Year ("
[:span {:x-text "parseMMDDYYYY(source_date).getFullYear() - 1"}]
")"])
(buttons/a-button- {"@click" "periods=getTwelveCalendarMonthsPeriods(source_date)"} [:span "12 months, ending "
[:span {:x-text "parseMMDDYYYY(source_date).toLocaleString('default', { month: 'long' })"}]])

View File

@@ -656,7 +656,7 @@
{:exact-match-id (:db/id p)})
:content (str (format "$%,.2f" (:payment/amount p))
(some-> (:payment/date p) coerce/to-date-time (atime/unparse-local atime/normal-date) (#(str " payment on " %))))}]
(:payment/transaction p) (conj {:link (hu/url (bidi/path-for ssr-routes/only-routes ::transaction-routes/all-page)
(:payment/transaction p) (conj {:link (hu/url (bidi/path-for ssr-routes/only-routes ::transaction-routes/page)
{:exact-match-id (:db/id (first (:payment/transaction p)))})
:color :secondary
:content "Transaction"})))))

View File

@@ -109,6 +109,33 @@
:placeholder "e.g., ABC-456"
:size :small}))
(when (:external? (:route-params request))
(com/field {:label "Source"}
(com/text-input {:name "source"
:id "source"
:class "hot-filter"
:value (:source (:query-params request))
:placeholder "e.g., invoice"
:size :small})))
(when (:external? (:route-params request))
(com/field {:label "External Id"}
(com/text-input {:name "external-id-like"
:id "external-id-like"
:class "hot-filter"
:value (:external-id-like (:query-params request))
:placeholder "e.g., ABC-123"
:size :small})))
(when (:external? (:route-params request))
(com/field {:label "Location"}
(com/text-input {:name "location"
:id "location"
:class "hot-filter"
:value (:location (:query-params request))
:placeholder "SC"
:size :small})))
(com/field {:label "Account Code"}
[:div.flex.space-x-4.items-baseline
(com/int-input {:name "numeric-code-gte"
@@ -455,6 +482,9 @@
[:account {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :account/name]}]]]
[:check-number {:optional true} [:maybe [:string {:decode/string strip}]]]
[:invoice-number {:optional true} [:maybe [:string {:decode/string strip}]]]
[:source {:optional true} [:maybe [:string {:decode/string strip}]]]
[:external-id-like {:optional true} [:maybe [:string {:decode/string strip}]]]
[:location {:optional true} [:maybe [:string {:decode/string strip}]]]
[:status {:optional true} [:maybe (ref->enum-schema "invoice-status")]]
[:exact-match-id {:optional true} [:maybe entity-id]]
[:all-selected {:optional true :default nil} [:maybe :boolean]]
@@ -528,7 +558,9 @@
:title (fn [r]
(str
(some-> r :route-params :status name str/capitalize (str " "))
"Register"))
(if (:external? (:route-params r))
"External Register"
"Register")))
:entity-name "register"
:route ::route/table
:csv-route ::route/csv
@@ -629,14 +661,15 @@
:color :primary
:content (format "Invoice '%s'" (-> i :journal-entry/original-entity :invoice/invoice-number))})
(-> i :journal-entry/original-entity :invoice/source-url)
(conj
{:link (-> i :journal-entry/original-entity :invoice/source-url)
:color :secondary
:content (str "File")}
:content (str "File")})
(-> i :journal-entry/original-entity :transaction/description-original)
(conj
{:link (hu/url (bidi/path-for ssr-routes/only-routes
::transaction-routes/all-page)
::transaction-routes/page)
{:exact-match-id (:db/id (:journal-entry/original-entity i))})
:color :primary
:content (format "Transaction '%s'" (-> i :journal-entry/original-entity :transaction/description-original))})

View File

@@ -423,7 +423,7 @@
{:exact-match-id (:db/id invoice)})
:content (str "Inv. " (:invoice/invoice-number invoice))})))
(some-> p :transaction/_payment ((fn [t]
[{:link (hu/url (bidi/path-for ssr-routes/only-routes ::transaction-routes/all-page)
[{:link (hu/url (bidi/path-for ssr-routes/only-routes ::transaction-routes/page)
{:exact-match-id (:db/id (first t))})
:color :secondary
:content "Transaction"}]))))))}]}))

View File

@@ -165,7 +165,7 @@
svg/external-link))
(when-let [transaction-id (-> e (:transaction/_expected-deposit) first :db/id)]
(com/a-button {:href (str (bidi/path-for ssr-routes/only-routes
::transaction-routes/all-page)
::transaction-routes/page)
"?exact-match-id="
transaction-id)} "Transaction"))])
:headers [{:key "client"

View File

@@ -2,7 +2,7 @@
(:require
[auto-ap.datomic
:refer [apply-pagination apply-sort-3 conn merge-query pull-many
query2]]
pull-many-by-id query2]]
[auto-ap.datomic.accounts :as d-accounts]
[auto-ap.graphql.utils :refer [extract-client-ids]]
[auto-ap.query-params :refer [wrap-copy-qp-pqp]]
@@ -128,6 +128,38 @@
(map #(:ledger-mapped/amount % 0.0))
(reduce + 0.0)))
(def csv-account-read
[:db/id
:account/name
:account/code
{:account/client-overrides [:account-client-override/name
{:account-client-override/client [:db/id]}]}])
(defn- csv-amount
"Renders an item's amount when it falls on `side`, fixed to cents so the CSV
never carries floating point noise."
[side item]
(when (= side (:ledger-mapped/ledger-side item))
(format "%.2f" (double (:ledger-mapped/amount item 0.0)))))
(defn summaries->csv-rows
"Flattens each summary into one row per item, so every category is its own
auditable CSV line. Accounts are resolved in a single batch pull and
clientized against the summary's own client."
[summaries]
(let [account-ids (into #{}
(comp (mapcat :sales-summary/items)
(keep (comp :db/id :ledger-mapped/account)))
summaries)
id->account (if (seq account-ids)
(pull-many-by-id (dc/db conn) csv-account-read account-ids)
{})]
(for [ss summaries
item (sort-items (:sales-summary/items ss))]
(assoc (merge item ss)
::account (some-> item :ledger-mapped/account :db/id id->account
(d-accounts/clientize (-> ss :sales-summary/client :db/id)))))))
(defn truncate [s max-len]
(if (> (count s) max-len)
(str (subs s 0 (- max-len 3)) "...")
@@ -215,7 +247,15 @@
:title "Sales Summaries"
:entity-name "Daily Summary"
:route ::route/table
:headers [{:key "client"
:csv-route ::route/csv
:page->csv-entities (fn [[summaries]]
(summaries->csv-rows summaries))
:headers [{:key "id"
:name "Id"
:render-csv :db/id
:render-for #{:csv}}
{:key "client"
:name "Client"
:sort-key "client"
:hide? (fn [args]
@@ -231,6 +271,7 @@
:name "Debits"
:sort-key "debits"
:class "w-72 align-top"
:render-for #{:html}
:render (fn [ss]
(let [items (:sales-summary/items ss)
debit-items (filter #(= :ledger-side/debit (:ledger-mapped/ledger-side %)) (sort-items items))
@@ -257,6 +298,7 @@
:name "Credits"
:sort-key "credits"
:class "w-72 align-top"
:render-for #{:html}
:render (fn [ss]
(let [items (:sales-summary/items ss)
credit-items (filter #(= :ledger-side/credit (:ledger-mapped/ledger-side %)) (sort-items items))
@@ -283,6 +325,7 @@
:name "Status"
:sort-key "balance"
:class "w-28 align-top"
:render-for #{:html}
:render (fn [ss]
(let [items (:sales-summary/items ss)
total-debits (total-debits items)
@@ -304,10 +347,41 @@
[:span.text-xs.uppercase.tracking-wider.text-red-600.font-medium.mt-0.5
(if (> total-debits total-credits) "Debit over" "Credit over")]])]))}
{:key "category"
:name "Category"
:render-csv :sales-summary-item/category
:render-for #{:csv}}
{:key "account-code"
:name "Account Code"
:render-csv #(-> % ::account :account/code)
:render-for #{:csv}}
{:key "account"
:name "Account"
:render-csv #(-> % ::account :account/name)
:render-for #{:csv}}
{:key "debit"
:name "Debit"
:render-csv (partial csv-amount :ledger-side/debit)
:render-for #{:csv}}
{:key "credit"
:name "Credit"
:render-csv (partial csv-amount :ledger-side/credit)
:render-for #{:csv}}
{:key "manual"
:name "Manual"
:render-csv #(boolean (:sales-summary-item/manual? %))
:render-for #{:csv}}
{:key "links"
:name "Links"
:show-starting "lg"
:class "w-8"
:render-for #{:html}
:render (fn [ss]
(let [ledger-entry (:journal-entry/original-entity ss)]
(when (seq ledger-entry)
@@ -754,6 +828,7 @@
(->>
{::route/page (helper/page-route grid-page)
::route/table (helper/table-route grid-page)
::route/csv (helper/csv-route grid-page)
::route/edit-wizard (-> mm/open-wizard-handler
(mm/wrap-wizard edit-wizard)
(mm/wrap-init-multi-form-state initial-edit-wizard-state)

View File

@@ -45,7 +45,7 @@
(def transaction-approval-status
{:transaction-approval-status/unapproved "Unapproved"
:transaction-approval-status/approved "Approved"
:transaction-approval-status/suppressed "Client Review"})
:transaction-approval-status/requires-feedback "Client Review"})
(def row* (partial helper/row* grid-page))
@@ -988,8 +988,8 @@
":class" "{ '!bg-primary-200 text-primary-800': approvalStatus === 'unapproved' }"
:class "rounded-r-lg"}
"Unapproved")
(com/button-group-button {"@click" "approvalStatus = 'suppressed'"
":class" "{ '!bg-primary-200 text-primary-800': approvalStatus === 'suppressed' }"
(com/button-group-button {"@click" "approvalStatus = 'requires-feedback'"
":class" "{ '!bg-primary-200 text-primary-800': approvalStatus === 'requires-feedback' }"
:class "rounded-r-lg"}
"Client Review")]])))]]]])
:footer
@@ -1049,7 +1049,7 @@
(when-not (dollars= (- (:transaction/amount transaction))
(:payment/amount payment))
(throw (ex-info "Amounts don't match" {:validation-error "Amounts don't match"})))
(form-validation-error "Amounts don't match"))
(if (is-already-linked-to-this-payment? transaction payment-id)
(save-memo-only request)
(save-linked-transaction request payment))
@@ -1077,16 +1077,16 @@
(exception->4xx #(assert-not-locked (-> transaction :transaction/client :db/id) (:transaction/date transaction)))
(when (:transaction/payment transaction)
(throw (ex-info "Transaction already linked" {:validation-error "Transaction already linked"})))
(form-validation-error "Transaction already linked"))
(when (or (> (count invoice-clients) 1)
(not= (-> transaction :transaction/client :db/id)
(first invoice-clients)))
(throw (ex-info "Clients don't match" {:validation-error "Invoice(s) and transaction client do not match."})))
(form-validation-error "Invoice(s) and transaction client do not match."))
(when-not (dollars= (- (:transaction/amount transaction))
invoice-amount)
(throw (ex-info "Amounts don't match" {:validation-error "Amounts don't match"})))
(form-validation-error "Amounts don't match"))
(let [payment-tx (i-transactions/add-new-payment
(dc/pull db [:transaction/amount :transaction/date :db/id] (:db/id transaction))
@@ -1122,16 +1122,16 @@
(when (or (> (count invoice-clients) 1)
(not= (-> transaction :transaction/client :db/id)
(first invoice-clients)))
(throw (ex-info "Clients don't match" {:validation-error "Invoice(s) and transaction client do not match."
(form-validation-error "Invoice(s) and transaction client do not match."
:transaction-client (-> transaction :transaction/client :db/id)
:invoice-clients invoice-clients})))
:invoice-clients invoice-clients))
(when-not (dollars= (- (:transaction/amount transaction))
invoice-amount)
(throw (ex-info "Amounts don't match" {:validation-error "Amounts don't match"})))
(form-validation-error "Amounts don't match"))
(when (:transaction/payment transaction)
(throw (ex-info "Transaction already linked" {:validation-error "Transaction already linked"})))
(form-validation-error "Transaction already linked"))
(let [payment-tx (i-transactions/add-new-payment
(dc/pull db [:transaction/amount :transaction/date :db/id] (:db/id transaction))
@@ -1178,12 +1178,10 @@
(let [description-pattern (some-> transaction-rule :transaction-rule/description iol-ion.query/->pattern)]
(when (not (rm/rule-applies? transaction {:transaction-rule/description description-pattern}))
(throw (ex-info "Transaction rule does not apply"
{:validation-error "Transaction rule does not apply"}))))
(form-validation-error "Transaction rule does not apply")))
(when (:transaction/payment transaction)
(throw (ex-info "Transaction already associated with a payment"
{:validation-error "Transaction already associated with a payment"})))
(form-validation-error "Transaction already associated with a payment"))
(let [locations (-> transaction :transaction/client :client/locations)
updated-tx (rm/apply-rule {:db/id (:db/id transaction)
@@ -1336,8 +1334,7 @@
(exception->4xx #(assert-not-locked (-> transaction :transaction/client :db/id) (:transaction/date transaction)))
(when (not= :payment-status/cleared (-> payment :payment/status))
(throw (ex-info "Payment can't be undone because it isn't cleared."
{:validation-error "Payment can't be undone because it isn't cleared."})))
(form-validation-error "Payment can't be undone because it isn't cleared."))
(let [is-autopay-payment? (some->> (dc/q {:find ['?sp]
:in ['$ '?payment]

View File

@@ -329,7 +329,7 @@
:request request}
(com/breadcrumbs {}
[:a {:href (bidi/path-for ssr-routes/only-routes
::transaction-routes/all-page)}
::transaction-routes/page)}
"Transactions"]
[:a {:href (bidi/path-for ssr-routes/only-routes
:transaction-insights)}

View File

@@ -334,7 +334,7 @@
(and (map? data)
(every? #(try (Long/parseLong %) true (catch Exception _ false)) (keys data)))
(into [] (->> (keys data)
sort
(sort-by #(Long/parseLong %))
(map data)))
(nil? data)
nil

View File

@@ -2,6 +2,7 @@
(def routes {"" {:get ::page
:put ::edit-wizard-submit}
"/table" ::table
"/csv" ::csv
["/" [#"\d+" :db/id]] {:get ::edit-wizard}
"/edit/navigate" ::edit-wizard-navigate
"/edit/sales-summary-item" ::new-summary-item

View File

@@ -70,3 +70,24 @@
(is (= "NICK THE GREEK" (:customer-identifier result)))
(is (= "600 VISTA WAY" (str/trim (:account-number result))))
(is (= "946.24" (:total result)))))))
(deftest parse-reel-produce-statement-28676
(testing "Should parse the Reel Produce statement layout that no longer prints 'Reel Produce' on the page"
(let [pdf-file (io/file "dev-resources/Statement1_from_REEL_Produce_Inc.28676.pdf")
pdf-text (:out (clojure.java.shell/sh "pdftotext" "-layout" (str pdf-file) "-"))
results (sut/parse pdf-text)]
(is (seq results) "Template should match and return results")
(is (= 7 (count results)) "Should parse 7 invoices from statement")
(doseq [result results]
(is (= "Reel Produce" (:vendor-code result)))
(is (= "Sushi Confidential - San Jose" (:customer-identifier result))))
(is (= ["454379" "454826" "455120" "455683" "456654" "456774" "457171"]
(mapv :invoice-number results)))
(is (= ["1003.10" "530.85" "605.00" "1187.40" "164.00" "675.60" "265.75"]
(mapv :total results)))
;; totals add up to the statement's $4,431.70 amount due
(is (= 4431.70 (->> results (map #(Double/parseDouble (:total %))) (reduce +))))
(let [d (:date (first results))]
(is (= 2026 (time/year d)))
(is (= 6 (time/month d)))
(is (= 23 (time/day d)))))))