fix(entities): Sync SalesOrder, Product, Partner entities with DDL

GAP-ENT-001: SalesOrder - aligned with sales.sales_orders DDL
- Renamed name -> orderNumber
- Changed currencyId (UUID) -> currency (string)
- Changed paymentTermId -> paymentTermDays (int)
- Added: partnerName, partnerEmail, billingAddress, shippingAddress
- Added: requestedDate, promisedDate, shippedDate, deliveredDate
- Added: warehouseId, discountAmount, shippingAmount
- Added: shippingMethod, trackingNumber, carrier, internalNotes
- Removed Odoo-style fields not in DDL

GAP-ENT-002: Product - aligned with products.products DDL
- Renamed salePrice -> price, costPrice -> cost
- Renamed satProductCode -> taxCode, conversionFactor -> uomConversion
- Added: shortDescription, volume, leadTimeDays, attributes (JSONB)
- Removed: inventoryProductId, shortName, satUnitCode, tags, notes
- Removed tracking fields (trackLots, trackSerials, trackExpiry)

GAP-ENT-003: Partner - aligned with partners.partners DDL
- Added: name (separate from displayName)
- Added: taxIdType, verifiedAt, settings (JSONB)
- Removed fields that belong in partner_tax_info: taxRegime, cfdiUse
- Removed: currentBalance, priceListId, discountPercent, salesRepId

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-02-03 18:11:59 -06:00
parent 390bdd3923
commit 0f7feff3f8
3 changed files with 161 additions and 196 deletions

View File

