feat: Implement invoice management features including data table, dialogs for viewing, editing, and deleting invoices, and server-side loading of invoice data
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
from enum import Enum
|
||||
from typing import Optional, List
|
||||
from sqlalchemy import BigInteger, Date, ForeignKey, Integer, Numeric, String, Text, TIMESTAMP
|
||||
from sqlalchemy import BigInteger, Boolean, Date, ForeignKey, Integer, Numeric, String, Text, TIMESTAMP
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from core.database import Base
|
||||
from datetime import datetime
|
||||
@@ -11,21 +11,35 @@ class OperationType(str, Enum):
|
||||
IMP = "imp" # Importación
|
||||
EXP = "exp" # Exportación
|
||||
|
||||
|
||||
class TransportType(str, Enum):
|
||||
NONE = "none" # Ninguno
|
||||
TRANSPORT = "transport" # Transporte
|
||||
BOX = "box" # Caja
|
||||
PLATES = "licence plates" # Placas
|
||||
TRUCK = "truck" # Camión
|
||||
VESSEL = "vessel" # Buque
|
||||
BARGE = "rail barge" # Ferrobarcaza
|
||||
CONTAINER = "container" # Contenedor
|
||||
AIRPLANE = "airplane" # Avión
|
||||
GONDOLA = "gondola" # Góndola
|
||||
FLATBED = "flatbed" # Plataforma
|
||||
|
||||
# --- 1. Invoice Header (invoice_header) ---
|
||||
|
||||
|
||||
class InvoiceHeader(Base, TenantScopedMixin, TimestampMixin):
|
||||
__tablename__ = "invoice_header"
|
||||
__tableargs__ = {'schema': 'a76'}
|
||||
__table_args__ = (
|
||||
{"schema": "a76"},
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
BigInteger, primary_key=True, autoincrement=True)
|
||||
|
||||
# Identifiers
|
||||
operation_type: Mapped[OperationType] = mapped_column(
|
||||
String(3)) # TIPOMOVIMIENTO / Clasifica IMP/EXP
|
||||
invoice_type: Mapped[Optional[str]] = mapped_column(
|
||||
ForeignKey("public.invoice_types.key")) # TIPOFACTURA
|
||||
String(3)) # TIPOMOVIMIENTO / Clasifica imp/exp
|
||||
invoice_type: Mapped[Optional[str]] = mapped_column(ForeignKey(
|
||||
"public.invoice_types.key")) # TIPOFACTURA (invoice_types)
|
||||
invoice_number: Mapped[Optional[str]] = mapped_column(
|
||||
String(20)) # FACTURAIMPO/FACTURAEXPO/FACTURAREMISION
|
||||
project_number: Mapped[Optional[str]] = mapped_column(
|
||||
@@ -42,20 +56,22 @@ class InvoiceHeader(Base, TenantScopedMixin, TimestampMixin):
|
||||
TIMESTAMP(timezone=False), default=datetime.now) # FECHACAPTURA + HORAACTUAL
|
||||
|
||||
# Status & Control
|
||||
status: Mapped[Optional[str]] = mapped_column(String(5)) # ESTATUS
|
||||
traffic_light_status: Mapped[Optional[str]] = mapped_column(
|
||||
String(50)) # SEMAFORO / SEMAFOROEXPO/IMPO
|
||||
process_log: Mapped[Optional[str]] = mapped_column(
|
||||
String(300)) # COMOFUEPROCESADA **<-- AÑADIDO**
|
||||
# OBSERVACIONE + OBSERVACIONI + COMENTARIOSESTATUS
|
||||
comments: Mapped[Optional[str]] = mapped_column(Text)
|
||||
is_updated: Mapped[Optional[str]] = mapped_column(Boolean) # ESTATUS
|
||||
updated_date: Mapped[Optional[datetime]] = mapped_column(TIMESTAMP(timezone=False)) # FECHAACTUALIZACION
|
||||
who_updated: Mapped[Optional[str]] = mapped_column(String(20)) # QUIENA ACTUALIZO
|
||||
|
||||
traffic_light_status: Mapped[Optional[str]] = mapped_column(String(50)) # SEMAFORO / SEMAFOROEXPO/IMPO
|
||||
process_log: Mapped[Optional[str]] = mapped_column(String(300)) # COMOFUEPROCESADA
|
||||
|
||||
# Comments
|
||||
observation_es: Mapped[Optional[str]] = mapped_column(Text) #OBSERVACIONE
|
||||
observation_en: Mapped[Optional[str]] = mapped_column(Text) #OBSERVACIONI
|
||||
comments_status: Mapped[Optional[str]] = mapped_column(Text) #COMENTARIOSESTATUS
|
||||
|
||||
# Digital Archive Links
|
||||
cfdi_uuid: Mapped[Optional[str]] = mapped_column(String(100)) # CFDIUUID
|
||||
path_pdf: Mapped[Optional[str]] = mapped_column(
|
||||
String(500)) # CFDIPATHPDF **<-- AÑADIDO**
|
||||
path_xml: Mapped[Optional[str]] = mapped_column(
|
||||
String(500)) # CFDIPATHXML **<-- AÑADIDO**
|
||||
path_pdf: Mapped[Optional[str]] = mapped_column(String(500)) # CFDIPATHPDF
|
||||
path_xml: Mapped[Optional[str]] = mapped_column(String(500)) # CFDIPATHXML
|
||||
|
||||
# Relationships (Para navegacion ORM)
|
||||
compliance_mx: Mapped["InvoiceComplianceMx"] = relationship(
|
||||
@@ -73,10 +89,12 @@ class InvoiceHeader(Base, TenantScopedMixin, TimestampMixin):
|
||||
# --- 2. Compliance MX (invoice_compliance_mx) ---
|
||||
class InvoiceComplianceMx(Base, TenantScopedMixin, TimestampMixin):
|
||||
__tablename__ = "invoice_compliance_mx"
|
||||
__tableargs__ = {'schema': 'a76'}
|
||||
__table_args__ = (
|
||||
{"schema": "a76"},
|
||||
)
|
||||
|
||||
invoice_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("invoice_header.id"), primary_key=True)
|
||||
ForeignKey("a76.invoice_header.id"), primary_key=True)
|
||||
|
||||
# Core Customs Data
|
||||
pedimento: Mapped[Optional[str]] = mapped_column(
|
||||
@@ -84,23 +102,29 @@ class InvoiceComplianceMx(Base, TenantScopedMixin, TimestampMixin):
|
||||
pedimento_code: Mapped[Optional[str]] = mapped_column(
|
||||
String(5)) # PEDIMENTOR1, K1
|
||||
remesa: Mapped[Optional[int]] = mapped_column(Integer)
|
||||
aduana: Mapped[Optional[str]] = mapped_column(String(5)) # ADUANA_CRUCE
|
||||
customs_agent: Mapped[Optional[str]] = mapped_column(
|
||||
String(10)) # AADUANAL
|
||||
aduana: Mapped[Optional[str]] = mapped_column(ForeignKey("public.customs_sections.customs_code")) # ADUANA_CRUCE
|
||||
|
||||
# Clients & Providers
|
||||
provider_header: Mapped[Optional[str]] = mapped_column(String(20)) # PROVEEDOREXPORTADOR
|
||||
provider_id: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # PROVEEDOR
|
||||
sold_to_header: Mapped[Optional[str]] = mapped_column(String(20)) # VENDIDOCONSIGNADO
|
||||
sold_to_id: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # VENDIDOA
|
||||
shipped_to_header: Mapped[Optional[str]] = mapped_column(String(20)) # ENVIADOTRANSFERIDO
|
||||
shipped_to_id: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # ENVIADOA
|
||||
shipped_by_header: Mapped[Optional[str]] = mapped_column(String(20)) # ENVIADOPORVENDIDOPOR
|
||||
shipped_by_id: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # VENDIDOPOR
|
||||
|
||||
customs_broker_id: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.customs_brokers.id")) # AADUANAL
|
||||
|
||||
# Flags & Specific Regimes
|
||||
is_mixed: Mapped[Optional[str]] = mapped_column(
|
||||
String(2)) # ESMIXTO **<-- AÑADIDO**
|
||||
waste_type: Mapped[Optional[str]] = mapped_column(
|
||||
String(1)) # TIPODESPERDICIO **<-- AÑADIDO**
|
||||
is_mixed: Mapped[Optional[bool]] = mapped_column(Boolean) # ESMIXTO
|
||||
waste_type: Mapped[Optional[str]] = mapped_column(String(1)) # TIPODESPERDICIO
|
||||
appendix_17: Mapped[Optional[int]] = mapped_column(Integer) # APENDICE17
|
||||
|
||||
# VUCEM / Digital
|
||||
edocument: Mapped[Optional[str]] = mapped_column(String(50)) # EDOCUMENT
|
||||
electronic_signature: Mapped[Optional[str]] = mapped_column(
|
||||
String(999)) # FIRMAELECTRONICA
|
||||
sem_id: Mapped[Optional[int]] = mapped_column(
|
||||
Integer) # SEM (de SFacEntradaSM/SFacSalidaSM)
|
||||
electronic_signature: Mapped[Optional[str]] = mapped_column(String(999)) # FIRMAELECTRONICA
|
||||
sem_id: Mapped[Optional[int]] = mapped_column(Integer) # SEM (de SFacEntradaSM/SFacSalidaSM)
|
||||
|
||||
# Relationship
|
||||
header: Mapped["InvoiceHeader"] = relationship(
|
||||
@@ -110,40 +134,33 @@ class InvoiceComplianceMx(Base, TenantScopedMixin, TimestampMixin):
|
||||
# --- 3. Financials (invoice_financials) ---
|
||||
class InvoiceFinancials(Base, TenantScopedMixin, TimestampMixin):
|
||||
__tablename__ = "invoice_financials"
|
||||
__tableargs__ = {'schema': 'a76'}
|
||||
__table_args__ = (
|
||||
{"schema": "a76"},
|
||||
)
|
||||
|
||||
invoice_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("invoice_header.id"), primary_key=True)
|
||||
id: Mapped[int] = mapped_column(
|
||||
BigInteger, primary_key=True, autoincrement=True)
|
||||
invoice_id: Mapped[int] = mapped_column(ForeignKey("a76.invoice_header.id"))
|
||||
|
||||
currency: Mapped[Optional[str]] = mapped_column(String(3)) # CLAVEMONEDA
|
||||
exchange_rate: Mapped[Optional[float]] = mapped_column(
|
||||
Numeric(13, 6)) # TIPOCAMBIO
|
||||
currency_type: Mapped[Optional[str]] = mapped_column(ForeignKey("public.currency_types.code")) # TIPOCLAVEMONEDA
|
||||
exchange_rate: Mapped[Optional[float]] = mapped_column(Numeric(13, 6)) # TIPOCAMBIO
|
||||
|
||||
# Merchandise Values
|
||||
value_mn: Mapped[Optional[float]] = mapped_column(
|
||||
Numeric(23, 8), default=0) # VALORIMPOMN/EXPOMN
|
||||
value_me: Mapped[Optional[float]] = mapped_column(
|
||||
Numeric(23, 8), default=0)
|
||||
customs_value_mn: Mapped[Optional[float]] = mapped_column(
|
||||
Numeric(23, 8), default=0) # VALORADUANASMN **<-- AÑADIDO**
|
||||
value_mn: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # VALORIMPOMN/EXPOMN
|
||||
value_me: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0)
|
||||
customs_value_mn: Mapped[Optional[float]] = mapped_column( Numeric(23, 8), default=0) # VALORADUANASMN
|
||||
|
||||
# Costs & Taxes
|
||||
freight: Mapped[Optional[float]] = mapped_column(
|
||||
Numeric(19, 8), default=0) # FLETE
|
||||
insurance: Mapped[Optional[float]] = mapped_column(
|
||||
Numeric(19, 8), default=0) # VALSEGUROS/SEGUROS
|
||||
iva_mn: Mapped[Optional[float]] = mapped_column(
|
||||
Numeric(23, 8), default=0) # IVAEXPOMN / VALORIVAMN
|
||||
iva_factor: Mapped[Optional[float]] = mapped_column(
|
||||
Numeric(17, 4)) # FACTORIVA
|
||||
freight: Mapped[Optional[float]] = mapped_column(Numeric(19, 8), default=0) # FLETE
|
||||
insurance: Mapped[Optional[float]] = mapped_column(Numeric(19, 8), default=0) # VALSEGUROS/SEGUROS
|
||||
iva_mn: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # IVAEXPOMN / VALORIVAMN
|
||||
iva_factor: Mapped[Optional[float]] = mapped_column(Numeric(17, 4)) # FACTORIVA
|
||||
|
||||
# Weights & Quantities
|
||||
total_quantity: Mapped[Optional[float]] = mapped_column(
|
||||
Numeric(19, 8)) # CANTEXPO / CANTIMPO
|
||||
gross_weight: Mapped[Optional[float]] = mapped_column(
|
||||
Numeric(19, 8)) # PESOBRUTO **<-- AÑADIDO**
|
||||
net_weight: Mapped[Optional[float]] = mapped_column(
|
||||
Numeric(19, 8)) # PESONETO **<-- AÑADIDO**
|
||||
total_quantity: Mapped[Optional[float]] = mapped_column(Numeric(19, 8)) # CANTEXPO / CANTIMPO
|
||||
gross_weight: Mapped[Optional[float]] = mapped_column(Numeric(19, 8)) # PESOBRUTO
|
||||
net_weight: Mapped[Optional[float]] = mapped_column(Numeric(19, 8)) # PESONETO
|
||||
bundle_count: Mapped[Optional[int]] = mapped_column(Integer) # CANTBULTOS
|
||||
|
||||
# Relationship
|
||||
@@ -153,17 +170,21 @@ class InvoiceFinancials(Base, TenantScopedMixin, TimestampMixin):
|
||||
# --- 4. Logistics (invoice_logistics) ---
|
||||
class InvoiceLogistics(Base, TenantScopedMixin, TimestampMixin):
|
||||
__tablename__ = "invoice_logistics"
|
||||
__tableargs__ = {'schema': 'a76'}
|
||||
__table_args__ = (
|
||||
{"schema": "a76"},
|
||||
)
|
||||
|
||||
logistics_id: Mapped[int] = mapped_column(
|
||||
id: Mapped[int] = mapped_column(
|
||||
BigInteger, primary_key=True, autoincrement=True)
|
||||
invoice_id: Mapped[int] = mapped_column(ForeignKey("invoice_header.id"))
|
||||
invoice_id: Mapped[int] = mapped_column(ForeignKey("a76.invoice_header.id"))
|
||||
|
||||
# Carrier Info
|
||||
carrier_id: Mapped[Optional[str]] = mapped_column(
|
||||
String(10)) # TRANSPORTISTA
|
||||
transport_type: Mapped[TransportType] = mapped_column(
|
||||
String(15), default="none") # TRANSPORTE
|
||||
transport_mode: Mapped[Optional[str]] = mapped_column(
|
||||
String(15)) # TRANSPORTE
|
||||
String(15)) # MODTRANS
|
||||
driver_name: Mapped[Optional[str]] = mapped_column(String(80)) # CONDUCTOR
|
||||
is_rail: Mapped[Optional[str]] = mapped_column(String(2)) # ESFERROCARRIL
|
||||
rail_id: Mapped[Optional[str]] = mapped_column(
|
||||
@@ -189,11 +210,13 @@ class InvoiceLogistics(Base, TenantScopedMixin, TimestampMixin):
|
||||
# --- 5. Sales Order Details (invoice_sales_details) ---
|
||||
class InvoiceSalesDetails(Base, TenantScopedMixin, TimestampMixin):
|
||||
__tablename__ = "invoice_sales_details"
|
||||
__tableargs__ = {'schema': 'a76'}
|
||||
__table_args__ = (
|
||||
{"schema": "a76"},
|
||||
)
|
||||
|
||||
detail_id: Mapped[int] = mapped_column(
|
||||
id: Mapped[int] = mapped_column(
|
||||
BigInteger, primary_key=True, autoincrement=True)
|
||||
invoice_id: Mapped[int] = mapped_column(ForeignKey("invoice_header.id"))
|
||||
invoice_id: Mapped[int] = mapped_column(ForeignKey("a76.invoice_header.id"))
|
||||
|
||||
line_number: Mapped[int] = mapped_column(Integer) # LINEA
|
||||
sales_order: Mapped[Optional[str]] = mapped_column(
|
||||
@@ -214,11 +237,13 @@ class InvoiceSalesDetails(Base, TenantScopedMixin, TimestampMixin):
|
||||
# --- 6. Collections (invoice_collections) ---
|
||||
class InvoiceCollections(Base, TenantScopedMixin, TimestampMixin):
|
||||
__tablename__ = "invoice_collections"
|
||||
__tableargs__ = {'schema': 'a76'}
|
||||
__table_args__ = (
|
||||
{"schema": "a76"},
|
||||
)
|
||||
|
||||
collection_id: Mapped[int] = mapped_column(
|
||||
id: Mapped[int] = mapped_column(
|
||||
BigInteger, primary_key=True, autoincrement=True)
|
||||
invoice_id: Mapped[int] = mapped_column(ForeignKey("invoice_header.id"))
|
||||
invoice_id: Mapped[int] = mapped_column(ForeignKey("a76.invoice_header.id"))
|
||||
|
||||
concept: Mapped[Optional[str]] = mapped_column(String(100)) # CONCEPTO
|
||||
is_collected: Mapped[Optional[int]] = mapped_column(Integer) # COBRADO
|
||||
|
||||
@@ -5,24 +5,35 @@ from pydantic import BaseModel, Field
|
||||
from .models import OperationType
|
||||
|
||||
|
||||
|
||||
# --- Base Schemas ---
|
||||
class InvoiceHeaderBase(BaseModel):
|
||||
"""Base fields for Invoice Header"""
|
||||
operation_type: Optional[OperationType] = Field(None, max_length=20, description="Operation type: imp/exp")
|
||||
invoice_type: Optional[str] = Field(None, max_length=5, description="Invoice type key")
|
||||
invoice_number: Optional[str] = Field(None, max_length=20, description="Invoice number")
|
||||
project_number: Optional[str] = Field(None, max_length=14, description="Project number")
|
||||
purchase_order: Optional[str] = Field(None, max_length=50, description="Purchase order")
|
||||
related_doc_id: Optional[int] = Field(None, description="Related document ID for rectifications")
|
||||
operation_type: Optional[OperationType] = Field(
|
||||
None, max_length=20, description="Operation type: imp/exp")
|
||||
invoice_type: Optional[str] = Field(
|
||||
None, max_length=5, description="Invoice type key")
|
||||
invoice_number: Optional[str] = Field(
|
||||
None, max_length=20, description="Invoice number")
|
||||
project_number: Optional[str] = Field(
|
||||
None, max_length=14, description="Project number")
|
||||
purchase_order: Optional[str] = Field(
|
||||
None, max_length=50, description="Purchase order")
|
||||
related_doc_id: Optional[int] = Field(
|
||||
None, description="Related document ID for rectifications")
|
||||
invoice_date: Optional[date] = Field(None, description="Invoice date")
|
||||
status: Optional[str] = Field(None, max_length=5, description="Status")
|
||||
traffic_light_status: Optional[str] = Field(None, max_length=50, description="Traffic light status (SEMAFORO)")
|
||||
process_log: Optional[str] = Field(None, max_length=300, description="Processing log")
|
||||
comments: Optional[str] = Field(None, description="Comments and observations")
|
||||
cfdi_uuid: Optional[str] = Field(None, max_length=100, description="CFDI UUID")
|
||||
path_pdf: Optional[str] = Field(None, max_length=500, description="Path to PDF file")
|
||||
path_xml: Optional[str] = Field(None, max_length=500, description="Path to XML file")
|
||||
is_updated: Optional[bool] = Field(None, description="Status")
|
||||
traffic_light_status: Optional[str] = Field(
|
||||
None, max_length=50, description="Traffic light status (SEMAFORO)")
|
||||
process_log: Optional[str] = Field(
|
||||
None, max_length=300, description="Processing log")
|
||||
comments_status: Optional[str] = Field(
|
||||
None, description="Comments and observations")
|
||||
cfdi_uuid: Optional[str] = Field(
|
||||
None, max_length=100, description="CFDI UUID")
|
||||
path_pdf: Optional[str] = Field(
|
||||
None, max_length=500, description="Path to PDF file")
|
||||
path_xml: Optional[str] = Field(
|
||||
None, max_length=500, description="Path to XML file")
|
||||
|
||||
|
||||
class InvoiceComplianceMxBase(BaseModel):
|
||||
@@ -36,8 +47,8 @@ class InvoiceComplianceMxBase(BaseModel):
|
||||
None, max_length=5, description="Customs office")
|
||||
customs_agent: Optional[str] = Field(
|
||||
None, max_length=10, description="Customs agent")
|
||||
is_mixed: Optional[str] = Field(
|
||||
None, max_length=2, description="Is mixed operation")
|
||||
is_mixed: Optional[bool] = Field(
|
||||
None, description="Is mixed operation")
|
||||
waste_type: Optional[str] = Field(
|
||||
None, max_length=1, description="Waste type")
|
||||
appendix_17: Optional[int] = Field(None, description="Appendix 17")
|
||||
|
||||
Reference in New Issue
Block a user