fix: create expense report immediately — remove broken confirmation gate

The old flow required a "confirm" reply after showing a parsed-receipt
table, but that follow-up dispatch call carries no receipts (they only
exist in the /upload context). The confirmation gate was architecturally
broken: the second turn would always create nothing.

Fix: create the expense sheet immediately when receipts are present.
Byte-exact and semantic duplicates are auto-skipped; the count of
skipped items is reported in the success message. The report is always
created in Odoo as a draft so users can review amounts and submit
manually via Odoo > Expenses.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Carlos Garcia
2026-05-19 20:58:47 -04:00
parent 8d1727b498
commit 0bd1810405

View File

@@ -186,18 +186,14 @@ class ExpensesAgent(BaseAgent):
else:
deduped.append((receipt, parsed))
# Always show confirmation summary before creating — lets user verify
# parsed amounts and review flagged duplicates in one step.
if not user_confirmed:
self._gathered_data['mode'] = 'awaiting_confirmation'
self._confirmation_items = [
(receipt, parsed, i in dup_indices)
for i, (receipt, parsed) in enumerate(paired)
]
self._deduped = deduped
return []
# User confirmed — apply dup decision
# Auto-skip semantic duplicates by default; keep_all only if user explicitly asked.
# Receipts are only available in this single /upload request — there is no
# persistent receipt store across turns, so a "confirm then create" flow would
# always fail on the follow-up turn (no receipts in context). Creating
# immediately in draft state is the correct approach: users review and
# submit inside Odoo > Expenses.
n_skipped = len(paired) - len(deduped)
self._gathered_data['n_skipped'] = n_skipped
final_list = paired if user_dup_decision == 'keep_all' else deduped
sheet_name = f'Expense Report - {_date.today().isoformat()}'
@@ -384,44 +380,15 @@ class ExpensesAgent(BaseAgent):
data = self._gathered_data
directive_id = self._directive.directive_id if self._directive else ''
if data.get('mode') == 'awaiting_confirmation':
items = getattr(self, '_confirmation_items', [])
n_dups = sum(1 for _, _, is_dup in items if is_dup)
lines = [f'I parsed {len(items)} receipt(s). Please review before I create the expense report:\n']
lines.append(f' {"#":>3} {"Vendor":<30} {"Amount":>8} {"Date":<12}')
lines.append(f' {"---":>3} {"-"*30} {"-"*8} {"-"*12}')
for i, (receipt, parsed, is_dup) in enumerate(items, 1):
vendor = str(parsed.get('vendor') or receipt.get('filename', '?'))[:30]
amt = float(parsed.get('amount') or 0)
dt = str(parsed.get('date') or '')
flag = ' !! duplicate' if is_dup else ''
lines.append(f' {i:>3}. {vendor:<30} ${amt:>7.2f} {dt}{flag}')
lines.append('')
if n_dups:
lines.append(
f'{n_dups} item(s) marked "!! duplicate" appear to be the same receipt '
f'as another entry (possibly an OCR amount mismatch).'
)
lines.append(
'Reply "confirm" to create the report and exclude duplicates (recommended).'
)
lines.append(
'Reply "confirm, keep all" to include every item even if duplicated.'
)
else:
lines.append('Reply "confirm" to create the expense report.')
return AgentReport(
directive_id=directive_id, agent=self.name, status='complete',
summary='\n'.join(lines), data=data,
escalations=[], actions_taken=[])
if data.get('mode') == 'create_from_receipts':
if self._actions_taken:
lines = '\n'.join(f'{a}' for a in self._actions_taken)
n_skipped = data.get('n_skipped', 0)
dup_note = f'\n({n_skipped} duplicate receipt(s) were automatically skipped.)' if n_skipped else ''
summary = (
f'Expense report created successfully:\n{lines}\n\n'
'The report is in draft. Please open Odoo Expenses, '
'review the entries, and click Submit to send for approval.'
f'Expense report created successfully:\n{lines}{dup_note}\n\n'
'The report is in draft open Odoo Expenses, '
'review the amounts, and click Submit to send for approval.'
)
status = 'complete'
else: