11 Commits

Author SHA1 Message Date
63eb5b5954 refactor: remove dead calc-aggregate-totals and unused schema attributes
The 13 sales-summary/total-* attributes were computed and stored but never
read — the only consumer (get-debits) was commented out. Active display code
computes totals on-the-fly from the items list instead.
2026-05-01 15:40:42 -07:00
af66049f39 consolidate sales summary ledger entry creation into upsert-sales-summary tx
Move journal entry calculation and creation from the reconcile-ledger
background job into the upsert-sales-summary tx function. Now any save
of a sales summary (job recalculation, admin edit wizard, or manual
touch) automatically creates the journal entry if balanced with all
accounts mapped, or retracts it if conditions no longer hold. Eliminates
the need for a separate upsert-sales-summary-ledger call and the
reconcile ledger pass for sales summaries.
2026-05-01 14:12:05 -07:00
2bcebc4424 feat: complete automatic sales summary calculations and ledger posting 2026-04-25 08:44:19 -07:00
297464c188 Merge branch 'master' of codecommit://integreat 2026-03-30 22:38:34 -07:00
6e3a024f66 adds stuff for dough burger 2026-03-30 22:36:12 -07:00
Bryce
28a755e9a9 fixes invoice date filtering 2026-03-02 23:20:14 -08:00
Bryce
01347ff3f5 fixes balance sheet 2026-03-02 22:53:53 -08:00
53625e4583 Makes invoices use the closed_at date. 2026-02-21 22:53:05 -08:00
8899c643ed Complete code review session - documented findings for auto_ap.permissions, iol-ion.query, and auto_ap.ss.admin.background-jobs 2026-02-08 09:31:26 -08:00
c196723913 ok. 2026-02-08 08:43:53 -08:00
395e445c99 Add test for Bonanza Produce invoice 03882095
Validates existing template correctly parses multi-page invoice with:
- Invoice number 03882095
- Customer identifier NICK THE GREEK
- Account number 600 VISTA WAY
- Total of $946.24
2026-02-08 08:40:40 -08:00
18 changed files with 822 additions and 175 deletions

View File

@@ -0,0 +1,174 @@
---
name: clojure-eval
description: Evaluate Clojure code via nREPL using clj-nrepl-eval. Use this when you need to test code, check if edited files compile, verify function behavior, or interact with a running REPL session.
---
# Clojure REPL Evaluation
## When to Use This Skill
Use this skill when you need to:
- **Verify that edited Clojure files compile and load correctly**
- Test function behavior interactively
- Check the current state of the REPL
- Debug code by evaluating expressions
- Require or load namespaces for testing
- Validate that code changes work before committing
## How It Works
The `clj-nrepl-eval` command evaluates Clojure code against an nREPL server. **Session state persists between evaluations**, so you can require a namespace in one evaluation and use it in subsequent calls. Each host:port combination maintains its own session file.
## Instructions
### 0. Discover and select nREPL server
First, discover what nREPL servers are running in the current directory:
```bash
clj-nrepl-eval --discover-ports
```
This will show all nREPL servers (Clojure, Babashka, shadow-cljs, etc.) running in the current project directory.
**Then use the AskUserQuestion tool:**
- **If ports are discovered:** Prompt user to select which nREPL port to use:
- **question:** "Which nREPL port would you like to use?"
- **header:** "nREPL Port"
- **options:** Present each discovered port as an option with:
- **label:** The port number
- **description:** The server type and status (e.g., "Clojure nREPL server in current directory")
- Include up to 4 discovered ports as options
- The user can select "Other" to enter a custom port number
- **If no ports are discovered:** Prompt user how to start an nREPL server:
- **question:** "No nREPL servers found. How would you like to start one?"
- **header:** "Start nREPL"
- **options:**
- **label:** "deps.edn alias", **description:** "Find and use an nREPL alias in deps.edn"
- **label:** "Leiningen", **description:** "Start nREPL using 'lein repl'"
- The user can select "Other" for alternative methods or if they already have a server running on a specific port
IMPORTANT: IF you start a REPL do not supply a port let the nREPL start and return the port that it was started on.
### 1. Evaluate Clojure Code
> Evaluation automatically connects to the given port
Use the `-p` flag to specify the port and pass your Clojure code.
**Recommended: Pass code as a command-line argument:**
```bash
clj-nrepl-eval -p <PORT> "(+ 1 2 3)"
```
**For multiple expressions (single line):**
```bash
clj-nrepl-eval -p <PORT> "(def x 10) (+ x 20)"
```
**Alternative: Using heredoc (may require permission approval for multiline commands):**
```bash
clj-nrepl-eval -p <PORT> <<'EOF'
(def x 10)
(+ x 20)
EOF
```
**Alternative: Via stdin pipe:**
```bash
echo "(+ 1 2 3)" | clj-nrepl-eval -p <PORT>
```
### 2. Display nREPL Sessions
**Discover all nREPL servers in current directory:**
```bash
clj-nrepl-eval --discover-ports
```
Shows all running nREPL servers in the current project directory, including their type (clj/bb/basilisp) and whether they match the current working directory.
**Check previously connected sessions:**
```bash
clj-nrepl-eval --connected-ports
```
Shows only connections you have made before (appears after first evaluation on a port).
### 3. Common Patterns
**Require a namespace (always use :reload to pick up changes):**
```bash
clj-nrepl-eval -p <PORT> "(require '[my.namespace :as ns] :reload)"
```
**Test a function after requiring:**
```bash
clj-nrepl-eval -p <PORT> "(ns/my-function arg1 arg2)"
```
**Check if a file compiles:**
```bash
clj-nrepl-eval -p <PORT> "(require 'my.namespace :reload)"
```
**Multiple expressions:**
```bash
clj-nrepl-eval -p <PORT> "(def x 10) (* x 2) (+ x 5)"
```
**Complex multiline code (using heredoc):**
```bash
clj-nrepl-eval -p <PORT> <<'EOF'
(def x 10)
(* x 2)
(+ x 5)
EOF
```
*Note: Heredoc syntax may require permission approval.*
**With custom timeout (in milliseconds):**
```bash
clj-nrepl-eval -p <PORT> --timeout 5000 "(long-running-fn)"
```
**Reset the session (clears all state):**
```bash
clj-nrepl-eval -p <PORT> --reset-session
clj-nrepl-eval -p <PORT> --reset-session "(def x 1)"
```
## Available Options
- `-p, --port PORT` - nREPL port (required)
- `-H, --host HOST` - nREPL host (default: 127.0.0.1)
- `-t, --timeout MILLISECONDS` - Timeout (default: 120000 = 2 minutes)
- `-r, --reset-session` - Reset the persistent nREPL session
- `-c, --connected-ports` - List previously connected nREPL sessions
- `-d, --discover-ports` - Discover nREPL servers in current directory
- `-h, --help` - Show help message
## Important Notes
- **Prefer command-line arguments:** Pass code as quoted strings: `clj-nrepl-eval -p <PORT> "(+ 1 2 3)"` - works with existing permissions
- **Heredoc for complex code:** Use heredoc (`<<'EOF' ... EOF`) for truly multiline code, but note it may require permission approval
- **Sessions persist:** State (vars, namespaces, loaded libraries) persists across invocations until the nREPL server restarts or `--reset-session` is used
- **Automatic delimiter repair:** The tool automatically repairs missing or mismatched parentheses
- **Always use :reload:** When requiring namespaces, use `:reload` to pick up recent changes
- **Default timeout:** 2 minutes (120000ms) - increase for long-running operations
- **Input precedence:** Command-line arguments take precedence over stdin
## Typical Workflow
1. Discover nREPL servers: `clj-nrepl-eval --discover-ports`
2. Use **AskUserQuestion** tool to prompt user to select a port
3. Require namespace:
```bash
clj-nrepl-eval -p <PORT> "(require '[my.ns :as ns] :reload)"
```
4. Test function:
```bash
clj-nrepl-eval -p <PORT> "(ns/my-fn ...)"
```
5. Iterate: Make changes, re-require with `:reload`, test again

