Update Docker Compose files for production environment and enhance unit of measure handling

- Changed the default environment variable from 'development' to 'production' in docker-compose.prod.yml and docker-compose.yml.
- Added SITAR API credentials to the environment variables in both Docker Compose files.
- Updated the seed data for customs units of measure to include a76_unit_code.
- Refactored the UnitOfMeasureCustoms model and related DTOs to replace scaii_unit_code with a76_unit_code for consistency.
- Adjusted frontend components to reflect the updated unit of measure structure and ensure proper handling of the new a76_unit_code field.
This commit is contained in:
AlexeerCT
2026-03-15 20:52:01 -06:00
parent 44e621c36d
commit a4cf394447
18 changed files with 226 additions and 95 deletions

View File

@@ -23,7 +23,7 @@ class UnitOfMeasureAmericanBase(BaseModel):
class UnitOfMeasureCustomsBase(BaseModel):
code: str = Field(..., max_length=10, description="Customs Code")
description: Optional[str] = Field(None, max_length=50)
scaii_unit_code: Optional[str] = Field(None, max_length=5)
a76_unit_code: Optional[str] = Field(None, max_length=5, description="Unidad SCAII")
class UnitOfMeasureBase(BaseModel):
@@ -86,7 +86,7 @@ class UnitOfMeasureAmericanUpdate(BaseModel):
class UnitOfMeasureCustomsUpdate(BaseModel):
code: Optional[str] = Field(None, max_length=10)
description: Optional[str] = Field(None, max_length=50)
scaii_unit_code: Optional[str] = Field(None, max_length=5)
a76_unit_code: Optional[str] = Field(None, max_length=5)
class UnitOfMeasureUpdate(BaseModel):

View File

@@ -1,24 +1,24 @@
seed = [
("1", "Kilo"),
("2", "Gramo"),
("3", "Metro Lineal"),
("4", "Metro Cuadrado"),
("5", "Metro Cubico"),
("6", "Pieza"),
("7", "Cabeza"),
("8", "Litro"),
("9", "Par"),
("10", "Kilowatt"),
("11", "Millar"),
("12", "Juego"),
("13", "Kilowatt/Hora"),
("14", "Tonelada"),
("15", "Barril"),
("16", "Gramo Neto"),
("17", "Decenas"),
("18", "Cientos"),
("19", "Decenas"),
("20", "Caja"),
("21", "Botella"),
("22", "Carat"),
("1", "Kilo", "KGS"),
("2", "Gramo", "GR"),
("3", "Metro Lineal", "MT"),
("4", "Metro Cuadrado", "M2"),
("5", "Metro Cubico", "M3"),
("6", "Pieza", "PZA"),
("7", "Cabeza", "PZA"),
("8", "Litro", "LT"),
("9", "Par", "PAR "),
("10", "Kilowatt", ""),
("11", "Millar", "MILLR"),
("12", "Juego", "JGO"),
("13", "Kilowatt/Hora", ""),
("14", "Tonelada", "TON"),
("15", "Barril", "BARR"),
("16", "Gramo Neto", ""),
("17", "Decenas", "DEC"),
("18", "Cientos", "CIEN"),
("19", "Decenas", "DOCE"),
("20", "Caja", "CAJA"),
("21", "Botella", "PZA"),
("22", "Carat", "CARAT"),
]

View File

@@ -7,7 +7,7 @@ from core.exceptions import ErrorCollector
def pre_validators(db: Session, invoice: InvoiceHeader, tenant_id: str, company_id: str, errors: ErrorCollector):
if invoice.status == InvoiceStatus.PROCESSED:
errors.add(
errors.add_error(
"status",
"La factura ya fue procesada y no puede ser exportada",
solution=["Verifique el estatus de la factura antes de intentar exportarla"],
@@ -79,7 +79,7 @@ def pre_validators(db: Session, invoice: InvoiceHeader, tenant_id: str, company_
LineItem.company_id == company_id,
).all()
fractions = {line.fraction for line in lines if line.fraction}
fractions = {line.customs.fraction for line in lines if line.customs.fraction}
if fractions:
warned_fractions = {
row.fraction
@@ -88,7 +88,7 @@ def pre_validators(db: Session, invoice: InvoiceHeader, tenant_id: str, company_
.all()
}
for line in lines:
if line.fraction in warned_fractions:
if line.customs.fraction in warned_fractions:
errors.add_warning(
field="fraction",
message="Advertencia: Esta mercancía, sólo podrá entrar al territorio nacional por las aduanas del país, de lunes a sábado de 8:00 a 13:00 hrs. Ley 10, 18, LIGIE 1, Capítulo 87, RGCE 4.5.31., Anexo 4.",

View File

@@ -1,30 +1,65 @@
from typing import List
from sqlalchemy import func, or_
from sqlalchemy.orm import Session
from api.v1.modules.a76.classes.models import Class
from api.v1.modules.a76.general_catalogs.fractions.historical_tariff_fractions.models import HistoricalTariffFraction
from api.v1.modules.a76.general_catalogs.fractions.historical_tariff_fractions.models import (
HistoricalTariffFraction,
)
from api.v1.modules.a76.general_catalogs.fractions.tariff_fractions.models import TariffFraction
from api.v1.modules.a76.invoices.models import InvoiceHeader
from api.v1.modules.a76.items.models import LineItem
from core.exceptions import ErrorCollector
def _fraction_exists_in_catalog(db: Session, fraction_code: str) -> bool:
"""Returns True if the fraction exists in TariffFraction (SFracciones) or
HistoricalTariffFraction (GFraccionesHistorico).
def _fraction_exists_via_sitar(fraction_code: str) -> bool:
"""Returns True if the fraction exists in SITAR (fracciones o fracciones-anteriores).
Fraction format: first 8 chars = base fraction, chars 9-10 = NICO/country (optional).
Falls back to False if SITAR is not configured or request fails.
"""
if not fraction_code:
return True
base_frac = fraction_code[:8].strip()
nico = fraction_code[8:10].strip() if len(fraction_code) > 8 else ""
try:
from api.v1.modules.sitar.fracciones.service import FraccionesService
from api.v1.modules.sitar.fracciones_anteriores.service import (
FraccionesAnterioresService,
)
# 1. Buscar en fracciones arancelarias (SITAR)
results = FraccionesService.search_sync(
fraccion=base_frac,
nico=nico if nico else None,
limit=1,
)
if results:
return True
# 2. Buscar en fracciones anteriores / histórico (SITAR)
hist_results = FraccionesAnterioresService.search_sync(
fraccion_anterior=base_frac,
limit=1,
)
return len(hist_results) > 0
except (ValueError, Exception):
# SITAR no configurado o error de red: se usa fallback a BD local
return False
def _fraction_exists_in_local_db(db: Session, fraction_code: str) -> bool:
"""Fallback: valida contra TariffFraction e HistoricalTariffFraction locales."""
if not fraction_code:
return True
base_frac = fraction_code[:8]
nico = fraction_code[8:10] if len(fraction_code) > 8 else ""
# Check SFracciones (TariffFraction)
tariff_q = db.query(TariffFraction).filter(
func.left(TariffFraction.code, 8) == base_frac
)
@@ -37,7 +72,6 @@ def _fraction_exists_in_catalog(db: Session, fraction_code: str) -> bool:
if tariff_q.first() is not None:
return True
# Check GFraccionesHistorico (HistoricalTariffFraction)
hist_q = db.query(HistoricalTariffFraction).filter(
HistoricalTariffFraction.historical_fraction == base_frac
)
@@ -53,6 +87,17 @@ def _fraction_exists_in_catalog(db: Session, fraction_code: str) -> bool:
return hist_q.first() is not None
def _fraction_exists_in_catalog(db: Session, fraction_code: str) -> bool:
"""Returns True if the fraction exists in SITAR (fracciones) or fallback a BD local.
Usa las funciones de SITAR de fracciones como fuente principal.
Si SITAR no está configurado o falla, valida contra TariffFraction e HistoricalTariffFraction.
"""
if _fraction_exists_via_sitar(fraction_code):
return True
return _fraction_exists_in_local_db(db, fraction_code)
def _validate_line_fraction(
db: Session, line: LineItem, errors: ErrorCollector
) -> None:

View File

@@ -4,6 +4,7 @@ from decimal import Decimal
from pydantic import BaseModel, Field, field_validator
from .models import (
DestinationOriginCove,
InvoiceStatus,
OperationType,
Currency,
TransportType,
@@ -56,7 +57,21 @@ class InvoiceHeaderBase(BaseModel):
)
invoice_date: date = Field(..., description="Invoice date")
emission_date: Optional[date] = Field(None, description="Emission date")
status: bool = Field(False, description="Status")
status: Optional[InvoiceStatus] = Field(None, description="Status: pending, processed, reversed")
@field_validator("status", mode="before")
@classmethod
def normalize_status(cls, v):
"""Coerce legacy boolean strings ('false'/'true') to InvoiceStatus."""
if v is None:
return v
if isinstance(v, str):
v_lower = v.lower()
if v_lower == "false":
return InvoiceStatus.PENDING.value
if v_lower == "true":
return InvoiceStatus.PROCESSED.value
return v
processed_date: Optional[datetime] = Field(None, description="Update date")
who_processed: Optional[str] = Field(None, max_length=20, description="Who processed")
capture_user: Optional[str] = Field(None, max_length=20, description="Capture user")