@ -6,11 +6,17 @@ import {
UpdateDateColumn,
DeleteDateColumn,
Index,
ManyToOne,
OneToMany,
JoinColumn,
} from 'typeorm';
/**
* Partner Entity
*
* Synchronized with DDL: database/ddl/16-partners.sql
* Table: partners.partners
*
* Represents customers, suppliers, and other business partners.
* Extended fiscal information is stored in partners.partner_tax_info (separate entity).
*/
@Entity({ name: 'partners', schema: 'partners' })
export class Partner {
@PrimaryGeneratedColumn('uuid')
@ -20,34 +26,34 @@ export class Partner {
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
// Identificacion
// Identification
@Index()
@Column({ type: 'varchar', length: 20, unique: true })
@Column({ type: 'varchar', length: 30 })
code: string;
@Column({ name: 'display_name', type: 'varchar', length: 200 })
@Column({ type: 'varchar', length: 200 })
name: string;
@Column({ name: 'display_name', type: 'varchar', length: 200, nullable: true })
displayName: string;
// Partner type
@Index()
@Column({ name: 'partner_type', type: 'varchar', length: 20, default: 'customer' })
partnerType: 'customer' | 'supplier' | 'both' | 'contact';
// Fiscal data
@Index()
@Column({ name: 'tax_id', type: 'varchar', length: 50, nullable: true })
taxId: string;
@Column({ name: 'tax_id_type', type: 'varchar', length: 20, nullable: true })
taxIdType: string;
@Column({ name: 'legal_name', type: 'varchar', length: 200, nullable: true })
legalName: string;
// Tipo de partner
@Index()
@Column({ name: 'partner_type', type: 'varchar', length: 20, default: 'customer' })
partnerType: 'customer' | 'supplier' | 'both';
// Fiscal
@Index()
@Column({ name: 'tax_id', type: 'varchar', length: 20, nullable: true })
taxId: string;
@Column({ name: 'tax_regime', type: 'varchar', length: 100, nullable: true })
taxRegime: string;
@Column({ name: 'cfdi_use', type: 'varchar', length: 10, nullable: true })
cfdiUse: string;
// Contacto principal
// Primary contact
@Column({ type: 'varchar', length: 255, nullable: true })
email: string;
@ -57,48 +63,43 @@ export class Partner {
@Column({ type: 'varchar', length: 30, nullable: true })
mobile: string;
@Column({ type: 'varchar', length: 500, nullable: true })
@Column({ type: 'varchar', length: 255, nullable: true })
website: string;
// Terminos de pago
@Column({ name: 'payment_term_days', type: 'int', default: 0 })
paymentTermDays: number;
// Credit and payments
@Column({ name: 'credit_limit', type: 'decimal', precision: 15, scale: 2, default: 0 })
creditLimit: number;
@Column({ name: 'current_balance', type: 'decimal', precision: 15, scale: 2, default: 0 })
currentBalance: number;
@Column({ name: 'payment_term_days', type: 'int', default: 0 })
paymentTermDays: number;
// Lista de precios
@Column({ name: 'price_list_id', type: 'uuid', nullable: true })
priceListId: string;
@Column({ name: 'payment_method', type: 'varchar', length: 50, nullable: true })
paymentMethod: string;
// Descuentos
@Column({ name: 'discount_percent', type: 'decimal', precision: 5, scale: 2, default: 0 })
discountPercent: number;
// Categoria
// Classification
@Column({ type: 'varchar', length: 50, nullable: true })
category: string;
@Column({ type: 'text', array: true, default: '{}' })
tags: string[];
// Notas
@Column({ type: 'text', nullable: true })
notes: string;
// Estado
// Status
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean;
@Column({ name: 'is_verified', type: 'boolean', default: false })
isVerified: boolean;
// Vendedor asignado
@Column({ name: 'sales_rep_id', type: 'uuid', nullable: true })
salesRepId: string;
@Column({ name: 'verified_at', type: 'timestamptz', nullable: true })
verifiedAt: Date | null;
// Settings (flexible JSONB)
@Column({ type: 'jsonb', default: '{}' })
settings: Record<string, unknown>;
// Notes
@Column({ type: 'text', nullable: true })
notes: string;
// Metadata
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })

View File

@ -12,22 +12,21 @@ import {
import { ProductCategory } from './product-category.entity';
/**
* Commerce Product Entity (schema: products.products)
* Commerce Product Entity
*
* Synchronized with DDL: database/ddl/17-products.sql
* Table: products.products
*
* NOTE: This is NOT a duplicate of inventory/entities/product.entity.ts
*
* Key differences:
* - This entity: products.products - Commerce/retail focused
* - Has: SAT codes, tax rates, detailed dimensions, min/max stock, reorder points
* - Has: SAT codes, tax rates, pricing, attributes
* - Used by: Sales, purchases, invoicing, POS
*
* - Inventory Product: inventory.products - Warehouse/stock management focused (Odoo-style)
* - Has: valuationMethod, tracking (lot/serial), isStorable, StockQuant/Lot relations
* - Used by: Inventory module for stock tracking, valuation, picking operations
*
* These are intentionally separate by domain. This commerce product entity handles
* pricing, tax compliance (SAT/CFDI), and business rules. For physical stock tracking,
* use the inventory module's product entity.
*/
@Entity({ name: 'products', schema: 'products' })
export class Product {
@ -46,19 +45,7 @@ export class Product {
@JoinColumn({ name: 'category_id' })
category: ProductCategory;
/**
* Optional link to inventory.products for unified stock management.
* This allows the commerce product to be linked to its inventory counterpart
* for stock tracking, valuation (FIFO/AVERAGE), and warehouse operations.
*
* The inventory product handles: stock levels, lot/serial tracking, valuation layers
* This commerce product handles: pricing, taxes, SAT compliance, commercial data
*/
@Index()
@Column({ name: 'inventory_product_id', type: 'uuid', nullable: true })
inventoryProductId: string | null;
// Identificacion
// Identification
@Index()
@Column({ type: 'varchar', length: 50 })
sku: string;
@ -70,55 +57,48 @@ export class Product {
@Column({ type: 'varchar', length: 200 })
name: string;
@Column({ name: 'short_name', type: 'varchar', length: 50, nullable: true })
shortName: string;
@Column({ type: 'text', nullable: true })
description: string;
// Tipo
@Column({ name: 'short_description', type: 'varchar', length: 500, nullable: true })
shortDescription: string;
// Type
@Index()
@Column({ name: 'product_type', type: 'varchar', length: 20, default: 'product' })
productType: 'product' | 'service' | 'consumable' | 'kit';
// Precios
@Column({ name: 'sale_price', type: 'decimal', precision: 15, scale: 4, default: 0 })
salePrice: number;
// Pricing
@Column({ type: 'decimal', precision: 15, scale: 4, default: 0 })
price: number;
@Column({ name: 'cost_price', type: 'decimal', precision: 15, scale: 4, default: 0 })
costPrice: number;
@Column({ name: 'min_sale_price', type: 'decimal', precision: 15, scale: 4, nullable: true })
minSalePrice: number;
@Column({ type: 'decimal', precision: 15, scale: 4, default: 0 })
cost: number;
@Column({ type: 'varchar', length: 3, default: 'MXN' })
currency: string;
// Impuestos
@Column({ name: 'tax_included', type: 'boolean', default: true })
taxIncluded: boolean;
// Taxes
@Column({ name: 'tax_rate', type: 'decimal', precision: 5, scale: 2, default: 16 })
taxRate: number;
@Column({ name: 'tax_included', type: 'boolean', default: false })
taxIncluded: boolean;
@Column({ name: 'tax_code', type: 'varchar', length: 20, nullable: true })
taxCode: string;
// SAT (Mexico)
@Column({ name: 'sat_product_code', type: 'varchar', length: 20, nullable: true })
satProductCode: string;
@Column({ name: 'sat_unit_code', type: 'varchar', length: 10, nullable: true })
satUnitCode: string;
// Unidad de medida
// Unit of measure
@Column({ type: 'varchar', length: 20, default: 'PZA' })
uom: string;
@Column({ name: 'uom_purchase', type: 'varchar', length: 20, nullable: true })
uomPurchase: string;
@Column({ name: 'conversion_factor', type: 'decimal', precision: 10, scale: 4, default: 1 })
conversionFactor: number;
@Column({ name: 'uom_conversion', type: 'decimal', precision: 10, scale: 4, default: 1 })
uomConversion: number;
// Inventario
// Inventory
@Column({ name: 'track_inventory', type: 'boolean', default: true })
trackInventory: boolean;
@ -131,26 +111,13 @@ export class Product {
@Column({ name: 'reorder_point', type: 'decimal', precision: 15, scale: 4, nullable: true })
reorderPoint: number;
@Column({ name: 'reorder_quantity', type: 'decimal', precision: 15, scale: 4, nullable: true })
reorderQuantity: number;
@Column({ name: 'lead_time_days', type: 'int', default: 0 })
leadTimeDays: number;
// Lotes y series
@Column({ name: 'track_lots', type: 'boolean', default: false })
trackLots: boolean;
@Column({ name: 'track_serials', type: 'boolean', default: false })
trackSerials: boolean;
@Column({ name: 'track_expiry', type: 'boolean', default: false })
trackExpiry: boolean;
// Dimensiones
// Physical characteristics
@Column({ type: 'decimal', precision: 10, scale: 4, nullable: true })
weight: number;
@Column({ name: 'weight_unit', type: 'varchar', length: 10, default: 'kg' })
weightUnit: string;
@Column({ type: 'decimal', precision: 10, scale: 4, nullable: true })
length: number;
@ -160,25 +127,21 @@ export class Product {
@Column({ type: 'decimal', precision: 10, scale: 4, nullable: true })
height: number;
@Column({ name: 'dimension_unit', type: 'varchar', length: 10, default: 'cm' })
dimensionUnit: string;
@Column({ type: 'decimal', precision: 10, scale: 4, nullable: true })
volume: number;
// Imagenes
// Images
@Column({ name: 'image_url', type: 'varchar', length: 500, nullable: true })
imageUrl: string;
@Column({ type: 'text', array: true, default: '{}' })
@Column({ type: 'jsonb', default: '[]' })
images: string[];
// Tags
@Column({ type: 'text', array: true, default: '{}' })
tags: string[];
// Attributes (flexible JSONB for custom attributes like color, size, material)
@Column({ type: 'jsonb', default: '{}' })
attributes: Record<string, unknown>;
// Notas
@Column({ type: 'text', nullable: true })
notes: string;
// Estado
// Status
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean;

View File

@ -1,15 +1,22 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, DeleteDateColumn, Index, ManyToOne, JoinColumn } from 'typeorm';
import { PaymentTerm } from '../../core/entities/payment-term.entity.js';
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
/**
* Sales Order Entity
*
* Aligned with SQL schema used by orders.service.ts
* Supports full Order-to-Cash flow with:
* - PaymentTerms integration
* - Automatic picking creation
* - Stock reservation
* - Invoice and delivery status tracking
* Synchronized with DDL: database/ddl/22-sales.sql
* Table: sales.sales_orders
*
* Represents confirmed sales orders with full order-to-delivery tracking.
*/
@Entity({ name: 'sales_orders', schema: 'sales' })
export class SalesOrder {
@ -20,110 +27,104 @@ export class SalesOrder {
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'company_id', type: 'uuid' })
companyId: string;
// Order identification
@Index()
@Column({ type: 'varchar', length: 30 })
name: string; // Order number (e.g., SO-000001)
@Column({ name: 'client_order_ref', type: 'varchar', length: 100, nullable: true })
clientOrderRef: string | null; // Customer's reference number
@Column({ name: 'order_number', type: 'varchar', length: 30 })
orderNumber: string;
// Origin (from quotation)
@Column({ name: 'quotation_id', type: 'uuid', nullable: true })
quotationId: string | null;
// Partner/Customer
// Customer
@Index()
@Column({ name: 'partner_id', type: 'uuid' })
partnerId: string;
@Column({ name: 'partner_name', type: 'varchar', length: 200, nullable: true })
partnerName: string | null;
@Column({ name: 'partner_email', type: 'varchar', length: 255, nullable: true })
partnerEmail: string | null;
// Addresses (JSONB)
@Column({ name: 'billing_address', type: 'jsonb', nullable: true })
billingAddress: Record<string, unknown> | null;
@Column({ name: 'shipping_address', type: 'jsonb', nullable: true })
shippingAddress: Record<string, unknown> | null;
// Dates
@Column({ name: 'order_date', type: 'date', default: () => 'CURRENT_DATE' })
orderDate: Date;
@Column({ name: 'validity_date', type: 'date', nullable: true })
validityDate: Date | null;
@Column({ name: 'requested_date', type: 'date', nullable: true })
requestedDate: Date | null;
@Column({ name: 'commitment_date', type: 'date', nullable: true })
commitmentDate: Date | null; // Promised delivery date
@Column({ name: 'promised_date', type: 'date', nullable: true })
promisedDate: Date | null;
// Currency and pricing
@Index()
@Column({ name: 'currency_id', type: 'uuid' })
currencyId: string;
@Column({ name: 'shipped_date', type: 'date', nullable: true })
shippedDate: Date | null;
@Column({ name: 'pricelist_id', type: 'uuid', nullable: true })
pricelistId: string | null;
@Column({ name: 'delivered_date', type: 'date', nullable: true })
deliveredDate: Date | null;
// Payment terms integration (TASK-003-01)
@Index()
@Column({ name: 'payment_term_id', type: 'uuid', nullable: true })
paymentTermId: string | null;
// Sales rep
@Column({ name: 'sales_rep_id', type: 'uuid', nullable: true })
salesRepId: string | null;
@ManyToOne(() => PaymentTerm)
@JoinColumn({ name: 'payment_term_id' })
paymentTerm: PaymentTerm;
// Warehouse
@Column({ name: 'warehouse_id', type: 'uuid', nullable: true })
warehouseId: string | null;
// Sales team
@Column({ name: 'user_id', type: 'uuid', nullable: true })
userId: string | null; // Sales representative
// Totals
@Column({ type: 'varchar', length: 3, default: 'MXN' })
currency: string;
@Column({ name: 'sales_team_id', type: 'uuid', nullable: true })
salesTeamId: string | null;
@Column({ type: 'decimal', precision: 15, scale: 2, default: 0 })
subtotal: number;
// Amounts
@Column({ name: 'amount_untaxed', type: 'decimal', precision: 15, scale: 2, default: 0 })
amountUntaxed: number;
@Column({ name: 'tax_amount', type: 'decimal', precision: 15, scale: 2, default: 0 })
taxAmount: number;
@Column({ name: 'amount_tax', type: 'decimal', precision: 15, scale: 2, default: 0 })
amountTax: number;
@Column({ name: 'discount_amount', type: 'decimal', precision: 15, scale: 2, default: 0 })
discountAmount: number;
@Column({ name: 'amount_total', type: 'decimal', precision: 15, scale: 2, default: 0 })
amountTotal: number;
@Column({ name: 'shipping_amount', type: 'decimal', precision: 15, scale: 2, default: 0 })
shippingAmount: number;
// Status fields (Order-to-Cash tracking)
@Column({ type: 'decimal', precision: 15, scale: 2, default: 0 })
total: number;
// Payment terms
@Column({ name: 'payment_term_days', type: 'int', default: 0 })
paymentTermDays: number;
@Column({ name: 'payment_method', type: 'varchar', length: 50, nullable: true })
paymentMethod: string | null;
// Status
@Index()
@Column({ type: 'varchar', length: 20, default: 'draft' })
status: 'draft' | 'sent' | 'sale' | 'done' | 'cancelled';
status: 'draft' | 'confirmed' | 'processing' | 'shipped' | 'delivered' | 'cancelled';
@Index()
@Column({ name: 'invoice_status', type: 'varchar', length: 20, default: 'pending' })
invoiceStatus: 'pending' | 'partial' | 'invoiced';
// Shipping
@Column({ name: 'shipping_method', type: 'varchar', length: 50, nullable: true })
shippingMethod: string | null;
@Index()
@Column({ name: 'delivery_status', type: 'varchar', length: 20, default: 'pending' })
deliveryStatus: 'pending' | 'partial' | 'delivered';
@Column({ name: 'tracking_number', type: 'varchar', length: 100, nullable: true })
trackingNumber: string | null;
@Column({ name: 'invoice_policy', type: 'varchar', length: 20, default: 'order' })
invoicePolicy: 'order' | 'delivery';
// Delivery/Picking integration (TASK-003-03)
@Column({ name: 'picking_id', type: 'uuid', nullable: true })
pickingId: string | null;
@Column({ type: 'varchar', length: 100, nullable: true })
carrier: string | null;
// Notes
@Column({ type: 'text', nullable: true })
notes: string | null;
@Column({ name: 'terms_conditions', type: 'text', nullable: true })
termsConditions: string | null;
// Confirmation tracking
@Column({ name: 'confirmed_at', type: 'timestamptz', nullable: true })
confirmedAt: Date | null;
@Column({ name: 'confirmed_by', type: 'uuid', nullable: true })
confirmedBy: string | null;
// Cancellation tracking
@Column({ name: 'cancelled_at', type: 'timestamptz', nullable: true })
cancelledAt: Date | null;
@Column({ name: 'cancelled_by', type: 'uuid', nullable: true })
cancelledBy: string | null;
@Column({ name: 'internal_notes', type: 'text', nullable: true })
internalNotes: string | null;
// Audit fields
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })