"""
Input validation functions.
Provides validation utilities for dates, amounts, and other common inputs.
"""
from __future__ import annotations
import re
from datetime import date, datetime
from typing import Any
from .exceptions import OdooValidationError
# =============================================================================
# Date Validation
# =============================================================================
DATE_PATTERN = re.compile(r"^\d{4}-\d{2}-\d{2}$")
def validate_date(value: str, field_name: str = "date") -> str:
"""
Validate a date string in YYYY-MM-DD format.
Args:
value: Date string to validate
field_name: Name of the field for error messages
Returns:
The validated date string
Raises:
OdooValidationError: If date format is invalid
"""
if not DATE_PATTERN.match(value):
raise OdooValidationError(
f"Invalid date format for {field_name}: '{value}'. Use YYYY-MM-DD.",
field=field_name,
)
try:
datetime.strptime(value, "%Y-%m-%d")
except ValueError as e:
raise OdooValidationError(
f"Invalid date value for {field_name}: '{value}'. {e}",
field=field_name,
)
return value
def validate_date_range(
date_from: str | None,
date_to: str | None,
) -> tuple[str | None, str | None]:
"""
Validate a date range.
Args:
date_from: Start date (optional)
date_to: End date (optional)
Returns:
Tuple of validated dates
Raises:
OdooValidationError: If dates are invalid or range is inverted
"""
if date_from:
date_from = validate_date(date_from, "date_from")
if date_to:
date_to = validate_date(date_to, "date_to")
if date_from and date_to and date_from > date_to:
raise OdooValidationError(
f"date_from ({date_from}) must be before date_to ({date_to})",
field="date_range",
)
return date_from, date_to
def today() -> str:
"""Get today's date in YYYY-MM-DD format."""
return date.today().isoformat()
# =============================================================================
# Amount Validation
# =============================================================================
def validate_positive_amount(
value: float | int,
field_name: str = "amount",
allow_zero: bool = False,
) -> float:
"""
Validate that an amount is positive.
Args:
value: Amount to validate
field_name: Name of the field for error messages
allow_zero: Whether zero is allowed
Returns:
The validated amount as float
Raises:
OdooValidationError: If amount is not positive
"""
value = float(value)
if allow_zero and value < 0:
raise OdooValidationError(
f"{field_name} must be non-negative, got {value}",
field=field_name,
)
elif not allow_zero and value <= 0:
raise OdooValidationError(
f"{field_name} must be positive, got {value}",
field=field_name,
)
return value
def validate_hours(value: float | int) -> float:
"""
Validate hours value for timesheets.
Args:
value: Hours value to validate
Returns:
The validated hours as float
Raises:
OdooValidationError: If hours are invalid
"""
value = validate_positive_amount(value, "hours")
if value > 24:
raise OdooValidationError(
f"Hours cannot exceed 24 per entry, got {value}",
field="hours",
)
return value
# =============================================================================
# String Validation
# =============================================================================
def validate_non_empty_string(
value: str,
field_name: str,
max_length: int | None = None,
) -> str:
"""
Validate that a string is not empty.
Args:
value: String to validate
field_name: Name of the field for error messages
max_length: Maximum allowed length (optional)
Returns:
The validated and stripped string
Raises:
OdooValidationError: If string is empty or too long
"""
value = value.strip()
if not value:
raise OdooValidationError(
f"{field_name} cannot be empty",
field=field_name,
)
if max_length and len(value) > max_length:
raise OdooValidationError(
f"{field_name} exceeds maximum length of {max_length}",
field=field_name,
)
return value
def sanitize_string(value: str) -> str:
"""
Sanitize a string for safe use in Odoo.
Removes potentially dangerous characters while preserving readability.
Args:
value: String to sanitize
Returns:
Sanitized string
"""
# Remove null bytes and other control characters except newlines/tabs
sanitized = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f]", "", value)
return sanitized.strip()
# =============================================================================
# ID Validation
# =============================================================================
def validate_id(value: Any, field_name: str = "id") -> int:
"""
Validate that a value is a positive integer ID.
Args:
value: Value to validate
field_name: Name of the field for error messages
Returns:
The validated ID as int
Raises:
OdooValidationError: If ID is not a positive integer
"""
try:
id_value = int(value)
except (ValueError, TypeError):
raise OdooValidationError(
f"{field_name} must be an integer, got {type(value).__name__}",
field=field_name,
)
if id_value <= 0:
raise OdooValidationError(
f"{field_name} must be positive, got {id_value}",
field=field_name,
)
return id_value
def validate_ids(values: list[Any], field_name: str = "ids") -> list[int]:
"""
Validate a list of IDs.
Args:
values: List of values to validate
field_name: Name of the field for error messages
Returns:
List of validated IDs
Raises:
OdooValidationError: If any ID is invalid
"""
return [validate_id(v, f"{field_name}[{i}]") for i, v in enumerate(values)]