changes
This commit is contained in:
@@ -140,6 +140,258 @@ def get_payment_schedule(env, params):
|
||||
}
|
||||
|
||||
|
||||
def search_partners(env, params):
|
||||
"""Search for partners/vendors by name keyword."""
|
||||
keyword = params.get('keyword', '')
|
||||
if not keyword or len(keyword) < 2:
|
||||
return {'error': 'Keyword must be at least 2 characters'}
|
||||
domain = [('name', 'ilike', keyword), ('company_id', 'in', [env.company.id, False])]
|
||||
if params.get('supplier_only'):
|
||||
domain.append(('supplier_rank', '>', 0))
|
||||
partners = env['res.partner'].search(domain, limit=int(params.get('limit', 20)))
|
||||
return {
|
||||
'count': len(partners),
|
||||
'partners': [{
|
||||
'id': p.id,
|
||||
'name': p.name,
|
||||
'supplier_rank': p.supplier_rank,
|
||||
'customer_rank': p.customer_rank,
|
||||
'vat': p.vat or '',
|
||||
'email': p.email or '',
|
||||
'phone': p.phone or '',
|
||||
} for p in partners],
|
||||
}
|
||||
|
||||
|
||||
def find_similar_bank_lines(env, params):
|
||||
"""Find past reconciled bank lines with similar description to suggest coding patterns.
|
||||
Also checks vendor bill tax patterns if a partner is identified."""
|
||||
keyword = params.get('keyword', '')
|
||||
if not keyword or len(keyword) < 3:
|
||||
return {'error': 'Keyword must be at least 3 characters'}
|
||||
# Find reconciled bank lines with matching payment_ref
|
||||
lines = env['account.bank.statement.line'].search([
|
||||
('is_reconciled', '=', True),
|
||||
('payment_ref', 'ilike', keyword),
|
||||
('company_id', '=', env.company.id),
|
||||
], order='date desc', limit=int(params.get('limit', 10)))
|
||||
|
||||
matches = []
|
||||
found_partner_id = None
|
||||
for line in lines:
|
||||
move = line.move_id
|
||||
if not move:
|
||||
continue
|
||||
expense_info = {'account_code': '', 'account_name': '', 'tax_applied': False, 'tax_amount': 0.0}
|
||||
for ml in move.line_ids:
|
||||
if ml.account_id.account_type in ('expense', 'expense_direct_cost', 'expense_depreciation'):
|
||||
expense_info['account_code'] = ml.account_id.code
|
||||
expense_info['account_name'] = ml.account_id.name
|
||||
expense_info['tax_applied'] = bool(ml.tax_ids)
|
||||
expense_info['tax_amount'] = sum(t.amount for t in ml.tax_ids) if ml.tax_ids else 0.0
|
||||
break
|
||||
if line.partner_id and not found_partner_id:
|
||||
found_partner_id = line.partner_id.id
|
||||
matches.append({
|
||||
'id': line.id,
|
||||
'date': str(line.date),
|
||||
'payment_ref': line.payment_ref or '',
|
||||
'amount': line.amount,
|
||||
'partner': line.partner_id.name if line.partner_id else '',
|
||||
'partner_id': line.partner_id.id if line.partner_id else None,
|
||||
'expense_account': expense_info['account_code'],
|
||||
'expense_account_name': expense_info['account_name'],
|
||||
'tax_applied': expense_info['tax_applied'],
|
||||
'tax_rate': expense_info['tax_amount'],
|
||||
})
|
||||
|
||||
result = {
|
||||
'keyword': keyword,
|
||||
'count': len(matches),
|
||||
'matches': matches,
|
||||
'suggestion': matches[0] if matches else None,
|
||||
}
|
||||
|
||||
# Check vendor tax profile cache first (fast), fall back to live query
|
||||
partner_id = found_partner_id or (int(params['partner_id']) if params.get('partner_id') else None)
|
||||
if partner_id:
|
||||
profile = env['fusion.vendor.tax.profile'].search([
|
||||
('partner_id', '=', partner_id),
|
||||
('company_id', '=', env.company.id),
|
||||
], limit=1)
|
||||
if profile:
|
||||
result['vendor_tax_pattern'] = {
|
||||
'source': 'cached_profile',
|
||||
'total_bills': profile.total_bills,
|
||||
'bills_with_tax': profile.bills_with_hst,
|
||||
'bills_no_tax': profile.bills_zero_rated,
|
||||
'avg_tax_pct': profile.avg_tax_pct,
|
||||
'tax_classification': profile.tax_classification,
|
||||
'tax_note': profile.tax_note,
|
||||
'primary_account_id': profile.primary_account_id.id if profile.primary_account_id else None,
|
||||
'primary_account_code': profile.primary_account_code or '',
|
||||
'is_foreign': profile.is_foreign,
|
||||
'is_po_vendor': profile.is_po_vendor,
|
||||
'po_count': profile.po_count,
|
||||
}
|
||||
else:
|
||||
# No cached profile — live query for new/small vendors
|
||||
bills = env['account.move'].search([
|
||||
('move_type', '=', 'in_invoice'), ('state', '=', 'posted'),
|
||||
('partner_id', '=', partner_id),
|
||||
], order='date desc', limit=10)
|
||||
tax_stats = {'source': 'live_query', 'total_bills': len(bills),
|
||||
'bills_with_tax': 0, 'bills_no_tax': 0,
|
||||
'avg_tax_pct': 0.0, 'tax_note': ''}
|
||||
tax_pcts = []
|
||||
for bill in bills:
|
||||
if bill.amount_tax > 0.01:
|
||||
tax_stats['bills_with_tax'] += 1
|
||||
if bill.amount_untaxed > 0:
|
||||
tax_pcts.append(round(bill.amount_tax / bill.amount_untaxed * 100, 2))
|
||||
else:
|
||||
tax_stats['bills_no_tax'] += 1
|
||||
if tax_pcts:
|
||||
tax_stats['avg_tax_pct'] = round(sum(tax_pcts) / len(tax_pcts), 2)
|
||||
if tax_stats['total_bills'] > 0:
|
||||
if tax_stats['bills_no_tax'] == tax_stats['total_bills']:
|
||||
tax_stats['tax_note'] = 'This vendor NEVER charges HST. All bills are zero-rated.'
|
||||
elif tax_stats['avg_tax_pct'] < 2.0 and tax_stats['bills_with_tax'] > 0:
|
||||
tax_stats['tax_note'] = (
|
||||
f'HST only on shipping (avg {tax_stats["avg_tax_pct"]}%). '
|
||||
f'Do NOT apply HST to full amount.'
|
||||
)
|
||||
elif tax_stats['avg_tax_pct'] >= 12.0:
|
||||
tax_stats['tax_note'] = f'Consistently charges HST at ~{tax_stats["avg_tax_pct"]}%.'
|
||||
result['vendor_tax_pattern'] = tax_stats
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def create_vendor_bill(env, params):
|
||||
"""[Tier 3] Create a vendor bill (account.move with move_type='in_invoice').
|
||||
Requires user approval before execution."""
|
||||
partner_id = int(params['partner_id'])
|
||||
invoice_date = params.get('invoice_date', str(fields.Date.today()))
|
||||
bill_lines = params.get('lines', [])
|
||||
if not bill_lines:
|
||||
return {'error': 'At least one invoice line is required'}
|
||||
|
||||
partner = env['res.partner'].browse(partner_id)
|
||||
if not partner.exists():
|
||||
return {'error': f'Partner not found: {partner_id}'}
|
||||
|
||||
invoice_line_vals = []
|
||||
for line in bill_lines:
|
||||
line_vals = {
|
||||
'name': line.get('description', 'Expense'),
|
||||
'price_unit': float(line.get('price_unit', 0)),
|
||||
'quantity': float(line.get('quantity', 1)),
|
||||
}
|
||||
if line.get('account_id'):
|
||||
line_vals['account_id'] = int(line['account_id'])
|
||||
if line.get('tax_ids'):
|
||||
line_vals['tax_ids'] = [(6, 0, [int(t) for t in line['tax_ids']])]
|
||||
invoice_line_vals.append((0, 0, line_vals))
|
||||
|
||||
try:
|
||||
bill = env['account.move'].create({
|
||||
'move_type': 'in_invoice',
|
||||
'partner_id': partner_id,
|
||||
'invoice_date': invoice_date,
|
||||
'date': invoice_date,
|
||||
'invoice_line_ids': invoice_line_vals,
|
||||
'company_id': env.company.id,
|
||||
})
|
||||
|
||||
if params.get('post', False):
|
||||
bill.action_post()
|
||||
|
||||
return {
|
||||
'status': 'created',
|
||||
'bill_id': bill.id,
|
||||
'bill_name': bill.name,
|
||||
'partner': partner.name,
|
||||
'amount_total': bill.amount_total,
|
||||
'state': bill.state,
|
||||
}
|
||||
except Exception as e:
|
||||
_logger.error("Failed to create vendor bill: %s", e)
|
||||
return {'error': str(e)}
|
||||
|
||||
|
||||
def register_bill_payment(env, params):
|
||||
"""[Tier 3] Register payment on a posted vendor bill and optionally reconcile to bank line.
|
||||
Requires user approval before execution."""
|
||||
bill_id = int(params['bill_id'])
|
||||
journal_id = int(params['journal_id'])
|
||||
bill = env['account.move'].browse(bill_id)
|
||||
if not bill.exists() or bill.state != 'posted':
|
||||
return {'error': 'Bill not found or not posted'}
|
||||
|
||||
payment_date = params.get('payment_date', str(fields.Date.today()))
|
||||
|
||||
try:
|
||||
# Use the payment register wizard
|
||||
ctx = {
|
||||
'active_model': 'account.move',
|
||||
'active_ids': [bill_id],
|
||||
}
|
||||
wizard = env['account.payment.register'].with_context(**ctx).create({
|
||||
'journal_id': journal_id,
|
||||
'payment_date': payment_date,
|
||||
})
|
||||
# Optionally set amount if provided (otherwise defaults to bill amount)
|
||||
if params.get('amount'):
|
||||
wizard.amount = float(params['amount'])
|
||||
|
||||
payments = wizard.action_create_payments()
|
||||
|
||||
# Find the created payment
|
||||
payment = None
|
||||
if isinstance(payments, dict) and payments.get('res_id'):
|
||||
payment = env['account.payment'].browse(payments['res_id'])
|
||||
elif isinstance(payments, dict) and payments.get('domain'):
|
||||
payment = env['account.payment'].search(payments['domain'], limit=1)
|
||||
else:
|
||||
# Fallback: find the latest payment for this bill
|
||||
payment = env['account.payment'].search([
|
||||
('partner_id', '=', bill.partner_id.id),
|
||||
], order='create_date desc', limit=1)
|
||||
|
||||
result = {
|
||||
'status': 'paid',
|
||||
'bill_id': bill_id,
|
||||
'bill_name': bill.name,
|
||||
'payment_state': bill.payment_state,
|
||||
}
|
||||
if payment:
|
||||
result['payment_id'] = payment.id
|
||||
result['payment_name'] = payment.name
|
||||
|
||||
# Optionally reconcile to a bank statement line
|
||||
if params.get('statement_line_id') and payment:
|
||||
try:
|
||||
st_line = env['account.bank.statement.line'].browse(int(params['statement_line_id']))
|
||||
if st_line.exists() and not st_line.is_reconciled:
|
||||
# Find the payment's move lines on the bank's outstanding account
|
||||
pay_move_lines = payment.move_id.line_ids.filtered(
|
||||
lambda l: l.account_id.reconcile and not l.reconciled
|
||||
)
|
||||
if pay_move_lines:
|
||||
st_line.set_line_bank_statement_line(pay_move_lines.ids)
|
||||
result['reconciled'] = True
|
||||
result['statement_line_id'] = st_line.id
|
||||
except Exception as e:
|
||||
_logger.warning("Payment created but bank reconciliation failed: %s", e)
|
||||
result['reconcile_error'] = str(e)
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
_logger.error("Failed to register payment: %s", e)
|
||||
return {'error': str(e)}
|
||||
|
||||
|
||||
TOOLS = {
|
||||
'get_ap_aging': get_ap_aging,
|
||||
'find_duplicate_bills': find_duplicate_bills,
|
||||
@@ -147,4 +399,8 @@ TOOLS = {
|
||||
'get_unpaid_bills': get_unpaid_bills,
|
||||
'verify_bill_taxes': verify_bill_taxes,
|
||||
'get_payment_schedule': get_payment_schedule,
|
||||
'search_partners': search_partners,
|
||||
'find_similar_bank_lines': find_similar_bank_lines,
|
||||
'create_vendor_bill': create_vendor_bill,
|
||||
'register_bill_payment': register_bill_payment,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user