Sagar Patel commited on
Commit
66cb625
·
1 Parent(s): fb99e77

Add guided judge polish

Browse files
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
- - `Inventory` to show stock, a selected item detail view, and the reorder list
 
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. 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,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
- - 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.
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 correction log shows edited review fields.
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="Low-stock reorder list",
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: generate_reorder_list(db_path),
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: _save_field_notes(who, tried, changed, db_path),
1053
- inputs=[field_who_input, field_tried_input, field_changed_input],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1054
  outputs=field_notes_status_output,
1055
  )
1056
  save_field_test_button.click(
1057
- fn=lambda checklist, who, tried, changed: _save_field_test_evidence(checklist, who, tried, changed, db_path),
1058
- inputs=[field_test_checklist, field_test_who_input, field_test_tried_input, field_test_changed_input],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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(who: str, tried: str, changed: str, db_path: str | Path | None) -> str:
 
 
 
 
 
 
 
 
 
 
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']} Changed: {settings['field_test_changed']}"
 
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;