View File

@@ -0,0 +1,82 @@
# clj-nrepl-eval Examples
## Discovery
```bash
clj-nrepl-eval --connected-ports
```
## Heredoc for Multiline Code
```bash
clj-nrepl-eval -p 7888 <<'EOF'
(defn greet [name]
(str "Hello, " name "!"))
(greet "Claude")
EOF
```
### Heredoc Simplifies String Escaping
Heredoc avoids shell escaping issues with quotes, backslashes, and special characters:
```bash
# With heredoc - no escaping needed
clj-nrepl-eval -p 7888 <<'EOF'
(def regex #"\\d{3}-\\d{4}")
(def message "She said \"Hello!\" and waved")
(def path "C:\\Users\\name\\file.txt")
(println message)
EOF
# Without heredoc - requires complex escaping
clj-nrepl-eval -p 7888 "(def message \"She said \\\"Hello!\\\" and waved\")"
```
## Working with Project Namespaces
```bash
# Test a function after requiring
clj-nrepl-eval -p 7888 <<'EOF'
(require '[clojure-mcp-light.delimiter-repair :as dr] :reload)
(dr/delimiter-error? "(defn foo [x]")
EOF
```
## Verify Compilation After Edit
```bash
# If this returns nil, the file compiled successfully
clj-nrepl-eval -p 7888 "(require 'clojure-mcp-light.hook :reload)"
```
## Session Management
```bash
# Reset session if state becomes corrupted
clj-nrepl-eval -p 7888 --reset-session
```
## Common Workflow Patterns
### Load, Test, Iterate
```bash
# After editing a file, reload and test in one command
clj-nrepl-eval -p 7888 <<'EOF'
(require '[my.namespace :as ns] :reload)
(ns/my-function test-data)
EOF
```
### Run Tests After Changes
```bash
clj-nrepl-eval -p 7888 <<'EOF'
(require '[my.project.core :as core] :reload)
(require '[my.project.core-test :as test] :reload)
(clojure.test/run-tests 'my.project.core-test)
EOF
```

3
.gitattributes vendored Normal file
View File

@@ -0,0 +1,3 @@
# Use bd merge for beads JSONL files
.beads/issues.jsonl merge=beads

54
AGENTS.md Normal file
View File

@@ -0,0 +1,54 @@
# Agent Instructions
This project uses **bd** (beads) for issue tracking. Run `bd onboard` to get started.
## Issue Tracking
This project uses **bd (beads)** for issue tracking.
Run `bd prime` for workflow context, or install hooks (`bd hooks install`) for auto-injection.
**Quick reference:**
- `bd ready` - Find unblocked work
- `bd create "Title" --type task --priority 2` - Create issue
- `bd close <id>` - Complete work
- `bd sync` - Sync with git (run at session end)
For full workflow details: `bd prime`
## Quick Reference
```bash
bd ready # Find available work
bd show <id> # View issue details
bd update <id> --status in_progress # Claim work
bd close <id> # Complete work
bd sync # Sync with git
```
## Landing the Plane (Session Completion)
**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds.
**MANDATORY WORKFLOW:**
1. **File issues for remaining work** - Create issues for anything that needs follow-up
2. **Run quality gates** (if code changed) - Tests, linters, builds
3. **Update issue status** - Close finished work, update in-progress items
4. **PUSH TO REMOTE** - This is MANDATORY:
```bash
git pull --rebase
bd sync
git push
git status # MUST show "up to date with origin"
```
5. **Clean up** - Clear stashes, prune remote branches
6. **Verify** - All changes committed AND pushed
7. **Hand off** - Provide context for next session
**CRITICAL RULES:**
- Work is NOT complete until `git push` succeeds
- NEVER stop before pushing - that leaves work stranded locally
- NEVER say "ready to push when you are" - YOU must push
- If push fails, resolve and retry until it succeeds
Use 'bd' for task tracking

Binary file not shown.

View File

