Spaces:
Sleeping
Sleeping
Sagar Patel commited on
Commit ·
fb99e77
1
Parent(s): 2808a57
Add language polish and seller shortcuts
Browse files- README.md +11 -3
- docs/demo-video-script.md +5 -3
- docs/field-notes.md +5 -4
- docs/submission-checklist.md +5 -1
- tests/test_ui_audio_flow.py +32 -0
- tests/test_whatsapp_summary.py +16 -0
- voiceledger/ledger/corrections.py +107 -0
- voiceledger/ledger/database.py +2 -0
- voiceledger/reports/whatsapp_summary.py +53 -7
- voiceledger/ui/gradio_app.py +130 -3
- voiceledger/ui/theme.py +17 -0
README.md
CHANGED
|
@@ -19,13 +19,16 @@ The submission version is feature-frozen around the complete demo loop:
|
|
| 19 |
|
| 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 |
- Use a first-run “Start in 60 seconds” guide for setup, first transaction, review, and closeout.
|
| 23 |
- Type or paste a transaction note.
|
| 24 |
- Bulk import multiple pasted notes for review and editing.
|
| 25 |
- Parse it with Modal-hosted NVIDIA Nemotron when configured, with local rules as a deterministic fallback.
|
| 26 |
- Parse common English, Hinglish, Hindi-lite, Gujarati-lite, Spanish, French, and Portuguese seller phrases with deterministic fallback rules.
|
| 27 |
- Review a human-friendly transaction card with warning badges before saving.
|
|
|
|
| 28 |
- Correct transaction type, item, customer, quantity, price, amount, notes, and confidence directly before saving.
|
|
|
|
| 29 |
- Choose `Cloud AI first` or `Local fallback only` from the Record screen to make the AI route explicit during demos.
|
| 30 |
- Save the structured transaction to SQLite.
|
| 31 |
- See a “Saved just now” receipt with bookkeeping side effects.
|
|
@@ -34,7 +37,8 @@ The submission version is feature-frozen around the complete demo loop:
|
|
| 34 |
- Use a mobile-first, business-style Gradio interface with custom styling.
|
| 35 |
- Run a Daily Closeout that prepares PDF, WhatsApp summary, and CSV exports together.
|
| 36 |
- Download a Daily Summary PDF report.
|
| 37 |
-
- Generate
|
|
|
|
| 38 |
- Generate customer follow-up reminders and an inventory reorder list.
|
| 39 |
- Capture field-test evidence with a seller checklist and anonymized feedback notes.
|
| 40 |
- Offload speech transcription and LLM parsing to optional Modal endpoints.
|
|
@@ -106,7 +110,7 @@ The same examples are available in `sample_data/demo_transactions.txt`.
|
|
| 106 |
2. Use the `Hackathon Demo Launchpad` on the first screen to seed demo transactions.
|
| 107 |
3. Open `Record Text & Voice`, speak or type `Sold 12 mangoes, 20 each`, parse it, and review the human-readable transaction card.
|
| 108 |
4. Correct one inline review field before saving, such as quantity or amount, to show the seller correction loop.
|
| 109 |
-
5. Show the parse status and warning badges: Modal/NVIDIA Nemotron when available, local fallback when needed.
|
| 110 |
6. Save the transaction and show the Command Center plus the receipt with stock, customer, or amount side effects.
|
| 111 |
7. Open `Dashboard`, `Customer Credit`, and `Inventory` to show the Insight Coach, timeline charts, customer detail, follow-up message, inventory detail, reorder list, and automatic bookkeeping updates.
|
| 112 |
8. Open `Field Test` to show what a seller tried and what changed after feedback.
|
|
@@ -196,10 +200,14 @@ curl -X POST https://sagarpat3199--voiceledger-api.modal.run/parse \
|
|
| 196 |
- Smart review warnings flag low confidence, missing fields, duplicate risk, and negative stock before save.
|
| 197 |
- Inline review editing lets sellers correct the parsed transaction before the save touches the ledger.
|
| 198 |
- Seller setup persists business name, currency label, low-stock threshold, and language style in SQLite.
|
|
|
|
| 199 |
- Field Test persists anonymized seller evidence and a workflow checklist in SQLite.
|
|
|
|
| 200 |
- Local fallback rules include common English, Hinglish, Hindi-lite, Gujarati-lite, Spanish, French, and Portuguese seller notes.
|
|
|
|
| 201 |
- Customer follow-up and inventory reorder helpers generate WhatsApp-ready action messages.
|
| 202 |
-
- WhatsApp summaries provide
|
|
|
|
| 203 |
- Bulk import splits pasted notes by line, parses each line, supports review edits, and saves all reviewed transactions.
|
| 204 |
- Modal integration lives in `backend/`; if endpoint URLs are not configured, local fallback stays active.
|
| 205 |
- The Record screen includes a local-only AI mode that skips Modal while preserving the same transaction review and save flow.
|
|
|
|
| 19 |
|
| 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.
|
| 26 |
- Parse it with Modal-hosted NVIDIA Nemotron when configured, with local rules as a deterministic fallback.
|
| 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.
|
| 33 |
- Save the structured transaction to SQLite.
|
| 34 |
- See a “Saved just now” receipt with bookkeeping side effects.
|
|
|
|
| 37 |
- Use a mobile-first, business-style Gradio interface with custom styling.
|
| 38 |
- Run a Daily Closeout that prepares PDF, WhatsApp summary, and CSV exports together.
|
| 39 |
- Download a Daily Summary PDF report.
|
| 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.
|
|
|
|
| 110 |
2. Use the `Hackathon Demo Launchpad` on the first screen to seed demo transactions.
|
| 111 |
3. Open `Record Text & Voice`, speak or type `Sold 12 mangoes, 20 each`, parse it, and review the human-readable transaction card.
|
| 112 |
4. Correct one inline review field before saving, such as quantity or amount, to show the seller correction loop.
|
| 113 |
+
5. Show the parse status, language/confidence chip, and warning badges: Modal/NVIDIA Nemotron when available, local fallback when needed.
|
| 114 |
6. Save the transaction and show the Command Center plus the receipt with stock, customer, or amount side effects.
|
| 115 |
7. Open `Dashboard`, `Customer Credit`, and `Inventory` to show the Insight Coach, timeline charts, customer detail, follow-up message, inventory detail, reorder list, and automatic bookkeeping updates.
|
| 116 |
8. Open `Field Test` to show what a seller tried and what changed after feedback.
|
|
|
|
| 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.
|
| 212 |
- Modal integration lives in `backend/`; if endpoint URLs are not configured, local fallback stays active.
|
| 213 |
- The Record screen includes a local-only AI mode that skips Modal while preserving the same transaction review and save flow.
|
docs/demo-video-script.md
CHANGED
|
@@ -16,7 +16,7 @@ On the first screen, show the `Judge Demo Flow` panel:
|
|
| 16 |
|
| 17 |
Click `Seed Demo Transactions`.
|
| 18 |
|
| 19 |
-
Show `Seller Setup` and mention that business name, currency, low-stock threshold, and language style are saved in SQLite.
|
| 20 |
|
| 21 |
Show the first-run guide and multilingual examples so judges see English, Hinglish, Gujarati-lite, Spanish, French, and Portuguese seller notes.
|
| 22 |
|
|
@@ -50,7 +50,9 @@ Click parse. Show:
|
|
| 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
|
|
|
|
| 53 |
- Command Center update
|
|
|
|
| 54 |
- “Saved just now” receipt with stock, customer, or amount side effects
|
| 55 |
|
| 56 |
## 0:45-1:05 — Bookkeeping Updates
|
|
@@ -71,13 +73,13 @@ In `Ledger`, load a transaction by id, update or delete it, and show balances re
|
|
| 71 |
|
| 72 |
Click `Download CSV`.
|
| 73 |
|
| 74 |
-
Open `Reports & PDF`, run `Daily Closeout`, then generate the PDF, WhatsApp summary, and CSV export.
|
| 75 |
|
| 76 |
Suggested screenshot/GIF moments:
|
| 77 |
|
| 78 |
- Record flow: first-run guide, multilingual examples, review card, inline correction, parse source, warning badges, save receipt.
|
| 79 |
- Seller setup: currency/threshold/language style and Command Center.
|
| 80 |
-
- Field Test: seller checklist
|
| 81 |
- Submission Story: AI pipeline and small-model fit card.
|
| 82 |
- Dashboard: sales, expenses, profit, credit, timeline, top item, low-stock inventory.
|
| 83 |
- Reports/Ledger: Daily Closeout, PDF download, WhatsApp summary, CSV export.
|
|
|
|
| 16 |
|
| 17 |
Click `Seed Demo Transactions`.
|
| 18 |
|
| 19 |
+
Show `Seller Setup` and mention that business name, currency preset, low-stock threshold, and language style are saved in SQLite.
|
| 20 |
|
| 21 |
Show the first-run guide and multilingual examples so judges see English, Hinglish, Gujarati-lite, Spanish, French, and Portuguese seller notes.
|
| 22 |
|
|
|
|
| 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
|
| 53 |
+
- Language/confidence chip for multilingual notes
|
| 54 |
- Command Center update
|
| 55 |
+
- Voice command shortcuts such as `close today`, `show Amit`, and `stock mangoes`
|
| 56 |
- “Saved just now” receipt with stock, customer, or amount side effects
|
| 57 |
|
| 58 |
## 0:45-1:05 — Bookkeeping Updates
|
|
|
|
| 73 |
|
| 74 |
Click `Download CSV`.
|
| 75 |
|
| 76 |
+
Open `Reports & PDF`, run `Daily Closeout`, then generate the PDF, translated WhatsApp summary, and CSV export.
|
| 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.
|
| 84 |
- Dashboard: sales, expenses, profit, credit, timeline, top item, low-stock inventory.
|
| 85 |
- Reports/Ledger: Daily Closeout, PDF download, WhatsApp summary, CSV export.
|
docs/field-notes.md
CHANGED
|
@@ -50,8 +50,9 @@ The hackathon version is intentionally frozen around the complete bookkeeping lo
|
|
| 50 |
- Voice and text capture.
|
| 51 |
- Modal/Nemotron parsing with local rule fallback.
|
| 52 |
- Seller setup for business name, currency, low-stock threshold, and language style.
|
|
|
|
| 53 |
- Hinglish/Hindi-lite/Gujarati-lite/Spanish/French/Portuguese fallback examples for common seller notes.
|
| 54 |
-
- First-run onboarding, multilingual examples, human-friendly review cards, inline review correction, warning badges, receipts, and save feedback.
|
| 55 |
- Command Center, dashboard timeline, seller-day timeline, customer follow-up, inventory reorder list, ledger correction, Daily Closeout, PDF, CSV, and WhatsApp exports.
|
| 56 |
- Field Test Mode for the seller checklist and anonymized “who/tried/changed” feedback evidence.
|
| 57 |
- Demo Health for backend observability.
|
|
@@ -61,13 +62,13 @@ This freeze keeps the demo reliable and makes the value proposition easier to ju
|
|
| 61 |
|
| 62 |
## What We Learned
|
| 63 |
|
| 64 |
-
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.
|
| 65 |
|
| 66 |
The small-model constraint helped keep the app honest. The model handles the fuzzy human input; the code owns the accounting state.
|
| 67 |
|
| 68 |
## Screenshot Moments
|
| 69 |
|
| 70 |
-
- Record flow: first-run onboarding, multilingual examples, review card, inline corrections, warning badges, source/status, save receipt.
|
| 71 |
- Dashboard: daily totals, Insight Coach, outstanding credit, sales/expense timeline, top seller, low-stock inventory.
|
| 72 |
-
- Field Test: workflow checklist and anonymized feedback evidence.
|
| 73 |
- Reports and exports: Daily Closeout, PDF download, WhatsApp summary, CSV ledger export.
|
|
|
|
| 50 |
- Voice and text capture.
|
| 51 |
- Modal/Nemotron parsing with local rule fallback.
|
| 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 |
+
- First-run onboarding, multilingual examples, language/confidence chips, human-friendly review cards, inline review correction, mistake logging, warning badges, receipts, and save feedback.
|
| 56 |
- Command Center, dashboard timeline, seller-day timeline, customer follow-up, inventory reorder list, 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 |
- Demo Health for backend observability.
|
|
|
|
| 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.
|
docs/submission-checklist.md
CHANGED
|
@@ -17,13 +17,16 @@
|
|
| 17 |
- `Demo Health` shows Modal reachable and backend version.
|
| 18 |
- `Demo Health` shows NVIDIA Nemotron parser status.
|
| 19 |
- Seller Setup saves business name, currency label, low-stock threshold, and language style.
|
|
|
|
| 20 |
- Text parse works with `Sold 12 mangoes, 20 each`.
|
| 21 |
- Local fallback parses `Amit ne 100 dene hai`, `Amit ne 50 diya`, and `50 mango kharida`.
|
| 22 |
- Voice parse works and transcript appears.
|
| 23 |
- Review card shows type, item, quantity, price, amount, customer, source, and confidence.
|
|
|
|
| 24 |
- Smart warnings appear for low confidence, missing fields, duplicate risk, and negative stock.
|
| 25 |
- Save shows a receipt with transaction type, amount, and side effects.
|
| 26 |
- Command Center updates after save and seed.
|
|
|
|
| 27 |
- Save updates Dashboard and Ledger.
|
| 28 |
- Dashboard timeline loads from saved transactions.
|
| 29 |
- Seller-day timeline shows recent saved transactions.
|
|
@@ -37,7 +40,8 @@
|
|
| 37 |
- CSV export downloads from Ledger.
|
| 38 |
- Daily Closeout generates PDF, CSV, WhatsApp summary, and a status line.
|
| 39 |
- PDF report downloads from Reports.
|
| 40 |
-
- WhatsApp summary generates copyable text.
|
|
|
|
| 41 |
|
| 42 |
## Prize Alignment
|
| 43 |
|
|
|
|
| 17 |
- `Demo Health` shows Modal reachable and backend version.
|
| 18 |
- `Demo Health` shows NVIDIA Nemotron parser status.
|
| 19 |
- Seller Setup saves business name, currency label, low-stock threshold, and language style.
|
| 20 |
+
- Currency preset updates the currency label for INR, USD, EUR, GBP, MXN, and BRL.
|
| 21 |
- Text parse works with `Sold 12 mangoes, 20 each`.
|
| 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.
|
| 28 |
- Command Center updates after save and seed.
|
| 29 |
+
- Voice command shortcuts work for `close today`, `show Amit`, and `stock mangoes`.
|
| 30 |
- Save updates Dashboard and Ledger.
|
| 31 |
- Dashboard timeline loads from saved transactions.
|
| 32 |
- Seller-day timeline shows recent saved transactions.
|
|
|
|
| 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 correction log shows edited review fields.
|
| 45 |
|
| 46 |
## Prize Alignment
|
| 47 |
|
tests/test_ui_audio_flow.py
CHANGED
|
@@ -97,6 +97,23 @@ def test_parse_note_local_mode_surfaces_local_fallback(monkeypatch) -> None:
|
|
| 97 |
assert "Safe to save" in review_card
|
| 98 |
|
| 99 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
def test_high_contrast_demo_panels_are_rendered() -> None:
|
| 101 |
panel = gradio_app._info_panel(
|
| 102 |
"Demo Health",
|
|
@@ -210,6 +227,9 @@ def test_apply_review_edits_updates_payload_and_review_card(tmp_path) -> None:
|
|
| 210 |
assert state == payload
|
| 211 |
assert "Review updated" in status
|
| 212 |
assert "250" in review_card
|
|
|
|
|
|
|
|
|
|
| 213 |
|
| 214 |
|
| 215 |
def test_receipt_card_summarizes_saved_sale(tmp_path) -> None:
|
|
@@ -267,6 +287,18 @@ def test_customer_followup_and_reorder_helpers(tmp_path) -> None:
|
|
| 267 |
assert "Onions" in message
|
| 268 |
|
| 269 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
def test_insight_coach_surfaces_credit_and_stock_actions(tmp_path) -> None:
|
| 271 |
db_path = tmp_path / "voiceledger.sqlite3"
|
| 272 |
gradio_app.add_transaction(gradio_app.local_parse_transaction("Amit owes 100"), db_path)
|
|
|
|
| 97 |
assert "Safe to save" in review_card
|
| 98 |
|
| 99 |
|
| 100 |
+
def test_parse_note_surfaces_language_confidence_chip(monkeypatch) -> None:
|
| 101 |
+
monkeypatch.setattr(
|
| 102 |
+
gradio_app.modal_api,
|
| 103 |
+
"parse_transaction_result",
|
| 104 |
+
lambda text, fallback, **kwargs: ParseResult(
|
| 105 |
+
transaction=fallback(text),
|
| 106 |
+
source="local",
|
| 107 |
+
message="Parsed locally with the rule parser.",
|
| 108 |
+
),
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
_, _, status, _ = gradio_app._parse_note("Vendí 12 mangos, 20 cada uno")
|
| 112 |
+
|
| 113 |
+
assert "Spanish" in status
|
| 114 |
+
assert "High confidence" in status
|
| 115 |
+
|
| 116 |
+
|
| 117 |
def test_high_contrast_demo_panels_are_rendered() -> None:
|
| 118 |
panel = gradio_app._info_panel(
|
| 119 |
"Demo Health",
|
|
|
|
| 227 |
assert state == payload
|
| 228 |
assert "Review updated" in status
|
| 229 |
assert "250" in review_card
|
| 230 |
+
correction_log = gradio_app.get_correction_log(db_path)
|
| 231 |
+
assert len(correction_log) == 1
|
| 232 |
+
assert "quantity" in correction_log.iloc[0]["changed_fields"]
|
| 233 |
|
| 234 |
|
| 235 |
def test_receipt_card_summarizes_saved_sale(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)
|
| 293 |
+
gradio_app.add_transaction(gradio_app.local_parse_transaction("Bought 3 onions"), db_path)
|
| 294 |
+
|
| 295 |
+
assert gradio_app._currency_symbol_for_preset("Brazil - BRL (R$)") == "R$"
|
| 296 |
+
assert gradio_app._currency_preset_for_symbol("€") == "European Union - EUR (€)"
|
| 297 |
+
assert "Amit" in gradio_app._run_voice_command("show Amit", db_path)
|
| 298 |
+
assert "onions" in gradio_app._run_voice_command("stock onions", db_path)
|
| 299 |
+
assert "Daily Closeout Ready" in gradio_app._run_voice_command("close today", db_path)
|
| 300 |
+
|
| 301 |
+
|
| 302 |
def test_insight_coach_surfaces_credit_and_stock_actions(tmp_path) -> None:
|
| 303 |
db_path = tmp_path / "voiceledger.sqlite3"
|
| 304 |
gradio_app.add_transaction(gradio_app.local_parse_transaction("Amit owes 100"), db_path)
|
tests/test_whatsapp_summary.py
CHANGED
|
@@ -35,3 +35,19 @@ def test_generate_whatsapp_summary_handles_empty_data(tmp_path: Path) -> None:
|
|
| 35 |
assert "Outstanding Credit: ₹0" in summary
|
| 36 |
assert "Top Product: None" in summary
|
| 37 |
assert "Low Stock: None" in summary
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
assert "Outstanding Credit: ₹0" in summary
|
| 36 |
assert "Top Product: None" in summary
|
| 37 |
assert "Low Stock: None" in summary
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def test_generate_whatsapp_summary_supports_language_labels(tmp_path: Path) -> None:
|
| 41 |
+
db_path = tmp_path / "voiceledger.sqlite3"
|
| 42 |
+
add_transaction(parse_transaction("Vendí 12 mangos, 20 cada uno"), db_path)
|
| 43 |
+
|
| 44 |
+
spanish = generate_whatsapp_summary(db_path=db_path, language="Spanish", currency_symbol="$")
|
| 45 |
+
french = generate_whatsapp_summary(db_path=db_path, language="French", currency_symbol="€")
|
| 46 |
+
portuguese = generate_whatsapp_summary(db_path=db_path, language="Portuguese", currency_symbol="R$")
|
| 47 |
+
|
| 48 |
+
assert "Resumen Diario" in spanish
|
| 49 |
+
assert "Ventas: $240" in spanish
|
| 50 |
+
assert "Resume Quotidien" in french
|
| 51 |
+
assert "Ventes: €240" in french
|
| 52 |
+
assert "Resumo Diario" in portuguese
|
| 53 |
+
assert "Vendas: R$240" in portuguese
|
voiceledger/ledger/corrections.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Correction log persistence for parsed transaction review edits."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
import sqlite3
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Any
|
| 10 |
+
|
| 11 |
+
import pandas as pd
|
| 12 |
+
|
| 13 |
+
from voiceledger.config import get_database_path
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
CORRECTION_SCHEMA_SQL = """
|
| 17 |
+
CREATE TABLE IF NOT EXISTS correction_log (
|
| 18 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 19 |
+
original_payload TEXT NOT NULL,
|
| 20 |
+
corrected_payload TEXT NOT NULL,
|
| 21 |
+
changed_fields TEXT NOT NULL,
|
| 22 |
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
| 23 |
+
);
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
CORRECTION_COLUMNS = ["id", "changed_fields", "original_payload", "corrected_payload", "created_at"]
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def initialize_correction_log_table(db_path: str | Path | None = None) -> Path:
|
| 30 |
+
"""Create the correction log table if needed."""
|
| 31 |
+
path = _resolve_db_path(db_path)
|
| 32 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 33 |
+
with sqlite3.connect(path) as connection:
|
| 34 |
+
connection.execute(CORRECTION_SCHEMA_SQL)
|
| 35 |
+
connection.commit()
|
| 36 |
+
return path
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def record_correction(
|
| 40 |
+
original_payload: dict[str, Any],
|
| 41 |
+
corrected_payload: dict[str, Any],
|
| 42 |
+
db_path: str | Path | None = None,
|
| 43 |
+
) -> int | None:
|
| 44 |
+
"""Persist a correction when review edits changed transaction fields."""
|
| 45 |
+
changed_fields = _changed_fields(original_payload, corrected_payload)
|
| 46 |
+
if not changed_fields:
|
| 47 |
+
return None
|
| 48 |
+
|
| 49 |
+
path = initialize_correction_log_table(db_path)
|
| 50 |
+
with sqlite3.connect(path) as connection:
|
| 51 |
+
cursor = connection.execute(
|
| 52 |
+
"""
|
| 53 |
+
INSERT INTO correction_log (
|
| 54 |
+
original_payload,
|
| 55 |
+
corrected_payload,
|
| 56 |
+
changed_fields,
|
| 57 |
+
created_at
|
| 58 |
+
)
|
| 59 |
+
VALUES (?, ?, ?, ?)
|
| 60 |
+
""",
|
| 61 |
+
(
|
| 62 |
+
json.dumps(original_payload, sort_keys=True),
|
| 63 |
+
json.dumps(corrected_payload, sort_keys=True),
|
| 64 |
+
", ".join(changed_fields),
|
| 65 |
+
datetime.now().isoformat(sep=" ", timespec="seconds"),
|
| 66 |
+
),
|
| 67 |
+
)
|
| 68 |
+
connection.commit()
|
| 69 |
+
return int(cursor.lastrowid)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def get_correction_log(db_path: str | Path | None = None) -> pd.DataFrame:
|
| 73 |
+
"""Return saved parse correction rows."""
|
| 74 |
+
path = initialize_correction_log_table(db_path)
|
| 75 |
+
with sqlite3.connect(path) as connection:
|
| 76 |
+
rows = connection.execute(
|
| 77 |
+
"""
|
| 78 |
+
SELECT id, changed_fields, original_payload, corrected_payload, created_at
|
| 79 |
+
FROM correction_log
|
| 80 |
+
ORDER BY id DESC
|
| 81 |
+
"""
|
| 82 |
+
).fetchall()
|
| 83 |
+
records = [dict(zip(CORRECTION_COLUMNS, row, strict=True)) for row in rows]
|
| 84 |
+
return pd.DataFrame.from_records(records, columns=CORRECTION_COLUMNS)
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def _changed_fields(original_payload: dict[str, Any], corrected_payload: dict[str, Any]) -> list[str]:
|
| 88 |
+
"""Return fields whose values changed after review editing."""
|
| 89 |
+
tracked_fields = (
|
| 90 |
+
"transaction_type",
|
| 91 |
+
"item",
|
| 92 |
+
"quantity",
|
| 93 |
+
"unit_price",
|
| 94 |
+
"amount",
|
| 95 |
+
"customer",
|
| 96 |
+
"payment_status",
|
| 97 |
+
"notes",
|
| 98 |
+
"confidence",
|
| 99 |
+
)
|
| 100 |
+
return [field for field in tracked_fields if original_payload.get(field) != corrected_payload.get(field)]
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def _resolve_db_path(db_path: str | Path | None) -> Path:
|
| 104 |
+
"""Resolve an explicit or configured database path."""
|
| 105 |
+
if db_path is None:
|
| 106 |
+
return get_database_path()
|
| 107 |
+
return Path(db_path).expanduser()
|
voiceledger/ledger/database.py
CHANGED
|
@@ -11,6 +11,7 @@ from typing import Any
|
|
| 11 |
import pandas as pd
|
| 12 |
|
| 13 |
from voiceledger.config import get_database_path
|
|
|
|
| 14 |
from voiceledger.ledger.customers import add_credit, initialize_customers_table, record_payment
|
| 15 |
from voiceledger.ledger.inventory import add_stock, initialize_inventory_table, remove_stock
|
| 16 |
from voiceledger.ledger.settings import initialize_business_settings_table
|
|
@@ -60,6 +61,7 @@ def initialize_database(db_path: str | Path | None = None) -> Path:
|
|
| 60 |
initialize_customers_table(path)
|
| 61 |
initialize_inventory_table(path)
|
| 62 |
initialize_business_settings_table(path)
|
|
|
|
| 63 |
return path
|
| 64 |
|
| 65 |
|
|
|
|
| 11 |
import pandas as pd
|
| 12 |
|
| 13 |
from voiceledger.config import get_database_path
|
| 14 |
+
from voiceledger.ledger.corrections import initialize_correction_log_table
|
| 15 |
from voiceledger.ledger.customers import add_credit, initialize_customers_table, record_payment
|
| 16 |
from voiceledger.ledger.inventory import add_stock, initialize_inventory_table, remove_stock
|
| 17 |
from voiceledger.ledger.settings import initialize_business_settings_table
|
|
|
|
| 61 |
initialize_customers_table(path)
|
| 62 |
initialize_inventory_table(path)
|
| 63 |
initialize_business_settings_table(path)
|
| 64 |
+
initialize_correction_log_table(path)
|
| 65 |
return path
|
| 66 |
|
| 67 |
|
voiceledger/reports/whatsapp_summary.py
CHANGED
|
@@ -23,8 +23,10 @@ def generate_whatsapp_summary(
|
|
| 23 |
low_stock_threshold: float = 5,
|
| 24 |
business_name: str = "VoiceLedger",
|
| 25 |
currency_symbol: str = "₹",
|
|
|
|
| 26 |
) -> str:
|
| 27 |
"""Generate a concise daily summary suitable for WhatsApp sharing."""
|
|
|
|
| 28 |
sales = calculate_daily_sales(db_path=db_path, report_date=report_date)
|
| 29 |
expenses = calculate_daily_expenses(db_path=db_path, report_date=report_date)
|
| 30 |
profit = calculate_net_profit(db_path=db_path, report_date=report_date)
|
|
@@ -34,16 +36,16 @@ def generate_whatsapp_summary(
|
|
| 34 |
|
| 35 |
return "\n".join(
|
| 36 |
[
|
| 37 |
-
f"{business_name}
|
| 38 |
"",
|
| 39 |
-
f"
|
| 40 |
-
f"
|
| 41 |
-
f"
|
| 42 |
"",
|
| 43 |
-
f"
|
| 44 |
"",
|
| 45 |
-
f"
|
| 46 |
-
f"
|
| 47 |
]
|
| 48 |
)
|
| 49 |
|
|
@@ -68,3 +70,47 @@ def _format_money(value: float, currency_symbol: str) -> str:
|
|
| 68 |
if amount.is_integer():
|
| 69 |
return f"{currency_symbol}{int(amount)}"
|
| 70 |
return f"{currency_symbol}{amount:,.2f}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
low_stock_threshold: float = 5,
|
| 24 |
business_name: str = "VoiceLedger",
|
| 25 |
currency_symbol: str = "₹",
|
| 26 |
+
language: str = "English",
|
| 27 |
) -> str:
|
| 28 |
"""Generate a concise daily summary suitable for WhatsApp sharing."""
|
| 29 |
+
labels = _summary_labels(language)
|
| 30 |
sales = calculate_daily_sales(db_path=db_path, report_date=report_date)
|
| 31 |
expenses = calculate_daily_expenses(db_path=db_path, report_date=report_date)
|
| 32 |
profit = calculate_net_profit(db_path=db_path, report_date=report_date)
|
|
|
|
| 36 |
|
| 37 |
return "\n".join(
|
| 38 |
[
|
| 39 |
+
f"{business_name} {labels['daily_summary']}",
|
| 40 |
"",
|
| 41 |
+
f"{labels['sales']}: {_format_money(sales, currency_symbol)}",
|
| 42 |
+
f"{labels['expenses']}: {_format_money(expenses, currency_symbol)}",
|
| 43 |
+
f"{labels['profit']}: {_format_money(profit, currency_symbol)}",
|
| 44 |
"",
|
| 45 |
+
f"{labels['outstanding_credit']}: {_format_money(credit, currency_symbol)}",
|
| 46 |
"",
|
| 47 |
+
f"{labels['top_product']}: {top_product}",
|
| 48 |
+
f"{labels['low_stock']}: {low_stock}",
|
| 49 |
]
|
| 50 |
)
|
| 51 |
|
|
|
|
| 70 |
if amount.is_integer():
|
| 71 |
return f"{currency_symbol}{int(amount)}"
|
| 72 |
return f"{currency_symbol}{amount:,.2f}"
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _summary_labels(language: str) -> dict[str, str]:
|
| 76 |
+
"""Return localized labels for a WhatsApp daily summary."""
|
| 77 |
+
normalized = (language or "English").strip().lower()
|
| 78 |
+
labels = {
|
| 79 |
+
"english": {
|
| 80 |
+
"daily_summary": "Daily Summary",
|
| 81 |
+
"sales": "Sales",
|
| 82 |
+
"expenses": "Expenses",
|
| 83 |
+
"profit": "Profit",
|
| 84 |
+
"outstanding_credit": "Outstanding Credit",
|
| 85 |
+
"top_product": "Top Product",
|
| 86 |
+
"low_stock": "Low Stock",
|
| 87 |
+
},
|
| 88 |
+
"spanish": {
|
| 89 |
+
"daily_summary": "Resumen Diario",
|
| 90 |
+
"sales": "Ventas",
|
| 91 |
+
"expenses": "Gastos",
|
| 92 |
+
"profit": "Ganancia",
|
| 93 |
+
"outstanding_credit": "Credito Pendiente",
|
| 94 |
+
"top_product": "Producto Principal",
|
| 95 |
+
"low_stock": "Bajo Stock",
|
| 96 |
+
},
|
| 97 |
+
"french": {
|
| 98 |
+
"daily_summary": "Resume Quotidien",
|
| 99 |
+
"sales": "Ventes",
|
| 100 |
+
"expenses": "Depenses",
|
| 101 |
+
"profit": "Profit",
|
| 102 |
+
"outstanding_credit": "Credit En Attente",
|
| 103 |
+
"top_product": "Meilleur Produit",
|
| 104 |
+
"low_stock": "Stock Bas",
|
| 105 |
+
},
|
| 106 |
+
"portuguese": {
|
| 107 |
+
"daily_summary": "Resumo Diario",
|
| 108 |
+
"sales": "Vendas",
|
| 109 |
+
"expenses": "Despesas",
|
| 110 |
+
"profit": "Lucro",
|
| 111 |
+
"outstanding_credit": "Credito Pendente",
|
| 112 |
+
"top_product": "Produto Principal",
|
| 113 |
+
"low_stock": "Estoque Baixo",
|
| 114 |
+
},
|
| 115 |
+
}
|
| 116 |
+
return labels.get(normalized, labels["english"])
|
voiceledger/ui/gradio_app.py
CHANGED
|
@@ -28,6 +28,7 @@ from voiceledger.ledger.analytics import (
|
|
| 28 |
top_selling_items,
|
| 29 |
)
|
| 30 |
from voiceledger.ledger.customers import get_customer_balances
|
|
|
|
| 31 |
from voiceledger.ledger.database import (
|
| 32 |
add_transaction,
|
| 33 |
delete_transaction,
|
|
@@ -68,6 +69,16 @@ LANGUAGE_STYLE_CHOICES = [
|
|
| 68 |
"Portuguese",
|
| 69 |
"Multilingual",
|
| 70 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
DEMO_NOTES = [
|
| 72 |
"Bought 60 mangoes",
|
| 73 |
"Sold 12 mangoes, 20 each",
|
|
@@ -128,11 +139,22 @@ def create_app(db_path: str | Path | None = None) -> gr.Blocks:
|
|
| 128 |
value=initial_settings["business_name"],
|
| 129 |
elem_classes="vl-panel",
|
| 130 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
currency_symbol_input = gr.Textbox(
|
| 132 |
label="Currency label",
|
| 133 |
value=initial_settings["currency_symbol"],
|
| 134 |
elem_classes="vl-panel",
|
| 135 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
with gr.Row():
|
| 137 |
low_stock_threshold_input = gr.Number(
|
| 138 |
label="Low-stock threshold",
|
|
@@ -186,6 +208,15 @@ def create_app(db_path: str | Path | None = None) -> gr.Blocks:
|
|
| 186 |
gr.HTML(
|
| 187 |
_multilingual_examples_panel()
|
| 188 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
with gr.Row(elem_classes="vl-example-row"):
|
| 190 |
example_sale_button = gr.Button("Try sale")
|
| 191 |
example_expense_button = gr.Button("Try expense")
|
|
@@ -298,6 +329,11 @@ def create_app(db_path: str | Path | None = None) -> gr.Blocks:
|
|
| 298 |
inputs=None,
|
| 299 |
outputs=[note_input, record_demo_status],
|
| 300 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
apply_review_edits_button.click(
|
| 302 |
fn=lambda parsed, transaction_type, item, quantity, unit_price, amount, customer, payment_status, notes, confidence: _apply_review_edits(
|
| 303 |
parsed,
|
|
@@ -505,6 +541,21 @@ def create_app(db_path: str | Path | None = None) -> gr.Blocks:
|
|
| 505 |
value=_field_test_summary(initial_settings),
|
| 506 |
elem_classes="vl-status",
|
| 507 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 508 |
|
| 509 |
with gr.Column(visible=False, elem_classes="vl-page-section") as bulk_page:
|
| 510 |
gr.HTML('<div id="vl-page-bulk" class="vl-page-anchor"></div>')
|
|
@@ -700,6 +751,12 @@ def create_app(db_path: str | Path | None = None) -> gr.Blocks:
|
|
| 700 |
)
|
| 701 |
gr.HTML(_section_heading("WhatsApp Summary"))
|
| 702 |
generate_whatsapp_button = gr.Button("Generate WhatsApp Summary")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 703 |
whatsapp_summary_output = gr.Textbox(
|
| 704 |
label="WhatsApp Summary",
|
| 705 |
lines=10,
|
|
@@ -717,8 +774,8 @@ def create_app(db_path: str | Path | None = None) -> gr.Blocks:
|
|
| 717 |
"""
|
| 718 |
)
|
| 719 |
generate_whatsapp_button.click(
|
| 720 |
-
fn=lambda: _generate_whatsapp_summary(db_path),
|
| 721 |
-
inputs=
|
| 722 |
outputs=whatsapp_summary_output,
|
| 723 |
)
|
| 724 |
|
|
@@ -1249,6 +1306,20 @@ def _ai_mode_status(ai_mode: str | None) -> str:
|
|
| 1249 |
"""
|
| 1250 |
|
| 1251 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1252 |
def _demo_health_placeholder() -> pd.DataFrame:
|
| 1253 |
"""Return placeholder health rows so the demo health table is never blank."""
|
| 1254 |
return pd.DataFrame(
|
|
@@ -1395,6 +1466,7 @@ def _apply_review_edits(
|
|
| 1395 |
confidence=confidence,
|
| 1396 |
)
|
| 1397 |
payload = transaction.model_dump()
|
|
|
|
| 1398 |
warnings = _review_warnings(transaction, db_path)
|
| 1399 |
status = _status_message(transaction, "Review updated from your edits.", warnings=warnings)
|
| 1400 |
return payload, payload, status, _review_card(transaction, "Review updated from your edits.", warnings)
|
|
@@ -1946,6 +2018,36 @@ def _get_inventory_detail(item: str | None, db_path: str | Path | None) -> tuple
|
|
| 1946 |
return summary, matches[columns].reset_index(drop=True)
|
| 1947 |
|
| 1948 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1949 |
def _generate_daily_summary_report(db_path: str | Path | None) -> tuple[str | None, str]:
|
| 1950 |
"""Generate the Daily Summary PDF for download in Gradio."""
|
| 1951 |
settings = get_business_settings(db_path)
|
|
@@ -2093,7 +2195,7 @@ def _metric_card(label: str, value: str, note: str, profit: float | None = None)
|
|
| 2093 |
"""
|
| 2094 |
|
| 2095 |
|
| 2096 |
-
def _generate_whatsapp_summary(db_path: str | Path | None) -> str:
|
| 2097 |
"""Generate a WhatsApp summary using seller settings."""
|
| 2098 |
settings = get_business_settings(db_path)
|
| 2099 |
return generate_whatsapp_summary(
|
|
@@ -2101,6 +2203,7 @@ def _generate_whatsapp_summary(db_path: str | Path | None) -> str:
|
|
| 2101 |
low_stock_threshold=get_low_stock_threshold(db_path),
|
| 2102 |
business_name=settings["business_name"],
|
| 2103 |
currency_symbol=settings["currency_symbol"],
|
|
|
|
| 2104 |
)
|
| 2105 |
|
| 2106 |
|
|
@@ -2327,6 +2430,7 @@ def _status_message(
|
|
| 2327 |
"""Return a human-readable parsing status."""
|
| 2328 |
parts = []
|
| 2329 |
parts.append(_source_chip(source_message, fallback_reason))
|
|
|
|
| 2330 |
if prefix:
|
| 2331 |
parts.append(prefix)
|
| 2332 |
parts.append(source_message)
|
|
@@ -2351,6 +2455,29 @@ def _source_chip(source_message: str, fallback_reason: str | None = None) -> str
|
|
| 2351 |
return '<span class="vl-status-chip">Parsed</span>'
|
| 2352 |
|
| 2353 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2354 |
def _transcription_status(result: modal_api.TranscriptionResult) -> str:
|
| 2355 |
"""Return a concise transcription source status."""
|
| 2356 |
status = result.message
|
|
|
|
| 28 |
top_selling_items,
|
| 29 |
)
|
| 30 |
from voiceledger.ledger.customers import get_customer_balances
|
| 31 |
+
from voiceledger.ledger.corrections import get_correction_log, record_correction
|
| 32 |
from voiceledger.ledger.database import (
|
| 33 |
add_transaction,
|
| 34 |
delete_transaction,
|
|
|
|
| 69 |
"Portuguese",
|
| 70 |
"Multilingual",
|
| 71 |
]
|
| 72 |
+
WHATSAPP_LANGUAGE_CHOICES = ["English", "Spanish", "French", "Portuguese"]
|
| 73 |
+
CURRENCY_PRESETS = {
|
| 74 |
+
"India - INR (₹)": "₹",
|
| 75 |
+
"United States - USD ($)": "$",
|
| 76 |
+
"European Union - EUR (€)": "€",
|
| 77 |
+
"United Kingdom - GBP (£)": "£",
|
| 78 |
+
"Mexico - MXN ($)": "$",
|
| 79 |
+
"Brazil - BRL (R$)": "R$",
|
| 80 |
+
"Custom": "",
|
| 81 |
+
}
|
| 82 |
DEMO_NOTES = [
|
| 83 |
"Bought 60 mangoes",
|
| 84 |
"Sold 12 mangoes, 20 each",
|
|
|
|
| 139 |
value=initial_settings["business_name"],
|
| 140 |
elem_classes="vl-panel",
|
| 141 |
)
|
| 142 |
+
currency_preset_input = gr.Dropdown(
|
| 143 |
+
choices=list(CURRENCY_PRESETS.keys()),
|
| 144 |
+
label="Currency preset",
|
| 145 |
+
value=_currency_preset_for_symbol(initial_settings["currency_symbol"]),
|
| 146 |
+
elem_classes="vl-panel",
|
| 147 |
+
)
|
| 148 |
currency_symbol_input = gr.Textbox(
|
| 149 |
label="Currency label",
|
| 150 |
value=initial_settings["currency_symbol"],
|
| 151 |
elem_classes="vl-panel",
|
| 152 |
)
|
| 153 |
+
currency_preset_input.change(
|
| 154 |
+
fn=_currency_symbol_for_preset,
|
| 155 |
+
inputs=currency_preset_input,
|
| 156 |
+
outputs=currency_symbol_input,
|
| 157 |
+
)
|
| 158 |
with gr.Row():
|
| 159 |
low_stock_threshold_input = gr.Number(
|
| 160 |
label="Low-stock threshold",
|
|
|
|
| 208 |
gr.HTML(
|
| 209 |
_multilingual_examples_panel()
|
| 210 |
)
|
| 211 |
+
gr.HTML(_section_heading("Voice Command Shortcuts"))
|
| 212 |
+
with gr.Row():
|
| 213 |
+
command_input = gr.Textbox(
|
| 214 |
+
label="Command",
|
| 215 |
+
placeholder="close today, show Amit, stock mangoes",
|
| 216 |
+
elem_classes="vl-panel",
|
| 217 |
+
)
|
| 218 |
+
command_button = gr.Button("Run Command")
|
| 219 |
+
command_output = gr.HTML(_empty_detail_card("Command result", "Try close today, show Amit, or stock mangoes."))
|
| 220 |
with gr.Row(elem_classes="vl-example-row"):
|
| 221 |
example_sale_button = gr.Button("Try sale")
|
| 222 |
example_expense_button = gr.Button("Try expense")
|
|
|
|
| 329 |
inputs=None,
|
| 330 |
outputs=[note_input, record_demo_status],
|
| 331 |
)
|
| 332 |
+
command_button.click(
|
| 333 |
+
fn=lambda command: _run_voice_command(command, db_path),
|
| 334 |
+
inputs=command_input,
|
| 335 |
+
outputs=command_output,
|
| 336 |
+
)
|
| 337 |
apply_review_edits_button.click(
|
| 338 |
fn=lambda parsed, transaction_type, item, quantity, unit_price, amount, customer, payment_status, notes, confidence: _apply_review_edits(
|
| 339 |
parsed,
|
|
|
|
| 541 |
value=_field_test_summary(initial_settings),
|
| 542 |
elem_classes="vl-status",
|
| 543 |
)
|
| 544 |
+
gr.HTML(_section_heading("Mistake Log"))
|
| 545 |
+
refresh_corrections_button = gr.Button("Refresh Correction Log")
|
| 546 |
+
correction_log_output = gr.Dataframe(
|
| 547 |
+
value=get_correction_log(db_path),
|
| 548 |
+
headers=["id", "changed_fields", "original_payload", "corrected_payload", "created_at"],
|
| 549 |
+
label="Corrections made before save",
|
| 550 |
+
interactive=False,
|
| 551 |
+
wrap=True,
|
| 552 |
+
elem_classes="vl-panel",
|
| 553 |
+
)
|
| 554 |
+
refresh_corrections_button.click(
|
| 555 |
+
fn=lambda: get_correction_log(db_path),
|
| 556 |
+
inputs=None,
|
| 557 |
+
outputs=correction_log_output,
|
| 558 |
+
)
|
| 559 |
|
| 560 |
with gr.Column(visible=False, elem_classes="vl-page-section") as bulk_page:
|
| 561 |
gr.HTML('<div id="vl-page-bulk" class="vl-page-anchor"></div>')
|
|
|
|
| 751 |
)
|
| 752 |
gr.HTML(_section_heading("WhatsApp Summary"))
|
| 753 |
generate_whatsapp_button = gr.Button("Generate WhatsApp Summary")
|
| 754 |
+
whatsapp_language_input = gr.Dropdown(
|
| 755 |
+
choices=WHATSAPP_LANGUAGE_CHOICES,
|
| 756 |
+
label="Summary language",
|
| 757 |
+
value="English",
|
| 758 |
+
elem_classes="vl-panel",
|
| 759 |
+
)
|
| 760 |
whatsapp_summary_output = gr.Textbox(
|
| 761 |
label="WhatsApp Summary",
|
| 762 |
lines=10,
|
|
|
|
| 774 |
"""
|
| 775 |
)
|
| 776 |
generate_whatsapp_button.click(
|
| 777 |
+
fn=lambda language: _generate_whatsapp_summary(db_path, language),
|
| 778 |
+
inputs=whatsapp_language_input,
|
| 779 |
outputs=whatsapp_summary_output,
|
| 780 |
)
|
| 781 |
|
|
|
|
| 1306 |
"""
|
| 1307 |
|
| 1308 |
|
| 1309 |
+
def _currency_symbol_for_preset(preset: str | None) -> str:
|
| 1310 |
+
"""Return the currency symbol for a selected preset."""
|
| 1311 |
+
return CURRENCY_PRESETS.get(preset or "Custom", "")
|
| 1312 |
+
|
| 1313 |
+
|
| 1314 |
+
def _currency_preset_for_symbol(symbol: str | None) -> str:
|
| 1315 |
+
"""Return a likely currency preset for an existing symbol."""
|
| 1316 |
+
cleaned = (symbol or "").strip()
|
| 1317 |
+
for preset, preset_symbol in CURRENCY_PRESETS.items():
|
| 1318 |
+
if preset != "Custom" and preset_symbol == cleaned:
|
| 1319 |
+
return preset
|
| 1320 |
+
return "Custom"
|
| 1321 |
+
|
| 1322 |
+
|
| 1323 |
def _demo_health_placeholder() -> pd.DataFrame:
|
| 1324 |
"""Return placeholder health rows so the demo health table is never blank."""
|
| 1325 |
return pd.DataFrame(
|
|
|
|
| 1466 |
confidence=confidence,
|
| 1467 |
)
|
| 1468 |
payload = transaction.model_dump()
|
| 1469 |
+
record_correction(transaction_payload, payload, db_path)
|
| 1470 |
warnings = _review_warnings(transaction, db_path)
|
| 1471 |
status = _status_message(transaction, "Review updated from your edits.", warnings=warnings)
|
| 1472 |
return payload, payload, status, _review_card(transaction, "Review updated from your edits.", warnings)
|
|
|
|
| 2018 |
return summary, matches[columns].reset_index(drop=True)
|
| 2019 |
|
| 2020 |
|
| 2021 |
+
def _run_voice_command(command: str | None, db_path: str | Path | None) -> str:
|
| 2022 |
+
"""Run a lightweight text/voice command shortcut."""
|
| 2023 |
+
cleaned = " ".join(str(command or "").split()).strip()
|
| 2024 |
+
if not cleaned:
|
| 2025 |
+
return _empty_detail_card("Command result", "Try close today, show Amit, or stock mangoes.")
|
| 2026 |
+
|
| 2027 |
+
lowered = cleaned.lower()
|
| 2028 |
+
if lowered in {"close today", "daily closeout", "closeout", "summary"}:
|
| 2029 |
+
summary, pdf_path, csv_path, whatsapp, status = _run_daily_closeout(db_path)
|
| 2030 |
+
file_note = f"PDF: {pdf_path or 'needs attention'}. CSV: {csv_path or 'needs attention'}."
|
| 2031 |
+
return f"{summary}<section class=\"vl-detail-card\"><h2>Command result</h2><p>{escape(status)} {escape(file_note)}</p><pre>{escape(whatsapp)}</pre></section>"
|
| 2032 |
+
|
| 2033 |
+
if lowered.startswith(("show ", "customer ")):
|
| 2034 |
+
customer = cleaned.split(" ", 1)[1] if " " in cleaned else ""
|
| 2035 |
+
summary, _ = _get_customer_detail(customer, db_path)
|
| 2036 |
+
return summary
|
| 2037 |
+
|
| 2038 |
+
if lowered.startswith(("stock ", "inventory ")):
|
| 2039 |
+
item = cleaned.split(" ", 1)[1] if " " in cleaned else ""
|
| 2040 |
+
summary, _ = _get_inventory_detail(item, db_path)
|
| 2041 |
+
return summary
|
| 2042 |
+
|
| 2043 |
+
parsed = local_parse_transaction(cleaned)
|
| 2044 |
+
if parsed.transaction_type != "unknown":
|
| 2045 |
+
warnings = _review_warnings(parsed, db_path)
|
| 2046 |
+
return _review_card(parsed, "Command parsed as a transaction. Copy it into the transaction note to save.", warnings)
|
| 2047 |
+
|
| 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)
|
|
|
|
| 2195 |
"""
|
| 2196 |
|
| 2197 |
|
| 2198 |
+
def _generate_whatsapp_summary(db_path: str | Path | None, language: str = "English") -> str:
|
| 2199 |
"""Generate a WhatsApp summary using seller settings."""
|
| 2200 |
settings = get_business_settings(db_path)
|
| 2201 |
return generate_whatsapp_summary(
|
|
|
|
| 2203 |
low_stock_threshold=get_low_stock_threshold(db_path),
|
| 2204 |
business_name=settings["business_name"],
|
| 2205 |
currency_symbol=settings["currency_symbol"],
|
| 2206 |
+
language=language,
|
| 2207 |
)
|
| 2208 |
|
| 2209 |
|
|
|
|
| 2430 |
"""Return a human-readable parsing status."""
|
| 2431 |
parts = []
|
| 2432 |
parts.append(_source_chip(source_message, fallback_reason))
|
| 2433 |
+
parts.append(_language_confidence_chip(transaction))
|
| 2434 |
if prefix:
|
| 2435 |
parts.append(prefix)
|
| 2436 |
parts.append(source_message)
|
|
|
|
| 2455 |
return '<span class="vl-status-chip">Parsed</span>'
|
| 2456 |
|
| 2457 |
|
| 2458 |
+
def _language_confidence_chip(transaction: Transaction) -> str:
|
| 2459 |
+
"""Return a compact language/confidence chip for parse status."""
|
| 2460 |
+
language = _detect_note_language(transaction.notes)
|
| 2461 |
+
confidence_label = "High confidence" if transaction.confidence >= 0.85 else "Needs review"
|
| 2462 |
+
return f'<span class="vl-status-chip vl-status-chip-language">{escape(language)} · {confidence_label}</span>'
|
| 2463 |
+
|
| 2464 |
+
|
| 2465 |
+
def _detect_note_language(note: str | None) -> str:
|
| 2466 |
+
"""Detect a lightweight language label from common seller note phrases."""
|
| 2467 |
+
text = (note or "").lower()
|
| 2468 |
+
if any(token in text for token in ("vendí", "pagué", " debe ", " pagó", " cada uno", "suministros")):
|
| 2469 |
+
return "Spanish"
|
| 2470 |
+
if any(token in text for token in ("vendu", "payé", " doit ", " a payé", "chacun", "fournitures", "acheté")):
|
| 2471 |
+
return "French"
|
| 2472 |
+
if any(token in text for token in ("vendi", "paguei", " deve ", "pagou", "suprimentos", "comprei")):
|
| 2473 |
+
return "Portuguese"
|
| 2474 |
+
if any(token in text for token in ("dene hai", "dena hai", "diya", "kharida", "chukaya")):
|
| 2475 |
+
return "Hinglish"
|
| 2476 |
+
if any(token in text for token in ("lidha", "aapva che", "apvana che", "aapya")):
|
| 2477 |
+
return "Gujarati-lite"
|
| 2478 |
+
return "English"
|
| 2479 |
+
|
| 2480 |
+
|
| 2481 |
def _transcription_status(result: modal_api.TranscriptionResult) -> str:
|
| 2482 |
"""Return a concise transcription source status."""
|
| 2483 |
status = result.message
|
voiceledger/ui/theme.py
CHANGED
|
@@ -637,6 +637,23 @@ body,
|
|
| 637 |
color: #5f370e !important;
|
| 638 |
}
|
| 639 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 640 |
.vl-chip {
|
| 641 |
background: #2c2926 !important;
|
| 642 |
border-radius: 6px !important;
|
|
|
|
| 637 |
color: #5f370e !important;
|
| 638 |
}
|
| 639 |
|
| 640 |
+
.vl-status-chip-language {
|
| 641 |
+
background: #f0eaff !important;
|
| 642 |
+
border-color: #bda8ef !important;
|
| 643 |
+
color: #43236d !important;
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
.vl-detail-card pre {
|
| 647 |
+
background: #f6f7f2 !important;
|
| 648 |
+
border: 1px solid var(--vl-border) !important;
|
| 649 |
+
border-radius: 10px !important;
|
| 650 |
+
color: var(--vl-text) !important;
|
| 651 |
+
font-size: 13px !important;
|
| 652 |
+
overflow-x: auto !important;
|
| 653 |
+
padding: 10px !important;
|
| 654 |
+
white-space: pre-wrap !important;
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
.vl-chip {
|
| 658 |
background: #2c2926 !important;
|
| 659 |
border-radius: 6px !important;
|