Sagar Patel commited on
Commit ·
66cb625
1
Parent(s): fb99e77
Add guided judge polish
Browse files- README.md +9 -1
- docs/demo-video-script.md +5 -3
- docs/field-notes.md +6 -5
- docs/submission-checklist.md +5 -1
- tests/test_settings.py +5 -0
- tests/test_ui_audio_flow.py +102 -0
- voiceledger/ledger/settings.py +5 -0
- voiceledger/ui/gradio_app.py +326 -9
- voiceledger/ui/theme.py +53 -0
README.md
CHANGED
|
@@ -20,6 +20,7 @@ The submission version is feature-frozen around the complete demo loop:
|
|
| 20 |
- Record a transaction with your microphone and transcribe it with faster-whisper.
|
| 21 |
- Configure seller context: business name, currency label, low-stock threshold, and language style.
|
| 22 |
- Pick common currency presets for INR, USD, EUR, GBP, MXN, and BRL, or enter a custom currency label.
|
|
|
|
| 23 |
- Use a first-run “Start in 60 seconds” guide for setup, first transaction, review, and closeout.
|
| 24 |
- Type or paste a transaction note.
|
| 25 |
- Bulk import multiple pasted notes for review and editing.
|
|
@@ -27,6 +28,7 @@ The submission version is feature-frozen around the complete demo loop:
|
|
| 27 |
- Parse common English, Hinglish, Hindi-lite, Gujarati-lite, Spanish, French, and Portuguese seller phrases with deterministic fallback rules.
|
| 28 |
- Review a human-friendly transaction card with warning badges before saving.
|
| 29 |
- See a language/confidence chip after parsing so multilingual notes are visibly handled.
|
|
|
|
| 30 |
- Correct transaction type, item, customer, quantity, price, amount, notes, and confidence directly before saving.
|
| 31 |
- Keep a correction log for review edits so model/rule mistakes become visible field-test evidence.
|
| 32 |
- Choose `Cloud AI first` or `Local fallback only` from the Record screen to make the AI route explicit during demos.
|
|
@@ -40,6 +42,8 @@ The submission version is feature-frozen around the complete demo loop:
|
|
| 40 |
- Generate WhatsApp-ready daily business summaries in English, Spanish, French, or Portuguese.
|
| 41 |
- Run shortcut commands like `close today`, `show Amit`, or `stock mangoes`.
|
| 42 |
- Generate customer follow-up reminders and an inventory reorder list.
|
|
|
|
|
|
|
| 43 |
- Capture field-test evidence with a seller checklist and anonymized feedback notes.
|
| 44 |
- Offload speech transcription and LLM parsing to optional Modal endpoints.
|
| 45 |
- Edit or delete saved transactions while keeping customer credit and inventory balances consistent.
|
|
@@ -122,7 +126,7 @@ The same examples are available in `sample_data/demo_transactions.txt`.
|
|
| 122 |
|
| 123 |
Capture these three moments for the Space README, demo video, or social post:
|
| 124 |
|
| 125 |
-
- `Record Text & Voice`: Today’s Work quick actions, review card, warning badges, and save receipt.
|
| 126 |
- `Dashboard`: Insight Coach, metrics, seller-day activity timeline, sales/expense timeline, top-selling item, low-stock table, and outstanding credit.
|
| 127 |
- `Field Test`: seller checklist, anonymized “who/tried/changed” notes, and saved evidence status.
|
| 128 |
- `Reports & PDF` plus `Ledger`: Daily Closeout, PDF/WhatsApp export, CSV download, and ledger correction.
|
|
@@ -198,14 +202,18 @@ curl -X POST https://sagarpat3199--voiceledger-api.modal.run/parse \
|
|
| 198 |
- The Insight Coach turns those dashboard signals into next actions, such as credit follow-up, restocking, and daily closeout.
|
| 199 |
- Customer and inventory detail views show transaction history for one customer or item.
|
| 200 |
- Smart review warnings flag low confidence, missing fields, duplicate risk, and negative stock before save.
|
|
|
|
| 201 |
- Inline review editing lets sellers correct the parsed transaction before the save touches the ledger.
|
| 202 |
- Seller setup persists business name, currency label, low-stock threshold, and language style in SQLite.
|
| 203 |
- Currency presets make the app easier to demo for INR, USD, EUR, GBP, MXN, BRL, and custom local labels.
|
| 204 |
- Field Test persists anonymized seller evidence and a workflow checklist in SQLite.
|
|
|
|
| 205 |
- The Field Test mistake log records corrected fields before save so product feedback is visible.
|
| 206 |
- Local fallback rules include common English, Hinglish, Hindi-lite, Gujarati-lite, Spanish, French, and Portuguese seller notes.
|
| 207 |
- Parse status includes a lightweight language/confidence chip for multilingual seller notes.
|
| 208 |
- Customer follow-up and inventory reorder helpers generate WhatsApp-ready action messages.
|
|
|
|
|
|
|
| 209 |
- WhatsApp summaries provide short copyable daily recaps in English, Spanish, French, or Portuguese.
|
| 210 |
- Voice command shortcuts provide quick access to daily closeout, customer detail, and inventory detail.
|
| 211 |
- Bulk import splits pasted notes by line, parses each line, supports review edits, and saves all reviewed transactions.
|
|
|
|
| 20 |
- Record a transaction with your microphone and transcribe it with faster-whisper.
|
| 21 |
- Configure seller context: business name, currency label, low-stock threshold, and language style.
|
| 22 |
- Pick common currency presets for INR, USD, EUR, GBP, MXN, and BRL, or enter a custom currency label.
|
| 23 |
+
- Follow a Guided Judge Mode path for health check, seeded data, sale capture, save, dashboard, and reports.
|
| 24 |
- Use a first-run “Start in 60 seconds” guide for setup, first transaction, review, and closeout.
|
| 25 |
- Type or paste a transaction note.
|
| 26 |
- Bulk import multiple pasted notes for review and editing.
|
|
|
|
| 28 |
- Parse common English, Hinglish, Hindi-lite, Gujarati-lite, Spanish, French, and Portuguese seller phrases with deterministic fallback rules.
|
| 29 |
- Review a human-friendly transaction card with warning badges before saving.
|
| 30 |
- See a language/confidence chip after parsing so multilingual notes are visibly handled.
|
| 31 |
+
- Use price memory to fill missing sale price/amount from the latest saved item sale.
|
| 32 |
- Correct transaction type, item, customer, quantity, price, amount, notes, and confidence directly before saving.
|
| 33 |
- Keep a correction log for review edits so model/rule mistakes become visible field-test evidence.
|
| 34 |
- Choose `Cloud AI first` or `Local fallback only` from the Record screen to make the AI route explicit during demos.
|
|
|
|
| 42 |
- Generate WhatsApp-ready daily business summaries in English, Spanish, French, or Portuguese.
|
| 43 |
- Run shortcut commands like `close today`, `show Amit`, or `stock mangoes`.
|
| 44 |
- Generate customer follow-up reminders and an inventory reorder list.
|
| 45 |
+
- Use a debt reminder queue sorted by highest customer balance.
|
| 46 |
+
- Use reorder intelligence that combines stock threshold with recent sold quantity.
|
| 47 |
- Capture field-test evidence with a seller checklist and anonymized feedback notes.
|
| 48 |
- Offload speech transcription and LLM parsing to optional Modal endpoints.
|
| 49 |
- Edit or delete saved transactions while keeping customer credit and inventory balances consistent.
|
|
|
|
| 126 |
|
| 127 |
Capture these three moments for the Space README, demo video, or social post:
|
| 128 |
|
| 129 |
+
- `Record Text & Voice`: Guided Judge Mode, Today’s Work quick actions, review card, warning badges, price memory, and save receipt.
|
| 130 |
- `Dashboard`: Insight Coach, metrics, seller-day activity timeline, sales/expense timeline, top-selling item, low-stock table, and outstanding credit.
|
| 131 |
- `Field Test`: seller checklist, anonymized “who/tried/changed” notes, and saved evidence status.
|
| 132 |
- `Reports & PDF` plus `Ledger`: Daily Closeout, PDF/WhatsApp export, CSV download, and ledger correction.
|
|
|
|
| 202 |
- The Insight Coach turns those dashboard signals into next actions, such as credit follow-up, restocking, and daily closeout.
|
| 203 |
- Customer and inventory detail views show transaction history for one customer or item.
|
| 204 |
- Smart review warnings flag low confidence, missing fields, duplicate risk, and negative stock before save.
|
| 205 |
+
- Price memory fills missing sale prices from the latest matching saved sale and marks the review with a `Price memory used` badge.
|
| 206 |
- Inline review editing lets sellers correct the parsed transaction before the save touches the ledger.
|
| 207 |
- Seller setup persists business name, currency label, low-stock threshold, and language style in SQLite.
|
| 208 |
- Currency presets make the app easier to demo for INR, USD, EUR, GBP, MXN, BRL, and custom local labels.
|
| 209 |
- Field Test persists anonymized seller evidence and a workflow checklist in SQLite.
|
| 210 |
+
- Field Test evidence includes pain point, before/after story, useful moments, and changed-after-feedback notes.
|
| 211 |
- The Field Test mistake log records corrected fields before save so product feedback is visible.
|
| 212 |
- Local fallback rules include common English, Hinglish, Hindi-lite, Gujarati-lite, Spanish, French, and Portuguese seller notes.
|
| 213 |
- Parse status includes a lightweight language/confidence chip for multilingual seller notes.
|
| 214 |
- Customer follow-up and inventory reorder helpers generate WhatsApp-ready action messages.
|
| 215 |
+
- Debt reminder queue sorts customers with outstanding balances by highest amount due.
|
| 216 |
+
- Inventory reorder intelligence adds recent sold quantity, low-stock/selling-fast status, and suggested actions.
|
| 217 |
- WhatsApp summaries provide short copyable daily recaps in English, Spanish, French, or Portuguese.
|
| 218 |
- Voice command shortcuts provide quick access to daily closeout, customer detail, and inventory detail.
|
| 219 |
- Bulk import splits pasted notes by line, parses each line, supports review edits, and saves all reviewed transactions.
|
docs/demo-video-script.md
CHANGED
|
@@ -8,7 +8,7 @@ VoiceLedger is a voice-first bookkeeping app for informal sellers and home busin
|
|
| 8 |
|
| 9 |
## 0:10-0:25 — Judge Flow and Health
|
| 10 |
|
| 11 |
-
On the first screen, show the `Judge Demo Flow` panel:
|
| 12 |
|
| 13 |
```text
|
| 14 |
1. Seed demo data -> 2. Record/type -> 3. Save -> 4. View dashboard/reports
|
|
@@ -47,6 +47,7 @@ Click parse. Show:
|
|
| 47 |
|
| 48 |
- Transcript if using voice
|
| 49 |
- Human-friendly transaction review card
|
|
|
|
| 50 |
- Inline review fields for correcting item, quantity, amount, customer, and notes before saving
|
| 51 |
- Warning badges for low confidence, missing fields, duplicate risk, or negative stock when present
|
| 52 |
- Parse source/status
|
|
@@ -62,7 +63,8 @@ Use the `Sections` navigation to open:
|
|
| 62 |
- `Dashboard` to show sales/profit/credit and the sales/expense timeline
|
| 63 |
- `Field Test` to show the seller checklist and anonymized “who/tried/changed” notes
|
| 64 |
- `Customer Credit` to show dues, a selected customer detail view, and a follow-up message
|
| 65 |
-
- `
|
|
|
|
| 66 |
- `Ledger` to show saved transactions
|
| 67 |
|
| 68 |
Say: “The model handles messy input, but the app owns the accounting state.”
|
|
@@ -77,7 +79,7 @@ Open `Reports & PDF`, run `Daily Closeout`, then generate the PDF, translated Wh
|
|
| 77 |
|
| 78 |
Suggested screenshot/GIF moments:
|
| 79 |
|
| 80 |
-
- Record flow: first-run guide, multilingual examples, review card, inline correction, parse source, warning badges, save receipt.
|
| 81 |
- Seller setup: currency/threshold/language style and Command Center.
|
| 82 |
- Field Test: seller checklist, feedback notes, and correction log.
|
| 83 |
- Submission Story: AI pipeline and small-model fit card.
|
|
|
|
| 8 |
|
| 9 |
## 0:10-0:25 — Judge Flow and Health
|
| 10 |
|
| 11 |
+
On the first screen, show `Guided Judge Mode` and the `Judge Demo Flow` panel:
|
| 12 |
|
| 13 |
```text
|
| 14 |
1. Seed demo data -> 2. Record/type -> 3. Save -> 4. View dashboard/reports
|
|
|
|
| 47 |
|
| 48 |
- Transcript if using voice
|
| 49 |
- Human-friendly transaction review card
|
| 50 |
+
- Price memory badge if a known item is missing price
|
| 51 |
- Inline review fields for correcting item, quantity, amount, customer, and notes before saving
|
| 52 |
- Warning badges for low confidence, missing fields, duplicate risk, or negative stock when present
|
| 53 |
- Parse source/status
|
|
|
|
| 63 |
- `Dashboard` to show sales/profit/credit and the sales/expense timeline
|
| 64 |
- `Field Test` to show the seller checklist and anonymized “who/tried/changed” notes
|
| 65 |
- `Customer Credit` to show dues, a selected customer detail view, and a follow-up message
|
| 66 |
+
- `Customer Credit` to show the debt reminder queue sorted by highest balance
|
| 67 |
+
- `Inventory` to show stock, a selected item detail view, and reorder intelligence
|
| 68 |
- `Ledger` to show saved transactions
|
| 69 |
|
| 70 |
Say: “The model handles messy input, but the app owns the accounting state.”
|
|
|
|
| 79 |
|
| 80 |
Suggested screenshot/GIF moments:
|
| 81 |
|
| 82 |
+
- Record flow: Guided Judge Mode, first-run guide, multilingual examples, review card, price memory, inline correction, parse source, warning badges, save receipt.
|
| 83 |
- Seller setup: currency/threshold/language style and Command Center.
|
| 84 |
- Field Test: seller checklist, feedback notes, and correction log.
|
| 85 |
- Submission Story: AI pipeline and small-model fit card.
|
docs/field-notes.md
CHANGED
|
@@ -34,7 +34,7 @@ This is a good small-model problem because the output space is constrained: a tr
|
|
| 34 |
1. Seed demo transactions from the first-screen Judge Demo Flow.
|
| 35 |
2. Record or type: `Sold 12 mangoes, 20 each`.
|
| 36 |
3. Show the parse source, review card, confidence, and warning badges.
|
| 37 |
-
4.
|
| 38 |
5. Save the transaction and show the receipt with bookkeeping side effects.
|
| 39 |
6. Show Dashboard totals, the Insight Coach, sales/expense timeline, Ledger row, Customer Credit detail, and Inventory detail.
|
| 40 |
7. Open Field Test to show the seller checklist and anonymized feedback notes.
|
|
@@ -52,9 +52,10 @@ The hackathon version is intentionally frozen around the complete bookkeeping lo
|
|
| 52 |
- Seller setup for business name, currency, low-stock threshold, and language style.
|
| 53 |
- Currency presets and translated WhatsApp summaries for sellers who demo in INR, USD, EUR, GBP, MXN, BRL, English, Spanish, French, or Portuguese contexts.
|
| 54 |
- Hinglish/Hindi-lite/Gujarati-lite/Spanish/French/Portuguese fallback examples for common seller notes.
|
| 55 |
-
-
|
| 56 |
-
- Command Center, dashboard timeline, seller-day timeline, customer follow-up, inventory reorder
|
| 57 |
- Field Test Mode for the seller checklist and anonymized “who/tried/changed” feedback evidence.
|
|
|
|
| 58 |
- Demo Health for backend observability.
|
| 59 |
- Submission Story for the AI pipeline and small-model fit rationale.
|
| 60 |
|
|
@@ -62,13 +63,13 @@ This freeze keeps the demo reliable and makes the value proposition easier to ju
|
|
| 62 |
|
| 63 |
## What We Learned
|
| 64 |
|
| 65 |
-
The strongest product behavior is not the model call itself. It is the reliable loop around it: voice input, readable review, warning badges, save receipt, correction, and export. Informal sellers need speed, but they also need a way to spot mistakes and fix them. The correction log makes those mistakes measurable during field tests, while edit/delete, detail drilldowns, Daily Closeout, and CSV export make the app credible as a bookkeeping tool instead of a parser demo.
|
| 66 |
|
| 67 |
The small-model constraint helped keep the app honest. The model handles the fuzzy human input; the code owns the accounting state.
|
| 68 |
|
| 69 |
## Screenshot Moments
|
| 70 |
|
| 71 |
-
- Record flow: first-run onboarding, multilingual examples, language/confidence chip, voice command shortcuts, review card, inline corrections, warning badges, source/status, save receipt.
|
| 72 |
- Dashboard: daily totals, Insight Coach, outstanding credit, sales/expense timeline, top seller, low-stock inventory.
|
| 73 |
- Field Test: workflow checklist, correction log, and anonymized feedback evidence.
|
| 74 |
- Reports and exports: Daily Closeout, PDF download, WhatsApp summary, CSV ledger export.
|
|
|
|
| 34 |
1. Seed demo transactions from the first-screen Judge Demo Flow.
|
| 35 |
2. Record or type: `Sold 12 mangoes, 20 each`.
|
| 36 |
3. Show the parse source, review card, confidence, and warning badges.
|
| 37 |
+
4. Show price memory when a known item is missing price, then correct one inline review field before saving to show that sellers can fix model or rule-parser mistakes immediately.
|
| 38 |
5. Save the transaction and show the receipt with bookkeeping side effects.
|
| 39 |
6. Show Dashboard totals, the Insight Coach, sales/expense timeline, Ledger row, Customer Credit detail, and Inventory detail.
|
| 40 |
7. Open Field Test to show the seller checklist and anonymized feedback notes.
|
|
|
|
| 52 |
- Seller setup for business name, currency, low-stock threshold, and language style.
|
| 53 |
- Currency presets and translated WhatsApp summaries for sellers who demo in INR, USD, EUR, GBP, MXN, BRL, English, Spanish, French, or Portuguese contexts.
|
| 54 |
- Hinglish/Hindi-lite/Gujarati-lite/Spanish/French/Portuguese fallback examples for common seller notes.
|
| 55 |
+
- Guided Judge Mode, first-run onboarding, multilingual examples, language/confidence chips, price memory, human-friendly review cards, inline review correction, mistake logging, warning badges, receipts, and save feedback.
|
| 56 |
+
- Command Center, dashboard timeline, seller-day timeline, debt reminder queue, customer follow-up, inventory reorder intelligence, ledger correction, Daily Closeout, PDF, CSV, and WhatsApp exports.
|
| 57 |
- Field Test Mode for the seller checklist and anonymized “who/tried/changed” feedback evidence.
|
| 58 |
+
- Richer Field Test Evidence for pain point, before/after workflow, useful moments, and changed-after-feedback notes.
|
| 59 |
- Demo Health for backend observability.
|
| 60 |
- Submission Story for the AI pipeline and small-model fit rationale.
|
| 61 |
|
|
|
|
| 63 |
|
| 64 |
## What We Learned
|
| 65 |
|
| 66 |
+
The strongest product behavior is not the model call itself. It is the reliable loop around it: voice input, readable review, price memory, warning badges, save receipt, correction, reminders, restocking, and export. Informal sellers need speed, but they also need a way to spot mistakes and fix them. The correction log makes those mistakes measurable during field tests, while edit/delete, detail drilldowns, Daily Closeout, and CSV export make the app credible as a bookkeeping tool instead of a parser demo.
|
| 67 |
|
| 68 |
The small-model constraint helped keep the app honest. The model handles the fuzzy human input; the code owns the accounting state.
|
| 69 |
|
| 70 |
## Screenshot Moments
|
| 71 |
|
| 72 |
+
- Record flow: Guided Judge Mode, first-run onboarding, multilingual examples, language/confidence chip, voice command shortcuts, price memory, review card, inline corrections, warning badges, source/status, save receipt.
|
| 73 |
- Dashboard: daily totals, Insight Coach, outstanding credit, sales/expense timeline, top seller, low-stock inventory.
|
| 74 |
- Field Test: workflow checklist, correction log, and anonymized feedback evidence.
|
| 75 |
- Reports and exports: Daily Closeout, PDF download, WhatsApp summary, CSV ledger export.
|
docs/submission-checklist.md
CHANGED
|
@@ -12,6 +12,7 @@
|
|
| 12 |
|
| 13 |
- `Sections` navigation opens the correct page and no old Gradio tab row is visible.
|
| 14 |
- First screen shows `1. Seed demo data -> 2. Record/type -> 3. Save -> 4. View dashboard/reports`.
|
|
|
|
| 15 |
- `Submission Story` shows the AI pipeline strip.
|
| 16 |
- `Submission Story` shows the “Why small models fit” card.
|
| 17 |
- `Demo Health` shows Modal reachable and backend version.
|
|
@@ -22,6 +23,7 @@
|
|
| 22 |
- Local fallback parses `Amit ne 100 dene hai`, `Amit ne 50 diya`, and `50 mango kharida`.
|
| 23 |
- Voice parse works and transcript appears.
|
| 24 |
- Review card shows type, item, quantity, price, amount, customer, source, and confidence.
|
|
|
|
| 25 |
- Parse status shows source plus language/confidence.
|
| 26 |
- Smart warnings appear for low confidence, missing fields, duplicate risk, and negative stock.
|
| 27 |
- Save shows a receipt with transaction type, amount, and side effects.
|
|
@@ -33,15 +35,17 @@
|
|
| 33 |
- Customer Credit section reflects `Amit owes 100` and `Amit paid 40`.
|
| 34 |
- Customer detail lookup shows customer transaction history and status.
|
| 35 |
- Customer follow-up generates a WhatsApp reminder.
|
|
|
|
| 36 |
- Inventory reflects purchases and sales.
|
| 37 |
- Inventory detail lookup shows bought, sold, current stock, and low-stock status.
|
| 38 |
- Inventory reorder list uses the configured threshold.
|
|
|
|
| 39 |
- Ledger edit/delete works and derived balances rebuild.
|
| 40 |
- CSV export downloads from Ledger.
|
| 41 |
- Daily Closeout generates PDF, CSV, WhatsApp summary, and a status line.
|
| 42 |
- PDF report downloads from Reports.
|
| 43 |
- WhatsApp summary generates copyable text in English, Spanish, French, and Portuguese.
|
| 44 |
-
- Field Test
|
| 45 |
|
| 46 |
## Prize Alignment
|
| 47 |
|
|
|
|
| 12 |
|
| 13 |
- `Sections` navigation opens the correct page and no old Gradio tab row is visible.
|
| 14 |
- First screen shows `1. Seed demo data -> 2. Record/type -> 3. Save -> 4. View dashboard/reports`.
|
| 15 |
+
- First screen shows Guided Judge Mode progress states.
|
| 16 |
- `Submission Story` shows the AI pipeline strip.
|
| 17 |
- `Submission Story` shows the “Why small models fit” card.
|
| 18 |
- `Demo Health` shows Modal reachable and backend version.
|
|
|
|
| 23 |
- Local fallback parses `Amit ne 100 dene hai`, `Amit ne 50 diya`, and `50 mango kharida`.
|
| 24 |
- Voice parse works and transcript appears.
|
| 25 |
- Review card shows type, item, quantity, price, amount, customer, source, and confidence.
|
| 26 |
+
- Price memory fills a missing sale price for a previously sold item and shows `Price memory used`.
|
| 27 |
- Parse status shows source plus language/confidence.
|
| 28 |
- Smart warnings appear for low confidence, missing fields, duplicate risk, and negative stock.
|
| 29 |
- Save shows a receipt with transaction type, amount, and side effects.
|
|
|
|
| 35 |
- Customer Credit section reflects `Amit owes 100` and `Amit paid 40`.
|
| 36 |
- Customer detail lookup shows customer transaction history and status.
|
| 37 |
- Customer follow-up generates a WhatsApp reminder.
|
| 38 |
+
- Debt reminder queue shows customers with outstanding balances sorted highest first.
|
| 39 |
- Inventory reflects purchases and sales.
|
| 40 |
- Inventory detail lookup shows bought, sold, current stock, and low-stock status.
|
| 41 |
- Inventory reorder list uses the configured threshold.
|
| 42 |
+
- Inventory reorder intelligence shows recent sold quantity, status, and suggested action.
|
| 43 |
- Ledger edit/delete works and derived balances rebuild.
|
| 44 |
- CSV export downloads from Ledger.
|
| 45 |
- Daily Closeout generates PDF, CSV, WhatsApp summary, and a status line.
|
| 46 |
- PDF report downloads from Reports.
|
| 47 |
- WhatsApp summary generates copyable text in English, Spanish, French, and Portuguese.
|
| 48 |
+
- Field Test evidence saves pain point, before/after story, useful moments, changed-after-feedback notes, and correction log.
|
| 49 |
|
| 50 |
## Prize Alignment
|
| 51 |
|
tests/test_settings.py
CHANGED
|
@@ -10,6 +10,11 @@ def test_business_settings_defaults_load_when_empty(tmp_path: Path) -> None:
|
|
| 10 |
|
| 11 |
assert settings["business_name"] == "VoiceLedger Seller"
|
| 12 |
assert settings["currency_symbol"] == "₹"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
assert get_low_stock_threshold(db_path) == 5
|
| 14 |
|
| 15 |
|
|
|
|
| 10 |
|
| 11 |
assert settings["business_name"] == "VoiceLedger Seller"
|
| 12 |
assert settings["currency_symbol"] == "₹"
|
| 13 |
+
assert settings["field_pain_point"]
|
| 14 |
+
assert settings["field_before"]
|
| 15 |
+
assert settings["field_after"]
|
| 16 |
+
assert settings["field_useful_moments"]
|
| 17 |
+
assert settings["field_changed_after_feedback"]
|
| 18 |
assert get_low_stock_threshold(db_path) == 5
|
| 19 |
|
| 20 |
|
tests/test_ui_audio_flow.py
CHANGED
|
@@ -204,6 +204,40 @@ def test_parse_note_for_editing_populates_review_fields(monkeypatch) -> None:
|
|
| 204 |
assert result[8] == 240
|
| 205 |
|
| 206 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
def test_apply_review_edits_updates_payload_and_review_card(tmp_path) -> None:
|
| 208 |
db_path = tmp_path / "voiceledger.sqlite3"
|
| 209 |
parsed = gradio_app.local_parse_transaction("Sold 12 mangoes, 20 each").model_dump()
|
|
@@ -287,6 +321,37 @@ def test_customer_followup_and_reorder_helpers(tmp_path) -> None:
|
|
| 287 |
assert "Onions" in message
|
| 288 |
|
| 289 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 290 |
def test_currency_presets_and_voice_commands(tmp_path) -> None:
|
| 291 |
db_path = tmp_path / "voiceledger.sqlite3"
|
| 292 |
gradio_app.add_transaction(gradio_app.local_parse_transaction("Amit owes 100"), db_path)
|
|
@@ -326,3 +391,40 @@ def test_field_test_evidence_persists_checklist_and_notes(tmp_path) -> None:
|
|
| 326 |
assert "2 checklist item" in status
|
| 327 |
assert settings["field_test_who"] == "Local snack seller"
|
| 328 |
assert gradio_app._field_test_checklist_values(settings) == ["Record sale", "Export report"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
assert result[8] == 240
|
| 205 |
|
| 206 |
|
| 207 |
+
def test_price_memory_fills_missing_sale_price(tmp_path, monkeypatch) -> None:
|
| 208 |
+
db_path = tmp_path / "voiceledger.sqlite3"
|
| 209 |
+
gradio_app.add_transaction(gradio_app.local_parse_transaction("Sold 12 mangoes, 20 each"), db_path)
|
| 210 |
+
monkeypatch.setattr(
|
| 211 |
+
gradio_app.modal_api,
|
| 212 |
+
"parse_transaction_result",
|
| 213 |
+
lambda text, fallback, **kwargs: ParseResult(
|
| 214 |
+
transaction=fallback(text),
|
| 215 |
+
source="local",
|
| 216 |
+
message="Parsed locally with the rule parser.",
|
| 217 |
+
),
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
result = gradio_app._parse_note_for_editing("Sold 5 mangoes", db_path)
|
| 221 |
+
|
| 222 |
+
assert result[0]["unit_price"] == 20
|
| 223 |
+
assert result[0]["amount"] == 100
|
| 224 |
+
assert result[7] == 20
|
| 225 |
+
assert result[8] == 100
|
| 226 |
+
assert "Price memory used" in result[3]
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
def test_price_memory_does_not_overwrite_explicit_price(tmp_path) -> None:
|
| 230 |
+
db_path = tmp_path / "voiceledger.sqlite3"
|
| 231 |
+
gradio_app.add_transaction(gradio_app.local_parse_transaction("Sold 12 mangoes, 20 each"), db_path)
|
| 232 |
+
transaction = gradio_app.local_parse_transaction("Sold 5 mangoes, 30 each")
|
| 233 |
+
|
| 234 |
+
updated, used = gradio_app.suggest_price_from_history(transaction, db_path)
|
| 235 |
+
|
| 236 |
+
assert used is False
|
| 237 |
+
assert updated.unit_price == 30
|
| 238 |
+
assert updated.amount == 150
|
| 239 |
+
|
| 240 |
+
|
| 241 |
def test_apply_review_edits_updates_payload_and_review_card(tmp_path) -> None:
|
| 242 |
db_path = tmp_path / "voiceledger.sqlite3"
|
| 243 |
parsed = gradio_app.local_parse_transaction("Sold 12 mangoes, 20 each").model_dump()
|
|
|
|
| 321 |
assert "Onions" in message
|
| 322 |
|
| 323 |
|
| 324 |
+
def test_debt_reminder_queue_sorts_positive_balances(tmp_path) -> None:
|
| 325 |
+
db_path = tmp_path / "voiceledger.sqlite3"
|
| 326 |
+
gradio_app.add_transaction(gradio_app.local_parse_transaction("Amit owes 100"), db_path)
|
| 327 |
+
gradio_app.add_transaction(gradio_app.local_parse_transaction("Ramesh owes 250"), db_path)
|
| 328 |
+
gradio_app.add_transaction(gradio_app.local_parse_transaction("Amit paid 40"), db_path)
|
| 329 |
+
|
| 330 |
+
queue = gradio_app.get_debt_reminder_queue(db_path)
|
| 331 |
+
reminder = gradio_app.generate_debt_reminder("Ramesh", db_path)
|
| 332 |
+
|
| 333 |
+
assert list(queue["customer"]) == ["Ramesh", "Amit"]
|
| 334 |
+
assert "₹250" in queue.iloc[0]["suggested_message"]
|
| 335 |
+
assert "Ramesh" in reminder
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
def test_reorder_intelligence_flags_low_and_fast_stock(tmp_path) -> None:
|
| 339 |
+
db_path = tmp_path / "voiceledger.sqlite3"
|
| 340 |
+
gradio_app.update_business_settings(low_stock_threshold=5, db_path=db_path)
|
| 341 |
+
gradio_app.add_transaction(gradio_app.local_parse_transaction("Bought 3 onions"), db_path)
|
| 342 |
+
gradio_app.add_transaction(gradio_app.local_parse_transaction("Bought 20 mangoes"), db_path)
|
| 343 |
+
gradio_app.add_transaction(gradio_app.local_parse_transaction("Sold 12 mangoes, 20 each"), db_path)
|
| 344 |
+
|
| 345 |
+
recommendations = gradio_app.generate_reorder_intelligence(db_path)
|
| 346 |
+
table, message = gradio_app._generate_reorder_intelligence_for_ui(db_path)
|
| 347 |
+
|
| 348 |
+
statuses = dict(zip(recommendations["item"], recommendations["status"], strict=True))
|
| 349 |
+
assert statuses["onions"] == "Low stock"
|
| 350 |
+
assert statuses["mangoes"] == "Selling fast"
|
| 351 |
+
assert "recent_sold" in table.columns
|
| 352 |
+
assert "Mangoes" in message
|
| 353 |
+
|
| 354 |
+
|
| 355 |
def test_currency_presets_and_voice_commands(tmp_path) -> None:
|
| 356 |
db_path = tmp_path / "voiceledger.sqlite3"
|
| 357 |
gradio_app.add_transaction(gradio_app.local_parse_transaction("Amit owes 100"), db_path)
|
|
|
|
| 391 |
assert "2 checklist item" in status
|
| 392 |
assert settings["field_test_who"] == "Local snack seller"
|
| 393 |
assert gradio_app._field_test_checklist_values(settings) == ["Record sale", "Export report"]
|
| 394 |
+
|
| 395 |
+
|
| 396 |
+
def test_field_test_evidence_persists_real_user_details(tmp_path) -> None:
|
| 397 |
+
db_path = tmp_path / "voiceledger.sqlite3"
|
| 398 |
+
|
| 399 |
+
status = gradio_app._save_field_test_evidence(
|
| 400 |
+
["Record sale"],
|
| 401 |
+
"Local seller",
|
| 402 |
+
"Voice note",
|
| 403 |
+
"Old change note",
|
| 404 |
+
db_path,
|
| 405 |
+
pain_point="Paper notes get lost",
|
| 406 |
+
before="Used memory",
|
| 407 |
+
after="Checks dashboard",
|
| 408 |
+
useful="Debt reminders",
|
| 409 |
+
changed_after_feedback="Added price memory",
|
| 410 |
+
)
|
| 411 |
+
settings = gradio_app.get_business_settings(db_path)
|
| 412 |
+
|
| 413 |
+
assert "Paper notes get lost" in status
|
| 414 |
+
assert settings["field_pain_point"] == "Paper notes get lost"
|
| 415 |
+
assert settings["field_before"] == "Used memory"
|
| 416 |
+
assert settings["field_after"] == "Checks dashboard"
|
| 417 |
+
assert settings["field_useful_moments"] == "Debt reminders"
|
| 418 |
+
assert settings["field_changed_after_feedback"] == "Added price memory"
|
| 419 |
+
|
| 420 |
+
|
| 421 |
+
def test_guided_demo_status_reflects_progress(tmp_path) -> None:
|
| 422 |
+
db_path = tmp_path / "voiceledger.sqlite3"
|
| 423 |
+
|
| 424 |
+
empty_status = gradio_app.get_guided_demo_status(db_path)
|
| 425 |
+
gradio_app.add_transaction(gradio_app.local_parse_transaction("Sold 12 mangoes, 20 each"), db_path)
|
| 426 |
+
progressed_status = gradio_app.get_guided_demo_status(db_path)
|
| 427 |
+
|
| 428 |
+
assert "Guided Judge Mode" in empty_status
|
| 429 |
+
assert "Seed Demo Data" in progressed_status
|
| 430 |
+
assert "Done" in progressed_status
|
voiceledger/ledger/settings.py
CHANGED
|
@@ -24,6 +24,11 @@ DEFAULT_BUSINESS_SETTINGS = {
|
|
| 24 |
"field_test_tried": "Voice notes, typed notes, customer credit, inventory updates, dashboard review, and exports.",
|
| 25 |
"field_test_changed": "Added review warnings, save receipts, edit/delete, CSV export, and clearer demo health.",
|
| 26 |
"field_test_checklist": "Record sale,Record expense,Customer owes,Customer paid,Bought stock,Review dashboard,Export report",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
}
|
| 28 |
|
| 29 |
|
|
|
|
| 24 |
"field_test_tried": "Voice notes, typed notes, customer credit, inventory updates, dashboard review, and exports.",
|
| 25 |
"field_test_changed": "Added review warnings, save receipts, edit/delete, CSV export, and clearer demo health.",
|
| 26 |
"field_test_checklist": "Record sale,Record expense,Customer owes,Customer paid,Bought stock,Review dashboard,Export report",
|
| 27 |
+
"field_pain_point": "Sales, customer dues, and stock are written in scattered notes during a busy selling day.",
|
| 28 |
+
"field_before": "The seller checked memory, WhatsApp messages, and paper notes before knowing profit or dues.",
|
| 29 |
+
"field_after": "VoiceLedger turns short notes into sales, credit, inventory, and closeout reports in one place.",
|
| 30 |
+
"field_useful_moments": "Voice sale capture, customer balance follow-up, low-stock alerts, and WhatsApp summary.",
|
| 31 |
+
"field_changed_after_feedback": "Made review cards clearer, added correction logging, and made exports easier to find.",
|
| 32 |
}
|
| 33 |
|
| 34 |
|
voiceledger/ui/gradio_app.py
CHANGED
|
@@ -79,6 +79,7 @@ CURRENCY_PRESETS = {
|
|
| 79 |
"Brazil - BRL (R$)": "R$",
|
| 80 |
"Custom": "",
|
| 81 |
}
|
|
|
|
| 82 |
DEMO_NOTES = [
|
| 83 |
"Bought 60 mangoes",
|
| 84 |
"Sold 12 mangoes, 20 each",
|
|
@@ -131,6 +132,8 @@ def create_app(db_path: str | Path | None = None) -> gr.Blocks:
|
|
| 131 |
)
|
| 132 |
)
|
| 133 |
gr.HTML(_first_run_onboarding_panel())
|
|
|
|
|
|
|
| 134 |
command_center_output = gr.HTML(_command_center(db_path))
|
| 135 |
gr.HTML(_section_heading("Seller Setup"))
|
| 136 |
with gr.Row():
|
|
@@ -199,6 +202,11 @@ def create_app(db_path: str | Path | None = None) -> gr.Blocks:
|
|
| 199 |
wrap=True,
|
| 200 |
elem_classes="vl-panel",
|
| 201 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
gr.HTML(
|
| 203 |
_info_panel(
|
| 204 |
"Workflow",
|
|
@@ -490,6 +498,32 @@ def create_app(db_path: str | Path | None = None) -> gr.Blocks:
|
|
| 490 |
lines=3,
|
| 491 |
elem_classes="vl-panel",
|
| 492 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 493 |
save_field_notes_button = gr.Button("Save Field Test Notes")
|
| 494 |
field_notes_status_output = gr.Markdown(elem_classes="vl-status")
|
| 495 |
seed_demo_button = gr.Button("Seed Demo Transactions", variant="primary")
|
|
@@ -536,6 +570,38 @@ def create_app(db_path: str | Path | None = None) -> gr.Blocks:
|
|
| 536 |
lines=4,
|
| 537 |
elem_classes="vl-panel",
|
| 538 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 539 |
save_field_test_button = gr.Button("Save Field Test Evidence", variant="primary")
|
| 540 |
field_test_status_output = gr.Markdown(
|
| 541 |
value=_field_test_summary(initial_settings),
|
|
@@ -650,6 +716,21 @@ def create_app(db_path: str | Path | None = None) -> gr.Blocks:
|
|
| 650 |
inputs=followup_customer_name,
|
| 651 |
outputs=followup_output,
|
| 652 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 653 |
|
| 654 |
with gr.Column(visible=False, elem_classes="vl-page-section") as inventory_page:
|
| 655 |
gr.HTML('<div id="vl-page-inventory" class="vl-page-anchor"></div>')
|
|
@@ -697,8 +778,8 @@ def create_app(db_path: str | Path | None = None) -> gr.Blocks:
|
|
| 697 |
gr.HTML(_section_heading("Inventory Reorder List"))
|
| 698 |
reorder_button = gr.Button("Generate Reorder List")
|
| 699 |
reorder_output = gr.Dataframe(
|
| 700 |
-
headers=["item", "current_stock", "suggested_action"],
|
| 701 |
-
label="
|
| 702 |
interactive=False,
|
| 703 |
wrap=True,
|
| 704 |
elem_classes="vl-panel",
|
|
@@ -710,7 +791,7 @@ def create_app(db_path: str | Path | None = None) -> gr.Blocks:
|
|
| 710 |
elem_classes="vl-panel",
|
| 711 |
)
|
| 712 |
reorder_button.click(
|
| 713 |
-
fn=lambda:
|
| 714 |
inputs=None,
|
| 715 |
outputs=[reorder_output, reorder_message_output],
|
| 716 |
)
|
|
@@ -1049,13 +1130,51 @@ def create_app(db_path: str | Path | None = None) -> gr.Blocks:
|
|
| 1049 |
],
|
| 1050 |
)
|
| 1051 |
save_field_notes_button.click(
|
| 1052 |
-
fn=lambda who, tried, changed
|
| 1053 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1054 |
outputs=field_notes_status_output,
|
| 1055 |
)
|
| 1056 |
save_field_test_button.click(
|
| 1057 |
-
fn=lambda checklist, who, tried, changed
|
| 1058 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1059 |
outputs=field_test_status_output,
|
| 1060 |
)
|
| 1061 |
record_health_button.click(
|
|
@@ -1365,8 +1484,11 @@ def _parse_note(
|
|
| 1365 |
force_local=_force_local_mode(ai_mode),
|
| 1366 |
)
|
| 1367 |
transaction = result.transaction
|
|
|
|
| 1368 |
payload = transaction.model_dump()
|
| 1369 |
warnings = _review_warnings(transaction, db_path)
|
|
|
|
|
|
|
| 1370 |
status = _status_message(transaction, result.message, result.fallback_reason, warnings=warnings)
|
| 1371 |
return payload, payload, status, _review_card(transaction, result.message, warnings)
|
| 1372 |
|
|
@@ -1404,8 +1526,11 @@ def _transcribe_and_parse_audio(
|
|
| 1404 |
force_local=force_local,
|
| 1405 |
)
|
| 1406 |
transaction = parse_result.transaction
|
|
|
|
| 1407 |
payload = transaction.model_dump()
|
| 1408 |
warnings = _review_warnings(transaction, db_path)
|
|
|
|
|
|
|
| 1409 |
status = _status_message(
|
| 1410 |
transaction,
|
| 1411 |
parse_result.message,
|
|
@@ -1702,13 +1827,27 @@ def _save_seller_setup_and_refresh(
|
|
| 1702 |
)
|
| 1703 |
|
| 1704 |
|
| 1705 |
-
def _save_field_notes(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1706 |
"""Persist anonymized field-test notes."""
|
| 1707 |
update_business_settings(
|
| 1708 |
db_path=db_path,
|
| 1709 |
field_test_who=who,
|
| 1710 |
field_test_tried=tried,
|
| 1711 |
field_test_changed=changed,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1712 |
)
|
| 1713 |
return "Saved anonymized field-test notes."
|
| 1714 |
|
|
@@ -1719,6 +1858,12 @@ def _save_field_test_evidence(
|
|
| 1719 |
tried: str,
|
| 1720 |
changed: str,
|
| 1721 |
db_path: str | Path | None,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1722 |
) -> str:
|
| 1723 |
"""Persist field-test checklist and notes."""
|
| 1724 |
checklist_value = ",".join(checklist or [])
|
|
@@ -1728,6 +1873,11 @@ def _save_field_test_evidence(
|
|
| 1728 |
field_test_tried=tried,
|
| 1729 |
field_test_changed=changed,
|
| 1730 |
field_test_checklist=checklist_value,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1731 |
)
|
| 1732 |
return _field_test_summary(settings)
|
| 1733 |
|
|
@@ -1743,7 +1893,8 @@ def _field_test_summary(settings: dict[str, str]) -> str:
|
|
| 1743 |
completed = _field_test_checklist_values(settings)
|
| 1744 |
return (
|
| 1745 |
f"Field test evidence saved: {len(completed)} checklist item(s). "
|
| 1746 |
-
f"Who: {settings['field_test_who']}
|
|
|
|
| 1747 |
)
|
| 1748 |
|
| 1749 |
|
|
@@ -1869,6 +2020,147 @@ def _seller_setup_status(settings: dict[str, str]) -> str:
|
|
| 1869 |
)
|
| 1870 |
|
| 1871 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1872 |
def _last_transaction_label(db_path: str | Path | None) -> str:
|
| 1873 |
"""Return a concise label for the newest transaction."""
|
| 1874 |
ledger = get_transactions(db_path)
|
|
@@ -2048,6 +2340,29 @@ def _run_voice_command(command: str | None, db_path: str | Path | None) -> str:
|
|
| 2048 |
return _empty_detail_card("Command result", "Command not recognized. Try close today, show Amit, stock mangoes, or a transaction note.")
|
| 2049 |
|
| 2050 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2051 |
def _generate_daily_summary_report(db_path: str | Path | None) -> tuple[str | None, str]:
|
| 2052 |
"""Generate the Daily Summary PDF for download in Gradio."""
|
| 2053 |
settings = get_business_settings(db_path)
|
|
@@ -2353,6 +2668,8 @@ def _review_readiness(transaction: Transaction, warnings: list[str]) -> str:
|
|
| 2353 |
|
| 2354 |
def _warning_badge_class(warning: str) -> str:
|
| 2355 |
"""Return a severity-aware warning badge class."""
|
|
|
|
|
|
|
| 2356 |
if warning in {"Inventory would go negative", "Possible duplicate"}:
|
| 2357 |
return "vl-warning-badge vl-warning-strong"
|
| 2358 |
return "vl-warning-badge"
|
|
|
|
| 79 |
"Brazil - BRL (R$)": "R$",
|
| 80 |
"Custom": "",
|
| 81 |
}
|
| 82 |
+
PRICE_MEMORY_BADGE = "Price memory used"
|
| 83 |
DEMO_NOTES = [
|
| 84 |
"Bought 60 mangoes",
|
| 85 |
"Sold 12 mangoes, 20 each",
|
|
|
|
| 132 |
)
|
| 133 |
)
|
| 134 |
gr.HTML(_first_run_onboarding_panel())
|
| 135 |
+
guided_demo_output = gr.HTML(get_guided_demo_status(db_path))
|
| 136 |
+
refresh_guided_demo_button = gr.Button("Refresh Guided Judge Mode")
|
| 137 |
command_center_output = gr.HTML(_command_center(db_path))
|
| 138 |
gr.HTML(_section_heading("Seller Setup"))
|
| 139 |
with gr.Row():
|
|
|
|
| 202 |
wrap=True,
|
| 203 |
elem_classes="vl-panel",
|
| 204 |
)
|
| 205 |
+
refresh_guided_demo_button.click(
|
| 206 |
+
fn=lambda: get_guided_demo_status(db_path),
|
| 207 |
+
inputs=None,
|
| 208 |
+
outputs=guided_demo_output,
|
| 209 |
+
)
|
| 210 |
gr.HTML(
|
| 211 |
_info_panel(
|
| 212 |
"Workflow",
|
|
|
|
| 498 |
lines=3,
|
| 499 |
elem_classes="vl-panel",
|
| 500 |
)
|
| 501 |
+
with gr.Row():
|
| 502 |
+
field_pain_point_input = gr.Textbox(
|
| 503 |
+
label="Pain point",
|
| 504 |
+
value=initial_settings["field_pain_point"],
|
| 505 |
+
lines=3,
|
| 506 |
+
elem_classes="vl-panel",
|
| 507 |
+
)
|
| 508 |
+
field_before_input = gr.Textbox(
|
| 509 |
+
label="Before VoiceLedger",
|
| 510 |
+
value=initial_settings["field_before"],
|
| 511 |
+
lines=3,
|
| 512 |
+
elem_classes="vl-panel",
|
| 513 |
+
)
|
| 514 |
+
with gr.Row():
|
| 515 |
+
field_after_input = gr.Textbox(
|
| 516 |
+
label="After VoiceLedger",
|
| 517 |
+
value=initial_settings["field_after"],
|
| 518 |
+
lines=3,
|
| 519 |
+
elem_classes="vl-panel",
|
| 520 |
+
)
|
| 521 |
+
field_useful_input = gr.Textbox(
|
| 522 |
+
label="Useful moments",
|
| 523 |
+
value=initial_settings["field_useful_moments"],
|
| 524 |
+
lines=3,
|
| 525 |
+
elem_classes="vl-panel",
|
| 526 |
+
)
|
| 527 |
save_field_notes_button = gr.Button("Save Field Test Notes")
|
| 528 |
field_notes_status_output = gr.Markdown(elem_classes="vl-status")
|
| 529 |
seed_demo_button = gr.Button("Seed Demo Transactions", variant="primary")
|
|
|
|
| 570 |
lines=4,
|
| 571 |
elem_classes="vl-panel",
|
| 572 |
)
|
| 573 |
+
with gr.Row():
|
| 574 |
+
field_test_pain_point_input = gr.Textbox(
|
| 575 |
+
label="Pain point",
|
| 576 |
+
value=initial_settings["field_pain_point"],
|
| 577 |
+
lines=3,
|
| 578 |
+
elem_classes="vl-panel",
|
| 579 |
+
)
|
| 580 |
+
field_test_before_input = gr.Textbox(
|
| 581 |
+
label="Before VoiceLedger",
|
| 582 |
+
value=initial_settings["field_before"],
|
| 583 |
+
lines=3,
|
| 584 |
+
elem_classes="vl-panel",
|
| 585 |
+
)
|
| 586 |
+
with gr.Row():
|
| 587 |
+
field_test_after_input = gr.Textbox(
|
| 588 |
+
label="After VoiceLedger",
|
| 589 |
+
value=initial_settings["field_after"],
|
| 590 |
+
lines=3,
|
| 591 |
+
elem_classes="vl-panel",
|
| 592 |
+
)
|
| 593 |
+
field_test_useful_input = gr.Textbox(
|
| 594 |
+
label="Useful moments",
|
| 595 |
+
value=initial_settings["field_useful_moments"],
|
| 596 |
+
lines=3,
|
| 597 |
+
elem_classes="vl-panel",
|
| 598 |
+
)
|
| 599 |
+
field_test_changed_after_feedback_input = gr.Textbox(
|
| 600 |
+
label="Changed after feedback",
|
| 601 |
+
value=initial_settings["field_changed_after_feedback"],
|
| 602 |
+
lines=3,
|
| 603 |
+
elem_classes="vl-panel",
|
| 604 |
+
)
|
| 605 |
save_field_test_button = gr.Button("Save Field Test Evidence", variant="primary")
|
| 606 |
field_test_status_output = gr.Markdown(
|
| 607 |
value=_field_test_summary(initial_settings),
|
|
|
|
| 716 |
inputs=followup_customer_name,
|
| 717 |
outputs=followup_output,
|
| 718 |
)
|
| 719 |
+
gr.HTML(_section_heading("Debt Reminder Queue"))
|
| 720 |
+
refresh_debt_queue_button = gr.Button("Refresh Reminder Queue")
|
| 721 |
+
debt_queue_output = gr.Dataframe(
|
| 722 |
+
value=get_debt_reminder_queue(db_path),
|
| 723 |
+
headers=["customer", "outstanding_balance", "suggested_message"],
|
| 724 |
+
label="Customers to follow up",
|
| 725 |
+
interactive=False,
|
| 726 |
+
wrap=True,
|
| 727 |
+
elem_classes="vl-panel",
|
| 728 |
+
)
|
| 729 |
+
refresh_debt_queue_button.click(
|
| 730 |
+
fn=lambda: get_debt_reminder_queue(db_path),
|
| 731 |
+
inputs=None,
|
| 732 |
+
outputs=debt_queue_output,
|
| 733 |
+
)
|
| 734 |
|
| 735 |
with gr.Column(visible=False, elem_classes="vl-page-section") as inventory_page:
|
| 736 |
gr.HTML('<div id="vl-page-inventory" class="vl-page-anchor"></div>')
|
|
|
|
| 778 |
gr.HTML(_section_heading("Inventory Reorder List"))
|
| 779 |
reorder_button = gr.Button("Generate Reorder List")
|
| 780 |
reorder_output = gr.Dataframe(
|
| 781 |
+
headers=["item", "current_stock", "recent_sold", "threshold", "status", "suggested_action"],
|
| 782 |
+
label="Inventory reorder intelligence",
|
| 783 |
interactive=False,
|
| 784 |
wrap=True,
|
| 785 |
elem_classes="vl-panel",
|
|
|
|
| 791 |
elem_classes="vl-panel",
|
| 792 |
)
|
| 793 |
reorder_button.click(
|
| 794 |
+
fn=lambda: _generate_reorder_intelligence_for_ui(db_path),
|
| 795 |
inputs=None,
|
| 796 |
outputs=[reorder_output, reorder_message_output],
|
| 797 |
)
|
|
|
|
| 1130 |
],
|
| 1131 |
)
|
| 1132 |
save_field_notes_button.click(
|
| 1133 |
+
fn=lambda who, tried, changed, pain_point, before, after, useful: _save_field_notes(
|
| 1134 |
+
who,
|
| 1135 |
+
tried,
|
| 1136 |
+
changed,
|
| 1137 |
+
db_path,
|
| 1138 |
+
pain_point=pain_point,
|
| 1139 |
+
before=before,
|
| 1140 |
+
after=after,
|
| 1141 |
+
useful=useful,
|
| 1142 |
+
),
|
| 1143 |
+
inputs=[
|
| 1144 |
+
field_who_input,
|
| 1145 |
+
field_tried_input,
|
| 1146 |
+
field_changed_input,
|
| 1147 |
+
field_pain_point_input,
|
| 1148 |
+
field_before_input,
|
| 1149 |
+
field_after_input,
|
| 1150 |
+
field_useful_input,
|
| 1151 |
+
],
|
| 1152 |
outputs=field_notes_status_output,
|
| 1153 |
)
|
| 1154 |
save_field_test_button.click(
|
| 1155 |
+
fn=lambda checklist, who, tried, changed, pain_point, before, after, useful, changed_after_feedback: _save_field_test_evidence(
|
| 1156 |
+
checklist,
|
| 1157 |
+
who,
|
| 1158 |
+
tried,
|
| 1159 |
+
changed,
|
| 1160 |
+
db_path,
|
| 1161 |
+
pain_point=pain_point,
|
| 1162 |
+
before=before,
|
| 1163 |
+
after=after,
|
| 1164 |
+
useful=useful,
|
| 1165 |
+
changed_after_feedback=changed_after_feedback,
|
| 1166 |
+
),
|
| 1167 |
+
inputs=[
|
| 1168 |
+
field_test_checklist,
|
| 1169 |
+
field_test_who_input,
|
| 1170 |
+
field_test_tried_input,
|
| 1171 |
+
field_test_changed_input,
|
| 1172 |
+
field_test_pain_point_input,
|
| 1173 |
+
field_test_before_input,
|
| 1174 |
+
field_test_after_input,
|
| 1175 |
+
field_test_useful_input,
|
| 1176 |
+
field_test_changed_after_feedback_input,
|
| 1177 |
+
],
|
| 1178 |
outputs=field_test_status_output,
|
| 1179 |
)
|
| 1180 |
record_health_button.click(
|
|
|
|
| 1484 |
force_local=_force_local_mode(ai_mode),
|
| 1485 |
)
|
| 1486 |
transaction = result.transaction
|
| 1487 |
+
transaction, price_memory_used = suggest_price_from_history(transaction, db_path)
|
| 1488 |
payload = transaction.model_dump()
|
| 1489 |
warnings = _review_warnings(transaction, db_path)
|
| 1490 |
+
if price_memory_used:
|
| 1491 |
+
warnings.append(PRICE_MEMORY_BADGE)
|
| 1492 |
status = _status_message(transaction, result.message, result.fallback_reason, warnings=warnings)
|
| 1493 |
return payload, payload, status, _review_card(transaction, result.message, warnings)
|
| 1494 |
|
|
|
|
| 1526 |
force_local=force_local,
|
| 1527 |
)
|
| 1528 |
transaction = parse_result.transaction
|
| 1529 |
+
transaction, price_memory_used = suggest_price_from_history(transaction, db_path)
|
| 1530 |
payload = transaction.model_dump()
|
| 1531 |
warnings = _review_warnings(transaction, db_path)
|
| 1532 |
+
if price_memory_used:
|
| 1533 |
+
warnings.append(PRICE_MEMORY_BADGE)
|
| 1534 |
status = _status_message(
|
| 1535 |
transaction,
|
| 1536 |
parse_result.message,
|
|
|
|
| 1827 |
)
|
| 1828 |
|
| 1829 |
|
| 1830 |
+
def _save_field_notes(
|
| 1831 |
+
who: str,
|
| 1832 |
+
tried: str,
|
| 1833 |
+
changed: str,
|
| 1834 |
+
db_path: str | Path | None,
|
| 1835 |
+
*,
|
| 1836 |
+
pain_point: str | None = None,
|
| 1837 |
+
before: str | None = None,
|
| 1838 |
+
after: str | None = None,
|
| 1839 |
+
useful: str | None = None,
|
| 1840 |
+
) -> str:
|
| 1841 |
"""Persist anonymized field-test notes."""
|
| 1842 |
update_business_settings(
|
| 1843 |
db_path=db_path,
|
| 1844 |
field_test_who=who,
|
| 1845 |
field_test_tried=tried,
|
| 1846 |
field_test_changed=changed,
|
| 1847 |
+
field_pain_point=pain_point,
|
| 1848 |
+
field_before=before,
|
| 1849 |
+
field_after=after,
|
| 1850 |
+
field_useful_moments=useful,
|
| 1851 |
)
|
| 1852 |
return "Saved anonymized field-test notes."
|
| 1853 |
|
|
|
|
| 1858 |
tried: str,
|
| 1859 |
changed: str,
|
| 1860 |
db_path: str | Path | None,
|
| 1861 |
+
*,
|
| 1862 |
+
pain_point: str | None = None,
|
| 1863 |
+
before: str | None = None,
|
| 1864 |
+
after: str | None = None,
|
| 1865 |
+
useful: str | None = None,
|
| 1866 |
+
changed_after_feedback: str | None = None,
|
| 1867 |
) -> str:
|
| 1868 |
"""Persist field-test checklist and notes."""
|
| 1869 |
checklist_value = ",".join(checklist or [])
|
|
|
|
| 1873 |
field_test_tried=tried,
|
| 1874 |
field_test_changed=changed,
|
| 1875 |
field_test_checklist=checklist_value,
|
| 1876 |
+
field_pain_point=pain_point,
|
| 1877 |
+
field_before=before,
|
| 1878 |
+
field_after=after,
|
| 1879 |
+
field_useful_moments=useful,
|
| 1880 |
+
field_changed_after_feedback=changed_after_feedback,
|
| 1881 |
)
|
| 1882 |
return _field_test_summary(settings)
|
| 1883 |
|
|
|
|
| 1893 |
completed = _field_test_checklist_values(settings)
|
| 1894 |
return (
|
| 1895 |
f"Field test evidence saved: {len(completed)} checklist item(s). "
|
| 1896 |
+
f"Who: {settings['field_test_who']} Pain point: {settings['field_pain_point']} "
|
| 1897 |
+
f"Changed: {settings['field_changed_after_feedback']}"
|
| 1898 |
)
|
| 1899 |
|
| 1900 |
|
|
|
|
| 2020 |
)
|
| 2021 |
|
| 2022 |
|
| 2023 |
+
def suggest_price_from_history(transaction: Transaction, db_path: str | Path | None) -> tuple[Transaction, bool]:
|
| 2024 |
+
"""Fill missing sale price fields from the latest saved sale for the same item."""
|
| 2025 |
+
if (
|
| 2026 |
+
transaction.transaction_type != "sale"
|
| 2027 |
+
or not transaction.item
|
| 2028 |
+
or transaction.quantity is None
|
| 2029 |
+
or transaction.unit_price is not None
|
| 2030 |
+
or transaction.amount is not None
|
| 2031 |
+
):
|
| 2032 |
+
return transaction, False
|
| 2033 |
+
|
| 2034 |
+
ledger = get_transactions(db_path)
|
| 2035 |
+
if ledger.empty:
|
| 2036 |
+
return transaction, False
|
| 2037 |
+
|
| 2038 |
+
item_name = transaction.item.lower()
|
| 2039 |
+
matches = ledger[
|
| 2040 |
+
(ledger["transaction_type"] == "sale")
|
| 2041 |
+
& (ledger["item"].fillna("").astype(str).str.lower() == item_name)
|
| 2042 |
+
].copy()
|
| 2043 |
+
if matches.empty:
|
| 2044 |
+
return transaction, False
|
| 2045 |
+
|
| 2046 |
+
matches["unit_price"] = pd.to_numeric(matches["unit_price"], errors="coerce")
|
| 2047 |
+
matches = matches[matches["unit_price"].notna() & (matches["unit_price"] > 0)]
|
| 2048 |
+
if matches.empty:
|
| 2049 |
+
return transaction, False
|
| 2050 |
+
|
| 2051 |
+
unit_price = float(matches.iloc[0]["unit_price"])
|
| 2052 |
+
payload = transaction.model_dump()
|
| 2053 |
+
payload["unit_price"] = unit_price
|
| 2054 |
+
payload["amount"] = round(float(transaction.quantity) * unit_price, 2)
|
| 2055 |
+
payload["confidence"] = max(float(transaction.confidence), 0.78)
|
| 2056 |
+
return Transaction(**payload), True
|
| 2057 |
+
|
| 2058 |
+
|
| 2059 |
+
def get_debt_reminder_queue(db_path: str | Path | None = None) -> pd.DataFrame:
|
| 2060 |
+
"""Return customers with positive outstanding balances sorted by balance."""
|
| 2061 |
+
balances = get_customer_balances(db_path)
|
| 2062 |
+
columns = ["customer", "outstanding_balance", "suggested_message"]
|
| 2063 |
+
if balances.empty:
|
| 2064 |
+
return pd.DataFrame(columns=columns)
|
| 2065 |
+
|
| 2066 |
+
debt = balances.copy()
|
| 2067 |
+
debt["outstanding_balance"] = pd.to_numeric(debt["outstanding_balance"], errors="coerce").fillna(0)
|
| 2068 |
+
debt = debt[debt["outstanding_balance"] > 0].sort_values("outstanding_balance", ascending=False)
|
| 2069 |
+
if debt.empty:
|
| 2070 |
+
return pd.DataFrame(columns=columns)
|
| 2071 |
+
|
| 2072 |
+
debt["suggested_message"] = debt["customer"].apply(lambda customer: generate_debt_reminder(str(customer), db_path))
|
| 2073 |
+
return debt[columns].reset_index(drop=True)
|
| 2074 |
+
|
| 2075 |
+
|
| 2076 |
+
def generate_debt_reminder(customer_name: str, db_path: str | Path | None = None) -> str:
|
| 2077 |
+
"""Generate a WhatsApp-ready reminder for one customer's outstanding balance."""
|
| 2078 |
+
return generate_customer_followup(customer_name, db_path)
|
| 2079 |
+
|
| 2080 |
+
|
| 2081 |
+
def generate_reorder_intelligence(db_path: str | Path | None = None) -> pd.DataFrame:
|
| 2082 |
+
"""Return stock recommendations with sales velocity context."""
|
| 2083 |
+
columns = ["item", "current_stock", "recent_sold", "threshold", "status", "suggested_action"]
|
| 2084 |
+
inventory = get_inventory(db_path)
|
| 2085 |
+
if inventory.empty:
|
| 2086 |
+
return pd.DataFrame(columns=columns)
|
| 2087 |
+
|
| 2088 |
+
threshold = get_low_stock_threshold(db_path)
|
| 2089 |
+
ledger = get_transactions(db_path)
|
| 2090 |
+
stock = inventory.copy()
|
| 2091 |
+
stock["current_stock"] = pd.to_numeric(stock["current_stock"], errors="coerce").fillna(0)
|
| 2092 |
+
sales_by_item: dict[str, float] = {}
|
| 2093 |
+
if not ledger.empty:
|
| 2094 |
+
sale_rows = ledger[ledger["transaction_type"] == "sale"].copy()
|
| 2095 |
+
sale_rows["quantity"] = pd.to_numeric(sale_rows["quantity"], errors="coerce").fillna(0)
|
| 2096 |
+
sale_rows["item_key"] = sale_rows["item"].fillna("").astype(str).str.lower()
|
| 2097 |
+
sales_by_item = sale_rows.groupby("item_key")["quantity"].sum().to_dict()
|
| 2098 |
+
|
| 2099 |
+
records: list[dict[str, Any]] = []
|
| 2100 |
+
for _, row in stock.iterrows():
|
| 2101 |
+
item = str(row["item"])
|
| 2102 |
+
current_stock = float(row["current_stock"])
|
| 2103 |
+
recent_sold = float(sales_by_item.get(item.lower(), 0.0))
|
| 2104 |
+
status = "Stock ok"
|
| 2105 |
+
if current_stock < threshold:
|
| 2106 |
+
status = "Low stock"
|
| 2107 |
+
elif recent_sold > 0 and current_stock <= max(threshold * 2, recent_sold):
|
| 2108 |
+
status = "Selling fast"
|
| 2109 |
+
if status == "Low stock":
|
| 2110 |
+
action = f"Restock {item} now; stock is below {_format_quantity(threshold)}."
|
| 2111 |
+
elif status == "Selling fast":
|
| 2112 |
+
action = f"Plan {item} restock soon; recent sales are {_format_quantity(recent_sold)}."
|
| 2113 |
+
else:
|
| 2114 |
+
action = f"{item} stock is ok."
|
| 2115 |
+
records.append(
|
| 2116 |
+
{
|
| 2117 |
+
"item": item,
|
| 2118 |
+
"current_stock": current_stock,
|
| 2119 |
+
"recent_sold": recent_sold,
|
| 2120 |
+
"threshold": threshold,
|
| 2121 |
+
"status": status,
|
| 2122 |
+
"suggested_action": action,
|
| 2123 |
+
}
|
| 2124 |
+
)
|
| 2125 |
+
|
| 2126 |
+
recommendations = pd.DataFrame.from_records(records, columns=columns)
|
| 2127 |
+
recommendations["_status_rank"] = recommendations["status"].map({"Low stock": 0, "Selling fast": 1, "Stock ok": 2})
|
| 2128 |
+
return recommendations.sort_values(["_status_rank", "current_stock", "item"]).drop(columns=["_status_rank"]).reset_index(drop=True)
|
| 2129 |
+
|
| 2130 |
+
|
| 2131 |
+
def get_guided_demo_status(db_path: str | Path | None = None) -> str:
|
| 2132 |
+
"""Return compact progress states for the judge demo flow."""
|
| 2133 |
+
ledger = get_transactions(db_path)
|
| 2134 |
+
has_transactions = not ledger.empty
|
| 2135 |
+
has_sale = has_transactions and bool((ledger["transaction_type"] == "sale").any())
|
| 2136 |
+
has_closeout_exports = Path(tempfile.gettempdir(), "voiceledger_transactions.csv").exists()
|
| 2137 |
+
health_ready = initialize_database(db_path).exists()
|
| 2138 |
+
steps = [
|
| 2139 |
+
("Check Health", health_ready, "Demo Health is reachable from this page."),
|
| 2140 |
+
("Seed Demo Data", has_transactions, "Adds realistic sales, expenses, credit, and stock."),
|
| 2141 |
+
("Record/Type Sale", has_sale, "Try Sold 12 mangoes, 20 each."),
|
| 2142 |
+
("Save", has_transactions, "Receipt, ledger, dashboard, credit, and stock refresh."),
|
| 2143 |
+
("Open Dashboard", has_transactions, "Metrics and timeline are ready once data exists."),
|
| 2144 |
+
("Open Reports", has_closeout_exports, "Daily Closeout prepares PDF, WhatsApp, and CSV."),
|
| 2145 |
+
]
|
| 2146 |
+
items = "".join(
|
| 2147 |
+
f"""
|
| 2148 |
+
<li class="{'vl-guided-done' if done else 'vl-guided-next'}">
|
| 2149 |
+
<strong>{escape(label)}</strong>
|
| 2150 |
+
<span>{'Done' if done else 'Next'} · {escape(help_text)}</span>
|
| 2151 |
+
</li>
|
| 2152 |
+
"""
|
| 2153 |
+
for label, done, help_text in steps
|
| 2154 |
+
)
|
| 2155 |
+
return f"""
|
| 2156 |
+
<section class="vl-guided-mode">
|
| 2157 |
+
<h2>Guided Judge Mode</h2>
|
| 2158 |
+
<p>Use this path for a 90-second demo: health, seed, record, save, dashboard, reports.</p>
|
| 2159 |
+
<ol>{items}</ol>
|
| 2160 |
+
</section>
|
| 2161 |
+
"""
|
| 2162 |
+
|
| 2163 |
+
|
| 2164 |
def _last_transaction_label(db_path: str | Path | None) -> str:
|
| 2165 |
"""Return a concise label for the newest transaction."""
|
| 2166 |
ledger = get_transactions(db_path)
|
|
|
|
| 2340 |
return _empty_detail_card("Command result", "Command not recognized. Try close today, show Amit, stock mangoes, or a transaction note.")
|
| 2341 |
|
| 2342 |
|
| 2343 |
+
def _generate_reorder_intelligence_for_ui(db_path: str | Path | None) -> tuple[pd.DataFrame, str]:
|
| 2344 |
+
"""Return reorder intelligence and a WhatsApp-ready restock message."""
|
| 2345 |
+
recommendations = generate_reorder_intelligence(db_path)
|
| 2346 |
+
if recommendations.empty:
|
| 2347 |
+
return recommendations, "No inventory recorded yet."
|
| 2348 |
+
|
| 2349 |
+
settings = get_business_settings(db_path)
|
| 2350 |
+
message_lines = [
|
| 2351 |
+
f"{settings['business_name']} Reorder Plan",
|
| 2352 |
+
f"Low-stock threshold: {_format_quantity(get_low_stock_threshold(db_path))}",
|
| 2353 |
+
"",
|
| 2354 |
+
]
|
| 2355 |
+
action_rows = recommendations[recommendations["status"].isin(["Low stock", "Selling fast"])]
|
| 2356 |
+
if action_rows.empty:
|
| 2357 |
+
message_lines.append("All stock looks ok today.")
|
| 2358 |
+
else:
|
| 2359 |
+
message_lines.extend(
|
| 2360 |
+
f"- {row['item'].title()}: {row['status']} ({_format_quantity(row['current_stock'])} left, {_format_quantity(row['recent_sold'])} sold recently)"
|
| 2361 |
+
for _, row in action_rows.head(8).iterrows()
|
| 2362 |
+
)
|
| 2363 |
+
return recommendations, "\n".join(message_lines)
|
| 2364 |
+
|
| 2365 |
+
|
| 2366 |
def _generate_daily_summary_report(db_path: str | Path | None) -> tuple[str | None, str]:
|
| 2367 |
"""Generate the Daily Summary PDF for download in Gradio."""
|
| 2368 |
settings = get_business_settings(db_path)
|
|
|
|
| 2668 |
|
| 2669 |
def _warning_badge_class(warning: str) -> str:
|
| 2670 |
"""Return a severity-aware warning badge class."""
|
| 2671 |
+
if warning == PRICE_MEMORY_BADGE:
|
| 2672 |
+
return "vl-success-badge"
|
| 2673 |
if warning in {"Inventory would go negative", "Possible duplicate"}:
|
| 2674 |
return "vl-warning-badge vl-warning-strong"
|
| 2675 |
return "vl-warning-badge"
|
voiceledger/ui/theme.py
CHANGED
|
@@ -316,6 +316,7 @@ body,
|
|
| 316 |
.vl-closeout-card,
|
| 317 |
.vl-pipeline-strip,
|
| 318 |
.vl-small-model-card,
|
|
|
|
| 319 |
.vl-command-center,
|
| 320 |
.vl-seller-timeline,
|
| 321 |
.vl-ai-mode-card,
|
|
@@ -337,6 +338,7 @@ body,
|
|
| 337 |
.vl-closeout-card *,
|
| 338 |
.vl-pipeline-strip *,
|
| 339 |
.vl-small-model-card *,
|
|
|
|
| 340 |
.vl-command-center *,
|
| 341 |
.vl-seller-timeline *,
|
| 342 |
.vl-ai-mode-card *,
|
|
@@ -354,6 +356,7 @@ body,
|
|
| 354 |
.vl-closeout-card h2,
|
| 355 |
.vl-pipeline-strip h2,
|
| 356 |
.vl-small-model-card h2,
|
|
|
|
| 357 |
.vl-command-center h2,
|
| 358 |
.vl-seller-timeline h2,
|
| 359 |
.vl-ai-mode-card h2,
|
|
@@ -424,6 +427,56 @@ body,
|
|
| 424 |
margin: 4px 0 0 !important;
|
| 425 |
}
|
| 426 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
.vl-warning-row {
|
| 428 |
display: flex !important;
|
| 429 |
flex-wrap: wrap !important;
|
|
|
|
| 316 |
.vl-closeout-card,
|
| 317 |
.vl-pipeline-strip,
|
| 318 |
.vl-small-model-card,
|
| 319 |
+
.vl-guided-mode,
|
| 320 |
.vl-command-center,
|
| 321 |
.vl-seller-timeline,
|
| 322 |
.vl-ai-mode-card,
|
|
|
|
| 338 |
.vl-closeout-card *,
|
| 339 |
.vl-pipeline-strip *,
|
| 340 |
.vl-small-model-card *,
|
| 341 |
+
.vl-guided-mode *,
|
| 342 |
.vl-command-center *,
|
| 343 |
.vl-seller-timeline *,
|
| 344 |
.vl-ai-mode-card *,
|
|
|
|
| 356 |
.vl-closeout-card h2,
|
| 357 |
.vl-pipeline-strip h2,
|
| 358 |
.vl-small-model-card h2,
|
| 359 |
+
.vl-guided-mode h2,
|
| 360 |
.vl-command-center h2,
|
| 361 |
.vl-seller-timeline h2,
|
| 362 |
.vl-ai-mode-card h2,
|
|
|
|
| 427 |
margin: 4px 0 0 !important;
|
| 428 |
}
|
| 429 |
|
| 430 |
+
.vl-guided-mode p {
|
| 431 |
+
color: #475247 !important;
|
| 432 |
+
font-size: 14px !important;
|
| 433 |
+
line-height: 1.4 !important;
|
| 434 |
+
margin: 4px 0 12px !important;
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
.vl-guided-mode ol {
|
| 438 |
+
display: grid !important;
|
| 439 |
+
gap: 10px !important;
|
| 440 |
+
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)) !important;
|
| 441 |
+
list-style: none !important;
|
| 442 |
+
margin: 0 !important;
|
| 443 |
+
padding: 0 !important;
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
.vl-guided-mode li {
|
| 447 |
+
background: #f6f7f2 !important;
|
| 448 |
+
border: 1px solid var(--vl-border) !important;
|
| 449 |
+
border-radius: 10px !important;
|
| 450 |
+
padding: 10px !important;
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
.vl-guided-mode li strong,
|
| 454 |
+
.vl-guided-mode li span {
|
| 455 |
+
display: block !important;
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
.vl-guided-mode li strong {
|
| 459 |
+
font-size: 14px !important;
|
| 460 |
+
font-weight: 900 !important;
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
.vl-guided-mode li span {
|
| 464 |
+
color: #475247 !important;
|
| 465 |
+
font-size: 12px !important;
|
| 466 |
+
line-height: 1.35 !important;
|
| 467 |
+
margin-top: 4px !important;
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
.vl-guided-done {
|
| 471 |
+
border-color: #9bd2ad !important;
|
| 472 |
+
background: #e7f6ed !important;
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
+
.vl-guided-next {
|
| 476 |
+
border-color: #e3bc5b !important;
|
| 477 |
+
background: #fff9e8 !important;
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
.vl-warning-row {
|
| 481 |
display: flex !important;
|
| 482 |
flex-wrap: wrap !important;
|