@@ -0,0 +1,219 @@
---
title: Complete Automatic Sales Summary Calculations and Ledger Posting
type: feat
status: completed
date: 2026-04-24
---
# Complete Automatic Sales Summary Calculations and Ledger Posting
## What's Incomplete
- **Automatic Totals**: Aggregate attributes (e.g., `:sales-summary/total-card-payments`) are not calculated/stored by the job.
- **Data Persistence**: Automatic recalculations risk overwriting manual user adjustments.
- **Automation Gap**: Ledger entries are currently imported from external Excel files rather than generated automatically from the summaries.
- **UI Polish**: "Clientization" and HTMX context (`client-id`) TODOs remain in the admin interface.
---
## Overview
...
This plan completes the implementation of automatic sales summary calculations and ensures they are correctly posted to the ledger. Currently, the `sales-summaries-v2` job calculates detailed daily summaries, but it doesn't store aggregate totals, preserve manual adjustments, or trigger the creation of actual ledger entries. Additionally, the admin UI has several unresolved TODOs.
---
## Problem Frame
The system currently aggregates raw sales data into a `sales-summary` entity, but the final step—creating balanced journal entries for the general ledger—is a manual process involving external Excel calculations and subsequent imports. This creates a dependency on external tools and increases the risk of data entry errors. The goal is to automate this pipeline entirely within the product.
---
## Requirements Trace
- R1. Calculate and store aggregate totals (e.g., `:sales-summary/total-card-payments`) on the `sales-summary` entity.
- R2. Preserve user-made manual adjustments (`:sales-summary-item/manual? true`) during automatic recalculations.
- R3. Aggregate detailed `sales-summary-item`s into balanced `journal-entry` lines by account and location.
- R4. Automate the posting of these aggregated totals to the ledger.
- R5. Resolve UI TODOs in the Sales Summaries admin page, specifically regarding client-scoping ("clientize") and HTMX context (`client-id`).
---
## Scope Boundaries
- **In-Scope**:
- Enhancements to the `sales-summaries-v2` job.
- Implementation of the summary-to-ledger aggregation and posting logic.
- Cleanup of the Sales Summaries admin UI.
- **Out-of-Scope**:
- Changing the fundamental calculation logic for sales orders/refunds.
- Creating new ledger accounts (assume existing account mapping is sufficient).
- Changing the naming of refunds/returns (user requested to keep as is).
---
## Context & Research
### Relevant Code and Patterns
- **Jobs**: `src/clj/auto_ap/jobs/sales_summaries.clj` contains the main calculation logic.
- **UI**: `src/clj/auto_ap/ssr/admin/sales_summaries.clj` implements the admin interface.
- **Ledger Posting**: `src/clj/auto_ap/ledger.clj` and `iol_ion/src/iol_ion/tx/upsert_ledger.clj` handle journal entry creation.
- **Reconciliation Pattern**: `reconcile-ledger` in `src/clj/auto_ap/ledger.clj` shows how to find missing ledger entries and trigger their creation.
### Institutional Learnings
- No existing documented patterns for sales summary posting were found in `docs/solutions/`. This implementation will establish the pattern.
---
## Key Technical Decisions
- **Detailed Summary $\to$ Aggregated Ledger**: The `sales-summary` will maintain granular detail (line items, specific fee types), but the ledger posting will aggregate these items by account and location to create balanced `journal-entry` lines.
- **Automatic Posting**: Posting to the ledger will be integrated into the reconciliation process, similar to how invoices and transactions are handled in `reconcile-ledger`.
- **Location Handling**: Since `sales-summary-item`s don't have a location, a default location for the client will be used for ledger posting.
---
## Open Questions
### Resolved During Planning
- **Architectural Decision**: Use a detailed summary that aggregates into the ledger.
- **Renaming**: Keep "Refunds/Returns" as is.
### Deferred to Implementation
- **Default Location Logic**: Exactly how the "default location" for a client is retrieved or defined.
---
## Implementation Units
- U1. **Enhance `sales-summaries-v2` Job**
**Goal:** Ensure the job stores aggregate totals and preserves manual adjustments.
**Requirements:** R1, R2
**Dependencies:** None
**Files:**
- Modify: `src/clj/auto_ap/jobs/sales_summaries.clj`
**Approach:**
- Update `sales-summaries-v2` to calculate totals for attributes like `:sales-summary/total-card-payments`, `:sales-summary/total-cash-payments`, etc., based on the generated items.
- Implement a merge strategy: when updating a summary, keep any items where `:sales-summary-item/manual?` is true, and only replace the automatically calculated items.
**Test scenarios:**
- Happy path: Running the job for a client with sales and refunds results in a `sales-summary` with correct `:sales-summary/total-*` attributes.
- Edge case: Running the job on a summary that already has a manual item ensures the manual item is not overwritten.
**Verification:**
- Datomic query shows `sales-summary` entities have populated total attributes and preserved manual items.
---
- U2. **Implement Summary-to-Ledger Aggregation**
**Goal:** Create a function to transform detailed summary items into balanced ledger lines.
**Requirements:** R3
**Dependencies:** U1
**Files:**
- Create: `src/clj/auto_ap/ledger/sales_summaries.clj` (or add to `src/clj/auto_ap/ledger.clj`)
- Test: `test/clj/auto_ap/ledger_test.clj`
**Approach:**
- Create a function `aggregate-summary-items` that:
1. Groups `sales-summary-item`s by `:ledger-mapped/account`.
2. Sums the `:ledger-mapped/amount` based on `:ledger-mapped/ledger-side` (debit vs credit).
3. Assigns a location (default client location).
4. Returns a list of `journal-entry-line` maps.
**Test scenarios:**
- Happy path: A set of items with mixed accounts and sides aggregates into the correct number of ledger lines with summed amounts.
- Edge case: Items with `nil` accounts are handled gracefully (e.g., mapped to an "Unknown" account or logged as error).
**Verification:**
- Unit tests verify that a list of `sales-summary-item`s is correctly transformed into `journal-entry-line`s.
---
- U3. **Implement Automatic Ledger Posting for Summaries**
**Goal:** Ensure sales summaries trigger the creation of ledger entries.
**Requirements:** R4
**Dependencies:** U2
**Files:**
- Modify: `src/clj/auto_ap/ledger.clj`
- Modify: `src/clj/auto_ap/jobs/sales_summaries.clj`
**Approach:**
- Implement a `:upsert-sales-summary-ledger` transaction or function that takes a `sales-summary` and uses the aggregation logic from U2 to post to the ledger.
- Integrate this into the `reconcile-ledger` function in `src/clj/auto_ap/ledger.clj` to find summaries missing ledger entries and post them.
**Test scenarios:**
- Integration: Running `reconcile-ledger` identifies a `sales-summary` missing a `journal-entry` and creates a balanced `journal-entry` for it.
- Happy path: The created `journal-entry` has the correct total amount and matches the summary totals.
**Verification:**
- A `sales-summary` entity is linked to a `journal-entry` via `:journal-entry/original-entity`.
---
- U4. **Resolve UI TODOs in Sales Summaries Admin**
**Goal:** Fix client-scoping and HTMX context in the admin UI.
**Requirements:** R5
**Dependencies:** None
**Files:**
- Modify: `src/clj/auto_ap/ssr/admin/sales_summaries.clj`
**Approach:**
- Resolve "clientize" TODOs: Ensure the data pulled for the table and edit wizard is correctly scoped and transformed using client-specific context.
- Fix HTMX `client-id` passing: Update the `new-summary-item` trigger to correctly pass the `client-id` via `hx-vals` from the form state.
- Clean up any remaining schema TODOs in the SSR file.
**Test scenarios:**
- Integration: Adding a new summary item in the UI correctly sends the `client-id` and the item is created for the correct client.
- Happy path: The summary table displays correctly and "missing account" warnings appear only for items without a mapped account.
**Verification:**
- Manual verification in the browser: New items are added correctly, and the UI is free of "missing account" red pills for mapped items.
---
## System-Wide Impact
- **Interaction graph**: The `sales-summaries-v2` job now feeds into the ledger system via `reconcile-ledger`.
- **Error propagation**: Failures in the aggregation logic will prevent the `journal-entry` from being created, which will be surfaced by `reconcile-ledger` as a missing entry.
- **State lifecycle risks**: Ensuring that `manual?` items are not overwritten during automatic recalculation is critical to avoid losing user adjustments.
- **Integration coverage**: Integration tests must cover the full flow: `sales-orders` $\to$ `sales-summary` $\to$ `journal-entry`.
---
## Risks & Dependencies
| Risk | Mitigation |
|------|------------|
| Overwriting manual adjustments | Implement explicit merge logic based on the `:sales-summary-item/manual?` flag. |
| Unbalanced ledger entries | Use a strict aggregation function that ensures debits = credits for every posted summary. |
| Missing location data | Implement a robust fallback to a default client location. |
---
## Sources & References
- Related code: `src/clj/auto_ap/jobs/sales_summaries.clj`
- Related code: `src/clj/auto_ap/ssr/admin/sales_summaries.clj`
- Related code: `src/clj/auto_ap/ledger.clj`
- Related code: `iol_ion/src/iol_ion/tx/upsert_ledger.clj`

View File

@@ -38,18 +38,19 @@
(defn regenerate-literals [] (defn regenerate-literals []
(require 'com.github.ivarref.gen-fn) (require 'com.github.ivarref.gen-fn)
(spit (spit
"resources/functions.edn" "resources/functions.edn"
(let [datomic-fn @(resolve 'com.github.ivarref.gen-fn/datomic-fn)] (let [datomic-fn @(resolve 'com.github.ivarref.gen-fn/datomic-fn)]
[(datomic-fn :pay #'iol-ion.tx.pay/pay) [(datomic-fn :pay #'iol-ion.tx.pay/pay)
(datomic-fn :plus #'iol-ion.tx.plus/plus) (datomic-fn :plus #'iol-ion.tx.plus/plus)
(datomic-fn :propose-invoice #'iol-ion.tx.propose-invoice/propose-invoice) (datomic-fn :propose-invoice #'iol-ion.tx.propose-invoice/propose-invoice)
(datomic-fn :reset-rels #'iol-ion.tx.reset-rels/reset-rels) (datomic-fn :reset-rels #'iol-ion.tx.reset-rels/reset-rels)
(datomic-fn :reset-scalars #'iol-ion.tx.reset-scalars/reset-scalars) (datomic-fn :reset-scalars #'iol-ion.tx.reset-scalars/reset-scalars)
(datomic-fn :upsert-entity #'iol-ion.tx.upsert-entity/upsert-entity) (datomic-fn :upsert-entity #'iol-ion.tx.upsert-entity/upsert-entity)
(datomic-fn :upsert-invoice #'iol-ion.tx.upsert-invoice/upsert-invoice) (datomic-fn :upsert-invoice #'iol-ion.tx.upsert-invoice/upsert-invoice)
(datomic-fn :upsert-ledger #'iol-ion.tx.upsert-ledger/upsert-ledger) (datomic-fn :upsert-ledger #'iol-ion.tx.upsert-ledger/upsert-ledger)
(datomic-fn :upsert-transaction #'iol-ion.tx.upsert-transaction/upsert-transaction)]))) (datomic-fn :upsert-transaction #'iol-ion.tx.upsert-transaction/upsert-transaction)
(datomic-fn :upsert-sales-summary #'iol-ion.tx.upsert-sales-summary-ledger/upsert-sales-summary)])))
(comment (comment
(regenerate-literals) (regenerate-literals)

View File

@@ -0,0 +1,60 @@
(ns iol-ion.tx.upsert-sales-summary-ledger
(:require [datomic.api :as dc]
[iol-ion.tx.upsert-entity :as upsert-entity]
[iol-ion.tx.upsert-ledger :as upsert-ledger]))
(defn summary->journal-entry [db summary-id]
(let [summary (dc/pull db '[:sales-summary/client
:sales-summary/date
{:sales-summary/items [:sales-summary-item/category
:ledger-mapped/account
:ledger-mapped/amount
:ledger-mapped/ledger-side]}]
summary-id)
items (:sales-summary/items summary)
aggregated (->> items
(filter :ledger-mapped/account)
(group-by :ledger-mapped/account)
(map (fn [[account acc-items]]
(reduce
(fn [m item]
(update m (:ledger-mapped/ledger-side item) (fnil + 0.0) (:ledger-mapped/amount item 0.0)))
{:account account}
acc-items))))
line-items (mapv (fn [{:keys [account] :as m}]
(cond-> {:db/id (str (java.util.UUID/randomUUID))
:journal-entry-line/account account
:journal-entry-line/location "A"}
(get m :ledger-side/debit) (assoc :journal-entry-line/debit (get m :ledger-side/debit))
(get m :ledger-side/credit) (assoc :journal-entry-line/credit (get m :ledger-side/credit))))
aggregated)
total-debits (reduce + 0.0 (map #(get % :ledger-side/debit 0.0) aggregated))
total-credits (reduce + 0.0 (map #(get % :ledger-side/credit 0.0) aggregated))]
(when (and (seq line-items)
(= (Math/round (* 1000 total-debits))
(Math/round (* 1000 total-credits))))
{:journal-entry/source "sales-summary"
:journal-entry/client (:db/id (:sales-summary/client summary))
:journal-entry/date (:sales-summary/date summary)
:journal-entry/original-entity summary-id
:journal-entry/amount total-debits
:journal-entry/line-items line-items})))
(defn upsert-sales-summary [db summary]
(let [upserted-summary [[:upsert-entity summary]]
db-after (-> (dc/with db upserted-summary) :db-after)
summary-id (:db/id summary)
client-id (-> (dc/pull db-after [{:sales-summary/client [:db/id]}] summary-id)
:sales-summary/client
:db/id)
journal-entry (summary->journal-entry db-after summary-id)]
(into upserted-summary
(if journal-entry
[[:upsert-ledger journal-entry]]
(let [existing-je-id (ffirst (dc/q '[:find ?je . :in $ ?ss
:where [?je :journal-entry/original-entity ?ss]]
db-after summary-id))]
(concat
(when existing-je-id [[:db/retractEntity existing-je-id]])
(when client-id [{:db/id client-id
:client/ledger-last-change (upsert-ledger/current-date db)}])))))))

View File

@@ -1949,12 +1949,12 @@
:db/unique :db.unique/identity :db/unique :db.unique/identity
:db/index true} :db/index true}
{:db/ident :sales-summary/client+dirty {:db/ident :sales-summary/client+dirty
:db/valueType :db.type/tuple :db/valueType :db.type/tuple
:db/tupleAttrs [:sales-summary/client :sales-summary/dirty] :db/tupleAttrs [:sales-summary/client :sales-summary/dirty]
:db/cardinality :db.cardinality/one :db/cardinality :db.cardinality/one
:db/index true} :db/index true}
{:db/ident :sales-summary-item/category {:db/ident :sales-summary-item/category
:db/valueType :db.type/string :db/valueType :db.type/string
:db/cardinality :db.cardinality/one} :db/cardinality :db.cardinality/one}
{:db/ident :sales-summary-item/sort-order {:db/ident :sales-summary-item/sort-order

View File

@@ -5,10 +5,11 @@
[iol-ion.tx.propose-invoice] [iol-ion.tx.propose-invoice]
[iol-ion.tx.reset-rels] [iol-ion.tx.reset-rels]
[iol-ion.tx.reset-scalars] [iol-ion.tx.reset-scalars]
[iol-ion.tx.upsert-entity] [iol-ion.tx.upsert-entity]
[iol-ion.tx.upsert-invoice] [iol-ion.tx.upsert-invoice]
[iol-ion.tx.upsert-ledger] [iol-ion.tx.upsert-ledger]
[iol-ion.tx.upsert-transaction] [iol-ion.tx.upsert-transaction]
[iol-ion.tx.upsert-sales-summary-ledger]
[com.github.ivarref.gen-fn :refer [gen-fn! datomic-fn]] [com.github.ivarref.gen-fn :refer [gen-fn! datomic-fn]]
[auto-ap.utils :refer [default-pagination-size by]] [auto-ap.utils :refer [default-pagination-size by]]
[clojure.edn :as edn] [clojure.edn :as edn]

View File

@@ -278,46 +278,42 @@
(defn sales-summaries-v2 [] (defn sales-summaries-v2 []
(doseq [[c client-code] (dc/q '[:find ?c ?client-code (doseq [[c client-code] (dc/q '[:find ?c ?client-code
:in $ :in $
:where [?c :client/code ?client-code]] :where [?c :client/code ?client-code]]
(dc/db conn)) (dc/db conn))
{:sales-summary/keys [date] :db/keys [id]} (dirty-sales-summaries c)] {:sales-summary/keys [date] :db/keys [id] :as existing-summary} (dirty-sales-summaries c)]
(mu/with-context {:client-code client-code (mu/with-context {:client-code client-code
:date date} :date date}
(alog/info ::updating) (alog/info ::updating)
(let [result {:db/id id (let [manual-items (->> existing-summary
:sales-summary/client c :sales-summary/items
:sales-summary/date date (filter :sales-summary-item/manual?))
:sales-summary/dirty false calculated-items (->>
:sales-summary/client+date [c date] (get-sales c date)
(concat (get-payment-items c date))
:sales-summary/items (concat (get-refund-items c date))
(->> (cons (get-discounts c date))
(get-sales c date) (cons (get-fees c date))
(concat (get-payment-items c date)) (cons (get-tax c date))
(concat (get-refund-items c date)) (cons (get-tip c date))
(cons (get-discounts c date)) (cons (get-returns c date))
(cons (get-fees c date)) (filter identity)
(cons (get-tax c date)) (map (fn [z]
(cons (get-tip c date)) (assoc z :ledger-mapped/account (some-> z :sales-summary-item/category str/lower-case name->number lookup-account)
(cons (get-returns c date)) :sales-summary-item/manual? false))))
(filter identity) all-items (concat calculated-items manual-items)
(map (fn [z] result {:db/id id
(assoc z :ledger-mapped/account (some-> z :sales-summary-item/category str/lower-case name->number lookup-account) :sales-summary/client c
:sales-summary-item/manual? false)) :sales-summary/date date
)) }] :sales-summary/dirty false
(if (seq (:sales-summary/items result)) :sales-summary/client+date [c date]
(do :sales-summary/items all-items}]
(alog/info ::upserting-summaries (if (seq (:sales-summary/items result))
:category-count (count (:sales-summary/items result))) (do
@(dc/transact conn [[:upsert-entity result]])) (alog/info ::upserting-summaries
@(dc/transact conn [{:db/id id :sales-summary/dirty false}])))))) :category-count (count (:sales-summary/items result)))
@(dc/transact conn [[:upsert-sales-summary result]]))
(let [c (auto-ap.datomic/pull-attr (dc/db conn) :db/id [:client/code "NGCL" ]) @(dc/transact conn [{:db/id id :sales-summary/dirty false}]))))))
date #inst "2024-04-14T00:00:00-07:00"]
(get-payment-items c date)
)
(defn reset-summaries [] (defn reset-summaries []
@@ -334,11 +330,6 @@
(comment (comment
(auto-ap.datomic/transact-schema conn) (auto-ap.datomic/transact-schema conn)
@(dc/transact conn [{:db/ident :sales-summary/total-unknown-processor-payments
:db/noHistory true,
:db/valueType :db.type/double
:db/cardinality :db.cardinality/one}])
(apply mark-dirty [:client/code "NGCL"] (last-n-days 30)) (apply mark-dirty [:client/code "NGCL"] (last-n-days 30))
(apply mark-dirty [:client/code "NGDG"] (last-n-days 30)) (apply mark-dirty [:client/code "NGDG"] (last-n-days 30))

View File

@@ -35,27 +35,42 @@
invoices-missing-ledger-entries (->> (dc/q {:find ['?t ] invoices-missing-ledger-entries (->> (dc/q {:find ['?t ]
:in ['$ '?sd] :in ['$ '?sd]
:where ['[?t :invoice/date ?d] :where ['[?t :invoice/date ?d]
'[(>= ?d ?sd)] '[(>= ?d ?sd)]
'(not [_ :journal-entry/original-entity ?t]) '(not [_ :journal-entry/original-entity ?t])
'[?t :invoice/total ?amt] '[?t :invoice/total ?amt]
'[(not= 0.0 ?amt)] '[(not= 0.0 ?amt)]
'(not [?t :invoice/status :invoice-status/voided]) '(not [?t :invoice/status :invoice-status/voided])
'(not [?t :invoice/import-status :import-status/pending]) '(not [?t :invoice/import-status :import-status/pending])
'(not [?t :invoice/exclude-from-ledger true]) '(not [?t :invoice/exclude-from-ledger true])
]} ]}
(dc/db conn) start-date) (dc/db conn) start-date)
(map first) (map first)
(mapv (fn [i] (mapv (fn [i]
[:upsert-invoice {:db/id i}]))) [:upsert-invoice {:db/id i}])))
repairs (vec (concat txes-missing-ledger-entries invoices-missing-ledger-entries))]
sales-summaries-missing-ledger-entries (->> (dc/q {:find ['?ss ]
:in ['$ '?sd]
:where ['[?ss :sales-summary/date ?d]
'[(>= ?d ?sd)]
'(not [_ :journal-entry/original-entity ?ss])
'[?ss :sales-summary/items ?item]
'[?item :ledger-mapped/account]
]}
(dc/db conn) start-date)
(map first)
(mapv (fn [ss]
[:upsert-sales-summary {:db/id ss}])))
repairs (vec (concat txes-missing-ledger-entries invoices-missing-ledger-entries sales-summaries-missing-ledger-entries))]
(when (seq repairs) (when (seq repairs)
(mu/log ::ledger-repairs-needed (mu/log ::ledger-repairs-needed
:sample (take 3 repairs) :sample (take 3 repairs)
:transaction-count (count txes-missing-ledger-entries) :transaction-count (count txes-missing-ledger-entries)
:invoice-count (count invoices-missing-ledger-entries)) :invoice-count (count invoices-missing-ledger-entries)
@(dc/transact conn repairs))))) :sales-summary-count (count sales-summaries-missing-ledger-entries))
@(dc/transact conn repairs)))))
(defn touch-transaction [e] (defn touch-transaction [e]

View File

@@ -293,7 +293,9 @@
(condp = (:name (:source order)) (condp = (:name (:source order))
"GRUBHUB" :ccp-processor/grubhub "GRUBHUB" :ccp-processor/grubhub
"UBEREATS" :ccp-processor/uber-eats "UBEREATS" :ccp-processor/uber-eats
"Uber Eats" :ccp-processor/uber-eats
"DOORDASH" :ccp-processor/doordash "DOORDASH" :ccp-processor/doordash
"DoorDash" :ccp-processor/doordash
"Koala" :ccp-processor/koala "Koala" :ccp-processor/koala
"koala-production" :ccp-processor/koala "koala-production" :ccp-processor/koala
:ccp-processor/na)) :ccp-processor/na))
@@ -349,7 +351,10 @@
(s/reduce conj []))] (s/reduce conj []))]
[(remove-nils [(remove-nils
#:sales-order #:sales-order
{:date (coerce/to-date (time/to-time-zone (coerce/to-date-time (:created_at order)) (time/time-zone-for-id "America/Los_Angeles"))) {:date (if (= "Invoices" (:name (:source order)))
(when (:closed_at order)
(coerce/to-date (time/to-time-zone (coerce/to-date-time (:closed_at order)) (time/time-zone-for-id "America/Los_Angeles"))))
(coerce/to-date (time/to-time-zone (coerce/to-date-time (:created_at order)) (time/time-zone-for-id "America/Los_Angeles"))))
:client (:db/id client) :client (:db/id client)
:location (:square-location/client-location location) :location (:square-location/client-location location)
:external-id (str "square/order/" (:client/code client) "-" (:square-location/client-location location) "-" (:id order)) :external-id (str "square/order/" (:client/code client) "-" (:square-location/client-location location) "-" (:id order))
@@ -379,6 +384,9 @@
;; sometimes orders stay open in square. At least one payment ;; sometimes orders stay open in square. At least one payment
;; is needed to import, in order to avoid importing orders in-progress. ;; is needed to import, in order to avoid importing orders in-progress.
(and (and
(if (= "Invoices" (:name (:source order)))
(boolean (:closed_at order))
true)
(or (> (count (:tenders order)) 0) (or (> (count (:tenders order)) 0)
(seq (:returns order))) (seq (:returns order)))
(or (= #{} (set (map #(:status (:card_details %)) (:tenders order)))) (or (= #{} (set (map #(:status (:card_details %)) (:tenders order))))
@@ -862,7 +870,11 @@
#_(comment #_(comment
(require 'auto-ap.time-reader) (require 'auto-ap.time-reader)
@(let [[c [l]] (get-square-client-and-location "DBFS") ]
(log/peek :x [ c l])
(search c l #clj-time/date-time "2026-03-28" #clj-time/date-time "2026-03-29")
)
@(let [[c [l]] (get-square-client-and-location "NGAK") ] @(let [[c [l]] (get-square-client-and-location "NGAK") ]
(log/peek :x [ c l]) (log/peek :x [ c l])
@@ -972,13 +984,14 @@
:headers (client-base-headers client) :headers (client-base-headers client)
:as :json}) :as :json})
:body))) :body)))
(->> (->>
@(let [[c [l]] (get-square-client-and-location "NGGG")] @(let [[c [l]] (get-square-client-and-location "NGGG")]
(search c l (time/plus (time/now)))) (search c l (time/now) (time/plus (time/now) (time/days -1))))
(filter (fn [r]
(str/starts-with? (:created_at r) "2024-03-14")))) (filter (fn [r]
(str/starts-with? (:created_at r) "2024-03-14"))))
(def refs (def refs
(->> (->>
@@ -995,29 +1008,29 @@
(map (fn [r] @(get-payment c (:payment_id r))) refs)) (map (fn [r] @(get-payment c (:payment_id r))) refs))
(get-square-client-and-location "NGGB") (get-square-client-and-location "NGGB")
(def my-results (def my-results
(let [[c [l]] (get-square-client-and-location "NGFA")])) (let [[c [l]] (get-square-client-and-location "NGFA")]))
(clojure.data.csv/write-csv *out* (clojure.data.csv/write-csv *out*
(for [c (get-square-clients) (for [c (get-square-clients)
l (:client/square-locations c) l (:client/square-locations c)
:when (:square-location/client-location l) :when (:square-location/client-location l)
bad-row (try (->> @(search c l (coerce/to-date-time #inst "2024-04-01T00:00:00-07:00") (coerce/to-date-time #inst "2024-04-15T23:59:00-07:00")) bad-row (try (->> @(search c l (coerce/to-date-time #inst "2024-04-01T00:00:00-07:00") (coerce/to-date-time #inst "2024-04-15T23:59:00-07:00"))
(filter #(not (should-import-order? %))) (filter #(not (should-import-order? %)))
(map #(first (deref (order->sales-order c l %)))) (map #(first (deref (order->sales-order c l %))))
(filter (fn already-exists [o] (filter (fn already-exists [o]
(when (:sales-order/external-id o) (when (:sales-order/external-id o)
(seq (dc/q '[:find ?i (seq (dc/q '[:find ?i
:in $ ?ei :in $ ?ei
:where [?i :sales-order/external-id ?ei]] :where [?i :sales-order/external-id ?ei]]
(dc/db conn) (dc/db conn)
(:sales-order/external-id o))))))) (:sales-order/external-id o)))))))
(catch Exception e (catch Exception e
[]))] []))]
[(:client/code c) (atime/unparse-local (clj-time.coerce/to-date-time (:sales-order/date bad-row)) atime/normal-date) (:sales-order/total bad-row) (:sales-order/tax bad-row) (:sales-order/tip bad-row) (:db/id bad-row)]) [(:client/code c) (atime/unparse-local (clj-time.coerce/to-date-time (:sales-order/date bad-row)) atime/normal-date) (:sales-order/total bad-row) (:sales-order/tax bad-row) (:sales-order/tip bad-row) (:db/id bad-row)])
:separator \tab) :separator \tab)
@@ -1035,7 +1048,7 @@
(def z @(search c l #clj-time/date-time "2025-02-23T00:00:00-08:00" (def z @(search c l #clj-time/date-time "2025-02-23T00:00:00-08:00"
#clj-time/date-time "2025-02-28T00:00:00-08:00")) #clj-time/date-time "2025-02-28T00:00:00-08:00"))
(take 10 (map #(first (deref (order->sales-order c l %))) z))) (take 10 (map #(first (deref (order->sales-order c l %))) z)))
@@ -1051,17 +1064,43 @@
(count) (count)
) )
(doseq [c (get-square-clients)]
(println "Upserting" (:client/name c))
@(upsert c))
(doseq [[code] (seq (dc/q '[:find ?code
:in $
:where [?o :sales-order/date ?d]
[(>= ?d #inst "2026-01-01")]
[?o :sales-order/source "Invoices"]
[?o :sales-order/client ?c]
[?c :client/code ?code]]
(dc/db conn)))
:let [[c [l]] (get-square-client-and-location code)
]
order @(search c l #clj-time/date-time "2026-01-01T00:00:00-08:00" (time/now))
:when (= "Invoices" (:name (:source order) ))
:let [[sales-order] @(order->sales-order c l order)]]
(when (should-import-order? order)
(println "DATE IS" (:sales-order/date sales-order))
(when (some-> (:sales-order/date sales-order) coerce/to-date-time (time/after? #clj-time/date-time "2026-2-16T00:00:00-08:00"))
(println "WOULD UPDATE" sales-order)
@(dc/transact auto-ap.datomic/conn [sales-order])
)
#_@(dc/transact )
(println "DONE"))
)
#_(filter (comp #{"OTHER"} :type) (mapcat :tenders z)) #_(filter (comp #{"OTHER"} :type) (mapcat :tenders z))
(let [[c [l]] (get-square-client-and-location "LFHH")] @(let [[c [l]] (get-square-client-and-location "NGRY")]
(search c l (clj-time.coerce/from-date #inst "2025-02-28") (clj-time.coerce/from-date #inst "2025-03-01")) #_(search c l (clj-time.coerce/from-date #inst "2025-02-28") (clj-time.coerce/from-date #inst "2025-03-01"))
(:order (get-order c l "CLjQqkzVfGa82o5hEFUrGtUGO6QZY" )) (order->sales-order c l (:order (get-order c l "KdvwntmfMNTKBu8NOocbxatOs18YY" )))
)
)
) )

View File

@@ -72,14 +72,14 @@
[:sales-summary/date :xform clj-time.coerce/from-date] [:sales-summary/date :xform clj-time.coerce/from-date]
{:sales-summary/client [:client/code :client/name :db/id]} {:sales-summary/client [:client/code :client/name :db/id]}
{:sales-summary/items [{[:ledger-mapped/ledger-side :xform iol-ion.query/ident] [:db/ident] {:sales-summary/items [{[:ledger-mapped/ledger-side :xform iol-ion.query/ident] [:db/ident]
} ;; TODO clientize }
:ledger-mapped/account :ledger-mapped/account
:ledger-mapped/amount :ledger-mapped/amount
:sales-summary-item/category :sales-summary-item/category
:sales-summary-item/sort-order :sales-summary-item/sort-order
:db/id :db/id
:sales-summary-item/manual?] :sales-summary-item/manual?]
} ]) ;; TODO } ])
(defn fetch-ids [db request] (defn fetch-ids [db request]
(let [query-params (:query-params request) (let [query-params (:query-params request)
@@ -129,26 +129,6 @@
[(->> (hydrate-results ids-to-retrieve db request)) [(->> (hydrate-results ids-to-retrieve db request))
matching-count])) matching-count]))
#_(defn get-debits [ss]
{:card-payments (+ (:sales-summary/total-card-payments ss 0.0)
(:sales-summary/total-card-fees ss 0.0)
(- (:sales-summary/total-card-refunds ss 0.0)))
:food-app-payments (+ (:sales-summary/total-food-app-payments ss 0.0)
(:sales-summary/total-food-app-fees ss 0.0)
(- (:sales-summary/total-food-app-refunds ss 0.0)))
:gift-card-payments (+ (:sales-summary/total-gift-card-payments ss 0.0)
(:sales-summary/total-gift-card-fees ss 0.0)
(- (:sales-summary/total-gift-card-refunds ss 0.0)))
#_#_:refunds (+ (:sales-summary/total-food-app-refunds ss 0.0)
(:sales-summary/total-card-refunds ss 0.0)
(:sales-summary/total-cash-refunds ss 0.0))
:fees (- (:sales-summary/total-card-fees ss 0.0))
:cash-payments (+ (:sales-summary/total-cash-payments ss 0.0)
(- (:sales-summary/total-cash-refunds ss 0.0)))
:total-unknown-processor-payments (:sales-summary/total-unknown-processor-payments ss 0.0)
:discounts (+ (:sales-summary/discount ss 0.0))
:returns (+ (:sales-summary/total-returns ss 0.0))})
(defn sort-items [ss] (defn sort-items [ss]
(sort-by (juxt :ledger-mapped/ledger-side :sales-summary-item/sort-order :sales-summary-item/category) ss)) (sort-by (juxt :ledger-mapped/ledger-side :sales-summary-item/sort-order :sales-summary-item/category) ss))
@@ -179,10 +159,8 @@
:db/id (:db/id entity))} :db/id (:db/id entity))}
svg/pencil)]) svg/pencil)])
:oob-render :oob-render
(fn [request] (fn [_request]
[#_(assoc-in (date-range-field {:value {:start (:start-date (:query-params request)) [])
:end (:end-date (:query-params request))}
:id "date-range"}) [1 :hx-swap-oob] true)]) ;; TODO
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
:admin)} :admin)}
"Admin"] "Admin"]
@@ -241,13 +219,9 @@
:primary :primary
:red)} "Total: " (format "$%,.2f" total-credits))]]))}]})) :red)} "Total: " (format "$%,.2f" total-credits))]]))}]}))
;; TODO schema cleanup ;; Architecture: Sales summary maintains granular detail (line items, fee types)
;; Decide on what should be calculated as generating ledger entries, and what should be calculated ;; and is aggregated into ledger entries by account/location. Manual adjustments
;; as part of the summary ;; are preserved during automatic recalculation.
;; default thought here is that the summary has more detail (e.g., line items), fees broken out by type
;; and aggregated into the final ledger entry
;; that allows customization at any level.
;; TODO rename refunds/returns
(def row* (partial helper/row* grid-page)) (def row* (partial helper/row* grid-page))
(def table* (partial helper/table* grid-page)) (def table* (partial helper/table* grid-page))
@@ -436,15 +410,14 @@
(com/data-grid-header {} "")]} (com/data-grid-header {} "")]}
(fc/with-field :sales-summary/items (fc/with-field :sales-summary/items
(list (list
(fc/cursor-map #(sales-summary-item-row* {:value % (fc/cursor-map #(sales-summary-item-row* {:value %
:client-id (:db/id (:sales-summary/client (:snapshot multi-form-state))) })) :client-id (:db/id (:sales-summary/client (:snapshot multi-form-state))) }))
;; TODO (com/data-grid-new-row {:colspan 5
(com/data-grid-new-row {:colspan 5 :hx-get (bidi/path-for ssr-routes/only-routes ::route/new-summary-item)
:hx-get (bidi/path-for ssr-routes/only-routes ::route/new-summary-item) :row-offset 0
:row-offset 0 :index (count (fc/field-value))
:index (count (fc/field-value)) :tr-params {:hx-vals (hx/json {:client-id (:db/id (:sales-summary/client (:snapshot multi-form-state)))})}}
:tr-params {:hx-vals (hx/json {:client-id (:db/id (:sales-summary/client (:snapshot multi-form-state)))})}} ;; TODO "New Summary Item")))
"New Summary Item")))
(summary-total-row* request) (summary-total-row* request)
(unbalanced-row* request)) ]) (unbalanced-row* request)) ])
@@ -490,7 +463,7 @@
edit-schema) edit-schema)
(submit [this {:keys [multi-form-state request-method identity] :as request}] (submit [this {:keys [multi-form-state request-method identity] :as request}]
(let [result (:snapshot multi-form-state ) (let [result (:snapshot multi-form-state )
transaction [:upsert-entity {:db/id (:db/id result) transaction [:upsert-sales-summary {:db/id (:db/id result)
:sales-summary/items (map :sales-summary/items (map
(fn [i] (fn [i]
(if (:sales-summary-item/manual? i) (if (:sales-summary-item/manual? i)

View File

@@ -1,11 +1,13 @@
(ns auto-ap.ssr.components.date-range (ns auto-ap.ssr.components.date-range
(:require [auto-ap.ssr.components :as com] (:require [auto-ap.ssr.components :as com]
[auto-ap.ssr.components.buttons :as but]
[auto-ap.ssr.svg :as svg]
[auto-ap.time :as atime] [auto-ap.time :as atime]
[clj-time.coerce :as c] [clj-time.coerce :as c]
[clj-time.core :as t] [clj-time.core :as t]
[clj-time.periodic :as per])) [clj-time.periodic :as per]))
(defn date-range-field [{:keys [value id]}] (defn date-range-field [{:keys [value id apply-button?]}]
[:div {:id id} [:div {:id id}
(com/field {:label "Date Range"} (com/field {:label "Date Range"}
[:div.space-y-4 [:div.space-y-4
@@ -21,11 +23,17 @@
(atime/unparse-local atime/normal-date)) (atime/unparse-local atime/normal-date))
:placeholder "Date" :placeholder "Date"
:size :small :size :small
:class "shrink"}) :class "shrink date-filter-input"})
(com/date-input {:name "end-date" (com/date-input {:name "end-date"
:value (some-> (:end value) :value (some-> (:end value)
(atime/unparse-local atime/normal-date)) (atime/unparse-local atime/normal-date))
:placeholder "Date" :placeholder "Date"
:size :small :size :small
:class "shrink"})]])]) :class "shrink date-filter-input"})
(when apply-button?
(but/button- {:color :secondary
:size :small
:type "button"
"x-on:click" "$dispatch('datesApplied')"}
"Apply"))]])])

View File

@@ -37,7 +37,7 @@
[auto-ap.ssr.invoice.common :refer [default-read]] [auto-ap.ssr.invoice.common :refer [default-read]]
[auto-ap.ssr.invoice.import :as invoice-import] [auto-ap.ssr.invoice.import :as invoice-import]
[auto-ap.ssr.invoice.new-invoice-wizard :as new-invoice-wizard :refer [location-select*]] [auto-ap.ssr.invoice.new-invoice-wizard :as new-invoice-wizard :refer [location-select*]]
[auto-ap.ssr.pos.common :refer [date-range-field*]] [auto-ap.ssr.components.date-range :as dr]
[auto-ap.ssr.svg :as svg] [auto-ap.ssr.svg :as svg]
[auto-ap.ssr.utils [auto-ap.ssr.utils
:refer [apply-middleware-to-all-handlers assert-schema :refer [apply-middleware-to-all-handlers assert-schema
@@ -77,7 +77,7 @@
[:div {:id "exact-match-id-tag"}])) [:div {:id "exact-match-id-tag"}]))
(defn filters [request] (defn filters [request]
[:form#invoice-filters {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms" [:form#invoice-filters {"hx-trigger" "datesApplied, change delay:500ms from:.filter-trigger, keyup changed from:.hot-filter delay:1000ms"
"hx-get" (bidi/path-for ssr-routes/only-routes "hx-get" (bidi/path-for ssr-routes/only-routes
::route/table) ::route/table)
"hx-target" "#entity-table" "hx-target" "#entity-table"
@@ -92,7 +92,8 @@
:url (bidi/path-for ssr-routes/only-routes :vendor-search) :url (bidi/path-for ssr-routes/only-routes :vendor-search)
:value (:vendor (:query-params request)) :value (:vendor (:query-params request))
:value-fn :db/id :value-fn :db/id
:content-fn :vendor/name})) :content-fn :vendor/name
:class "filter-trigger"}))
(com/field {:label "Account"} (com/field {:label "Account"}
(com/typeahead {:name "account" (com/typeahead {:name "account"
:id "account" :id "account"
@@ -100,8 +101,12 @@
:value (:account (:query-params request)) :value (:account (:query-params request))
:value-fn :db/id :value-fn :db/id
:content-fn #(:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read (:db/id %)) :content-fn #(:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read (:db/id %))
(:db/id (:client request))))})) (:db/id (:client request))))
(date-range-field* request) :class "filter-trigger"}))
(dr/date-range-field {:value {:start (:start-date (:query-params request))
:end (:end-date (:query-params request))}
:id "date-range"
:apply-button? true})
(com/field {:label "Check #"} (com/field {:label "Check #"}
(com/text-input {:name "check-number" (com/text-input {:name "check-number"
:id "check-number" :id "check-number"
@@ -486,7 +491,10 @@
:fetch-page fetch-page :fetch-page fetch-page
:oob-render :oob-render
(fn [request] (fn [request]
[(assoc-in (date-range-field* request) [1 :hx-swap-oob] true) [(assoc-in (dr/date-range-field {:value {:start (:start-date (:query-params request))
:end (:end-date (:query-params request))}
:id "date-range"
:apply-button? true}) [1 :hx-swap-oob] true)
(assoc-in (exact-match-id* request) [1 :hx-swap-oob] true)]) (assoc-in (exact-match-id* request) [1 :hx-swap-oob] true)])
:query-schema query-schema :query-schema query-schema
:parse-query-params (fn [p] :parse-query-params (fn [p]

View File

@@ -81,7 +81,7 @@
data (into [] data (into []
(for [client-id client-ids (for [client-id client-ids
d date d date
[client-id account-id location debits credits balance count] (iol-ion.query/detailed-account-snapshot (dc/db conn) client-id (coerce/to-date (time/plus d (time/days 1)))) [client-id account-id location debits credits balance count] (iol-ion.query/detailed-account-snapshot (dc/db conn) client-id (coerce/to-date d))
:let [account ((or (lookup-account client-id) {}) account-id)]] :let [account ((or (lookup-account client-id) {}) account-id)]]
{:client-id client-id {:client-id client-id
:account-id account-id :account-id account-id

View File

@@ -51,3 +51,22 @@
(is (= "720.33" (:total (nth results 1)))) (is (= "720.33" (:total (nth results 1))))
(is (= "853.16" (:total (nth results 2)))) (is (= "853.16" (:total (nth results 2))))
(is (= "1066.60" (:total (nth results 3))))))) (is (= "1066.60" (:total (nth results 3)))))))
(deftest parse-bonanza-produce-invoice-03882095
(testing "Should parse Bonanza Produce invoice 03882095 with customer identifier including address"
(let [pdf-file (io/file "dev-resources/INVOICE - 03882095.pdf")
pdf-text (:out (clojure.java.shell/sh "pdftotext" "-layout" (str pdf-file) "-"))
results (sut/parse pdf-text)
result (first results)]
(is (some? results) "parse should return a result")
(is (some? result) "Template should match and return a result")
(when result
(is (= "Bonanza Produce" (:vendor-code result)))
(is (= "03882095" (:invoice-number result)))
(let [d (:date result)]
(is (= 2026 (time/year d)))
(is (= 1 (time/month d)))
(is (= 23 (time/day d))))
(is (= "NICK THE GREEK" (:customer-identifier result)))
(is (= "600 VISTA WAY" (str/trim (:account-number result))))
(is (= "946.24" (:total result)))))))