From c8d7990313583c6e1a4eb37d04b3f36aa2efbbd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Thu, 22 Jan 2026 17:23:36 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=88=98=EC=A3=BC(Order)=20=E2=86=94?= =?UTF-8?q?=20=EB=A7=A4=EC=B6=9C(Sale)=20=EC=97=B0=EB=8F=99=20=EC=8A=A4?= =?UTF-8?q?=ED=82=A4=EB=A7=88=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sales 테이블: order_id, shipment_id, source_type 컬럼 추가 - orders 테이블: sales_recognition, sale_id 컬럼 추가 - Sale 모델: order(), shipment() 관계 및 createFromOrder/Shipment 팩토리 메서드 - Order 모델: sale(), sales() 관계 및 shouldCreateSaleOnConfirm/Shipment 헬퍼 - 매출 인식 시점: 수주확정 시 / 출하완료 시 / 수동 선택 가능 Co-Authored-By: Claude Opus 4.5 --- app/Models/Orders/Order.php | 58 ++++++++++++ app/Models/Tenants/Sale.php | 88 +++++++++++++++++++ ...00000_add_order_linkage_to_sales_table.php | 40 +++++++++ ..._add_sales_recognition_to_orders_table.php | 38 ++++++++ 4 files changed, 224 insertions(+) create mode 100644 database/migrations/2026_01_22_100000_add_order_linkage_to_sales_table.php create mode 100644 database/migrations/2026_01_22_100001_add_sales_recognition_to_orders_table.php diff --git a/app/Models/Orders/Order.php b/app/Models/Orders/Order.php index 726a3d0..f36d0ce 100644 --- a/app/Models/Orders/Order.php +++ b/app/Models/Orders/Order.php @@ -5,11 +5,13 @@ use App\Models\Items\Item; use App\Models\Production\WorkOrder; use App\Models\Quote\Quote; +use App\Models\Tenants\Sale; use App\Models\Tenants\Shipment; use App\Traits\BelongsToTenant; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\SoftDeletes; /** @@ -75,6 +77,19 @@ class Order extends Model public const TYPE_PURCHASE = 'PURCHASE'; // 발주 + // 매출 인식 시점 + public const SALES_ON_ORDER_CONFIRM = 'on_order_confirm'; // 수주확정 시 + + public const SALES_ON_SHIPMENT = 'on_shipment'; // 출하완료 시 + + public const SALES_MANUAL = 'manual'; // 수동 등록 + + public const SALES_RECOGNITION_TYPES = [ + self::SALES_ON_ORDER_CONFIRM => '수주확정 시', + self::SALES_ON_SHIPMENT => '출하완료 시', + self::SALES_MANUAL => '수동 등록', + ]; + protected $table = 'orders'; protected $fillable = [ @@ -105,6 +120,9 @@ class Order extends Model 'remarks', 'note', 'options', + // 매출 연동 + 'sales_recognition', + 'sale_id', // 감사 'created_by', 'updated_by', @@ -198,6 +216,46 @@ public function shipments(): HasMany return $this->hasMany(Shipment::class, 'order_id'); } + /** + * 연결된 매출 + */ + public function sale(): BelongsTo + { + return $this->belongsTo(Sale::class); + } + + /** + * 이 수주에서 생성된 모든 매출 (출하별 포함) + */ + public function sales(): HasMany + { + return $this->hasMany(Sale::class, 'order_id'); + } + + /** + * 매출 인식 시점 라벨 + */ + public function getSalesRecognitionLabelAttribute(): string + { + return self::SALES_RECOGNITION_TYPES[$this->sales_recognition] ?? '출하완료 시'; + } + + /** + * 수주확정 시 매출 생성 여부 + */ + public function shouldCreateSaleOnConfirm(): bool + { + return $this->sales_recognition === self::SALES_ON_ORDER_CONFIRM; + } + + /** + * 출하완료 시 매출 생성 여부 + */ + public function shouldCreateSaleOnShipment(): bool + { + return $this->sales_recognition === self::SALES_ON_SHIPMENT; + } + /** * 품목들로부터 금액 합계 재계산 */ diff --git a/app/Models/Tenants/Sale.php b/app/Models/Tenants/Sale.php index 029b6f3..aa9577f 100644 --- a/app/Models/Tenants/Sale.php +++ b/app/Models/Tenants/Sale.php @@ -2,6 +2,7 @@ namespace App\Models\Tenants; +use App\Models\Orders\Order; use App\Traits\BelongsToTenant; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -11,8 +12,25 @@ class Sale extends Model { use BelongsToTenant, SoftDeletes; + /** + * 매출 생성 시점 상수 + */ + public const SOURCE_ORDER_CONFIRM = 'order_confirm'; // 수주확정 시 + + public const SOURCE_SHIPMENT_COMPLETE = 'shipment_complete'; // 출하완료 시 + + public const SOURCE_MANUAL = 'manual'; // 수동 등록 + + public const SOURCE_TYPES = [ + self::SOURCE_ORDER_CONFIRM => '수주확정', + self::SOURCE_SHIPMENT_COMPLETE => '출하완료', + self::SOURCE_MANUAL => '수동등록', + ]; + protected $fillable = [ 'tenant_id', + 'order_id', + 'shipment_id', 'sale_number', 'sale_date', 'client_id', @@ -21,6 +39,7 @@ class Sale extends Model 'total_amount', 'description', 'status', + 'source_type', 'account_code', 'tax_invoice_issued', 'transaction_statement_issued', @@ -37,6 +56,8 @@ class Sale extends Model 'tax_amount' => 'decimal:2', 'total_amount' => 'decimal:2', 'client_id' => 'integer', + 'order_id' => 'integer', + 'shipment_id' => 'integer', 'tax_invoice_issued' => 'boolean', 'transaction_statement_issued' => 'boolean', 'tax_invoice_id' => 'integer', @@ -52,6 +73,22 @@ class Sale extends Model 'invoiced' => '세금계산서발행', ]; + /** + * 수주 관계 + */ + public function order(): BelongsTo + { + return $this->belongsTo(Order::class); + } + + /** + * 출하 관계 + */ + public function shipment(): BelongsTo + { + return $this->belongsTo(Shipment::class); + } + /** * 거래처 관계 */ @@ -107,4 +144,55 @@ public function canDelete(): bool { return $this->status === 'draft'; } + + /** + * 생성 시점 라벨 + */ + public function getSourceTypeLabelAttribute(): string + { + return self::SOURCE_TYPES[$this->source_type] ?? $this->source_type ?? '수동등록'; + } + + /** + * 수주에서 매출 생성 + */ + public static function createFromOrder(Order $order, string $saleNumber): self + { + return new self([ + 'tenant_id' => $order->tenant_id, + 'order_id' => $order->id, + 'sale_number' => $saleNumber, + 'sale_date' => now()->toDateString(), + 'client_id' => $order->client_id, + 'supply_amount' => $order->supply_amount, + 'tax_amount' => $order->tax_amount, + 'total_amount' => $order->total_amount, + 'description' => "수주 {$order->order_no} 매출", + 'status' => 'draft', + 'source_type' => self::SOURCE_ORDER_CONFIRM, + 'created_by' => $order->updated_by ?? $order->created_by, + ]); + } + + /** + * 출하에서 매출 생성 + */ + public static function createFromShipment(Shipment $shipment, string $saleNumber): self + { + return new self([ + 'tenant_id' => $shipment->tenant_id, + 'order_id' => $shipment->order_id, + 'shipment_id' => $shipment->id, + 'sale_number' => $saleNumber, + 'sale_date' => $shipment->shipped_date ?? now()->toDateString(), + 'client_id' => $shipment->order?->client_id, + 'supply_amount' => $shipment->total_amount / 1.1, // 세전 역산 + 'tax_amount' => $shipment->total_amount - ($shipment->total_amount / 1.1), + 'total_amount' => $shipment->total_amount, + 'description' => "출하 {$shipment->shipment_no} 매출", + 'status' => 'draft', + 'source_type' => self::SOURCE_SHIPMENT_COMPLETE, + 'created_by' => $shipment->updated_by ?? $shipment->created_by, + ]); + } } diff --git a/database/migrations/2026_01_22_100000_add_order_linkage_to_sales_table.php b/database/migrations/2026_01_22_100000_add_order_linkage_to_sales_table.php new file mode 100644 index 0000000..d54f35d --- /dev/null +++ b/database/migrations/2026_01_22_100000_add_order_linkage_to_sales_table.php @@ -0,0 +1,40 @@ +unsignedBigInteger('order_id')->nullable()->after('tenant_id')->comment('수주 ID'); + $table->unsignedBigInteger('shipment_id')->nullable()->after('order_id')->comment('출하 ID'); + $table->string('source_type', 30)->nullable()->after('status')->comment('매출 생성 시점: order_confirm/shipment_complete/manual'); + + // 인덱스 + $table->index('order_id', 'idx_sales_order_id'); + $table->index('shipment_id', 'idx_sales_shipment_id'); + $table->index('source_type', 'idx_sales_source_type'); + }); + } + + public function down(): void + { + Schema::table('sales', function (Blueprint $table) { + $table->dropIndex('idx_sales_order_id'); + $table->dropIndex('idx_sales_shipment_id'); + $table->dropIndex('idx_sales_source_type'); + + $table->dropColumn(['order_id', 'shipment_id', 'source_type']); + }); + } +}; \ No newline at end of file diff --git a/database/migrations/2026_01_22_100001_add_sales_recognition_to_orders_table.php b/database/migrations/2026_01_22_100001_add_sales_recognition_to_orders_table.php new file mode 100644 index 0000000..9902f9b --- /dev/null +++ b/database/migrations/2026_01_22_100001_add_sales_recognition_to_orders_table.php @@ -0,0 +1,38 @@ +string('sales_recognition', 20)->default('on_shipment')->after('options')->comment('매출 인식 시점: on_order_confirm/on_shipment/manual'); + $table->unsignedBigInteger('sale_id')->nullable()->after('sales_recognition')->comment('연결된 매출 ID'); + + // 인덱스 + $table->index('sales_recognition', 'idx_orders_sales_recognition'); + $table->index('sale_id', 'idx_orders_sale_id'); + }); + } + + public function down(): void + { + Schema::table('orders', function (Blueprint $table) { + $table->dropIndex('idx_orders_sales_recognition'); + $table->dropIndex('idx_orders_sale_id'); + + $table->dropColumn(['sales_recognition', 'sale_id']); + }); + } +};