"""
Invoice management tools.
Provides tools for listing and viewing invoices.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from ..constants import INVOICE_TYPE_LABELS, InvoiceType, OdooModel
from ..decorators import handle_odoo_errors
from ..exceptions import OdooValidationError
from ..formatters import MarkdownBuilder, format_money
from ..validators import validate_date
from .base import extract_name, format_state, get_odoo_client, normalize_pagination
if TYPE_CHECKING:
from mcp.server.fastmcp import FastMCP
# Invoice states
INVOICE_STATE_LABELS = {
"draft": "Draft",
"posted": "Posted",
"cancel": "Cancelled",
}
def register_tools(mcp: "FastMCP") -> None:
"""Register invoice tools with the MCP server."""
@mcp.tool()
@handle_odoo_errors
def list_invoices(
invoice_type: str | None = None,
state: str | None = None,
partner_id: int | None = None,
date_from: str | None = None,
date_to: str | None = None,
limit: int = 50,
offset: int = 0,
) -> str:
"""
List invoices in Odoo.
Args:
invoice_type: Invoice type (out_invoice=customer, in_invoice=supplier, out_refund=customer credit note, in_refund=supplier credit note)
state: State (draft, posted, cancel)
partner_id: Filter by partner
date_from: Start date (YYYY-MM-DD)
date_to: End date (YYYY-MM-DD)
limit: Maximum number of invoices (default: 50)
offset: Offset for pagination (default: 0)
Returns:
List of invoices
"""
limit, offset = normalize_pagination(limit, offset)
client = get_odoo_client()
# Base domain for invoices (not entries)
domain: list = [("move_type", "in", list(InvoiceType))]
if invoice_type:
valid_types = [t.value for t in InvoiceType]
if invoice_type not in valid_types:
raise OdooValidationError(
f"Invalid invoice_type: {invoice_type}. Valid: {', '.join(valid_types)}",
field="invoice_type",
)
domain.append(("move_type", "=", invoice_type))
if state:
valid_states = ["draft", "posted", "cancel"]
if state not in valid_states:
raise OdooValidationError(
f"Invalid state: {state}. Valid: {', '.join(valid_states)}",
field="state",
)
domain.append(("state", "=", state))
if partner_id:
domain.append(("partner_id", "=", partner_id))
if date_from:
domain.append(("invoice_date", ">=", validate_date(date_from, "date_from")))
if date_to:
domain.append(("invoice_date", "<=", validate_date(date_to, "date_to")))
invoices = client.search_read(
OdooModel.INVOICE,
domain,
[
"name",
"partner_id",
"invoice_date",
"invoice_date_due",
"amount_total",
"amount_residual",
"state",
"move_type",
"currency_id",
],
limit=limit,
offset=offset,
order="invoice_date desc, name desc",
)
if not invoices:
return "No invoices found."
builder = MarkdownBuilder("Invoices")
rows = []
for inv in invoices:
currency = extract_name(inv.get("currency_id"), "EUR")
inv_type = INVOICE_TYPE_LABELS.get(inv.get("move_type", ""), inv.get("move_type", ""))
rows.append([
inv["id"],
inv["name"] or "Draft",
extract_name(inv.get("partner_id"), "-"),
inv.get("invoice_date") or "-",
inv_type,
format_money(inv.get("amount_total", 0), currency),
format_money(inv.get("amount_residual", 0), currency),
format_state(inv.get("state", ""), INVOICE_STATE_LABELS),
])
builder.add_table(
["ID", "Number", "Partner", "Date", "Type", "Total", "Due", "Status"],
rows,
alignments=["right", "left", "left", "center", "left", "right", "right", "center"],
)
builder.add_pagination(len(invoices), limit, offset)
return builder.build()
@mcp.tool()
@handle_odoo_errors
def get_invoice(invoice_id: int) -> str:
"""
Get details of a specific invoice with its lines.
Args:
invoice_id: Invoice ID
Returns:
Complete invoice details
"""
client = get_odoo_client()
invoice = client.get_record(
OdooModel.INVOICE,
invoice_id,
[
"name",
"partner_id",
"invoice_date",
"invoice_date_due",
"amount_untaxed",
"amount_tax",
"amount_total",
"amount_residual",
"state",
"move_type",
"currency_id",
"ref",
"narration",
"invoice_line_ids",
],
)
currency = extract_name(invoice.get("currency_id"), "EUR")
inv_type = INVOICE_TYPE_LABELS.get(invoice.get("move_type", ""), invoice.get("move_type", ""))
builder = MarkdownBuilder(f"Invoice: {invoice['name'] or 'Draft'}")
# Header info
builder.add_heading("General Information", 2)
builder.add_key_value({
"ID": invoice["id"],
"Number": invoice["name"] or "Draft",
"Type": inv_type,
"Partner": extract_name(invoice.get("partner_id")),
"Date": invoice.get("invoice_date") or "-",
"Due Date": invoice.get("invoice_date_due") or "-",
"Reference": invoice.get("ref") or "-",
"Status": format_state(invoice.get("state", ""), INVOICE_STATE_LABELS),
})
# Invoice lines
line_ids = invoice.get("invoice_line_ids", [])
if line_ids:
lines = client.read(
OdooModel.INVOICE_LINE,
line_ids,
["name", "quantity", "price_unit", "price_subtotal", "product_id", "account_id"],
)
builder.add_heading("Invoice Lines", 2)
rows = []
for line in lines:
if line.get("price_subtotal", 0) != 0: # Skip zero lines
rows.append([
extract_name(line.get("product_id"), line.get("name", "-")[:40]),
f"{line.get('quantity', 0):.2f}",
format_money(line.get("price_unit", 0), currency),
format_money(line.get("price_subtotal", 0), currency),
])
if rows:
builder.add_table(
["Product/Description", "Qty", "Unit Price", "Subtotal"],
rows,
alignments=["left", "right", "right", "right"],
)
# Totals
builder.add_heading("Totals", 2)
builder.add_key_value({
"Subtotal": format_money(invoice.get("amount_untaxed", 0), currency),
"Tax": format_money(invoice.get("amount_tax", 0), currency),
"Total": format_money(invoice.get("amount_total", 0), currency),
"Amount Due": format_money(invoice.get("amount_residual", 0), currency),
})
if invoice.get("narration"):
builder.add_heading("Notes", 2)
builder.add_text(invoice["narration"])
return builder.build()