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, UpdateDateColumn,
DeleteDateColumn, DeleteDateColumn,
Index, Index,
ManyToOne,
OneToMany,
JoinColumn,
} from 'typeorm'; } 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' }) @Entity({ name: 'partners', schema: 'partners' })
export class Partner { export class Partner {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
@ -20,34 +26,34 @@ export class Partner {
@Column({ name: 'tenant_id', type: 'uuid' }) @Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string; tenantId: string;
// Identificacion // Identification
@Index() @Index()
@Column({ type: 'varchar', length: 20, unique: true }) @Column({ type: 'varchar', length: 30 })
code: string; 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; 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 }) @Column({ name: 'legal_name', type: 'varchar', length: 200, nullable: true })
legalName: string; legalName: string;
// Tipo de partner // Primary contact
@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
@Column({ type: 'varchar', length: 255, nullable: true }) @Column({ type: 'varchar', length: 255, nullable: true })
email: string; email: string;
@ -57,48 +63,43 @@ export class Partner {
@Column({ type: 'varchar', length: 30, nullable: true }) @Column({ type: 'varchar', length: 30, nullable: true })
mobile: string; mobile: string;
@Column({ type: 'varchar', length: 500, nullable: true }) @Column({ type: 'varchar', length: 255, nullable: true })
website: string; website: string;
// Terminos de pago // Credit and payments
@Column({ name: 'payment_term_days', type: 'int', default: 0 })
paymentTermDays: number;
@Column({ name: 'credit_limit', type: 'decimal', precision: 15, scale: 2, default: 0 }) @Column({ name: 'credit_limit', type: 'decimal', precision: 15, scale: 2, default: 0 })
creditLimit: number; creditLimit: number;
@Column({ name: 'current_balance', type: 'decimal', precision: 15, scale: 2, default: 0 }) @Column({ name: 'payment_term_days', type: 'int', default: 0 })
currentBalance: number; paymentTermDays: number;
// Lista de precios @Column({ name: 'payment_method', type: 'varchar', length: 50, nullable: true })
@Column({ name: 'price_list_id', type: 'uuid', nullable: true }) paymentMethod: string;
priceListId: string;
// Descuentos // Classification
@Column({ name: 'discount_percent', type: 'decimal', precision: 5, scale: 2, default: 0 })
discountPercent: number;
// Categoria
@Column({ type: 'varchar', length: 50, nullable: true }) @Column({ type: 'varchar', length: 50, nullable: true })
category: string; category: string;
@Column({ type: 'text', array: true, default: '{}' }) @Column({ type: 'text', array: true, default: '{}' })
tags: string[]; tags: string[];
// Notas // Status
@Column({ type: 'text', nullable: true })
notes: string;
// Estado
@Column({ name: 'is_active', type: 'boolean', default: true }) @Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean; isActive: boolean;
@Column({ name: 'is_verified', type: 'boolean', default: false }) @Column({ name: 'is_verified', type: 'boolean', default: false })
isVerified: boolean; isVerified: boolean;
// Vendedor asignado @Column({ name: 'verified_at', type: 'timestamptz', nullable: true })
@Column({ name: 'sales_rep_id', type: 'uuid', nullable: true }) verifiedAt: Date | null;
salesRepId: string;
// Settings (flexible JSONB)
@Column({ type: 'jsonb', default: '{}' })
settings: Record<string, unknown>;
// Notes
@Column({ type: 'text', nullable: true })
notes: string;
// Metadata // Metadata
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) @CreateDateColumn({ name: 'created_at', type: 'timestamptz' })

View File

@ -12,22 +12,21 @@ import {
import { ProductCategory } from './product-category.entity'; 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 * NOTE: This is NOT a duplicate of inventory/entities/product.entity.ts
* *
* Key differences: * Key differences:
* - This entity: products.products - Commerce/retail focused * - 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 * - Used by: Sales, purchases, invoicing, POS
* *
* - Inventory Product: inventory.products - Warehouse/stock management focused (Odoo-style) * - Inventory Product: inventory.products - Warehouse/stock management focused (Odoo-style)
* - Has: valuationMethod, tracking (lot/serial), isStorable, StockQuant/Lot relations * - Has: valuationMethod, tracking (lot/serial), isStorable, StockQuant/Lot relations
* - Used by: Inventory module for stock tracking, valuation, picking operations * - 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' }) @Entity({ name: 'products', schema: 'products' })
export class Product { export class Product {
@ -46,19 +45,7 @@ export class Product {
@JoinColumn({ name: 'category_id' }) @JoinColumn({ name: 'category_id' })
category: ProductCategory; category: ProductCategory;
/** // Identification
* 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
@Index() @Index()
@Column({ type: 'varchar', length: 50 }) @Column({ type: 'varchar', length: 50 })
sku: string; sku: string;
@ -70,55 +57,48 @@ export class Product {
@Column({ type: 'varchar', length: 200 }) @Column({ type: 'varchar', length: 200 })
name: string; name: string;
@Column({ name: 'short_name', type: 'varchar', length: 50, nullable: true })
shortName: string;
@Column({ type: 'text', nullable: true }) @Column({ type: 'text', nullable: true })
description: string; description: string;
// Tipo @Column({ name: 'short_description', type: 'varchar', length: 500, nullable: true })
shortDescription: string;
// Type
@Index() @Index()
@Column({ name: 'product_type', type: 'varchar', length: 20, default: 'product' }) @Column({ name: 'product_type', type: 'varchar', length: 20, default: 'product' })
productType: 'product' | 'service' | 'consumable' | 'kit'; productType: 'product' | 'service' | 'consumable' | 'kit';
// Precios // Pricing
@Column({ name: 'sale_price', type: 'decimal', precision: 15, scale: 4, default: 0 }) @Column({ type: 'decimal', precision: 15, scale: 4, default: 0 })
salePrice: number; price: number;
@Column({ name: 'cost_price', type: 'decimal', precision: 15, scale: 4, default: 0 }) @Column({ type: 'decimal', precision: 15, scale: 4, default: 0 })
costPrice: number; cost: number;
@Column({ name: 'min_sale_price', type: 'decimal', precision: 15, scale: 4, nullable: true })
minSalePrice: number;
@Column({ type: 'varchar', length: 3, default: 'MXN' }) @Column({ type: 'varchar', length: 3, default: 'MXN' })
currency: string; 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 }) @Column({ name: 'tax_rate', type: 'decimal', precision: 5, scale: 2, default: 16 })
taxRate: number; taxRate: number;
@Column({ name: 'tax_included', type: 'boolean', default: false }) @Column({ name: 'tax_code', type: 'varchar', length: 20, nullable: true })
taxIncluded: boolean; taxCode: string;
// SAT (Mexico) // Unit of measure
@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
@Column({ type: 'varchar', length: 20, default: 'PZA' }) @Column({ type: 'varchar', length: 20, default: 'PZA' })
uom: string; uom: string;
@Column({ name: 'uom_purchase', type: 'varchar', length: 20, nullable: true }) @Column({ name: 'uom_purchase', type: 'varchar', length: 20, nullable: true })
uomPurchase: string; uomPurchase: string;
@Column({ name: 'conversion_factor', type: 'decimal', precision: 10, scale: 4, default: 1 }) @Column({ name: 'uom_conversion', type: 'decimal', precision: 10, scale: 4, default: 1 })
conversionFactor: number; uomConversion: number;
// Inventario // Inventory
@Column({ name: 'track_inventory', type: 'boolean', default: true }) @Column({ name: 'track_inventory', type: 'boolean', default: true })
trackInventory: boolean; trackInventory: boolean;
@ -131,26 +111,13 @@ export class Product {
@Column({ name: 'reorder_point', type: 'decimal', precision: 15, scale: 4, nullable: true }) @Column({ name: 'reorder_point', type: 'decimal', precision: 15, scale: 4, nullable: true })
reorderPoint: number; reorderPoint: number;
@Column({ name: 'reorder_quantity', type: 'decimal', precision: 15, scale: 4, nullable: true }) @Column({ name: 'lead_time_days', type: 'int', default: 0 })
reorderQuantity: number; leadTimeDays: number;
// Lotes y series // Physical characteristics
@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
@Column({ type: 'decimal', precision: 10, scale: 4, nullable: true }) @Column({ type: 'decimal', precision: 10, scale: 4, nullable: true })
weight: number; weight: number;
@Column({ name: 'weight_unit', type: 'varchar', length: 10, default: 'kg' })
weightUnit: string;
@Column({ type: 'decimal', precision: 10, scale: 4, nullable: true }) @Column({ type: 'decimal', precision: 10, scale: 4, nullable: true })
length: number; length: number;
@ -160,25 +127,21 @@ export class Product {
@Column({ type: 'decimal', precision: 10, scale: 4, nullable: true }) @Column({ type: 'decimal', precision: 10, scale: 4, nullable: true })
height: number; height: number;
@Column({ name: 'dimension_unit', type: 'varchar', length: 10, default: 'cm' }) @Column({ type: 'decimal', precision: 10, scale: 4, nullable: true })
dimensionUnit: string; volume: number;
// Imagenes // Images
@Column({ name: 'image_url', type: 'varchar', length: 500, nullable: true }) @Column({ name: 'image_url', type: 'varchar', length: 500, nullable: true })
imageUrl: string; imageUrl: string;
@Column({ type: 'text', array: true, default: '{}' }) @Column({ type: 'jsonb', default: '[]' })
images: string[]; images: string[];
// Tags // Attributes (flexible JSONB for custom attributes like color, size, material)
@Column({ type: 'text', array: true, default: '{}' }) @Column({ type: 'jsonb', default: '{}' })
tags: string[]; attributes: Record<string, unknown>;
// Notas // Status
@Column({ type: 'text', nullable: true })
notes: string;
// Estado
@Column({ name: 'is_active', type: 'boolean', default: true }) @Column({ name: 'is_active', type: 'boolean', default: true })
isActive: boolean; isActive: boolean;

View File

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