Skip to content

Commit 86bce87

Browse files
bosdclaude
andcommitted
Add VIES/VAT validation management workflow
This adds a comprehensive workflow for managing VAT validation during contact imports, addressing VIES API timeouts in large imports. Features: - Local VAT format validation with regex patterns for all EU countries - Checksum validation for BE, DE, NL - Support for custom validators (e.g., Rust-based via PyO3) - Save/restore VAT validation settings across companies - Disable both VIES (online) and stdnum (local) validation - Batch VIES validation with user notifications CLI commands: - vat get-settings: Display current VAT validation settings - vat disable: Disable VAT validation, save settings to JSON - vat restore: Restore settings from JSON file - vat validate: Batch VIES validation with notifications 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent d3acf0c commit 86bce87

File tree

3 files changed

+1708
-0
lines changed

3 files changed

+1708
-0
lines changed

src/odoo_data_flow/__main__.py

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@
1616
run_module_uninstallation,
1717
run_update_module_list,
1818
)
19+
from .lib.actions.vies_manager import (
20+
disable_vat_validation,
21+
get_vat_validation_settings,
22+
restore_vat_validation_settings,
23+
run_vies_validation,
24+
)
1925
from .lib.validation import display_validation_results, validate_csv_data
2026
from .logging_config import log, setup_logging
2127
from .migrator import run_migration
@@ -286,6 +292,315 @@ def invoice_v9_cmd(connection_file: str, **kwargs: Any) -> None:
286292
run_invoice_v9_workflow(**kwargs)
287293

288294

295+
# --- VAT Validation Command Group ---
296+
@cli.group(name="vat")
297+
def vat_group() -> None:
298+
"""Commands for managing VAT/VIES validation settings."""
299+
pass
300+
301+
302+
@vat_group.command(name="get-settings")
303+
@click.option(
304+
"--connection-file",
305+
required=True,
306+
type=click.Path(exists=True, dir_okay=False),
307+
help="Path to the Odoo connection file.",
308+
)
309+
@click.option(
310+
"--company-ids",
311+
default=None,
312+
help="Comma-separated list of company IDs to check. If not specified, checks all.",
313+
)
314+
@click.option(
315+
"--include-stdnum/--no-stdnum",
316+
default=True,
317+
help="Include stdnum validation settings. Default: True.",
318+
)
319+
def vat_get_settings_cmd(
320+
connection_file: str,
321+
company_ids: Optional[str],
322+
include_stdnum: bool,
323+
) -> None:
324+
"""Get current VAT validation settings for all companies."""
325+
from rich.console import Console
326+
from rich.table import Table
327+
328+
company_id_list: Optional[list[int]] = None
329+
if company_ids:
330+
company_id_list = [int(c.strip()) for c in company_ids.split(",") if c.strip()]
331+
332+
settings = get_vat_validation_settings(
333+
config=connection_file,
334+
company_ids=company_id_list,
335+
include_stdnum=include_stdnum,
336+
)
337+
338+
if not settings:
339+
Console().print("[red]Failed to retrieve VAT settings.[/red]")
340+
return
341+
342+
console = Console()
343+
table = Table(title="VAT Validation Settings")
344+
table.add_column("Company ID", style="cyan")
345+
table.add_column("VIES Check", style="green")
346+
347+
for company_id, vies_enabled in sorted(settings.vies_settings.items()):
348+
table.add_row(str(company_id), "✓ Enabled" if vies_enabled else "✗ Disabled")
349+
350+
console.print(table)
351+
352+
if include_stdnum and settings.stdnum_settings:
353+
console.print("\n[bold]stdnum Settings (ir.config_parameter):[/bold]")
354+
for key, value in settings.stdnum_settings.items():
355+
console.print(f" {key}: {value}")
356+
357+
358+
@vat_group.command(name="disable")
359+
@click.option(
360+
"--connection-file",
361+
required=True,
362+
type=click.Path(exists=True, dir_okay=False),
363+
help="Path to the Odoo connection file.",
364+
)
365+
@click.option(
366+
"--company-ids",
367+
default=None,
368+
help="Comma-separated list of company IDs. If not specified, disables for all.",
369+
)
370+
@click.option(
371+
"--vies/--no-vies",
372+
default=True,
373+
help="Disable VIES online check. Default: True.",
374+
)
375+
@click.option(
376+
"--stdnum/--no-stdnum",
377+
default=True,
378+
help="Disable stdnum format validation. Default: True.",
379+
)
380+
@click.option(
381+
"--save-settings",
382+
is_flag=True,
383+
default=True,
384+
help="Save current settings for later restoration. Default: True.",
385+
)
386+
@click.option(
387+
"--output",
388+
default=None,
389+
type=click.Path(dir_okay=False),
390+
help="Save settings to a JSON file for later restoration.",
391+
)
392+
def vat_disable_cmd(
393+
connection_file: str,
394+
company_ids: Optional[str],
395+
vies: bool,
396+
stdnum: bool,
397+
save_settings: bool,
398+
output: Optional[str],
399+
) -> None:
400+
"""Disable VAT validation (VIES and/or stdnum) for companies."""
401+
import json
402+
403+
from rich.console import Console
404+
405+
console = Console()
406+
407+
company_id_list: Optional[list[int]] = None
408+
if company_ids:
409+
company_id_list = [int(c.strip()) for c in company_ids.split(",") if c.strip()]
410+
411+
settings = disable_vat_validation(
412+
config=connection_file,
413+
company_ids=company_id_list,
414+
disable_vies=vies,
415+
disable_stdnum=stdnum,
416+
save_settings=save_settings,
417+
)
418+
419+
if not settings:
420+
console.print("[red]Failed to disable VAT validation.[/red]")
421+
return
422+
423+
console.print("[green]VAT validation disabled successfully.[/green]")
424+
425+
if output:
426+
settings_dict = {
427+
"vies_settings": settings.vies_settings,
428+
"stdnum_settings": settings.stdnum_settings,
429+
"timestamp": settings.timestamp,
430+
}
431+
with open(output, "w") as f:
432+
json.dump(settings_dict, f, indent=2)
433+
console.print(f"Settings saved to: {output}")
434+
elif save_settings:
435+
console.print(
436+
"[dim]Settings stored in memory. Use 'vat restore' to restore them.[/dim]"
437+
)
438+
439+
440+
@vat_group.command(name="restore")
441+
@click.option(
442+
"--connection-file",
443+
required=True,
444+
type=click.Path(exists=True, dir_okay=False),
445+
help="Path to the Odoo connection file.",
446+
)
447+
@click.option(
448+
"--input",
449+
"input_file",
450+
default=None,
451+
type=click.Path(exists=True, dir_okay=False),
452+
help="Restore settings from a JSON file saved by 'vat disable --output'.",
453+
)
454+
def vat_restore_cmd(
455+
connection_file: str,
456+
input_file: Optional[str],
457+
) -> None:
458+
"""Restore VAT validation settings to their original state."""
459+
import json
460+
461+
from rich.console import Console
462+
463+
from .lib.actions.vies_manager import VatValidationSettings
464+
465+
console = Console()
466+
467+
if input_file:
468+
with open(input_file) as f:
469+
data = json.load(f)
470+
# Convert string keys back to int for company IDs
471+
vies_settings = {int(k): v for k, v in data.get("vies_settings", {}).items()}
472+
settings = VatValidationSettings(
473+
vies_settings=vies_settings,
474+
stdnum_settings=data.get("stdnum_settings", {}),
475+
timestamp=data.get("timestamp", 0),
476+
)
477+
else:
478+
console.print(
479+
"[red]No settings file provided. "
480+
"Use --input to specify a settings file.[/red]"
481+
)
482+
console.print(
483+
"[dim]Tip: Use 'vat disable --output settings.json' to save settings.[/dim]"
484+
)
485+
return
486+
487+
success = restore_vat_validation_settings(
488+
config=connection_file,
489+
settings=settings,
490+
)
491+
492+
if success:
493+
console.print("[green]VAT validation settings restored successfully.[/green]")
494+
else:
495+
console.print("[red]Failed to restore VAT validation settings.[/red]")
496+
497+
498+
@vat_group.command(name="validate")
499+
@click.option(
500+
"--connection-file",
501+
required=True,
502+
type=click.Path(exists=True, dir_okay=False),
503+
help="Path to the Odoo connection file.",
504+
)
505+
@click.option(
506+
"--batch-size",
507+
default=50,
508+
type=int,
509+
help="Number of records to validate per batch. Default: 50.",
510+
)
511+
@click.option(
512+
"--delay",
513+
default=1.0,
514+
type=float,
515+
help="Delay between batches in seconds. Default: 1.0.",
516+
)
517+
@click.option(
518+
"--notify-users",
519+
default=None,
520+
help="Comma-separated list of user IDs to notify on failures.",
521+
)
522+
@click.option(
523+
"--domain",
524+
default=None,
525+
help="Odoo domain filter as a list string. "
526+
"Example: \"[('is_company', '=', True)]\"",
527+
)
528+
@click.option(
529+
"--max-records",
530+
default=None,
531+
type=int,
532+
help="Maximum number of records to validate.",
533+
)
534+
def vat_validate_cmd(
535+
connection_file: str,
536+
batch_size: int,
537+
delay: float,
538+
notify_users: Optional[str],
539+
domain: Optional[str],
540+
max_records: Optional[int],
541+
) -> None:
542+
"""Validate VAT numbers against VIES in batches with optional notifications."""
543+
import ast
544+
545+
from rich.console import Console
546+
from rich.table import Table
547+
548+
console = Console()
549+
550+
notify_user_ids: Optional[list[int]] = None
551+
if notify_users:
552+
notify_user_ids = [int(u.strip()) for u in notify_users.split(",") if u.strip()]
553+
554+
parsed_domain: Optional[list[Any]] = None
555+
if domain:
556+
try:
557+
parsed_domain = ast.literal_eval(domain)
558+
except (ValueError, SyntaxError) as e:
559+
console.print(f"[red]Invalid domain format: {e}[/red]")
560+
return
561+
562+
console.print(f"Starting VIES validation (batch size: {batch_size})...")
563+
564+
result = run_vies_validation(
565+
config=connection_file,
566+
batch_size=batch_size,
567+
delay_between_batches=delay,
568+
notify_user_ids=notify_user_ids,
569+
domain=parsed_domain,
570+
max_records=max_records,
571+
)
572+
573+
# Display results
574+
table = Table(title="VIES Validation Results")
575+
table.add_column("Metric", style="cyan")
576+
table.add_column("Value", style="green")
577+
578+
table.add_row("Total Checked", str(result.total_checked))
579+
table.add_row("Valid", str(result.valid_count))
580+
table.add_row("Invalid", str(result.invalid_count))
581+
table.add_row("Errors", str(result.error_count))
582+
583+
console.print(table)
584+
585+
if result.invalid_partners:
586+
console.print("\n[bold red]Invalid VAT Numbers:[/bold red]")
587+
for partner in result.invalid_partners[:20]:
588+
console.print(
589+
f" Partner {partner['id']}: {partner['vat']} - {partner['name']}"
590+
)
591+
if len(result.invalid_partners) > 20:
592+
console.print(f" ... and {len(result.invalid_partners) - 20} more")
593+
594+
if result.error_partners:
595+
console.print("\n[bold yellow]Errors:[/bold yellow]")
596+
for partner in result.error_partners[:10]:
597+
console.print(
598+
f" Partner {partner['id']}: {partner['vat']} - {partner['error']}"
599+
)
600+
if len(result.error_partners) > 10:
601+
console.print(f" ... and {len(result.error_partners) - 10} more")
602+
603+
289604
# --- Import Command ---
290605
@cli.command(name="import")
291606
@click.option(

0 commit comments

Comments
 (0)