merge: origin/develop 병합
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,49 @@
|
||||
## 2026-01-29 (수) - 경동기업 견적 로직 Phase 4 완료
|
||||
|
||||
### 작업 목표
|
||||
- 경동기업(tenant_id=287) 전용 견적 계산 로직 구현
|
||||
- 5130 레거시 시스템의 BOM/견적 로직을 SAM에 이식
|
||||
- 동적 BOM 계산: 모터, 제어기, 절곡품(10종), 부자재(3종)
|
||||
|
||||
### 생성된 파일
|
||||
| 파일명 | 설명 |
|
||||
|--------|------|
|
||||
| `app/Models/Kyungdong/KdPriceTable.php` | 경동기업 전용 단가 테이블 모델 |
|
||||
| `app/Services/Quote/Handlers/KyungdongFormulaHandler.php` | 경동기업 견적 계산 핸들러 |
|
||||
| `database/migrations/2026_01_29_004736_create_kd_price_tables_table.php` | kd_price_tables 마이그레이션 |
|
||||
| `database/seeders/Kyungdong/KdPriceTableSeeder.php` | 단가 데이터 시더 (47건) |
|
||||
|
||||
### 수정된 파일
|
||||
| 파일명 | 설명 |
|
||||
|--------|------|
|
||||
| `app/Services/Quote/FormulaEvaluatorService.php` | tenant_id=287 라우팅 추가 |
|
||||
|
||||
### 구현된 기능
|
||||
| 기능 | 설명 |
|
||||
|------|------|
|
||||
| 모터 용량 계산 | 제품타입 × 인치 × 중량 3차원 조건 |
|
||||
| 브라켓 크기 결정 | 중량 기반 530*320, 600*350, 690*390 |
|
||||
| 주자재 계산 | W × (H + 550) / 1,000,000 × 단가 |
|
||||
| 절곡품 계산 (10종) | 케이스, 마구리, 가이드레일, 하장바, L바, 평철, 환봉 등 |
|
||||
| 부자재 계산 (3종) | 감기샤프트, 각파이프, 앵글 |
|
||||
|
||||
### 테스트 결과
|
||||
```
|
||||
입력: W0=3000, H0=2500, 철재형, 5인치, KSS01 SUS
|
||||
출력: 16개 항목, 합계 751,200원 ✅
|
||||
```
|
||||
|
||||
### 검증 완료
|
||||
- [x] Pint 코드 스타일 통과
|
||||
- [x] 마이그레이션 실행 완료 (kd_price_tables)
|
||||
- [x] 시더 실행 완료 (47건 단가 데이터)
|
||||
- [x] tinker 테스트 통과 (16개 항목 정상 계산)
|
||||
|
||||
### 계획 문서
|
||||
- `docs/plans/kd-quote-logic-plan.md` - Phase 0~4 완료 (100%)
|
||||
|
||||
---
|
||||
|
||||
## 2026-01-21 (화) - TodayIssue 헤더 알림 API (Phase 3 완료)
|
||||
|
||||
### 작업 목표
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 논리적 데이터베이스 관계 문서
|
||||
|
||||
> **자동 생성**: 2026-01-29 09:14:06
|
||||
> **자동 생성**: 2026-01-29 19:41:35
|
||||
> **소스**: Eloquent 모델 관계 분석
|
||||
|
||||
## 📊 모델별 관계 현황
|
||||
@@ -194,6 +194,35 @@ ### model_versions
|
||||
- **model()**: belongsTo → `models`
|
||||
- **bomTemplates()**: hasMany → `bom_templates`
|
||||
|
||||
### documents
|
||||
**모델**: `App\Models\Documents\Document`
|
||||
|
||||
- **creator()**: belongsTo → `users`
|
||||
- **updater()**: belongsTo → `users`
|
||||
- **approvals()**: hasMany → `document_approvals`
|
||||
- **data()**: hasMany → `document_data`
|
||||
- **attachments()**: hasMany → `document_attachments`
|
||||
- **linkable()**: morphTo → `(Polymorphic)`
|
||||
|
||||
### document_approvals
|
||||
**모델**: `App\Models\Documents\DocumentApproval`
|
||||
|
||||
- **document()**: belongsTo → `documents`
|
||||
- **user()**: belongsTo → `users`
|
||||
- **creator()**: belongsTo → `users`
|
||||
|
||||
### document_attachments
|
||||
**모델**: `App\Models\Documents\DocumentAttachment`
|
||||
|
||||
- **document()**: belongsTo → `documents`
|
||||
- **file()**: belongsTo → `files`
|
||||
- **creator()**: belongsTo → `users`
|
||||
|
||||
### document_datas
|
||||
**모델**: `App\Models\Documents\DocumentData`
|
||||
|
||||
- **document()**: belongsTo → `documents`
|
||||
|
||||
### estimates
|
||||
**모델**: `App\Models\Estimate\Estimate`
|
||||
|
||||
@@ -878,6 +907,7 @@ ### stocks
|
||||
- **item()**: belongsTo → `items`
|
||||
- **creator()**: belongsTo → `users`
|
||||
- **lots()**: hasMany → `stock_lots`
|
||||
- **transactions()**: hasMany → `stock_transactions`
|
||||
|
||||
### stock_lots
|
||||
**모델**: `App\Models\Tenants\StockLot`
|
||||
@@ -886,6 +916,13 @@ ### stock_lots
|
||||
- **receiving()**: belongsTo → `receivings`
|
||||
- **creator()**: belongsTo → `users`
|
||||
|
||||
### stock_transactions
|
||||
**모델**: `App\Models\Tenants\StockTransaction`
|
||||
|
||||
- **stock()**: belongsTo → `stocks`
|
||||
- **stockLot()**: belongsTo → `stock_lots`
|
||||
- **creator()**: belongsTo → `users`
|
||||
|
||||
### subscriptions
|
||||
**모델**: `App\Models\Tenants\Subscription`
|
||||
|
||||
|
||||
640
app/Console/Commands/MigrateBDModelsPrices.php
Normal file
640
app/Console/Commands/MigrateBDModelsPrices.php
Normal file
@@ -0,0 +1,640 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* chandj 원본 가격 테이블 → items + item_details + prices 마이그레이션
|
||||
*
|
||||
* 레거시 chandj DB의 BDmodels, price_motor, price_raw_materials,
|
||||
* price_shaft, price_pipe, price_angle, price_smokeban 데이터를
|
||||
* items + item_details + prices 통합 구조로 마이그레이션
|
||||
*/
|
||||
class MigrateBDModelsPrices extends Command
|
||||
{
|
||||
protected $signature = 'kd:migrate-prices
|
||||
{--dry-run : 실제 DB 변경 없이 미리보기}
|
||||
{--fresh : 기존 EST-* 항목 삭제 후 재생성}';
|
||||
|
||||
protected $description = '경동 견적 단가를 chandj 원본에서 items+item_details+prices로 마이그레이션';
|
||||
|
||||
private const TENANT_ID = 287;
|
||||
|
||||
private int $created = 0;
|
||||
|
||||
private int $updated = 0;
|
||||
|
||||
private int $skipped = 0;
|
||||
|
||||
private int $deleted = 0;
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$dryRun = $this->option('dry-run');
|
||||
$fresh = $this->option('fresh');
|
||||
|
||||
$this->info('=== 경동 견적 단가 마이그레이션 (chandj 원본) ===');
|
||||
$this->info($dryRun ? '[DRY RUN] 실제 변경 없음' : '[LIVE] DB에 반영합니다');
|
||||
$this->newLine();
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
// --fresh: 기존 EST-* 항목 삭제
|
||||
if ($fresh) {
|
||||
$this->cleanExistingEstItems($dryRun);
|
||||
}
|
||||
|
||||
// 1. BDmodels (절곡품: 케이스, 가이드레일, 하단마감재, 마구리, 연기차단재, L바, 보강평철)
|
||||
$this->migrateBDModels($dryRun);
|
||||
|
||||
// 2. price_motor (모터 + 제어기)
|
||||
$this->migrateMotors($dryRun);
|
||||
|
||||
// 3. price_raw_materials (원자재: 실리카, 화이바, 와이어 등)
|
||||
$this->migrateRawMaterials($dryRun);
|
||||
|
||||
// 4. price_shaft (감기샤프트)
|
||||
$this->migrateShafts($dryRun);
|
||||
|
||||
// 5. price_pipe (각파이프)
|
||||
$this->migratePipes($dryRun);
|
||||
|
||||
// 6. price_angle (앵글)
|
||||
$this->migrateAngles($dryRun);
|
||||
|
||||
// 7. price_smokeban (연기차단재 - BDmodels에 없는 경우 보완)
|
||||
$this->migrateSmokeBan($dryRun);
|
||||
|
||||
if ($dryRun) {
|
||||
DB::rollBack();
|
||||
$this->warn('[DRY RUN] 롤백 완료');
|
||||
} else {
|
||||
DB::commit();
|
||||
$this->info('커밋 완료');
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->info("생성: {$this->created}건, 업데이트: {$this->updated}건, 스킵: {$this->skipped}건, 삭제: {$this->deleted}건");
|
||||
|
||||
return Command::SUCCESS;
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
$this->error("오류: {$e->getMessage()}");
|
||||
$this->error($e->getTraceAsString());
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 EST-* 항목 삭제 (--fresh 옵션)
|
||||
*/
|
||||
private function cleanExistingEstItems(bool $dryRun): void
|
||||
{
|
||||
$this->info('--- 기존 EST-* 항목 삭제 ---');
|
||||
|
||||
$items = DB::table('items')
|
||||
->where('tenant_id', self::TENANT_ID)
|
||||
->where('code', 'LIKE', 'EST-%')
|
||||
->whereNull('deleted_at')
|
||||
->get(['id', 'code']);
|
||||
|
||||
foreach ($items as $item) {
|
||||
$this->line(" [삭제] {$item->code}");
|
||||
if (! $dryRun) {
|
||||
DB::table('prices')->where('item_id', $item->id)->delete();
|
||||
DB::table('item_details')->where('item_id', $item->id)->delete();
|
||||
DB::table('items')->where('id', $item->id)->delete();
|
||||
}
|
||||
$this->deleted++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* chandj.BDmodels → items + item_details + prices
|
||||
*/
|
||||
private function migrateBDModels(bool $dryRun): void
|
||||
{
|
||||
$this->info('--- BDmodels (절곡품) ---');
|
||||
|
||||
$rows = DB::connection('chandj')->select("
|
||||
SELECT model_name, seconditem, finishing_type, spec, unitprice, description
|
||||
FROM BDmodels
|
||||
WHERE is_deleted = 0
|
||||
ORDER BY model_name, seconditem, finishing_type, spec
|
||||
");
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$modelName = trim($row->model_name ?? '');
|
||||
$secondItem = trim($row->seconditem ?? '');
|
||||
$finishingType = trim($row->finishing_type ?? '');
|
||||
$spec = trim($row->spec ?? '');
|
||||
$unitPrice = (float) str_replace(',', '', $row->unitprice ?? '0');
|
||||
|
||||
// finishing_type 정규화: 'SUS마감' → 'SUS', 'EGI마감' → 'EGI'
|
||||
$finishingType = str_replace('마감', '', $finishingType);
|
||||
|
||||
if (empty($secondItem) || $unitPrice <= 0) {
|
||||
$this->skipped++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$codeParts = ['BD', $secondItem];
|
||||
if ($modelName) {
|
||||
$codeParts[] = $modelName;
|
||||
}
|
||||
if ($finishingType) {
|
||||
$codeParts[] = $finishingType;
|
||||
}
|
||||
if ($spec) {
|
||||
$codeParts[] = $spec;
|
||||
}
|
||||
$code = implode('-', $codeParts);
|
||||
|
||||
$nameParts = [$secondItem];
|
||||
if ($modelName) {
|
||||
$nameParts[] = $modelName;
|
||||
}
|
||||
if ($finishingType) {
|
||||
$nameParts[] = $finishingType;
|
||||
}
|
||||
if ($spec) {
|
||||
$nameParts[] = $spec;
|
||||
}
|
||||
$name = implode(' ', $nameParts);
|
||||
|
||||
$this->upsertEstimateItem(
|
||||
code: $code,
|
||||
name: $name,
|
||||
productCategory: 'bdmodels',
|
||||
partType: $secondItem,
|
||||
specification: $spec ?: null,
|
||||
attributes: array_filter([
|
||||
'model_name' => $modelName ?: null,
|
||||
'finishing_type' => $finishingType ?: null,
|
||||
'bdmodel_source' => 'BDmodels',
|
||||
'description' => $row->description ?: null,
|
||||
]),
|
||||
salesPrice: $unitPrice,
|
||||
note: 'chandj.BDmodels',
|
||||
dryRun: $dryRun
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* chandj.price_motor → 모터 + 제어기
|
||||
*
|
||||
* col1: 전압 (220, 380, 제어기, 방화, 방범)
|
||||
* col2: 용량/종류 (150K(S), 300K, 매립형, 노출형 등)
|
||||
* col13: 판매가
|
||||
*/
|
||||
private function migrateMotors(bool $dryRun): void
|
||||
{
|
||||
$this->info('--- price_motor (모터/제어기) ---');
|
||||
|
||||
$row = DB::connection('chandj')->selectOne(
|
||||
"SELECT itemList FROM price_motor WHERE is_deleted IS NULL OR is_deleted = 0 ORDER BY NUM LIMIT 1"
|
||||
);
|
||||
if (! $row) {
|
||||
return;
|
||||
}
|
||||
|
||||
$items = json_decode($row->itemList, true);
|
||||
|
||||
foreach ($items as $item) {
|
||||
$category = trim($item['col1'] ?? ''); // 220, 380, 제어기, 방화, 방범
|
||||
$name = trim($item['col2'] ?? ''); // 150K(S), 매립형 등
|
||||
$price = (int) str_replace(',', '', $item['col13'] ?? '0');
|
||||
|
||||
if (empty($name) || $price <= 0) {
|
||||
$this->skipped++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// 카테고리 분류
|
||||
if (in_array($category, ['220', '380'])) {
|
||||
$productCategory = 'motor';
|
||||
$code = "EST-MOTOR-{$category}V-{$name}";
|
||||
$displayName = "모터 {$name} ({$category}V)";
|
||||
$partType = $name;
|
||||
} elseif ($category === '제어기') {
|
||||
$productCategory = 'controller';
|
||||
$code = "EST-CTRL-{$name}";
|
||||
$displayName = "제어기 {$name}";
|
||||
$partType = $name;
|
||||
} else {
|
||||
// 방화, 방범 등
|
||||
$productCategory = 'controller';
|
||||
$code = "EST-CTRL-{$category}-{$name}";
|
||||
$displayName = "{$category} {$name}";
|
||||
$partType = "{$category} {$name}";
|
||||
}
|
||||
|
||||
$this->upsertEstimateItem(
|
||||
code: $code,
|
||||
name: $displayName,
|
||||
productCategory: $productCategory,
|
||||
partType: $partType,
|
||||
specification: null,
|
||||
attributes: ['voltage' => $category, 'source' => 'price_motor'],
|
||||
salesPrice: (float) $price,
|
||||
note: 'chandj.price_motor',
|
||||
dryRun: $dryRun
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* chandj.price_raw_materials → 원자재
|
||||
*
|
||||
* col1: 카테고리 (슬랫, 스크린)
|
||||
* col2: 품명 (방화, 실리카, 화이바, 와이어 등)
|
||||
* col13: 판매단가
|
||||
*/
|
||||
private function migrateRawMaterials(bool $dryRun): void
|
||||
{
|
||||
$this->info('--- price_raw_materials (원자재) ---');
|
||||
|
||||
$row = DB::connection('chandj')->selectOne(
|
||||
"SELECT itemList FROM price_raw_materials WHERE is_deleted IS NULL OR is_deleted = 0 ORDER BY registedate DESC LIMIT 1"
|
||||
);
|
||||
if (! $row) {
|
||||
return;
|
||||
}
|
||||
|
||||
$items = json_decode($row->itemList, true);
|
||||
|
||||
foreach ($items as $item) {
|
||||
$category = trim($item['col1'] ?? '');
|
||||
$name = trim($item['col2'] ?? '');
|
||||
$price = (int) str_replace(',', '', $item['col13'] ?? '0');
|
||||
|
||||
if (empty($name) || $price <= 0) {
|
||||
$this->skipped++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$code = "EST-RAW-{$category}-{$name}";
|
||||
$displayName = "{$category} {$name}";
|
||||
|
||||
$this->upsertEstimateItem(
|
||||
code: $code,
|
||||
name: $displayName,
|
||||
productCategory: 'raw_material',
|
||||
partType: $name,
|
||||
specification: $category,
|
||||
attributes: ['category' => $category, 'source' => 'price_raw_materials'],
|
||||
salesPrice: (float) $price,
|
||||
note: 'chandj.price_raw_materials',
|
||||
dryRun: $dryRun
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* chandj.price_shaft → 감기샤프트
|
||||
*
|
||||
* col4: 인치 (3, 4, 5, 6, 8, 10, 12)
|
||||
* col10: 길이 (m)
|
||||
* col19: 판매가
|
||||
*/
|
||||
private function migrateShafts(bool $dryRun): void
|
||||
{
|
||||
$this->info('--- price_shaft (감기샤프트) ---');
|
||||
|
||||
$row = DB::connection('chandj')->selectOne(
|
||||
"SELECT itemList FROM price_shaft WHERE is_deleted IS NULL OR is_deleted = 0 ORDER BY NUM LIMIT 1"
|
||||
);
|
||||
if (! $row) {
|
||||
return;
|
||||
}
|
||||
|
||||
$items = json_decode($row->itemList, true);
|
||||
|
||||
foreach ($items as $item) {
|
||||
$inch = trim($item['col4'] ?? '');
|
||||
$lengthM = trim($item['col10'] ?? '');
|
||||
$price = (int) str_replace(',', '', $item['col19'] ?? '0');
|
||||
|
||||
if (empty($inch) || empty($lengthM) || $price <= 0) {
|
||||
$this->skipped++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$code = "EST-SHAFT-{$inch}-{$lengthM}";
|
||||
$name = "감기샤프트 {$inch}인치 {$lengthM}m";
|
||||
|
||||
$this->upsertEstimateItem(
|
||||
code: $code,
|
||||
name: $name,
|
||||
productCategory: 'shaft',
|
||||
partType: $inch,
|
||||
specification: $lengthM,
|
||||
attributes: ['source' => 'price_shaft'],
|
||||
salesPrice: (float) $price,
|
||||
note: 'chandj.price_shaft',
|
||||
dryRun: $dryRun
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* chandj.price_pipe → 각파이프
|
||||
*
|
||||
* col4: 두께 (1.4, 2)
|
||||
* col2: 길이 (3,000 / 6,000)
|
||||
* col8: 판매가
|
||||
*/
|
||||
private function migratePipes(bool $dryRun): void
|
||||
{
|
||||
$this->info('--- price_pipe (각파이프) ---');
|
||||
|
||||
$row = DB::connection('chandj')->selectOne(
|
||||
"SELECT itemList FROM price_pipe WHERE (is_deleted IS NULL OR is_deleted = 0 OR is_deleted = '') ORDER BY NUM LIMIT 1"
|
||||
);
|
||||
if (! $row) {
|
||||
return;
|
||||
}
|
||||
|
||||
$items = json_decode($row->itemList, true);
|
||||
|
||||
foreach ($items as $item) {
|
||||
$thickness = trim($item['col4'] ?? '');
|
||||
$length = (int) str_replace(',', '', $item['col2'] ?? '0');
|
||||
$price = (int) str_replace(',', '', $item['col8'] ?? '0');
|
||||
|
||||
if (empty($thickness) || $length <= 0 || $price <= 0) {
|
||||
$this->skipped++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$code = "EST-PIPE-{$thickness}-{$length}";
|
||||
$name = "각파이프 {$thickness}T {$length}mm";
|
||||
|
||||
$this->upsertEstimateItem(
|
||||
code: $code,
|
||||
name: $name,
|
||||
productCategory: 'pipe',
|
||||
partType: $thickness,
|
||||
specification: (string) $length,
|
||||
attributes: ['spec' => $item['col3'] ?? '', 'source' => 'price_pipe'],
|
||||
salesPrice: (float) $price,
|
||||
note: 'chandj.price_pipe',
|
||||
dryRun: $dryRun
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* chandj.price_angle → 앵글 (bracket + main 분리)
|
||||
*
|
||||
* bracket angle (모터 받침용): col2가 텍스트 (스크린용, 철제300K 등)
|
||||
* - col2: 검색옵션, col3: 브라켓크기, col4: 앵글타입, col19: 판매가
|
||||
*
|
||||
* main angle (부자재용): col2가 숫자 (4 등)
|
||||
* - col4: 종류 (앵글3T, 앵글4T), col10: 길이 (2.5, 10), col19: 판매가
|
||||
*/
|
||||
private function migrateAngles(bool $dryRun): void
|
||||
{
|
||||
$this->info('--- price_angle (앵글) ---');
|
||||
|
||||
$row = DB::connection('chandj')->selectOne(
|
||||
"SELECT itemList FROM price_angle WHERE is_deleted IS NULL OR is_deleted = 0 ORDER BY NUM LIMIT 1"
|
||||
);
|
||||
if (! $row) {
|
||||
return;
|
||||
}
|
||||
|
||||
$items = json_decode($row->itemList, true);
|
||||
|
||||
foreach ($items as $item) {
|
||||
$col2 = trim($item['col2'] ?? '');
|
||||
$col3 = trim($item['col3'] ?? '');
|
||||
$col4 = trim($item['col4'] ?? '');
|
||||
$col10 = trim($item['col10'] ?? '');
|
||||
$price = (int) str_replace(',', '', $item['col19'] ?? '0');
|
||||
|
||||
if ($price <= 0) {
|
||||
$this->skipped++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// col2가 숫자이면 main angle, 텍스트이면 bracket angle
|
||||
if (is_numeric($col2)) {
|
||||
// Main angle (부자재용): col4=앵글3T, col10=2.5
|
||||
if (empty($col4) || empty($col10)) {
|
||||
$this->skipped++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$code = "EST-ANGLE-MAIN-{$col4}-{$col10}";
|
||||
$name = "앵글 {$col4} {$col10}m";
|
||||
|
||||
$this->upsertEstimateItem(
|
||||
code: $code,
|
||||
name: $name,
|
||||
productCategory: 'angle_main',
|
||||
partType: $col4,
|
||||
specification: $col10,
|
||||
attributes: ['source' => 'price_angle'],
|
||||
salesPrice: (float) $price,
|
||||
note: 'chandj.price_angle (main)',
|
||||
dryRun: $dryRun
|
||||
);
|
||||
} else {
|
||||
// Bracket angle (모터 받침용): col2=스크린용, col3=380*180
|
||||
if (empty($col2)) {
|
||||
$this->skipped++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$code = "EST-ANGLE-BRACKET-{$col2}";
|
||||
$name = "모터받침 앵글 {$col2}";
|
||||
|
||||
$this->upsertEstimateItem(
|
||||
code: $code,
|
||||
name: $name,
|
||||
productCategory: 'angle_bracket',
|
||||
partType: $col2,
|
||||
specification: $col3 ?: null,
|
||||
attributes: [
|
||||
'angle_type' => $col4,
|
||||
'source' => 'price_angle',
|
||||
],
|
||||
salesPrice: (float) $price,
|
||||
note: 'chandj.price_angle (bracket)',
|
||||
dryRun: $dryRun
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* chandj.price_smokeban → 연기차단재
|
||||
*
|
||||
* col2: 용도 (레일용, 케이스용)
|
||||
* col11: 판매가
|
||||
*/
|
||||
private function migrateSmokeBan(bool $dryRun): void
|
||||
{
|
||||
$this->info('--- price_smokeban (연기차단재) ---');
|
||||
|
||||
$row = DB::connection('chandj')->selectOne(
|
||||
"SELECT itemList FROM price_smokeban WHERE is_deleted IS NULL OR is_deleted = 0 ORDER BY NUM LIMIT 1"
|
||||
);
|
||||
if (! $row) {
|
||||
return;
|
||||
}
|
||||
|
||||
$items = json_decode($row->itemList, true);
|
||||
|
||||
foreach ($items as $item) {
|
||||
$usage = trim($item['col2'] ?? '');
|
||||
$price = (int) str_replace(',', '', $item['col11'] ?? '0');
|
||||
|
||||
if (empty($usage) || $price <= 0) {
|
||||
$this->skipped++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$code = "EST-SMOKE-{$usage}";
|
||||
$name = "연기차단재 {$usage}";
|
||||
|
||||
$this->upsertEstimateItem(
|
||||
code: $code,
|
||||
name: $name,
|
||||
productCategory: 'smokeban',
|
||||
partType: $usage,
|
||||
specification: null,
|
||||
attributes: ['source' => 'price_smokeban'],
|
||||
salesPrice: (float) $price,
|
||||
note: 'chandj.price_smokeban',
|
||||
dryRun: $dryRun
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 견적 품목 생성 또는 가격 업데이트
|
||||
*/
|
||||
private function upsertEstimateItem(
|
||||
string $code,
|
||||
string $name,
|
||||
string $productCategory,
|
||||
string $partType,
|
||||
?string $specification,
|
||||
array $attributes,
|
||||
float $salesPrice,
|
||||
string $note,
|
||||
bool $dryRun
|
||||
): void {
|
||||
$existing = DB::table('items')
|
||||
->where('tenant_id', self::TENANT_ID)
|
||||
->where('code', $code)
|
||||
->whereNull('deleted_at')
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
// 가격 업데이트
|
||||
$currentPrice = DB::table('prices')
|
||||
->where('item_id', $existing->id)
|
||||
->where('status', 'active')
|
||||
->orderByDesc('id')
|
||||
->value('sales_price');
|
||||
|
||||
if ((float) $currentPrice === $salesPrice) {
|
||||
$this->skipped++;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->line(" [업데이트] {$code} 가격: " . number_format($currentPrice ?? 0) . " → " . number_format($salesPrice));
|
||||
|
||||
if (! $dryRun) {
|
||||
// 기존 가격 비활성화
|
||||
DB::table('prices')
|
||||
->where('item_id', $existing->id)
|
||||
->where('status', 'active')
|
||||
->update(['status' => 'inactive', 'updated_at' => now()]);
|
||||
|
||||
// 새 가격 추가
|
||||
DB::table('prices')->insert([
|
||||
'tenant_id' => self::TENANT_ID,
|
||||
'item_type_code' => 'PT',
|
||||
'item_id' => $existing->id,
|
||||
'sales_price' => $salesPrice,
|
||||
'effective_from' => now()->toDateString(),
|
||||
'status' => 'active',
|
||||
'note' => $note,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
$this->updated++;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 신규 생성
|
||||
$this->line(" [생성] {$code} ({$name}) = " . number_format($salesPrice));
|
||||
|
||||
if ($dryRun) {
|
||||
$this->created++;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$now = now();
|
||||
|
||||
$itemId = DB::table('items')->insertGetId([
|
||||
'tenant_id' => self::TENANT_ID,
|
||||
'item_type' => 'PT',
|
||||
'code' => $code,
|
||||
'name' => $name,
|
||||
'unit' => 'EA',
|
||||
'attributes' => json_encode($attributes, JSON_UNESCAPED_UNICODE),
|
||||
'is_active' => true,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
DB::table('item_details')->insert([
|
||||
'item_id' => $itemId,
|
||||
'product_category' => $productCategory,
|
||||
'part_type' => $partType,
|
||||
'specification' => $specification,
|
||||
'item_name' => $name,
|
||||
'is_purchasable' => true,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
DB::table('prices')->insert([
|
||||
'tenant_id' => self::TENANT_ID,
|
||||
'item_type_code' => 'PT',
|
||||
'item_id' => $itemId,
|
||||
'sales_price' => $salesPrice,
|
||||
'effective_from' => $now->toDateString(),
|
||||
'status' => 'active',
|
||||
'note' => $note,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
$this->created++;
|
||||
}
|
||||
}
|
||||
60
app/Console/Commands/StatAggregateDailyCommand.php
Normal file
60
app/Console/Commands/StatAggregateDailyCommand.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\Stats\StatAggregatorService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class StatAggregateDailyCommand extends Command
|
||||
{
|
||||
protected $signature = 'stat:aggregate-daily
|
||||
{--date= : 집계 대상 날짜 (YYYY-MM-DD, 기본: 전일)}
|
||||
{--domain= : 특정 도메인만 집계 (sales, finance, production)}
|
||||
{--tenant= : 특정 테넌트만 집계}';
|
||||
|
||||
protected $description = '일간 통계 집계 (sam_stat DB)';
|
||||
|
||||
public function handle(StatAggregatorService $aggregator): int
|
||||
{
|
||||
$date = $this->option('date')
|
||||
? Carbon::parse($this->option('date'))
|
||||
: Carbon::yesterday();
|
||||
|
||||
$domain = $this->option('domain');
|
||||
$tenantId = $this->option('tenant') ? (int) $this->option('tenant') : null;
|
||||
|
||||
$this->info("📊 일간 통계 집계 시작: {$date->format('Y-m-d')}");
|
||||
|
||||
if ($domain) {
|
||||
$this->info(" 도메인 필터: {$domain}");
|
||||
}
|
||||
if ($tenantId) {
|
||||
$this->info(" 테넌트 필터: {$tenantId}");
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $aggregator->aggregateDaily($date, $domain, $tenantId);
|
||||
|
||||
$this->info('✅ 일간 집계 완료:');
|
||||
$this->info(" - 처리 테넌트: {$result['tenants_processed']}");
|
||||
$this->info(" - 처리 도메인: {$result['domains_processed']}");
|
||||
$this->info(" - 소요 시간: {$result['duration_ms']}ms");
|
||||
|
||||
if (! empty($result['errors'])) {
|
||||
$this->warn(' ⚠️ 에러 발생: '.count($result['errors']).'건');
|
||||
foreach ($result['errors'] as $error) {
|
||||
$this->error(" - {$error}");
|
||||
}
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
} catch (\Throwable $e) {
|
||||
$this->error("❌ 집계 실패: {$e->getMessage()}");
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
54
app/Console/Commands/StatAggregateMonthlyCommand.php
Normal file
54
app/Console/Commands/StatAggregateMonthlyCommand.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\Stats\StatAggregatorService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class StatAggregateMonthlyCommand extends Command
|
||||
{
|
||||
protected $signature = 'stat:aggregate-monthly
|
||||
{--year= : 집계 대상 연도 (기본: 전월 기준)}
|
||||
{--month= : 집계 대상 월 (기본: 전월)}
|
||||
{--domain= : 특정 도메인만 집계}
|
||||
{--tenant= : 특정 테넌트만 집계}';
|
||||
|
||||
protected $description = '월간 통계 집계 (sam_stat DB)';
|
||||
|
||||
public function handle(StatAggregatorService $aggregator): int
|
||||
{
|
||||
$lastMonth = Carbon::now()->subMonth();
|
||||
$year = $this->option('year') ? (int) $this->option('year') : $lastMonth->year;
|
||||
$month = $this->option('month') ? (int) $this->option('month') : $lastMonth->month;
|
||||
|
||||
$domain = $this->option('domain');
|
||||
$tenantId = $this->option('tenant') ? (int) $this->option('tenant') : null;
|
||||
|
||||
$this->info("📊 월간 통계 집계 시작: {$year}-".str_pad($month, 2, '0', STR_PAD_LEFT));
|
||||
|
||||
try {
|
||||
$result = $aggregator->aggregateMonthly($year, $month, $domain, $tenantId);
|
||||
|
||||
$this->info('✅ 월간 집계 완료:');
|
||||
$this->info(" - 처리 테넌트: {$result['tenants_processed']}");
|
||||
$this->info(" - 처리 도메인: {$result['domains_processed']}");
|
||||
$this->info(" - 소요 시간: {$result['duration_ms']}ms");
|
||||
|
||||
if (! empty($result['errors'])) {
|
||||
$this->warn(' ⚠️ 에러 발생: '.count($result['errors']).'건');
|
||||
foreach ($result['errors'] as $error) {
|
||||
$this->error(" - {$error}");
|
||||
}
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
} catch (\Throwable $e) {
|
||||
$this->error("❌ 집계 실패: {$e->getMessage()}");
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
174
app/Console/Commands/StatBackfillCommand.php
Normal file
174
app/Console/Commands/StatBackfillCommand.php
Normal file
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\Stats\DimensionSyncService;
|
||||
use App\Services\Stats\StatAggregatorService;
|
||||
use Carbon\Carbon;
|
||||
use Carbon\CarbonPeriod;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class StatBackfillCommand extends Command
|
||||
{
|
||||
protected $signature = 'stat:backfill
|
||||
{--from= : 시작 날짜 (YYYY-MM-DD, 필수)}
|
||||
{--to= : 종료 날짜 (YYYY-MM-DD, 기본: 어제)}
|
||||
{--domain= : 특정 도메인만 집계}
|
||||
{--tenant= : 특정 테넌트만 집계}
|
||||
{--skip-monthly : 월간 집계 건너뛰기}
|
||||
{--skip-dimensions : 차원 동기화 건너뛰기}';
|
||||
|
||||
protected $description = '과거 데이터 일괄 통계 집계 (백필)';
|
||||
|
||||
public function handle(StatAggregatorService $aggregator, DimensionSyncService $dimensionSync): int
|
||||
{
|
||||
$from = $this->option('from');
|
||||
if (! $from) {
|
||||
$this->error('--from 옵션은 필수입니다. 예: --from=2024-01-01');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$startDate = Carbon::parse($from);
|
||||
$endDate = $this->option('to')
|
||||
? Carbon::parse($this->option('to'))
|
||||
: Carbon::yesterday();
|
||||
|
||||
$domain = $this->option('domain');
|
||||
$tenantId = $this->option('tenant') ? (int) $this->option('tenant') : null;
|
||||
|
||||
$totalDays = $startDate->diffInDays($endDate) + 1;
|
||||
|
||||
$this->info("📊 백필 시작: {$startDate->format('Y-m-d')} ~ {$endDate->format('Y-m-d')} ({$totalDays}일)");
|
||||
if ($domain) {
|
||||
$this->info(" 도메인 필터: {$domain}");
|
||||
}
|
||||
if ($tenantId) {
|
||||
$this->info(" 테넌트 필터: {$tenantId}");
|
||||
}
|
||||
|
||||
$totalErrors = [];
|
||||
$totalDomainsProcessed = 0;
|
||||
$startTime = microtime(true);
|
||||
|
||||
// 1. 차원 테이블 동기화 (최초 1회)
|
||||
if (! $this->option('skip-dimensions')) {
|
||||
$this->info('');
|
||||
$this->info('🔄 차원 테이블 동기화...');
|
||||
try {
|
||||
$tenants = $this->getTargetTenants($tenantId);
|
||||
foreach ($tenants as $tenant) {
|
||||
$clients = $dimensionSync->syncClients($tenant->id);
|
||||
$products = $dimensionSync->syncProducts($tenant->id);
|
||||
$this->line(" tenant={$tenant->id}: 고객 {$clients}건, 제품 {$products}건");
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->warn(" 차원 동기화 실패: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 일간 집계
|
||||
$this->info('');
|
||||
$this->info('📅 일간 집계 시작...');
|
||||
$bar = $this->output->createProgressBar($totalDays);
|
||||
$bar->setFormat(' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %message%');
|
||||
$bar->setMessage('');
|
||||
|
||||
$period = CarbonPeriod::create($startDate, $endDate);
|
||||
|
||||
foreach ($period as $date) {
|
||||
$bar->setMessage($date->format('Y-m-d'));
|
||||
|
||||
try {
|
||||
$result = $aggregator->aggregateDaily($date, $domain, $tenantId);
|
||||
$totalDomainsProcessed += $result['domains_processed'];
|
||||
|
||||
if (! empty($result['errors'])) {
|
||||
$totalErrors = array_merge($totalErrors, $result['errors']);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$totalErrors[] = "daily {$date->format('Y-m-d')}: {$e->getMessage()}";
|
||||
}
|
||||
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
$this->newLine();
|
||||
|
||||
// 3. 월간 집계
|
||||
if (! $this->option('skip-monthly')) {
|
||||
$this->info('');
|
||||
$this->info('📆 월간 집계 시작...');
|
||||
|
||||
$months = $this->getMonthRange($startDate, $endDate);
|
||||
$monthBar = $this->output->createProgressBar(count($months));
|
||||
|
||||
foreach ($months as [$year, $month]) {
|
||||
$monthBar->setMessage("{$year}-".str_pad($month, 2, '0', STR_PAD_LEFT));
|
||||
|
||||
try {
|
||||
$result = $aggregator->aggregateMonthly($year, $month, $domain, $tenantId);
|
||||
$totalDomainsProcessed += $result['domains_processed'];
|
||||
|
||||
if (! empty($result['errors'])) {
|
||||
$totalErrors = array_merge($totalErrors, $result['errors']);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$totalErrors[] = "monthly {$year}-{$month}: {$e->getMessage()}";
|
||||
}
|
||||
|
||||
$monthBar->advance();
|
||||
}
|
||||
|
||||
$monthBar->finish();
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
$durationSec = round(microtime(true) - $startTime, 1);
|
||||
|
||||
$this->info('');
|
||||
$this->info('✅ 백필 완료:');
|
||||
$this->info(" - 기간: {$startDate->format('Y-m-d')} ~ {$endDate->format('Y-m-d')} ({$totalDays}일)");
|
||||
$this->info(" - 처리 도메인-테넌트: {$totalDomainsProcessed}건");
|
||||
$this->info(" - 소요 시간: {$durationSec}초");
|
||||
|
||||
if (! empty($totalErrors)) {
|
||||
$this->warn(' - 에러: '.count($totalErrors).'건');
|
||||
foreach (array_slice($totalErrors, 0, 20) as $error) {
|
||||
$this->error(" - {$error}");
|
||||
}
|
||||
if (count($totalErrors) > 20) {
|
||||
$this->warn(' ... 외 '.(count($totalErrors) - 20).'건');
|
||||
}
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function getTargetTenants(?int $tenantId): \Illuminate\Database\Eloquent\Collection
|
||||
{
|
||||
$query = \App\Models\Tenants\Tenant::where('tenant_st_code', '!=', 'none');
|
||||
if ($tenantId) {
|
||||
$query->where('id', $tenantId);
|
||||
}
|
||||
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
private function getMonthRange(Carbon $start, Carbon $end): array
|
||||
{
|
||||
$months = [];
|
||||
$current = $start->copy()->startOfMonth();
|
||||
$endMonth = $end->copy()->startOfMonth();
|
||||
|
||||
while ($current->lte($endMonth)) {
|
||||
$months[] = [$current->year, $current->month];
|
||||
$current->addMonth();
|
||||
}
|
||||
|
||||
return $months;
|
||||
}
|
||||
}
|
||||
33
app/Console/Commands/StatCheckKpiAlertsCommand.php
Normal file
33
app/Console/Commands/StatCheckKpiAlertsCommand.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\Stats\KpiAlertService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class StatCheckKpiAlertsCommand extends Command
|
||||
{
|
||||
protected $signature = 'stat:check-kpi-alerts';
|
||||
|
||||
protected $description = 'KPI 목표 대비 실적을 체크하고 미달 시 알림을 생성합니다';
|
||||
|
||||
public function handle(KpiAlertService $service): int
|
||||
{
|
||||
$this->info('KPI 알림 체크 시작...');
|
||||
|
||||
$result = $service->checkKpiAlerts();
|
||||
|
||||
$this->info("알림 생성: {$result['alerts_created']}건");
|
||||
|
||||
if (! empty($result['errors'])) {
|
||||
$this->warn('오류 발생:');
|
||||
foreach ($result['errors'] as $error) {
|
||||
$this->error(" - {$error}");
|
||||
}
|
||||
}
|
||||
|
||||
$this->info('KPI 알림 체크 완료.');
|
||||
|
||||
return empty($result['errors']) ? self::SUCCESS : self::FAILURE;
|
||||
}
|
||||
}
|
||||
211
app/Console/Commands/StatVerifyCommand.php
Normal file
211
app/Console/Commands/StatVerifyCommand.php
Normal file
@@ -0,0 +1,211 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Stats\Daily\StatFinanceDaily;
|
||||
use App\Models\Stats\Daily\StatSalesDaily;
|
||||
use App\Models\Stats\Daily\StatSystemDaily;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class StatVerifyCommand extends Command
|
||||
{
|
||||
protected $signature = 'stat:verify
|
||||
{--date= : 검증 날짜 (YYYY-MM-DD, 기본: 어제)}
|
||||
{--tenant= : 특정 테넌트만 검증}
|
||||
{--domain= : 특정 도메인만 검증 (sales,finance,system)}
|
||||
{--fix : 불일치 시 자동 재집계}';
|
||||
|
||||
protected $description = '원본 DB와 sam_stat 통계 정합성 교차 검증';
|
||||
|
||||
private int $totalChecks = 0;
|
||||
|
||||
private int $passedChecks = 0;
|
||||
|
||||
private int $failedChecks = 0;
|
||||
|
||||
private array $mismatches = [];
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$date = $this->option('date')
|
||||
? Carbon::parse($this->option('date'))
|
||||
: Carbon::yesterday();
|
||||
$tenantId = $this->option('tenant') ? (int) $this->option('tenant') : null;
|
||||
$domain = $this->option('domain');
|
||||
$dateStr = $date->format('Y-m-d');
|
||||
|
||||
$this->info("🔍 정합성 검증: {$dateStr}");
|
||||
|
||||
$tenants = $this->getTargetTenants($tenantId);
|
||||
|
||||
$domains = $domain
|
||||
? [$domain]
|
||||
: ['sales', 'finance', 'system'];
|
||||
|
||||
foreach ($tenants as $tenant) {
|
||||
$this->info('');
|
||||
$this->info("── tenant={$tenant->id} ──");
|
||||
|
||||
foreach ($domains as $d) {
|
||||
match ($d) {
|
||||
'sales' => $this->verifySales($tenant->id, $dateStr),
|
||||
'finance' => $this->verifyFinance($tenant->id, $dateStr),
|
||||
'system' => $this->verifySystem($tenant->id, $dateStr),
|
||||
default => $this->warn(" 미지원 도메인: {$d}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
$this->printSummary();
|
||||
|
||||
if ($this->failedChecks > 0 && $this->option('fix')) {
|
||||
$this->info('');
|
||||
$this->info('🔧 불일치 항목 재집계...');
|
||||
$this->reAggregate($date, $tenantId, $domains);
|
||||
}
|
||||
|
||||
return $this->failedChecks > 0 ? self::FAILURE : self::SUCCESS;
|
||||
}
|
||||
|
||||
private function verifySales(int $tenantId, string $dateStr): void
|
||||
{
|
||||
$this->line(' [sales]');
|
||||
|
||||
$originOrderCount = DB::connection('mysql')
|
||||
->table('orders')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereDate('created_at', $dateStr)
|
||||
->whereNull('deleted_at')
|
||||
->count();
|
||||
|
||||
$originSalesAmount = (float) DB::connection('mysql')
|
||||
->table('sales')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('sale_date', $dateStr)
|
||||
->whereNull('deleted_at')
|
||||
->sum('supply_amount');
|
||||
|
||||
$stat = StatSalesDaily::where('tenant_id', $tenantId)
|
||||
->where('stat_date', $dateStr)
|
||||
->first();
|
||||
|
||||
$this->check('수주건수', $originOrderCount, $stat?->order_count ?? 0, $tenantId, 'sales');
|
||||
$this->check('매출금액', $originSalesAmount, (float) ($stat?->sales_amount ?? 0), $tenantId, 'sales');
|
||||
}
|
||||
|
||||
private function verifyFinance(int $tenantId, string $dateStr): void
|
||||
{
|
||||
$this->line(' [finance]');
|
||||
|
||||
$originDepositAmount = (float) DB::connection('mysql')
|
||||
->table('deposits')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('deposit_date', $dateStr)
|
||||
->whereNull('deleted_at')
|
||||
->sum('amount');
|
||||
|
||||
$originWithdrawalAmount = (float) DB::connection('mysql')
|
||||
->table('withdrawals')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('withdrawal_date', $dateStr)
|
||||
->whereNull('deleted_at')
|
||||
->sum('amount');
|
||||
|
||||
$stat = StatFinanceDaily::where('tenant_id', $tenantId)
|
||||
->where('stat_date', $dateStr)
|
||||
->first();
|
||||
|
||||
$this->check('입금액', $originDepositAmount, (float) ($stat?->deposit_amount ?? 0), $tenantId, 'finance');
|
||||
$this->check('출금액', $originWithdrawalAmount, (float) ($stat?->withdrawal_amount ?? 0), $tenantId, 'finance');
|
||||
}
|
||||
|
||||
private function verifySystem(int $tenantId, string $dateStr): void
|
||||
{
|
||||
$this->line(' [system]');
|
||||
|
||||
$originApiCount = DB::connection('mysql')
|
||||
->table('api_request_logs')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereDate('created_at', $dateStr)
|
||||
->count();
|
||||
|
||||
$originAuditCount = DB::connection('mysql')
|
||||
->table('audit_logs')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereDate('created_at', $dateStr)
|
||||
->count();
|
||||
|
||||
$stat = StatSystemDaily::where('tenant_id', $tenantId)
|
||||
->where('stat_date', $dateStr)
|
||||
->first();
|
||||
|
||||
$this->check('API요청수', $originApiCount, $stat?->api_request_count ?? 0, $tenantId, 'system');
|
||||
|
||||
$statAuditTotal = ($stat?->audit_create_count ?? 0)
|
||||
+ ($stat?->audit_update_count ?? 0)
|
||||
+ ($stat?->audit_delete_count ?? 0);
|
||||
$this->check('감사로그수', $originAuditCount, $statAuditTotal, $tenantId, 'system');
|
||||
}
|
||||
|
||||
private function check(string $label, float|int $expected, float|int $actual, int $tenantId, string $domain): void
|
||||
{
|
||||
$this->totalChecks++;
|
||||
|
||||
$tolerance = is_float($expected) ? 0.01 : 0;
|
||||
$match = abs($expected - $actual) <= $tolerance;
|
||||
|
||||
if ($match) {
|
||||
$this->passedChecks++;
|
||||
$this->line(" ✅ {$label}: {$actual}");
|
||||
} else {
|
||||
$this->failedChecks++;
|
||||
$this->error(" ❌ {$label}: 원본={$expected} / 통계={$actual} (차이=".($actual - $expected).')');
|
||||
$this->mismatches[] = compact('tenantId', 'domain', 'label', 'expected', 'actual');
|
||||
}
|
||||
}
|
||||
|
||||
private function printSummary(): void
|
||||
{
|
||||
$this->info('');
|
||||
$this->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
$this->info("📊 검증 결과: {$this->totalChecks}건 검사, ✅ {$this->passedChecks}건 일치, ❌ {$this->failedChecks}건 불일치");
|
||||
|
||||
if ($this->failedChecks > 0) {
|
||||
$this->warn('');
|
||||
$this->warn('불일치 목록:');
|
||||
foreach ($this->mismatches as $m) {
|
||||
$this->warn(" - tenant={$m['tenantId']} [{$m['domain']}] {$m['label']}: 원본={$m['expected']} / 통계={$m['actual']}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function reAggregate(Carbon $date, ?int $tenantId, array $domains): void
|
||||
{
|
||||
$aggregator = app(\App\Services\Stats\StatAggregatorService::class);
|
||||
|
||||
foreach ($domains as $d) {
|
||||
$result = $aggregator->aggregateDaily($date, $d, $tenantId);
|
||||
$this->line(" {$d}: 재집계 완료 ({$result['domains_processed']}건)");
|
||||
|
||||
if (! empty($result['errors'])) {
|
||||
foreach ($result['errors'] as $error) {
|
||||
$this->error(" {$error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->info('✅ 재집계 완료. stat:verify를 다시 실행하여 확인하세요.');
|
||||
}
|
||||
|
||||
private function getTargetTenants(?int $tenantId): \Illuminate\Database\Eloquent\Collection
|
||||
{
|
||||
$query = \App\Models\Tenants\Tenant::where('tenant_st_code', '!=', 'none');
|
||||
if ($tenantId) {
|
||||
$query->where('id', $tenantId);
|
||||
}
|
||||
|
||||
return $query->get();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\v1;
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
@@ -106,6 +106,22 @@ public function tenantBoards()
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시판 상세 조회 (코드 기반)
|
||||
*/
|
||||
public function showByCode(string $code)
|
||||
{
|
||||
return ApiResponse::handle(function () use ($code) {
|
||||
$board = $this->boardService->getBoardByCode($code);
|
||||
|
||||
if (! $board) {
|
||||
abort(404, __('error.board.not_found'));
|
||||
}
|
||||
|
||||
return $board;
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시판 필드 목록 조회
|
||||
*/
|
||||
|
||||
79
app/Http/Controllers/Api/V1/Documents/DocumentController.php
Normal file
79
app/Http/Controllers/Api/V1/Documents/DocumentController.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Documents;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Document\IndexRequest;
|
||||
use App\Http\Requests\Document\StoreRequest;
|
||||
use App\Http\Requests\Document\UpdateRequest;
|
||||
use App\Services\DocumentService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class DocumentController extends Controller
|
||||
{
|
||||
public function __construct(private DocumentService $service) {}
|
||||
|
||||
/**
|
||||
* 문서 목록 조회
|
||||
* GET /v1/documents
|
||||
*/
|
||||
public function index(IndexRequest $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->list($request->validated());
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 문서 상세 조회
|
||||
* GET /v1/documents/{id}
|
||||
*/
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->show($id);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 문서 생성
|
||||
* POST /v1/documents
|
||||
*/
|
||||
public function store(StoreRequest $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->create($request->validated());
|
||||
}, __('message.created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 문서 수정
|
||||
* PATCH /v1/documents/{id}
|
||||
*/
|
||||
public function update(int $id, UpdateRequest $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id, $request) {
|
||||
return $this->service->update($id, $request->validated());
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 문서 삭제
|
||||
* DELETE /v1/documents/{id}
|
||||
*/
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->destroy($id);
|
||||
}, __('message.deleted'));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 결재 관련 메서드 (보류 - 기존 시스템 연동 필요)
|
||||
// =========================================================================
|
||||
// public function submit(int $id): JsonResponse
|
||||
// public function approve(int $id, ApproveRequest $request): JsonResponse
|
||||
// public function reject(int $id, RejectRequest $request): JsonResponse
|
||||
// public function cancel(int $id): JsonResponse
|
||||
}
|
||||
@@ -30,6 +30,7 @@ public function index(Request $request)
|
||||
'item_category' => $request->input('item_category'),
|
||||
'group_id' => $request->input('group_id'),
|
||||
'active' => $request->input('is_active') ?? $request->input('active'),
|
||||
'has_bom' => $request->input('has_bom'),
|
||||
];
|
||||
|
||||
return $this->service->index($params);
|
||||
|
||||
@@ -81,19 +81,8 @@ public function store(QuoteStoreRequest $request)
|
||||
*/
|
||||
public function update(QuoteUpdateRequest $request, int $id)
|
||||
{
|
||||
$validated = $request->validated();
|
||||
|
||||
// 🔍 디버깅: 요청 데이터 확인
|
||||
\Log::info('🔍 [QuoteController::update] 요청 수신', [
|
||||
'id' => $id,
|
||||
'raw_options_keys' => $request->input('options') ? array_keys($request->input('options')) : null,
|
||||
'raw_options_detail_items_count' => $request->input('options.detail_items') ? count($request->input('options.detail_items')) : 0,
|
||||
'validated_options_keys' => isset($validated['options']) ? array_keys($validated['options']) : null,
|
||||
'validated_options_detail_items_count' => isset($validated['options']['detail_items']) ? count($validated['options']['detail_items']) : 0,
|
||||
]);
|
||||
|
||||
return ApiResponse::handle(function () use ($validated, $id) {
|
||||
return $this->quoteService->update($id, $validated);
|
||||
return ApiResponse::handle(function () use ($request, $id) {
|
||||
return $this->quoteService->update($id, $request->validated());
|
||||
}, __('message.quote.updated'));
|
||||
}
|
||||
|
||||
@@ -270,4 +259,22 @@ public function sendHistory(int $id)
|
||||
return $this->documentService->getSendHistory($id);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 품목 단가 조회
|
||||
*
|
||||
* 품목 코드 배열을 받아 단가를 조회합니다.
|
||||
* 수동 품목 추가 시 단가를 조회하여 견적금액에 반영합니다.
|
||||
*/
|
||||
public function getItemPrices(\Illuminate\Http\Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'item_codes' => 'required|array|min:1',
|
||||
'item_codes.*' => 'required|string',
|
||||
]);
|
||||
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->calculationService->getItemPrices($request->input('item_codes'));
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
}
|
||||
|
||||
64
app/Http/Controllers/Api/V1/StatController.php
Normal file
64
app/Http/Controllers/Api/V1/StatController.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\V1\Stat\StatAlertRequest;
|
||||
use App\Http\Requests\V1\Stat\StatDailyRequest;
|
||||
use App\Http\Requests\V1\Stat\StatMonthlyRequest;
|
||||
use App\Services\Stats\StatQueryService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class StatController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly StatQueryService $statQueryService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 대시보드 요약 통계 (sam_stat 기반)
|
||||
*/
|
||||
public function summary(): JsonResponse
|
||||
{
|
||||
$data = $this->statQueryService->getDashboardSummary();
|
||||
|
||||
return ApiResponse::handle(['data' => $data], __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 일간 통계 조회
|
||||
*/
|
||||
public function daily(StatDailyRequest $request): JsonResponse
|
||||
{
|
||||
$data = $this->statQueryService->getDailyStat(
|
||||
$request->validated('domain'),
|
||||
$request->validated()
|
||||
);
|
||||
|
||||
return ApiResponse::handle(['data' => $data], __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 월간 통계 조회
|
||||
*/
|
||||
public function monthly(StatMonthlyRequest $request): JsonResponse
|
||||
{
|
||||
$data = $this->statQueryService->getMonthlyStat(
|
||||
$request->validated('domain'),
|
||||
$request->validated()
|
||||
);
|
||||
|
||||
return ApiResponse::handle(['data' => $data], __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 알림 목록 조회
|
||||
*/
|
||||
public function alerts(StatAlertRequest $request): JsonResponse
|
||||
{
|
||||
$data = $this->statQueryService->getAlerts($request->validated());
|
||||
|
||||
return ApiResponse::handle(['data' => $data], __('message.fetched'));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\v1;
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
171
app/Http/Middleware/ApiVersionMiddleware.php
Normal file
171
app/Http/Middleware/ApiVersionMiddleware.php
Normal file
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* API 버전 관리 미들웨어
|
||||
*
|
||||
* - 헤더 Accept-Version으로 버전 선택 (기본: v1)
|
||||
* - 요청된 버전의 라우트가 없으면 하위 버전으로 fallback
|
||||
* - 응답 헤더에 실제 사용된 버전 표시
|
||||
*/
|
||||
class ApiVersionMiddleware
|
||||
{
|
||||
/**
|
||||
* 지원하는 API 버전 목록 (우선순위 순)
|
||||
*/
|
||||
protected array $supportedVersions = ['v2', 'v1'];
|
||||
|
||||
/**
|
||||
* 기본 버전
|
||||
*/
|
||||
protected string $defaultVersion = 'v1';
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
// 1. 요청된 버전 확인 (헤더 > 쿼리 파라미터 > 기본값)
|
||||
$requestedVersion = $this->getRequestedVersion($request);
|
||||
|
||||
// 2. 실제 사용할 버전 결정 (fallback 적용)
|
||||
$actualVersion = $this->resolveVersion($request, $requestedVersion);
|
||||
|
||||
// 3. 요청에 버전 정보 저장 (컨트롤러에서 사용 가능)
|
||||
$request->attributes->set('api_version', $actualVersion);
|
||||
$request->attributes->set('api_version_requested', $requestedVersion);
|
||||
$request->attributes->set('api_version_fallback', $actualVersion !== $requestedVersion);
|
||||
|
||||
// 4. 요청 처리
|
||||
$response = $next($request);
|
||||
|
||||
// 5. 응답 헤더에 버전 정보 추가
|
||||
$response->headers->set('X-API-Version', $actualVersion);
|
||||
if ($actualVersion !== $requestedVersion) {
|
||||
$response->headers->set('X-API-Version-Fallback', 'true');
|
||||
$response->headers->set('X-API-Version-Requested', $requestedVersion);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 요청에서 버전 정보 추출
|
||||
*/
|
||||
protected function getRequestedVersion(Request $request): string
|
||||
{
|
||||
// 1. Accept-Version 헤더 (권장)
|
||||
$version = $request->header('Accept-Version');
|
||||
if ($version && $this->isValidVersion($version)) {
|
||||
return $version;
|
||||
}
|
||||
|
||||
// 2. X-API-Version 헤더 (대안)
|
||||
$version = $request->header('X-API-Version');
|
||||
if ($version && $this->isValidVersion($version)) {
|
||||
return $version;
|
||||
}
|
||||
|
||||
// 3. 쿼리 파라미터 (테스트용)
|
||||
$version = $request->query('api_version');
|
||||
if ($version && $this->isValidVersion($version)) {
|
||||
return $version;
|
||||
}
|
||||
|
||||
return $this->defaultVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* 유효한 버전인지 확인
|
||||
*/
|
||||
protected function isValidVersion(string $version): bool
|
||||
{
|
||||
return in_array($version, $this->supportedVersions, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 실제 사용할 버전 결정 (fallback 로직)
|
||||
*/
|
||||
protected function resolveVersion(Request $request, string $requestedVersion): string
|
||||
{
|
||||
// 요청된 버전부터 하위 버전까지 순차 확인
|
||||
$startIndex = array_search($requestedVersion, $this->supportedVersions, true);
|
||||
|
||||
if ($startIndex === false) {
|
||||
return $this->defaultVersion;
|
||||
}
|
||||
|
||||
// 요청된 버전부터 하위 버전까지 체크
|
||||
for ($i = $startIndex; $i < count($this->supportedVersions); $i++) {
|
||||
$version = $this->supportedVersions[$i];
|
||||
|
||||
if ($this->versionRouteExists($request, $version)) {
|
||||
return $version;
|
||||
}
|
||||
}
|
||||
|
||||
// 모든 버전에서 라우트를 찾지 못하면 기본값 반환
|
||||
return $this->defaultVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* 해당 버전의 라우트가 존재하는지 확인
|
||||
*/
|
||||
protected function versionRouteExists(Request $request, string $version): bool
|
||||
{
|
||||
$path = $request->path();
|
||||
|
||||
// URL에서 버전 부분 교체
|
||||
// /api/v1/users → /api/v2/users
|
||||
$versionedPath = preg_replace('/^api\/v\d+/', "api/{$version}", $path);
|
||||
|
||||
// 해당 경로의 라우트가 존재하는지 확인
|
||||
$routes = Route::getRoutes();
|
||||
|
||||
foreach ($routes as $route) {
|
||||
$routeUri = $route->uri();
|
||||
|
||||
// 정확히 일치하거나 파라미터 패턴 매칭
|
||||
if ($this->matchesRoute($versionedPath, $routeUri, $request->method())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 경로가 라우트와 일치하는지 확인
|
||||
*/
|
||||
protected function matchesRoute(string $path, string $routeUri, string $method): bool
|
||||
{
|
||||
// 라우트 URI의 파라미터를 정규식으로 변환
|
||||
// {id} → [^/]+
|
||||
$pattern = preg_replace('/\{[^}]+\}/', '[^/]+', $routeUri);
|
||||
$pattern = '#^'.$pattern.'$#';
|
||||
|
||||
return (bool) preg_match($pattern, $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 지원 버전 목록 반환 (외부에서 사용)
|
||||
*/
|
||||
public function getSupportedVersions(): array
|
||||
{
|
||||
return $this->supportedVersions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 기본 버전 반환
|
||||
*/
|
||||
public function getDefaultVersion(): string
|
||||
{
|
||||
return $this->defaultVersion;
|
||||
}
|
||||
}
|
||||
@@ -96,6 +96,7 @@ public function rules(): array
|
||||
// 기타
|
||||
'memo' => 'nullable|string',
|
||||
'is_active' => 'nullable|boolean',
|
||||
'is_overdue' => 'nullable|boolean',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ public function rules(): array
|
||||
// 기타
|
||||
'memo' => 'nullable|string',
|
||||
'is_active' => 'nullable|boolean',
|
||||
'is_overdue' => 'nullable|boolean',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
37
app/Http/Requests/Document/IndexRequest.php
Normal file
37
app/Http/Requests/Document/IndexRequest.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Document;
|
||||
|
||||
use App\Models\Documents\Document;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class IndexRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$statuses = implode(',', [
|
||||
Document::STATUS_DRAFT,
|
||||
Document::STATUS_PENDING,
|
||||
Document::STATUS_APPROVED,
|
||||
Document::STATUS_REJECTED,
|
||||
Document::STATUS_CANCELLED,
|
||||
]);
|
||||
|
||||
return [
|
||||
'status' => "nullable|string|in:{$statuses}",
|
||||
'template_id' => 'nullable|integer',
|
||||
'search' => 'nullable|string|max:100',
|
||||
'from_date' => 'nullable|date',
|
||||
'to_date' => 'nullable|date|after_or_equal:from_date',
|
||||
'sort_by' => 'nullable|string|in:created_at,document_no,title,status,submitted_at,completed_at',
|
||||
'sort_dir' => 'nullable|string|in:asc,desc',
|
||||
'per_page' => 'nullable|integer|min:1|max:100',
|
||||
'page' => 'nullable|integer|min:1',
|
||||
];
|
||||
}
|
||||
}
|
||||
59
app/Http/Requests/Document/StoreRequest.php
Normal file
59
app/Http/Requests/Document/StoreRequest.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Document;
|
||||
|
||||
use App\Models\Documents\DocumentAttachment;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$attachmentTypes = implode(',', DocumentAttachment::TYPES);
|
||||
|
||||
return [
|
||||
// 기본 정보
|
||||
'template_id' => 'required|integer|exists:document_templates,id',
|
||||
'title' => 'required|string|max:255',
|
||||
'linkable_type' => 'nullable|string|max:100',
|
||||
'linkable_id' => 'nullable|integer',
|
||||
|
||||
// 결재선 (보류 상태이지만 구조는 유지)
|
||||
'approvers' => 'nullable|array',
|
||||
'approvers.*.user_id' => 'required_with:approvers|integer|exists:users,id',
|
||||
'approvers.*.role' => 'nullable|string|max:50',
|
||||
|
||||
// 문서 데이터 (EAV)
|
||||
'data' => 'nullable|array',
|
||||
'data.*.section_id' => 'nullable|integer',
|
||||
'data.*.column_id' => 'nullable|integer',
|
||||
'data.*.row_index' => 'nullable|integer|min:0',
|
||||
'data.*.field_key' => 'required_with:data|string|max:100',
|
||||
'data.*.field_value' => 'nullable|string',
|
||||
|
||||
// 첨부파일
|
||||
'attachments' => 'nullable|array',
|
||||
'attachments.*.file_id' => 'required_with:attachments|integer|exists:files,id',
|
||||
'attachments.*.attachment_type' => "nullable|string|in:{$attachmentTypes}",
|
||||
'attachments.*.description' => 'nullable|string|max:255',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'template_id.required' => __('validation.required', ['attribute' => '템플릿']),
|
||||
'template_id.exists' => __('validation.exists', ['attribute' => '템플릿']),
|
||||
'title.required' => __('validation.required', ['attribute' => '제목']),
|
||||
'title.max' => __('validation.max.string', ['attribute' => '제목', 'max' => 255]),
|
||||
'approvers.*.user_id.exists' => __('validation.exists', ['attribute' => '결재자']),
|
||||
'data.*.field_key.required_with' => __('validation.required', ['attribute' => '필드 키']),
|
||||
'attachments.*.file_id.exists' => __('validation.exists', ['attribute' => '파일']),
|
||||
];
|
||||
}
|
||||
}
|
||||
55
app/Http/Requests/Document/UpdateRequest.php
Normal file
55
app/Http/Requests/Document/UpdateRequest.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Document;
|
||||
|
||||
use App\Models\Documents\DocumentAttachment;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$attachmentTypes = implode(',', DocumentAttachment::TYPES);
|
||||
|
||||
return [
|
||||
// 기본 정보
|
||||
'title' => 'nullable|string|max:255',
|
||||
'linkable_type' => 'nullable|string|max:100',
|
||||
'linkable_id' => 'nullable|integer',
|
||||
|
||||
// 결재선 (보류 상태이지만 구조는 유지)
|
||||
'approvers' => 'nullable|array',
|
||||
'approvers.*.user_id' => 'required_with:approvers|integer|exists:users,id',
|
||||
'approvers.*.role' => 'nullable|string|max:50',
|
||||
|
||||
// 문서 데이터 (EAV)
|
||||
'data' => 'nullable|array',
|
||||
'data.*.section_id' => 'nullable|integer',
|
||||
'data.*.column_id' => 'nullable|integer',
|
||||
'data.*.row_index' => 'nullable|integer|min:0',
|
||||
'data.*.field_key' => 'required_with:data|string|max:100',
|
||||
'data.*.field_value' => 'nullable|string',
|
||||
|
||||
// 첨부파일
|
||||
'attachments' => 'nullable|array',
|
||||
'attachments.*.file_id' => 'required_with:attachments|integer|exists:files,id',
|
||||
'attachments.*.attachment_type' => "nullable|string|in:{$attachmentTypes}",
|
||||
'attachments.*.description' => 'nullable|string|max:255',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'title.max' => __('validation.max.string', ['attribute' => '제목', 'max' => 255]),
|
||||
'approvers.*.user_id.exists' => __('validation.exists', ['attribute' => '결재자']),
|
||||
'data.*.field_key.required_with' => __('validation.required', ['attribute' => '필드 키']),
|
||||
'attachments.*.file_id.exists' => __('validation.exists', ['attribute' => '파일']),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,7 @@ public function rules(): array
|
||||
'labor_cost' => 'nullable|numeric|min:0',
|
||||
'install_cost' => 'nullable|numeric|min:0',
|
||||
'discount_rate' => 'nullable|numeric|min:0|max:100',
|
||||
'discount_amount' => 'nullable|numeric|min:0',
|
||||
'total_amount' => 'nullable|numeric|min:0',
|
||||
|
||||
// 기타 정보
|
||||
|
||||
@@ -69,6 +69,7 @@ public function rules(): array
|
||||
'labor_cost' => 'nullable|numeric|min:0',
|
||||
'install_cost' => 'nullable|numeric|min:0',
|
||||
'discount_rate' => 'nullable|numeric|min:0|max:100',
|
||||
'discount_amount' => 'nullable|numeric|min:0',
|
||||
'total_amount' => 'nullable|numeric|min:0',
|
||||
|
||||
// 기타 정보
|
||||
|
||||
@@ -21,7 +21,7 @@ public function rules(): array
|
||||
'item_name' => ['required', 'string', 'max:200'],
|
||||
'specification' => ['nullable', 'string', 'max:200'],
|
||||
'supplier' => ['required', 'string', 'max:100'],
|
||||
'order_qty' => ['required', 'numeric', 'min:0'],
|
||||
'order_qty' => ['nullable', 'numeric', 'min:0'],
|
||||
'order_unit' => ['nullable', 'string', 'max:20'],
|
||||
'due_date' => ['nullable', 'date'],
|
||||
'status' => ['nullable', 'string', 'in:order_completed,shipping,inspection_pending,receiving_pending'],
|
||||
|
||||
@@ -23,8 +23,11 @@ public function rules(): array
|
||||
'order_qty' => ['sometimes', 'numeric', 'min:0'],
|
||||
'order_unit' => ['nullable', 'string', 'max:20'],
|
||||
'due_date' => ['nullable', 'date'],
|
||||
'status' => ['sometimes', 'string', 'in:order_completed,shipping,inspection_pending,receiving_pending'],
|
||||
'status' => ['sometimes', 'string', 'in:order_completed,shipping,inspection_pending,receiving_pending,completed'],
|
||||
'remark' => ['nullable', 'string', 'max:1000'],
|
||||
'receiving_qty' => ['nullable', 'numeric', 'min:0'],
|
||||
'receiving_date' => ['nullable', 'date'],
|
||||
'lot_no' => ['nullable', 'string', 'max:50'],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
21
app/Http/Requests/V1/Stat/StatAlertRequest.php
Normal file
21
app/Http/Requests/V1/Stat/StatAlertRequest.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\V1\Stat;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StatAlertRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'limit' => 'nullable|integer|min:1|max:100',
|
||||
'unread_only' => 'nullable|boolean',
|
||||
];
|
||||
}
|
||||
}
|
||||
22
app/Http/Requests/V1/Stat/StatDailyRequest.php
Normal file
22
app/Http/Requests/V1/Stat/StatDailyRequest.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\V1\Stat;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StatDailyRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'domain' => 'required|string|in:sales,finance,production,inventory,quote,hr,system',
|
||||
'start_date' => 'required|date|date_format:Y-m-d',
|
||||
'end_date' => 'required|date|date_format:Y-m-d|after_or_equal:start_date',
|
||||
];
|
||||
}
|
||||
}
|
||||
22
app/Http/Requests/V1/Stat/StatMonthlyRequest.php
Normal file
22
app/Http/Requests/V1/Stat/StatMonthlyRequest.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\V1\Stat;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StatMonthlyRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'domain' => 'required|string|in:sales,finance,production,project',
|
||||
'year' => 'required|integer|min:2020|max:2099',
|
||||
'month' => 'nullable|integer|min:1|max:12',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ public function rules(): array
|
||||
{
|
||||
return [
|
||||
'use_gps' => ['sometimes', 'boolean'],
|
||||
'use_auto' => ['sometimes', 'boolean'],
|
||||
'allowed_radius' => ['sometimes', 'integer', 'min:10', 'max:10000'],
|
||||
'hq_address' => ['nullable', 'string', 'max:255'],
|
||||
'hq_latitude' => ['nullable', 'numeric', 'between:-90,90'],
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
use App\Models\Members\User;
|
||||
use App\Models\Orders\Client;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
@@ -12,7 +13,7 @@
|
||||
|
||||
class BadDebt extends Model
|
||||
{
|
||||
use BelongsToTenant, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
use App\Models\Members\User;
|
||||
use App\Models\Orders\Client;
|
||||
use App\Models\Quote\Quote;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -13,7 +14,7 @@
|
||||
|
||||
class Bidding extends Model
|
||||
{
|
||||
use BelongsToTenant, HasFactory, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Tenants\Tenant;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
@@ -15,7 +16,7 @@
|
||||
*/
|
||||
class CategoryGroup extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
use Auditable, BelongsToTenant;
|
||||
|
||||
protected $table = 'category_groups';
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models\Commons;
|
||||
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -9,7 +10,7 @@
|
||||
|
||||
class Category extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id', 'parent_id', 'code_group', 'code', 'name',
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models\Commons;
|
||||
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -9,7 +10,7 @@
|
||||
|
||||
class CategoryField extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $table = 'category_fields';
|
||||
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
namespace App\Models\Commons;
|
||||
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class CategoryLog extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait;
|
||||
use Auditable, BelongsToTenant, ModelTrait;
|
||||
|
||||
protected $table = 'category_logs';
|
||||
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
namespace App\Models\Commons;
|
||||
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class CategoryTemplate extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait;
|
||||
use Auditable, BelongsToTenant, ModelTrait;
|
||||
|
||||
protected $table = 'category_templates';
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models\Commons;
|
||||
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -9,7 +10,7 @@
|
||||
|
||||
class Classification extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models\Commons;
|
||||
|
||||
use App\Models\Scopes\TenantScope;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -15,7 +16,7 @@
|
||||
*/
|
||||
class Menu extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id', 'parent_id', 'global_menu_id', 'name', 'url', 'is_active', 'sort_order',
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models\Construction;
|
||||
|
||||
use App\Models\Members\User;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -42,7 +43,7 @@
|
||||
*/
|
||||
class Contract extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $table = 'contracts';
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models\Construction;
|
||||
|
||||
use App\Models\Members\User;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -49,7 +50,7 @@
|
||||
*/
|
||||
class HandoverReport extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $table = 'handover_reports';
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models\Construction;
|
||||
|
||||
use App\Models\Members\User;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
@@ -25,7 +26,7 @@
|
||||
*/
|
||||
class HandoverReportItem extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
use Auditable, BelongsToTenant;
|
||||
|
||||
protected $table = 'handover_report_items';
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models\Construction;
|
||||
|
||||
use App\Models\Members\User;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
@@ -24,7 +25,7 @@
|
||||
*/
|
||||
class HandoverReportManager extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
use Auditable, BelongsToTenant;
|
||||
|
||||
protected $table = 'handover_report_managers';
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
use App\Models\Members\User;
|
||||
use App\Models\Site;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -38,7 +39,7 @@
|
||||
*/
|
||||
class StructureReview extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $table = 'structure_reviews';
|
||||
|
||||
|
||||
247
app/Models/Documents/Document.php
Normal file
247
app/Models/Documents/Document.php
Normal file
@@ -0,0 +1,247 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Documents;
|
||||
|
||||
use App\Models\Members\User;
|
||||
use App\Traits\Auditable;
|
||||
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\MorphTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* 문서 모델
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $tenant_id
|
||||
* @property int $template_id
|
||||
* @property string $document_no 문서번호
|
||||
* @property string $title 문서 제목
|
||||
* @property string $status 상태 (DRAFT/PENDING/APPROVED/REJECTED/CANCELLED)
|
||||
* @property string|null $linkable_type 연결 모델 타입
|
||||
* @property int|null $linkable_id 연결 모델 ID
|
||||
* @property \Carbon\Carbon|null $submitted_at 결재 요청일
|
||||
* @property \Carbon\Carbon|null $completed_at 결재 완료일
|
||||
* @property int|null $created_by
|
||||
* @property int|null $updated_by
|
||||
* @property int|null $deleted_by
|
||||
* @property \Carbon\Carbon|null $created_at
|
||||
* @property \Carbon\Carbon|null $updated_at
|
||||
* @property \Carbon\Carbon|null $deleted_at
|
||||
*/
|
||||
class Document extends Model
|
||||
{
|
||||
use Auditable, BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $table = 'documents';
|
||||
|
||||
// =========================================================================
|
||||
// 상태 상수
|
||||
// =========================================================================
|
||||
|
||||
public const STATUS_DRAFT = 'DRAFT';
|
||||
|
||||
public const STATUS_PENDING = 'PENDING';
|
||||
|
||||
public const STATUS_APPROVED = 'APPROVED';
|
||||
|
||||
public const STATUS_REJECTED = 'REJECTED';
|
||||
|
||||
public const STATUS_CANCELLED = 'CANCELLED';
|
||||
|
||||
public const STATUSES = [
|
||||
self::STATUS_DRAFT,
|
||||
self::STATUS_PENDING,
|
||||
self::STATUS_APPROVED,
|
||||
self::STATUS_REJECTED,
|
||||
self::STATUS_CANCELLED,
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// Fillable & Casts
|
||||
// =========================================================================
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'template_id',
|
||||
'document_no',
|
||||
'title',
|
||||
'status',
|
||||
'linkable_type',
|
||||
'linkable_id',
|
||||
'submitted_at',
|
||||
'completed_at',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'submitted_at' => 'datetime',
|
||||
'completed_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
'status' => self::STATUS_DRAFT,
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// Relationships
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 템플릿
|
||||
*/
|
||||
public function template(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\DocumentTemplate::class, 'template_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 목록
|
||||
*/
|
||||
public function approvals(): HasMany
|
||||
{
|
||||
return $this->hasMany(DocumentApproval::class)->orderBy('step');
|
||||
}
|
||||
|
||||
/**
|
||||
* 문서 데이터
|
||||
*/
|
||||
public function data(): HasMany
|
||||
{
|
||||
return $this->hasMany(DocumentData::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 첨부파일
|
||||
*/
|
||||
public function attachments(): HasMany
|
||||
{
|
||||
return $this->hasMany(DocumentAttachment::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 연결된 엔티티 (다형성)
|
||||
*/
|
||||
public function linkable(): MorphTo
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
/**
|
||||
* 생성자
|
||||
*/
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* 수정자
|
||||
*/
|
||||
public function updater(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'updated_by');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Scopes
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 상태로 필터링
|
||||
*/
|
||||
public function scopeStatus($query, string $status)
|
||||
{
|
||||
return $query->where('status', $status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 임시저장 문서
|
||||
*/
|
||||
public function scopeDraft($query)
|
||||
{
|
||||
return $query->where('status', self::STATUS_DRAFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 대기 문서
|
||||
*/
|
||||
public function scopePending($query)
|
||||
{
|
||||
return $query->where('status', self::STATUS_PENDING);
|
||||
}
|
||||
|
||||
/**
|
||||
* 승인된 문서
|
||||
*/
|
||||
public function scopeApproved($query)
|
||||
{
|
||||
return $query->where('status', self::STATUS_APPROVED);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Helper Methods
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 편집 가능 여부
|
||||
*/
|
||||
public function canEdit(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_DRAFT
|
||||
|| $this->status === self::STATUS_REJECTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 요청 가능 여부
|
||||
*/
|
||||
public function canSubmit(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_DRAFT
|
||||
|| $this->status === self::STATUS_REJECTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 처리 가능 여부
|
||||
*/
|
||||
public function canApprove(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_PENDING;
|
||||
}
|
||||
|
||||
/**
|
||||
* 취소 가능 여부
|
||||
*/
|
||||
public function canCancel(): bool
|
||||
{
|
||||
return in_array($this->status, [
|
||||
self::STATUS_DRAFT,
|
||||
self::STATUS_PENDING,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 결재 단계 가져오기
|
||||
*/
|
||||
public function getCurrentApprovalStep(): ?DocumentApproval
|
||||
{
|
||||
return $this->approvals()
|
||||
->where('status', DocumentApproval::STATUS_PENDING)
|
||||
->orderBy('step')
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 사용자의 결재 차례 확인
|
||||
*/
|
||||
public function isUserTurn(int $userId): bool
|
||||
{
|
||||
$currentStep = $this->getCurrentApprovalStep();
|
||||
|
||||
return $currentStep && $currentStep->user_id === $userId;
|
||||
}
|
||||
}
|
||||
180
app/Models/Documents/DocumentApproval.php
Normal file
180
app/Models/Documents/DocumentApproval.php
Normal file
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Documents;
|
||||
|
||||
use App\Models\Members\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* 문서 결재 모델
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $document_id
|
||||
* @property int $user_id
|
||||
* @property int $step 결재 순서
|
||||
* @property string $role 역할 (작성/검토/승인)
|
||||
* @property string $status 상태 (PENDING/APPROVED/REJECTED)
|
||||
* @property string|null $comment 결재 의견
|
||||
* @property \Carbon\Carbon|null $acted_at 결재 처리일
|
||||
* @property int|null $created_by
|
||||
* @property int|null $updated_by
|
||||
* @property \Carbon\Carbon|null $created_at
|
||||
* @property \Carbon\Carbon|null $updated_at
|
||||
*/
|
||||
class DocumentApproval extends Model
|
||||
{
|
||||
protected $table = 'document_approvals';
|
||||
|
||||
// =========================================================================
|
||||
// 상태 상수
|
||||
// =========================================================================
|
||||
|
||||
public const STATUS_PENDING = 'PENDING';
|
||||
|
||||
public const STATUS_APPROVED = 'APPROVED';
|
||||
|
||||
public const STATUS_REJECTED = 'REJECTED';
|
||||
|
||||
public const STATUSES = [
|
||||
self::STATUS_PENDING,
|
||||
self::STATUS_APPROVED,
|
||||
self::STATUS_REJECTED,
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// 역할 상수
|
||||
// =========================================================================
|
||||
|
||||
public const ROLE_WRITER = '작성';
|
||||
|
||||
public const ROLE_REVIEWER = '검토';
|
||||
|
||||
public const ROLE_APPROVER = '승인';
|
||||
|
||||
// =========================================================================
|
||||
// Fillable & Casts
|
||||
// =========================================================================
|
||||
|
||||
protected $fillable = [
|
||||
'document_id',
|
||||
'user_id',
|
||||
'step',
|
||||
'role',
|
||||
'status',
|
||||
'comment',
|
||||
'acted_at',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'step' => 'integer',
|
||||
'acted_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
'step' => 1,
|
||||
'status' => self::STATUS_PENDING,
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// Relationships
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 문서
|
||||
*/
|
||||
public function document(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Document::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재자
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 생성자
|
||||
*/
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Scopes
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 대기 중인 결재
|
||||
*/
|
||||
public function scopePending($query)
|
||||
{
|
||||
return $query->where('status', self::STATUS_PENDING);
|
||||
}
|
||||
|
||||
/**
|
||||
* 승인된 결재
|
||||
*/
|
||||
public function scopeApproved($query)
|
||||
{
|
||||
return $query->where('status', self::STATUS_APPROVED);
|
||||
}
|
||||
|
||||
/**
|
||||
* 반려된 결재
|
||||
*/
|
||||
public function scopeRejected($query)
|
||||
{
|
||||
return $query->where('status', self::STATUS_REJECTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 사용자의 결재
|
||||
*/
|
||||
public function scopeForUser($query, int $userId)
|
||||
{
|
||||
return $query->where('user_id', $userId);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Helper Methods
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 대기 상태 여부
|
||||
*/
|
||||
public function isPending(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_PENDING;
|
||||
}
|
||||
|
||||
/**
|
||||
* 승인 상태 여부
|
||||
*/
|
||||
public function isApproved(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_APPROVED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 반려 상태 여부
|
||||
*/
|
||||
public function isRejected(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_REJECTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 처리 완료 여부
|
||||
*/
|
||||
public function isProcessed(): bool
|
||||
{
|
||||
return ! $this->isPending();
|
||||
}
|
||||
}
|
||||
144
app/Models/Documents/DocumentAttachment.php
Normal file
144
app/Models/Documents/DocumentAttachment.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Documents;
|
||||
|
||||
use App\Models\Commons\File;
|
||||
use App\Models\Members\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* 문서 첨부파일 모델
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $document_id
|
||||
* @property int $file_id
|
||||
* @property string $attachment_type 첨부 유형 (general, signature, image 등)
|
||||
* @property string|null $description 설명
|
||||
* @property int|null $created_by
|
||||
* @property \Carbon\Carbon|null $created_at
|
||||
* @property \Carbon\Carbon|null $updated_at
|
||||
*/
|
||||
class DocumentAttachment extends Model
|
||||
{
|
||||
protected $table = 'document_attachments';
|
||||
|
||||
// =========================================================================
|
||||
// 첨부 유형 상수
|
||||
// =========================================================================
|
||||
|
||||
public const TYPE_GENERAL = 'general';
|
||||
|
||||
public const TYPE_SIGNATURE = 'signature';
|
||||
|
||||
public const TYPE_IMAGE = 'image';
|
||||
|
||||
public const TYPE_REFERENCE = 'reference';
|
||||
|
||||
public const TYPES = [
|
||||
self::TYPE_GENERAL,
|
||||
self::TYPE_SIGNATURE,
|
||||
self::TYPE_IMAGE,
|
||||
self::TYPE_REFERENCE,
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// Fillable & Casts
|
||||
// =========================================================================
|
||||
|
||||
protected $fillable = [
|
||||
'document_id',
|
||||
'file_id',
|
||||
'attachment_type',
|
||||
'description',
|
||||
'created_by',
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
'attachment_type' => self::TYPE_GENERAL,
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// Relationships
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 문서
|
||||
*/
|
||||
public function document(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Document::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 파일
|
||||
*/
|
||||
public function file(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(File::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 생성자
|
||||
*/
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Scopes
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 특정 유형의 첨부파일
|
||||
*/
|
||||
public function scopeOfType($query, string $type)
|
||||
{
|
||||
return $query->where('attachment_type', $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 일반 첨부파일
|
||||
*/
|
||||
public function scopeGeneral($query)
|
||||
{
|
||||
return $query->where('attachment_type', self::TYPE_GENERAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* 서명 첨부파일
|
||||
*/
|
||||
public function scopeSignatures($query)
|
||||
{
|
||||
return $query->where('attachment_type', self::TYPE_SIGNATURE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 첨부파일
|
||||
*/
|
||||
public function scopeImages($query)
|
||||
{
|
||||
return $query->where('attachment_type', self::TYPE_IMAGE);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Helper Methods
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 서명 첨부파일 여부
|
||||
*/
|
||||
public function isSignature(): bool
|
||||
{
|
||||
return $this->attachment_type === self::TYPE_SIGNATURE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 첨부파일 여부
|
||||
*/
|
||||
public function isImage(): bool
|
||||
{
|
||||
return $this->attachment_type === self::TYPE_IMAGE;
|
||||
}
|
||||
}
|
||||
105
app/Models/Documents/DocumentData.php
Normal file
105
app/Models/Documents/DocumentData.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Documents;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* 문서 데이터 모델 (EAV 패턴)
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $document_id
|
||||
* @property int|null $section_id 섹션 ID
|
||||
* @property int|null $column_id 컬럼 ID
|
||||
* @property int $row_index 행 인덱스
|
||||
* @property string $field_key 필드 키
|
||||
* @property string|null $field_value 필드 값
|
||||
* @property \Carbon\Carbon|null $created_at
|
||||
* @property \Carbon\Carbon|null $updated_at
|
||||
*/
|
||||
class DocumentData extends Model
|
||||
{
|
||||
protected $table = 'document_data';
|
||||
|
||||
// =========================================================================
|
||||
// Fillable & Casts
|
||||
// =========================================================================
|
||||
|
||||
protected $fillable = [
|
||||
'document_id',
|
||||
'section_id',
|
||||
'column_id',
|
||||
'row_index',
|
||||
'field_key',
|
||||
'field_value',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'section_id' => 'integer',
|
||||
'column_id' => 'integer',
|
||||
'row_index' => 'integer',
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
'row_index' => 0,
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// Relationships
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 문서
|
||||
*/
|
||||
public function document(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Document::class);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Scopes
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 특정 섹션의 데이터
|
||||
*/
|
||||
public function scopeForSection($query, int $sectionId)
|
||||
{
|
||||
return $query->where('section_id', $sectionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 필드 키의 데이터
|
||||
*/
|
||||
public function scopeForField($query, string $fieldKey)
|
||||
{
|
||||
return $query->where('field_key', $fieldKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 행의 데이터
|
||||
*/
|
||||
public function scopeForRow($query, int $rowIndex)
|
||||
{
|
||||
return $query->where('row_index', $rowIndex);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Helper Methods
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 값을 특정 타입으로 캐스팅
|
||||
*/
|
||||
public function getTypedValue(string $type = 'string'): mixed
|
||||
{
|
||||
return match ($type) {
|
||||
'integer', 'int' => (int) $this->field_value,
|
||||
'float', 'double' => (float) $this->field_value,
|
||||
'boolean', 'bool' => filter_var($this->field_value, FILTER_VALIDATE_BOOLEAN),
|
||||
'array', 'json' => json_decode($this->field_value, true),
|
||||
default => $this->field_value,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models\Estimate;
|
||||
|
||||
use App\Models\Commons\Category;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -12,7 +13,7 @@
|
||||
|
||||
class Estimate extends Model
|
||||
{
|
||||
use BelongsToTenant, HasFactory, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models\Estimate;
|
||||
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -10,7 +11,7 @@
|
||||
|
||||
class EstimateItem extends Model
|
||||
{
|
||||
use BelongsToTenant, HasFactory, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models\ItemMaster;
|
||||
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -9,7 +10,7 @@
|
||||
|
||||
class CustomTab extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models\ItemMaster;
|
||||
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -17,7 +18,7 @@
|
||||
*/
|
||||
class EntityRelationship extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait;
|
||||
use Auditable, BelongsToTenant, ModelTrait;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models\ItemMaster;
|
||||
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -9,7 +10,7 @@
|
||||
|
||||
class ItemBomItem extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models\ItemMaster;
|
||||
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -9,7 +10,7 @@
|
||||
|
||||
class ItemField extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models\ItemMaster;
|
||||
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -9,7 +10,7 @@
|
||||
|
||||
class ItemPage extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models\ItemMaster;
|
||||
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -9,7 +10,7 @@
|
||||
|
||||
class ItemSection extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
namespace App\Models\ItemMaster;
|
||||
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class TabColumn extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait;
|
||||
use Auditable, BelongsToTenant, ModelTrait;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models\ItemMaster;
|
||||
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -9,7 +10,7 @@
|
||||
|
||||
class UnitOption extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
use App\Models\Commons\Category;
|
||||
use App\Models\Commons\File;
|
||||
use App\Models\Commons\Tag;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -18,7 +19,7 @@
|
||||
*/
|
||||
class Item extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models\Items;
|
||||
|
||||
use App\Models\Members\User;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
@@ -30,7 +31,7 @@
|
||||
*/
|
||||
class ItemReceipt extends Model
|
||||
{
|
||||
use BelongsToTenant, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $table = 'item_receipts';
|
||||
|
||||
|
||||
341
app/Models/Kyungdong/KdPriceTable.php
Normal file
341
app/Models/Kyungdong/KdPriceTable.php
Normal file
@@ -0,0 +1,341 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Kyungdong;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* 경동기업 전용 단가 테이블 모델
|
||||
*
|
||||
* 5130 레거시 price_* 테이블 데이터 조회용
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $tenant_id
|
||||
* @property string $table_type
|
||||
* @property string|null $item_code
|
||||
* @property string|null $item_name
|
||||
* @property string|null $category
|
||||
* @property string|null $spec1
|
||||
* @property string|null $spec2
|
||||
* @property string|null $spec3
|
||||
* @property float $unit_price
|
||||
* @property string $unit
|
||||
* @property array|null $raw_data
|
||||
* @property bool $is_active
|
||||
*/
|
||||
class KdPriceTable extends Model
|
||||
{
|
||||
// 테이블 유형 상수
|
||||
public const TYPE_MOTOR = 'motor';
|
||||
|
||||
public const TYPE_SHAFT = 'shaft';
|
||||
|
||||
public const TYPE_PIPE = 'pipe';
|
||||
|
||||
public const TYPE_ANGLE = 'angle';
|
||||
|
||||
public const TYPE_RAW_MATERIAL = 'raw_material';
|
||||
|
||||
public const TYPE_BDMODELS = 'bdmodels';
|
||||
|
||||
// 경동기업 테넌트 ID
|
||||
public const TENANT_ID = 287;
|
||||
|
||||
protected $table = 'kd_price_tables';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'table_type',
|
||||
'item_code',
|
||||
'item_name',
|
||||
'category',
|
||||
'spec1',
|
||||
'spec2',
|
||||
'spec3',
|
||||
'unit_price',
|
||||
'unit',
|
||||
'raw_data',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'unit_price' => 'decimal:2',
|
||||
'raw_data' => 'array',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
// Scopes
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 테이블 유형으로 필터링
|
||||
*/
|
||||
public function scopeOfType(Builder $query, string $type): Builder
|
||||
{
|
||||
return $query->where('table_type', $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 활성 데이터만
|
||||
*/
|
||||
public function scopeActive(Builder $query): Builder
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 모터 단가 조회
|
||||
*/
|
||||
public function scopeMotor(Builder $query): Builder
|
||||
{
|
||||
return $query->ofType(self::TYPE_MOTOR)->active();
|
||||
}
|
||||
|
||||
/**
|
||||
* 샤프트 단가 조회
|
||||
*/
|
||||
public function scopeShaft(Builder $query): Builder
|
||||
{
|
||||
return $query->ofType(self::TYPE_SHAFT)->active();
|
||||
}
|
||||
|
||||
/**
|
||||
* 파이프 단가 조회
|
||||
*/
|
||||
public function scopePipeType(Builder $query): Builder
|
||||
{
|
||||
return $query->ofType(self::TYPE_PIPE)->active();
|
||||
}
|
||||
|
||||
/**
|
||||
* 앵글 단가 조회
|
||||
*/
|
||||
public function scopeAngle(Builder $query): Builder
|
||||
{
|
||||
return $query->ofType(self::TYPE_ANGLE)->active();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Static Query Methods
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 모터 단가 조회
|
||||
*
|
||||
* @param string $motorCapacity 모터 용량 (150K, 300K, 400K, 500K, 600K, 800K, 1000K)
|
||||
*/
|
||||
public static function getMotorPrice(string $motorCapacity): float
|
||||
{
|
||||
$record = self::motor()
|
||||
->where('category', $motorCapacity)
|
||||
->first();
|
||||
|
||||
return (float) ($record?->unit_price ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 제어기 단가 조회
|
||||
*
|
||||
* @param string $controllerType 제어기 타입 (매립형, 노출형, 뒷박스)
|
||||
*/
|
||||
public static function getControllerPrice(string $controllerType): float
|
||||
{
|
||||
$record = self::motor()
|
||||
->where('category', $controllerType)
|
||||
->first();
|
||||
|
||||
return (float) ($record?->unit_price ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 샤프트 단가 조회
|
||||
*
|
||||
* @param string $size 사이즈 (3, 4, 5인치)
|
||||
* @param float $length 길이 (m 단위)
|
||||
*/
|
||||
public static function getShaftPrice(string $size, float $length): float
|
||||
{
|
||||
// 길이를 소수점 1자리 문자열로 변환 (DB 저장 형식: '3.0', '4.0')
|
||||
$lengthStr = number_format($length, 1, '.', '');
|
||||
|
||||
$record = self::shaft()
|
||||
->where('spec1', $size)
|
||||
->where('spec2', $lengthStr)
|
||||
->first();
|
||||
|
||||
return (float) ($record?->unit_price ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 파이프 단가 조회
|
||||
*
|
||||
* @param string $thickness 두께 (1.4 등)
|
||||
* @param int $length 길이 (3000, 6000)
|
||||
*/
|
||||
public static function getPipePrice(string $thickness, int $length): float
|
||||
{
|
||||
$record = self::pipeType()
|
||||
->where('spec1', $thickness)
|
||||
->where('spec2', (string) $length)
|
||||
->first();
|
||||
|
||||
return (float) ($record?->unit_price ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 앵글 단가 조회
|
||||
*
|
||||
* @param string $type 타입 (스크린용, 철재용)
|
||||
* @param string $bracketSize 브라켓크기 (530*320, 600*350, 690*390)
|
||||
* @param string $angleType 앵글타입 (앵글3T, 앵글4T)
|
||||
*/
|
||||
public static function getAnglePrice(string $type, string $bracketSize, string $angleType): float
|
||||
{
|
||||
$record = self::angle()
|
||||
->where('category', $type)
|
||||
->where('spec1', $bracketSize)
|
||||
->where('spec2', $angleType)
|
||||
->first();
|
||||
|
||||
return (float) ($record?->unit_price ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 원자재 단가 조회
|
||||
*
|
||||
* @param string $materialName 원자재명 (실리카, 스크린 등)
|
||||
*/
|
||||
public static function getRawMaterialPrice(string $materialName): float
|
||||
{
|
||||
$record = self::ofType(self::TYPE_RAW_MATERIAL)
|
||||
->active()
|
||||
->where('item_name', $materialName)
|
||||
->first();
|
||||
|
||||
return (float) ($record?->unit_price ?? 0);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// BDmodels 단가 조회 (절곡품)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* BDmodels 스코프
|
||||
*/
|
||||
public function scopeBdmodels(Builder $query): Builder
|
||||
{
|
||||
return $query->ofType(self::TYPE_BDMODELS)->active();
|
||||
}
|
||||
|
||||
/**
|
||||
* BDmodels 단가 조회 (케이스, 가이드레일, 하단마감재 등)
|
||||
*
|
||||
* @param string $secondItem 부품분류 (케이스, 가이드레일, 하단마감재, L-BAR 등)
|
||||
* @param string|null $modelName 모델코드 (KSS01, KWS01 등)
|
||||
* @param string|null $finishingType 마감재질 (SUS, EGI)
|
||||
* @param string|null $spec 규격 (120*70, 650*550 등)
|
||||
*/
|
||||
public static function getBDModelPrice(
|
||||
string $secondItem,
|
||||
?string $modelName = null,
|
||||
?string $finishingType = null,
|
||||
?string $spec = null
|
||||
): float {
|
||||
$query = self::bdmodels()->where('category', $secondItem);
|
||||
|
||||
if ($modelName) {
|
||||
$query->where('item_code', $modelName);
|
||||
}
|
||||
|
||||
if ($finishingType) {
|
||||
$query->where('spec1', $finishingType);
|
||||
}
|
||||
|
||||
if ($spec) {
|
||||
$query->where('spec2', $spec);
|
||||
}
|
||||
|
||||
$record = $query->first();
|
||||
|
||||
return (float) ($record?->unit_price ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 케이스 단가 조회
|
||||
*
|
||||
* @param string $spec 케이스 규격 (500*380, 650*550 등)
|
||||
*/
|
||||
public static function getCasePrice(string $spec): float
|
||||
{
|
||||
return self::getBDModelPrice('케이스', null, null, $spec);
|
||||
}
|
||||
|
||||
/**
|
||||
* 가이드레일 단가 조회
|
||||
*
|
||||
* @param string $modelName 모델코드 (KSS01 등)
|
||||
* @param string $finishingType 마감재질 (SUS, EGI)
|
||||
* @param string $spec 규격 (120*70, 120*100)
|
||||
*/
|
||||
public static function getGuideRailPrice(string $modelName, string $finishingType, string $spec): float
|
||||
{
|
||||
return self::getBDModelPrice('가이드레일', $modelName, $finishingType, $spec);
|
||||
}
|
||||
|
||||
/**
|
||||
* 하단마감재(하장바) 단가 조회
|
||||
*
|
||||
* @param string $modelName 모델코드
|
||||
* @param string $finishingType 마감재질
|
||||
*/
|
||||
public static function getBottomBarPrice(string $modelName, string $finishingType): float
|
||||
{
|
||||
return self::getBDModelPrice('하단마감재', $modelName, $finishingType);
|
||||
}
|
||||
|
||||
/**
|
||||
* L-BAR 단가 조회
|
||||
*
|
||||
* @param string $modelName 모델코드
|
||||
*/
|
||||
public static function getLBarPrice(string $modelName): float
|
||||
{
|
||||
return self::getBDModelPrice('L-BAR', $modelName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 보강평철 단가 조회
|
||||
*/
|
||||
public static function getFlatBarPrice(): float
|
||||
{
|
||||
return self::getBDModelPrice('보강평철');
|
||||
}
|
||||
|
||||
/**
|
||||
* 케이스 마구리 단가 조회
|
||||
*
|
||||
* @param string $spec 규격
|
||||
*/
|
||||
public static function getCaseCapPrice(string $spec): float
|
||||
{
|
||||
return self::getBDModelPrice('마구리', null, null, $spec);
|
||||
}
|
||||
|
||||
/**
|
||||
* 케이스용 연기차단재 단가 조회
|
||||
*/
|
||||
public static function getCaseSmokeBlockPrice(): float
|
||||
{
|
||||
return self::getBDModelPrice('케이스용 연기차단재');
|
||||
}
|
||||
|
||||
/**
|
||||
* 가이드레일용 연기차단재 단가 조회
|
||||
*/
|
||||
public static function getRailSmokeBlockPrice(): float
|
||||
{
|
||||
return self::getBDModelPrice('가이드레일용 연기차단재');
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -9,7 +10,7 @@
|
||||
|
||||
class Labor extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $table = 'labors';
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
use App\Models\Commons\File;
|
||||
use App\Models\Commons\Tag;
|
||||
use App\Models\Qualitys\Lot;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -16,7 +17,7 @@
|
||||
*/
|
||||
class Material extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
use App\Models\Permissions\Role;
|
||||
use App\Models\Tenants\Tenant;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
@@ -13,7 +14,7 @@
|
||||
*/
|
||||
class UserRole extends Model
|
||||
{
|
||||
use BelongsToTenant, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id', 'tenant_id', 'role_id', 'assigned_at',
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models\Members;
|
||||
|
||||
use App\Models\Tenants\Tenant;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -13,7 +14,7 @@
|
||||
*/
|
||||
class UserTenant extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id', 'tenant_id', 'is_active', 'is_default', 'joined_at', 'left_at',
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class NotificationSetting extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
use Auditable, BelongsToTenant;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
@@ -9,7 +10,7 @@
|
||||
|
||||
class NotificationSettingGroup extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
use Auditable, BelongsToTenant;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class NotificationSettingGroupState extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
use Auditable, BelongsToTenant;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models\Orders;
|
||||
|
||||
use App\Models\BadDebts\BadDebt;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -10,7 +11,7 @@
|
||||
|
||||
class Client extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait;
|
||||
use Auditable, BelongsToTenant, ModelTrait;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models\Orders;
|
||||
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -9,7 +10,7 @@
|
||||
|
||||
class ClientGroup extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
use App\Models\Quote\Quote;
|
||||
use App\Models\Tenants\Sale;
|
||||
use App\Models\Tenants\Shipment;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
@@ -21,7 +22,7 @@
|
||||
*/
|
||||
class Order extends Model
|
||||
{
|
||||
use BelongsToTenant, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, SoftDeletes;
|
||||
|
||||
// 상태 코드
|
||||
public const STATUS_DRAFT = 'DRAFT';
|
||||
@@ -149,6 +150,7 @@ class Order extends Model
|
||||
*/
|
||||
protected $appends = [
|
||||
'delivery_method_label',
|
||||
'shipping_cost_label',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -255,6 +257,16 @@ public function getDeliveryMethodLabelAttribute(): string
|
||||
return CommonCode::getLabel('delivery_method', $this->delivery_method_code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 운임비용 라벨 (common_codes 테이블에서 조회)
|
||||
*/
|
||||
public function getShippingCostLabelAttribute(): string
|
||||
{
|
||||
$shippingCostCode = $this->options['shipping_cost_code'] ?? null;
|
||||
|
||||
return $shippingCostCode ? CommonCode::getLabel('shipping_cost', $shippingCostCode) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 수주확정 시 매출 생성 여부
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
use App\Models\Items\Item;
|
||||
use App\Models\Quote\Quote;
|
||||
use App\Models\Quote\QuoteItem;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
@@ -18,7 +19,7 @@
|
||||
*/
|
||||
class OrderItem extends Model
|
||||
{
|
||||
use BelongsToTenant, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $table = 'order_items';
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
use App\Models\Members\User;
|
||||
use App\Models\Members\UserRole;
|
||||
use App\Models\Tenants\Tenant;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
@@ -15,7 +16,7 @@
|
||||
*/
|
||||
class Role extends Model
|
||||
{
|
||||
use BelongsToTenant, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
use App\Models\Members\User;
|
||||
use App\Models\Tenants\Department;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -12,7 +13,7 @@
|
||||
|
||||
class Popup extends Model
|
||||
{
|
||||
use BelongsToTenant, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -12,7 +13,7 @@
|
||||
|
||||
class Process extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
use Auditable, BelongsToTenant;
|
||||
use HasFactory;
|
||||
use ModelTrait;
|
||||
use SoftDeletes;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
use App\Models\Process;
|
||||
use App\Models\Tenants\Department;
|
||||
use App\Models\Tenants\Shipment;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -22,7 +23,7 @@
|
||||
*/
|
||||
class WorkOrder extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $table = 'work_orders';
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models\Production;
|
||||
|
||||
use App\Models\Members\User;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -15,7 +16,7 @@
|
||||
*/
|
||||
class WorkOrderAssignee extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait;
|
||||
use Auditable, BelongsToTenant, ModelTrait;
|
||||
|
||||
protected $table = 'work_order_assignees';
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models\Production;
|
||||
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
@@ -13,7 +14,7 @@
|
||||
*/
|
||||
class WorkOrderBendingDetail extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
use Auditable, BelongsToTenant;
|
||||
|
||||
protected $table = 'work_order_bending_details';
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models\Production;
|
||||
|
||||
use App\Models\Members\User;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
@@ -14,7 +15,7 @@
|
||||
*/
|
||||
class WorkOrderIssue extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
use Auditable, BelongsToTenant;
|
||||
|
||||
protected $table = 'work_order_issues';
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models\Production;
|
||||
|
||||
use App\Models\Items\Item;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
@@ -12,7 +13,7 @@
|
||||
*/
|
||||
class WorkOrderItem extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
use Auditable, BelongsToTenant;
|
||||
|
||||
protected $table = 'work_order_items';
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models\Production;
|
||||
|
||||
use App\Models\Members\User;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -16,7 +17,7 @@
|
||||
*/
|
||||
class WorkResult extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $table = 'work_results';
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models\Products;
|
||||
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -12,7 +13,7 @@
|
||||
*/
|
||||
class CommonCode extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $table = 'common_codes';
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models\Products;
|
||||
|
||||
use App\Models\Orders\ClientGroup;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -12,7 +13,7 @@
|
||||
|
||||
class Price extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $table = 'prices';
|
||||
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
namespace App\Models\Products;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class PriceRevision extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
use Auditable, BelongsToTenant;
|
||||
|
||||
protected $table = 'price_revisions';
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
use App\Models\Commons\Category;
|
||||
use App\Models\Commons\File;
|
||||
use App\Models\Commons\Tag;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -12,7 +13,7 @@
|
||||
|
||||
class Product extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id', 'code', 'name', 'unit', 'category_id',
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models\Products;
|
||||
|
||||
use App\Models\Materials\Material;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -10,7 +11,7 @@
|
||||
|
||||
class ProductComponent extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $table = 'product_components';
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
@@ -9,7 +10,7 @@
|
||||
|
||||
class PushDeviceToken extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
use Auditable, BelongsToTenant;
|
||||
use SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class PushNotificationSetting extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
use Auditable, BelongsToTenant;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
use App\Models\Items\Item;
|
||||
use App\Models\Members\User;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -32,7 +33,7 @@
|
||||
*/
|
||||
class Inspection extends Model
|
||||
{
|
||||
use BelongsToTenant, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $table = 'inspections';
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
use App\Models\Orders\Client;
|
||||
use App\Models\Orders\Order;
|
||||
use App\Models\Tenants\SiteBriefing;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -16,7 +17,7 @@
|
||||
|
||||
class Quote extends Model
|
||||
{
|
||||
use BelongsToTenant, HasFactory, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models\Quote;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
@@ -29,7 +30,7 @@
|
||||
*/
|
||||
class QuoteFormula extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $table = 'quote_formulas';
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models\Quote;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
@@ -26,7 +27,7 @@
|
||||
*/
|
||||
class QuoteFormulaCategory extends Model
|
||||
{
|
||||
use BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
use Auditable, BelongsToTenant, ModelTrait, SoftDeletes;
|
||||
|
||||
protected $table = 'quote_formula_categories';
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models\Quote;
|
||||
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -9,7 +10,7 @@
|
||||
|
||||
class QuoteItem extends Model
|
||||
{
|
||||
use BelongsToTenant, HasFactory;
|
||||
use Auditable, BelongsToTenant, HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'quote_id',
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models\Quote;
|
||||
|
||||
use App\Models\Members\User;
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -10,7 +11,7 @@
|
||||
|
||||
class QuoteRevision extends Model
|
||||
{
|
||||
use BelongsToTenant, HasFactory;
|
||||
use Auditable, BelongsToTenant, HasFactory;
|
||||
|
||||
const UPDATED_AT = null;
|
||||
|
||||
|
||||
12
app/Models/Stats/BaseStatModel.php
Normal file
12
app/Models/Stats/BaseStatModel.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Stats;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
abstract class BaseStatModel extends Model
|
||||
{
|
||||
protected $connection = 'sam_stat';
|
||||
|
||||
protected $guarded = ['id'];
|
||||
}
|
||||
26
app/Models/Stats/Daily/StatFinanceDaily.php
Normal file
26
app/Models/Stats/Daily/StatFinanceDaily.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Stats\Daily;
|
||||
|
||||
use App\Models\Stats\BaseStatModel;
|
||||
|
||||
class StatFinanceDaily extends BaseStatModel
|
||||
{
|
||||
protected $table = 'stat_finance_daily';
|
||||
|
||||
protected $casts = [
|
||||
'stat_date' => 'date',
|
||||
'deposit_amount' => 'decimal:2',
|
||||
'withdrawal_amount' => 'decimal:2',
|
||||
'net_cashflow' => 'decimal:2',
|
||||
'purchase_amount' => 'decimal:2',
|
||||
'purchase_tax_amount' => 'decimal:2',
|
||||
'receivable_balance' => 'decimal:2',
|
||||
'payable_balance' => 'decimal:2',
|
||||
'overdue_receivable' => 'decimal:2',
|
||||
'bill_issued_amount' => 'decimal:2',
|
||||
'bill_matured_amount' => 'decimal:2',
|
||||
'card_transaction_amount' => 'decimal:2',
|
||||
'bank_balance_total' => 'decimal:2',
|
||||
];
|
||||
}
|
||||
17
app/Models/Stats/Daily/StatHrAttendanceDaily.php
Normal file
17
app/Models/Stats/Daily/StatHrAttendanceDaily.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Stats\Daily;
|
||||
|
||||
use App\Models\Stats\BaseStatModel;
|
||||
|
||||
class StatHrAttendanceDaily extends BaseStatModel
|
||||
{
|
||||
protected $table = 'stat_hr_attendance_daily';
|
||||
|
||||
protected $casts = [
|
||||
'stat_date' => 'date',
|
||||
'attendance_rate' => 'decimal:2',
|
||||
'overtime_hours' => 'decimal:2',
|
||||
'total_labor_cost' => 'decimal:2',
|
||||
];
|
||||
}
|
||||
22
app/Models/Stats/Daily/StatInventoryDaily.php
Normal file
22
app/Models/Stats/Daily/StatInventoryDaily.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Stats\Daily;
|
||||
|
||||
use App\Models\Stats\BaseStatModel;
|
||||
|
||||
class StatInventoryDaily extends BaseStatModel
|
||||
{
|
||||
protected $table = 'stat_inventory_daily';
|
||||
|
||||
protected $casts = [
|
||||
'stat_date' => 'date',
|
||||
'total_stock_qty' => 'decimal:2',
|
||||
'total_stock_value' => 'decimal:2',
|
||||
'receipt_qty' => 'decimal:2',
|
||||
'receipt_amount' => 'decimal:2',
|
||||
'issue_qty' => 'decimal:2',
|
||||
'issue_amount' => 'decimal:2',
|
||||
'inspection_pass_rate' => 'decimal:2',
|
||||
'turnover_rate' => 'decimal:2',
|
||||
];
|
||||
}
|
||||
21
app/Models/Stats/Daily/StatProductionDaily.php
Normal file
21
app/Models/Stats/Daily/StatProductionDaily.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Stats\Daily;
|
||||
|
||||
use App\Models\Stats\BaseStatModel;
|
||||
|
||||
class StatProductionDaily extends BaseStatModel
|
||||
{
|
||||
protected $table = 'stat_production_daily';
|
||||
|
||||
protected $casts = [
|
||||
'stat_date' => 'date',
|
||||
'production_qty' => 'decimal:2',
|
||||
'defect_qty' => 'decimal:2',
|
||||
'defect_rate' => 'decimal:2',
|
||||
'planned_hours' => 'decimal:2',
|
||||
'actual_hours' => 'decimal:2',
|
||||
'efficiency_rate' => 'decimal:2',
|
||||
'delivery_rate' => 'decimal:2',
|
||||
];
|
||||
}
|
||||
18
app/Models/Stats/Daily/StatQuotePipelineDaily.php
Normal file
18
app/Models/Stats/Daily/StatQuotePipelineDaily.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Stats\Daily;
|
||||
|
||||
use App\Models\Stats\BaseStatModel;
|
||||
|
||||
class StatQuotePipelineDaily extends BaseStatModel
|
||||
{
|
||||
protected $table = 'stat_quote_pipeline_daily';
|
||||
|
||||
protected $casts = [
|
||||
'stat_date' => 'date',
|
||||
'quote_amount' => 'decimal:2',
|
||||
'quote_conversion_rate' => 'decimal:2',
|
||||
'prospect_amount' => 'decimal:2',
|
||||
'bidding_amount' => 'decimal:2',
|
||||
];
|
||||
}
|
||||
18
app/Models/Stats/Daily/StatSalesDaily.php
Normal file
18
app/Models/Stats/Daily/StatSalesDaily.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Stats\Daily;
|
||||
|
||||
use App\Models\Stats\BaseStatModel;
|
||||
|
||||
class StatSalesDaily extends BaseStatModel
|
||||
{
|
||||
protected $table = 'stat_sales_daily';
|
||||
|
||||
protected $casts = [
|
||||
'stat_date' => 'date',
|
||||
'order_amount' => 'decimal:2',
|
||||
'sales_amount' => 'decimal:2',
|
||||
'sales_tax_amount' => 'decimal:2',
|
||||
'shipment_amount' => 'decimal:2',
|
||||
];
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user