diff --git a/app/Http/Controllers/Api/V1/BillController.php b/app/Http/Controllers/Api/V1/BillController.php new file mode 100644 index 0000000..0f9a831 --- /dev/null +++ b/app/Http/Controllers/Api/V1/BillController.php @@ -0,0 +1,112 @@ +only([ + 'search', + 'bill_type', + 'status', + 'client_id', + 'is_electronic', + 'issue_start_date', + 'issue_end_date', + 'maturity_start_date', + 'maturity_end_date', + 'sort_by', + 'sort_dir', + 'per_page', + 'page', + ]); + + $bills = $this->service->index($params); + + return ApiResponse::success($bills, __('message.fetched')); + } + + /** + * 어음 등록 + */ + public function store(StoreBillRequest $request) + { + $bill = $this->service->store($request->validated()); + + return ApiResponse::success($bill, __('message.created'), [], 201); + } + + /** + * 어음 상세 + */ + public function show(int $id) + { + $bill = $this->service->show($id); + + return ApiResponse::success($bill, __('message.fetched')); + } + + /** + * 어음 수정 + */ + public function update(int $id, UpdateBillRequest $request) + { + $bill = $this->service->update($id, $request->validated()); + + return ApiResponse::success($bill, __('message.updated')); + } + + /** + * 어음 삭제 + */ + public function destroy(int $id) + { + $this->service->destroy($id); + + return ApiResponse::success(null, __('message.deleted')); + } + + /** + * 어음 상태 변경 + */ + public function updateStatus(int $id, UpdateBillStatusRequest $request) + { + $bill = $this->service->updateStatus($id, $request->validated()['status']); + + return ApiResponse::success($bill, __('message.updated')); + } + + /** + * 어음 요약 (기간별 합계) + */ + public function summary(Request $request) + { + $params = $request->only([ + 'bill_type', + 'issue_start_date', + 'issue_end_date', + 'maturity_start_date', + 'maturity_end_date', + ]); + + $summary = $this->service->summary($params); + + return ApiResponse::success($summary, __('message.fetched')); + } +} diff --git a/app/Http/Requests/V1/Bill/StoreBillRequest.php b/app/Http/Requests/V1/Bill/StoreBillRequest.php new file mode 100644 index 0000000..01c262e --- /dev/null +++ b/app/Http/Requests/V1/Bill/StoreBillRequest.php @@ -0,0 +1,67 @@ + ['nullable', 'string', 'max:50'], + 'bill_type' => ['required', 'string', 'in:received,issued'], + 'client_id' => ['nullable', 'integer', 'exists:clients,id'], + 'client_name' => ['nullable', 'string', 'max:100'], + 'amount' => ['required', 'numeric', 'min:0'], + 'issue_date' => ['required', 'date'], + 'maturity_date' => ['required', 'date', 'after_or_equal:issue_date'], + 'status' => ['nullable', 'string', 'in:stored,maturityAlert,maturityResult,paymentComplete,dishonored,collectionRequest,collectionComplete,suing'], + 'reason' => ['nullable', 'string', 'max:255'], + 'installment_count' => ['nullable', 'integer', 'min:0'], + 'note' => ['nullable', 'string', 'max:1000'], + 'is_electronic' => ['nullable', 'boolean'], + 'bank_account_id' => ['nullable', 'integer', 'exists:bank_accounts,id'], + 'installments' => ['nullable', 'array'], + 'installments.*.date' => ['required_with:installments', 'date'], + 'installments.*.amount' => ['required_with:installments', 'numeric', 'min:0'], + 'installments.*.note' => ['nullable', 'string', 'max:255'], + ]; + } + + public function messages(): array + { + return [ + 'bill_type.required' => __('validation.required', ['attribute' => __('validation.attributes.bill_type')]), + 'bill_type.in' => __('validation.in', ['attribute' => __('validation.attributes.bill_type')]), + 'amount.required' => __('validation.required', ['attribute' => __('validation.attributes.amount')]), + 'amount.min' => __('validation.min.numeric', ['attribute' => __('validation.attributes.amount'), 'min' => 0]), + 'issue_date.required' => __('validation.required', ['attribute' => __('validation.attributes.issue_date')]), + 'maturity_date.required' => __('validation.required', ['attribute' => __('validation.attributes.maturity_date')]), + 'maturity_date.after_or_equal' => __('validation.after_or_equal', ['attribute' => __('validation.attributes.maturity_date'), 'date' => __('validation.attributes.issue_date')]), + ]; + } + + public function attributes(): array + { + return [ + 'bill_number' => __('validation.attributes.bill_number'), + 'bill_type' => __('validation.attributes.bill_type'), + 'client_id' => __('validation.attributes.client_id'), + 'client_name' => __('validation.attributes.client_name'), + 'amount' => __('validation.attributes.amount'), + 'issue_date' => __('validation.attributes.issue_date'), + 'maturity_date' => __('validation.attributes.maturity_date'), + 'status' => __('validation.attributes.status'), + 'reason' => __('validation.attributes.reason'), + 'note' => __('validation.attributes.note'), + 'is_electronic' => __('validation.attributes.is_electronic'), + 'bank_account_id' => __('validation.attributes.bank_account_id'), + ]; + } +} diff --git a/app/Http/Requests/V1/Bill/UpdateBillRequest.php b/app/Http/Requests/V1/Bill/UpdateBillRequest.php new file mode 100644 index 0000000..029da50 --- /dev/null +++ b/app/Http/Requests/V1/Bill/UpdateBillRequest.php @@ -0,0 +1,63 @@ + ['nullable', 'string', 'max:50'], + 'bill_type' => ['nullable', 'string', 'in:received,issued'], + 'client_id' => ['nullable', 'integer', 'exists:clients,id'], + 'client_name' => ['nullable', 'string', 'max:100'], + 'amount' => ['nullable', 'numeric', 'min:0'], + 'issue_date' => ['nullable', 'date'], + 'maturity_date' => ['nullable', 'date', 'after_or_equal:issue_date'], + 'status' => ['nullable', 'string', 'in:stored,maturityAlert,maturityResult,paymentComplete,dishonored,collectionRequest,collectionComplete,suing'], + 'reason' => ['nullable', 'string', 'max:255'], + 'installment_count' => ['nullable', 'integer', 'min:0'], + 'note' => ['nullable', 'string', 'max:1000'], + 'is_electronic' => ['nullable', 'boolean'], + 'bank_account_id' => ['nullable', 'integer', 'exists:bank_accounts,id'], + 'installments' => ['nullable', 'array'], + 'installments.*.date' => ['required_with:installments', 'date'], + 'installments.*.amount' => ['required_with:installments', 'numeric', 'min:0'], + 'installments.*.note' => ['nullable', 'string', 'max:255'], + ]; + } + + public function messages(): array + { + return [ + 'bill_type.in' => __('validation.in', ['attribute' => __('validation.attributes.bill_type')]), + 'amount.min' => __('validation.min.numeric', ['attribute' => __('validation.attributes.amount'), 'min' => 0]), + 'maturity_date.after_or_equal' => __('validation.after_or_equal', ['attribute' => __('validation.attributes.maturity_date'), 'date' => __('validation.attributes.issue_date')]), + ]; + } + + public function attributes(): array + { + return [ + 'bill_number' => __('validation.attributes.bill_number'), + 'bill_type' => __('validation.attributes.bill_type'), + 'client_id' => __('validation.attributes.client_id'), + 'client_name' => __('validation.attributes.client_name'), + 'amount' => __('validation.attributes.amount'), + 'issue_date' => __('validation.attributes.issue_date'), + 'maturity_date' => __('validation.attributes.maturity_date'), + 'status' => __('validation.attributes.status'), + 'reason' => __('validation.attributes.reason'), + 'note' => __('validation.attributes.note'), + 'is_electronic' => __('validation.attributes.is_electronic'), + 'bank_account_id' => __('validation.attributes.bank_account_id'), + ]; + } +} diff --git a/app/Http/Requests/V1/Bill/UpdateBillStatusRequest.php b/app/Http/Requests/V1/Bill/UpdateBillStatusRequest.php new file mode 100644 index 0000000..e4adef7 --- /dev/null +++ b/app/Http/Requests/V1/Bill/UpdateBillStatusRequest.php @@ -0,0 +1,35 @@ + ['required', 'string', 'in:stored,maturityAlert,maturityResult,paymentComplete,dishonored,collectionRequest,collectionComplete,suing'], + ]; + } + + public function messages(): array + { + return [ + 'status.required' => __('validation.required', ['attribute' => __('validation.attributes.status')]), + 'status.in' => __('validation.in', ['attribute' => __('validation.attributes.status')]), + ]; + } + + public function attributes(): array + { + return [ + 'status' => __('validation.attributes.status'), + ]; + } +} diff --git a/app/Models/Tenants/Bill.php b/app/Models/Tenants/Bill.php new file mode 100644 index 0000000..878984f --- /dev/null +++ b/app/Models/Tenants/Bill.php @@ -0,0 +1,175 @@ + 'date', + 'maturity_date' => 'date', + 'amount' => 'decimal:2', + 'client_id' => 'integer', + 'bank_account_id' => 'integer', + 'installment_count' => 'integer', + 'is_electronic' => 'boolean', + ]; + + /** + * 배열/JSON 변환 시 날짜 형식 지정 + */ + public function toArray(): array + { + $array = parent::toArray(); + + // 날짜 필드를 Y-m-d 형식으로 변환 + if (isset($array['issue_date']) && $this->issue_date) { + $array['issue_date'] = $this->issue_date->format('Y-m-d'); + } + if (isset($array['maturity_date']) && $this->maturity_date) { + $array['maturity_date'] = $this->maturity_date->format('Y-m-d'); + } + + return $array; + } + + /** + * 어음 구분 목록 + */ + public const BILL_TYPES = [ + 'received' => '수취', + 'issued' => '발행', + ]; + + /** + * 수취 어음 상태 목록 + */ + public const RECEIVED_STATUSES = [ + 'stored' => '보관중', + 'maturityAlert' => '만기입금(7일전)', + 'maturityResult' => '만기결과', + 'paymentComplete' => '결제완료', + 'dishonored' => '부도', + ]; + + /** + * 발행 어음 상태 목록 + */ + public const ISSUED_STATUSES = [ + 'stored' => '보관중', + 'maturityAlert' => '만기입금(7일전)', + 'collectionRequest' => '추심의뢰', + 'collectionComplete' => '추심완료', + 'suing' => '추소중', + 'dishonored' => '부도', + ]; + + /** + * 거래처 관계 + */ + public function client(): BelongsTo + { + return $this->belongsTo(\App\Models\Orders\Client::class); + } + + /** + * 입금/출금 계좌 관계 + */ + public function bankAccount(): BelongsTo + { + return $this->belongsTo(BankAccount::class); + } + + /** + * 차수 관계 + */ + public function installments(): HasMany + { + return $this->hasMany(BillInstallment::class); + } + + /** + * 생성자 관계 + */ + public function creator(): BelongsTo + { + return $this->belongsTo(\App\Models\Members\User::class, 'created_by'); + } + + /** + * 거래처명 조회 (회원/비회원 통합) + */ + public function getDisplayClientNameAttribute(): string + { + if ($this->client) { + return $this->client->name; + } + + return $this->client_name ?? ''; + } + + /** + * 어음 구분 라벨 + */ + public function getBillTypeLabelAttribute(): string + { + return self::BILL_TYPES[$this->bill_type] ?? $this->bill_type; + } + + /** + * 상태 라벨 + */ + public function getStatusLabelAttribute(): string + { + if ($this->bill_type === 'received') { + return self::RECEIVED_STATUSES[$this->status] ?? $this->status; + } + + return self::ISSUED_STATUSES[$this->status] ?? $this->status; + } + + /** + * 만기까지 남은 일수 + */ + public function getDaysToMaturityAttribute(): int + { + return now()->diffInDays($this->maturity_date, false); + } + + /** + * 만기 7일 전 여부 + */ + public function isMaturityAlertPeriod(): bool + { + $days = $this->days_to_maturity; + + return $days >= 0 && $days <= 7; + } +} diff --git a/app/Models/Tenants/BillInstallment.php b/app/Models/Tenants/BillInstallment.php new file mode 100644 index 0000000..40d119c --- /dev/null +++ b/app/Models/Tenants/BillInstallment.php @@ -0,0 +1,53 @@ + 'date', + 'amount' => 'decimal:2', + 'bill_id' => 'integer', + ]; + + /** + * 배열/JSON 변환 시 날짜 형식 지정 + */ + public function toArray(): array + { + $array = parent::toArray(); + + if (isset($array['installment_date']) && $this->installment_date) { + $array['installment_date'] = $this->installment_date->format('Y-m-d'); + } + + return $array; + } + + /** + * 어음 관계 + */ + public function bill(): BelongsTo + { + return $this->belongsTo(Bill::class); + } + + /** + * 생성자 관계 + */ + public function creator(): BelongsTo + { + return $this->belongsTo(\App\Models\Members\User::class, 'created_by'); + } +} diff --git a/app/Services/BillService.php b/app/Services/BillService.php new file mode 100644 index 0000000..8fcddc9 --- /dev/null +++ b/app/Services/BillService.php @@ -0,0 +1,352 @@ +tenantId(); + + $query = Bill::query() + ->where('tenant_id', $tenantId) + ->with(['client:id,name', 'bankAccount:id,bank_name,account_name', 'installments']); + + // 검색어 필터 + if (! empty($params['search'])) { + $search = $params['search']; + $query->where(function ($q) use ($search) { + $q->where('bill_number', 'like', "%{$search}%") + ->orWhere('client_name', 'like', "%{$search}%") + ->orWhere('note', 'like', "%{$search}%") + ->orWhereHas('client', function ($q) use ($search) { + $q->where('name', 'like', "%{$search}%"); + }); + }); + } + + // 어음 구분 필터 (received/issued) + if (! empty($params['bill_type'])) { + $query->where('bill_type', $params['bill_type']); + } + + // 상태 필터 + if (! empty($params['status'])) { + $query->where('status', $params['status']); + } + + // 거래처 필터 + if (! empty($params['client_id'])) { + $query->where('client_id', $params['client_id']); + } + + // 전자어음 필터 + if (isset($params['is_electronic']) && $params['is_electronic'] !== '') { + $query->where('is_electronic', (bool) $params['is_electronic']); + } + + // 발행일 범위 필터 + if (! empty($params['issue_start_date'])) { + $query->where('issue_date', '>=', $params['issue_start_date']); + } + if (! empty($params['issue_end_date'])) { + $query->where('issue_date', '<=', $params['issue_end_date']); + } + + // 만기일 범위 필터 + if (! empty($params['maturity_start_date'])) { + $query->where('maturity_date', '>=', $params['maturity_start_date']); + } + if (! empty($params['maturity_end_date'])) { + $query->where('maturity_date', '<=', $params['maturity_end_date']); + } + + // 정렬 + $sortBy = $params['sort_by'] ?? 'issue_date'; + $sortDir = $params['sort_dir'] ?? 'desc'; + $query->orderBy($sortBy, $sortDir); + + // 페이지네이션 + $perPage = $params['per_page'] ?? 20; + + return $query->paginate($perPage); + } + + /** + * 어음 상세 조회 + */ + public function show(int $id): Bill + { + $tenantId = $this->tenantId(); + + return Bill::query() + ->where('tenant_id', $tenantId) + ->with(['client:id,name', 'bankAccount:id,bank_name,account_name', 'installments', 'creator:id,name']) + ->findOrFail($id); + } + + /** + * 어음 등록 + */ + public function store(array $data): Bill + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + return DB::transaction(function () use ($data, $tenantId, $userId) { + // 어음번호 자동 생성 (없을 경우) + $billNumber = $data['bill_number'] ?? $this->generateBillNumber($tenantId); + + $bill = new Bill; + $bill->tenant_id = $tenantId; + $bill->bill_number = $billNumber; + $bill->bill_type = $data['bill_type']; + $bill->client_id = $data['client_id'] ?? null; + $bill->client_name = $data['client_name'] ?? null; + $bill->amount = $data['amount']; + $bill->issue_date = $data['issue_date']; + $bill->maturity_date = $data['maturity_date']; + $bill->status = $data['status'] ?? 'stored'; + $bill->reason = $data['reason'] ?? null; + $bill->installment_count = $data['installment_count'] ?? 0; + $bill->note = $data['note'] ?? null; + $bill->is_electronic = $data['is_electronic'] ?? false; + $bill->bank_account_id = $data['bank_account_id'] ?? null; + $bill->created_by = $userId; + $bill->updated_by = $userId; + $bill->save(); + + // 차수 관리 저장 + if (! empty($data['installments'])) { + foreach ($data['installments'] as $installment) { + BillInstallment::create([ + 'bill_id' => $bill->id, + 'installment_date' => $installment['date'], + 'amount' => $installment['amount'], + 'note' => $installment['note'] ?? null, + 'created_by' => $userId, + ]); + } + // 차수 카운트 업데이트 + $bill->installment_count = count($data['installments']); + $bill->save(); + } + + return $bill->load(['client:id,name', 'bankAccount:id,bank_name,account_name', 'installments']); + }); + } + + /** + * 어음 수정 + */ + public function update(int $id, array $data): Bill + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + return DB::transaction(function () use ($id, $data, $tenantId, $userId) { + $bill = Bill::query() + ->where('tenant_id', $tenantId) + ->findOrFail($id); + + if (isset($data['bill_number'])) { + $bill->bill_number = $data['bill_number']; + } + if (isset($data['bill_type'])) { + $bill->bill_type = $data['bill_type']; + } + if (array_key_exists('client_id', $data)) { + $bill->client_id = $data['client_id']; + } + if (array_key_exists('client_name', $data)) { + $bill->client_name = $data['client_name']; + } + if (isset($data['amount'])) { + $bill->amount = $data['amount']; + } + if (isset($data['issue_date'])) { + $bill->issue_date = $data['issue_date']; + } + if (isset($data['maturity_date'])) { + $bill->maturity_date = $data['maturity_date']; + } + if (isset($data['status'])) { + $bill->status = $data['status']; + } + if (array_key_exists('reason', $data)) { + $bill->reason = $data['reason']; + } + if (array_key_exists('note', $data)) { + $bill->note = $data['note']; + } + if (isset($data['is_electronic'])) { + $bill->is_electronic = $data['is_electronic']; + } + if (array_key_exists('bank_account_id', $data)) { + $bill->bank_account_id = $data['bank_account_id']; + } + + $bill->updated_by = $userId; + $bill->save(); + + // 차수 관리 업데이트 (전체 교체) + if (isset($data['installments'])) { + // 기존 차수 삭제 + $bill->installments()->delete(); + + // 새 차수 추가 + foreach ($data['installments'] as $installment) { + BillInstallment::create([ + 'bill_id' => $bill->id, + 'installment_date' => $installment['date'], + 'amount' => $installment['amount'], + 'note' => $installment['note'] ?? null, + 'created_by' => $userId, + ]); + } + // 차수 카운트 업데이트 + $bill->installment_count = count($data['installments']); + $bill->save(); + } + + return $bill->fresh(['client:id,name', 'bankAccount:id,bank_name,account_name', 'installments']); + }); + } + + /** + * 어음 삭제 + */ + public function destroy(int $id): bool + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + return DB::transaction(function () use ($id, $tenantId, $userId) { + $bill = Bill::query() + ->where('tenant_id', $tenantId) + ->findOrFail($id); + + $bill->deleted_by = $userId; + $bill->save(); + $bill->delete(); + + return true; + }); + } + + /** + * 어음 상태 변경 + */ + public function updateStatus(int $id, string $status): Bill + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + $bill = Bill::query() + ->where('tenant_id', $tenantId) + ->findOrFail($id); + + $bill->status = $status; + $bill->updated_by = $userId; + $bill->save(); + + return $bill->fresh(['client:id,name', 'bankAccount:id,bank_name,account_name', 'installments']); + } + + /** + * 어음 요약 (기간별 합계) + */ + public function summary(array $params): array + { + $tenantId = $this->tenantId(); + + $query = Bill::query() + ->where('tenant_id', $tenantId); + + // 어음 구분 필터 + if (! empty($params['bill_type'])) { + $query->where('bill_type', $params['bill_type']); + } + + // 발행일 범위 필터 + if (! empty($params['issue_start_date'])) { + $query->where('issue_date', '>=', $params['issue_start_date']); + } + if (! empty($params['issue_end_date'])) { + $query->where('issue_date', '<=', $params['issue_end_date']); + } + + // 만기일 범위 필터 + if (! empty($params['maturity_start_date'])) { + $query->where('maturity_date', '>=', $params['maturity_start_date']); + } + if (! empty($params['maturity_end_date'])) { + $query->where('maturity_date', '<=', $params['maturity_end_date']); + } + + // 전체 합계 + $total = (clone $query)->sum('amount'); + $count = (clone $query)->count(); + + // 구분별 합계 + $byType = (clone $query) + ->select('bill_type', DB::raw('SUM(amount) as total'), DB::raw('COUNT(*) as count')) + ->groupBy('bill_type') + ->get() + ->keyBy('bill_type') + ->toArray(); + + // 상태별 합계 + $byStatus = (clone $query) + ->select('status', DB::raw('SUM(amount) as total'), DB::raw('COUNT(*) as count')) + ->groupBy('status') + ->get() + ->keyBy('status') + ->toArray(); + + // 만기 임박 (7일 이내) + $maturityAlert = (clone $query) + ->where('maturity_date', '>=', now()->toDateString()) + ->where('maturity_date', '<=', now()->addDays(7)->toDateString()) + ->whereNotIn('status', ['paymentComplete', 'collectionComplete', 'dishonored']) + ->sum('amount'); + + return [ + 'total_amount' => (float) $total, + 'total_count' => $count, + 'by_type' => $byType, + 'by_status' => $byStatus, + 'maturity_alert_amount' => (float) $maturityAlert, + ]; + } + + /** + * 어음번호 자동 생성 + */ + private function generateBillNumber(int $tenantId): string + { + $prefix = date('Ym'); + $lastBill = Bill::query() + ->where('tenant_id', $tenantId) + ->where('bill_number', 'like', $prefix.'%') + ->orderBy('bill_number', 'desc') + ->first(); + + if ($lastBill) { + $lastNumber = (int) substr($lastBill->bill_number, strlen($prefix)); + $nextNumber = $lastNumber + 1; + } else { + $nextNumber = 1; + } + + return $prefix.str_pad((string) $nextNumber, 6, '0', STR_PAD_LEFT); + } +} diff --git a/app/Swagger/v1/BillApi.php b/app/Swagger/v1/BillApi.php new file mode 100644 index 0000000..3ae51c6 --- /dev/null +++ b/app/Swagger/v1/BillApi.php @@ -0,0 +1,347 @@ +id(); + $table->unsignedBigInteger('tenant_id')->comment('테넌트 ID'); + $table->string('bill_number', 50)->comment('어음번호'); + $table->enum('bill_type', ['received', 'issued'])->comment('어음 구분: received=수취, issued=발행'); + $table->unsignedBigInteger('client_id')->nullable()->comment('거래처 ID'); + $table->string('client_name', 100)->nullable()->comment('비회원 거래처명'); + $table->decimal('amount', 15, 2)->comment('금액'); + $table->date('issue_date')->comment('발행일'); + $table->date('maturity_date')->comment('만기일'); + $table->string('status', 30)->default('stored')->comment('상태: stored/maturityAlert/maturityResult/paymentComplete/dishonored/collectionRequest/collectionComplete/suing'); + $table->string('reason', 255)->nullable()->comment('사유'); + $table->unsignedInteger('installment_count')->default(0)->comment('차수'); + $table->text('note')->nullable()->comment('메모/비고'); + $table->boolean('is_electronic')->default(false)->comment('전자어음 여부'); + $table->unsignedBigInteger('bank_account_id')->nullable()->comment('입금/출금 계좌 ID'); + $table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID'); + $table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID'); + $table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자 ID'); + $table->softDeletes(); + $table->timestamps(); + + $table->index(['tenant_id', 'bill_type', 'status'], 'idx_tenant_type_status'); + $table->index(['tenant_id', 'maturity_date'], 'idx_tenant_maturity'); + $table->index('client_id', 'idx_client'); + $table->unique(['tenant_id', 'bill_number'], 'uk_tenant_bill_number'); + }); + + // 어음 차수 관리 테이블 (installments) + Schema::create('bill_installments', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('bill_id')->comment('어음 ID'); + $table->date('installment_date')->comment('차수 일자'); + $table->decimal('amount', 15, 2)->comment('차수 금액'); + $table->text('note')->nullable()->comment('비고'); + $table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID'); + $table->timestamps(); + + $table->foreign('bill_id')->references('id')->on('bills')->onDelete('cascade'); + $table->index('bill_id', 'idx_bill'); + }); + } + + public function down(): void + { + Schema::dropIfExists('bill_installments'); + Schema::dropIfExists('bills'); + } +}; diff --git a/database/seeders/Dummy/DummyBillSeeder.php b/database/seeders/Dummy/DummyBillSeeder.php new file mode 100644 index 0000000..99222f9 --- /dev/null +++ b/database/seeders/Dummy/DummyBillSeeder.php @@ -0,0 +1,167 @@ +get()->keyBy('name'); + + // 은행계좌 (대표계좌) + $primaryBankId = BankAccount::where('tenant_id', $tenantId) + ->where('is_primary', true) + ->value('id'); + + // 수취 어음 데이터 (received) - 15건 + $receivedBills = [ + ['bill_number' => '202501000001', 'client' => '삼성전자', 'amount' => 50000000, 'issue_date' => '2025-01-15', 'maturity_date' => '2025-04-15', 'status' => 'paymentComplete'], + ['bill_number' => '202501000002', 'client' => 'LG전자', 'amount' => 35000000, 'issue_date' => '2025-02-10', 'maturity_date' => '2025-05-10', 'status' => 'paymentComplete'], + ['bill_number' => '202502000001', 'client' => 'SK하이닉스', 'amount' => 80000000, 'issue_date' => '2025-02-20', 'maturity_date' => '2025-05-20', 'status' => 'paymentComplete'], + ['bill_number' => '202503000001', 'client' => '현대자동차', 'amount' => 45000000, 'issue_date' => '2025-03-05', 'maturity_date' => '2025-06-05', 'status' => 'maturityResult'], + ['bill_number' => '202504000001', 'client' => '네이버', 'amount' => 25000000, 'issue_date' => '2025-04-12', 'maturity_date' => '2025-07-12', 'status' => 'maturityResult'], + ['bill_number' => '202505000001', 'client' => '카카오', 'amount' => 18000000, 'issue_date' => '2025-05-08', 'maturity_date' => '2025-08-08', 'status' => 'stored'], + ['bill_number' => '202506000001', 'client' => '쿠팡', 'amount' => 32000000, 'issue_date' => '2025-06-15', 'maturity_date' => '2025-09-15', 'status' => 'stored'], + ['bill_number' => '202507000001', 'client' => '삼성SDS', 'amount' => 65000000, 'issue_date' => '2025-07-20', 'maturity_date' => '2025-10-20', 'status' => 'stored'], + ['bill_number' => '202508000001', 'client' => '토스', 'amount' => 15000000, 'issue_date' => '2025-08-10', 'maturity_date' => '2025-11-10', 'status' => 'stored'], + ['bill_number' => '202509000001', 'client' => '두산에너빌리티', 'amount' => 55000000, 'issue_date' => '2025-09-05', 'maturity_date' => '2025-12-05', 'status' => 'maturityAlert'], + ['bill_number' => '202510000001', 'client' => '삼성전자', 'amount' => 42000000, 'issue_date' => '2025-10-15', 'maturity_date' => '2026-01-15', 'status' => 'stored'], + ['bill_number' => '202511000001', 'client' => 'LG전자', 'amount' => 28000000, 'issue_date' => '2025-11-08', 'maturity_date' => '2026-02-08', 'status' => 'stored'], + ['bill_number' => '202511000002', 'client' => '네이버', 'amount' => 38000000, 'issue_date' => '2025-11-20', 'maturity_date' => '2026-02-20', 'status' => 'stored'], + ['bill_number' => '202512000001', 'client' => '현대자동차', 'amount' => 52000000, 'issue_date' => '2025-12-10', 'maturity_date' => '2026-03-10', 'status' => 'stored'], + ['bill_number' => '202512000002', 'client' => 'SK하이닉스', 'amount' => 70000000, 'issue_date' => '2025-12-18', 'maturity_date' => '2026-03-18', 'status' => 'stored'], + ]; + + // 발행 어음 데이터 (issued) - 15건 + $issuedBills = [ + ['bill_number' => '202501100001', 'client' => '한화솔루션', 'amount' => 40000000, 'issue_date' => '2025-01-20', 'maturity_date' => '2025-04-20', 'status' => 'collectionComplete'], + ['bill_number' => '202502100001', 'client' => '포스코', 'amount' => 55000000, 'issue_date' => '2025-02-15', 'maturity_date' => '2025-05-15', 'status' => 'collectionComplete'], + ['bill_number' => '202503100001', 'client' => '롯데케미칼', 'amount' => 30000000, 'issue_date' => '2025-03-10', 'maturity_date' => '2025-06-10', 'status' => 'collectionComplete'], + ['bill_number' => '202504100001', 'client' => 'GS칼텍스', 'amount' => 22000000, 'issue_date' => '2025-04-18', 'maturity_date' => '2025-07-18', 'status' => 'collectionComplete'], + ['bill_number' => '202505100001', 'client' => '대한항공', 'amount' => 18000000, 'issue_date' => '2025-05-12', 'maturity_date' => '2025-08-12', 'status' => 'collectionRequest'], + ['bill_number' => '202506100001', 'client' => '현대제철', 'amount' => 48000000, 'issue_date' => '2025-06-20', 'maturity_date' => '2025-09-20', 'status' => 'collectionRequest'], + ['bill_number' => '202507100001', 'client' => 'SK이노베이션', 'amount' => 35000000, 'issue_date' => '2025-07-15', 'maturity_date' => '2025-10-15', 'status' => 'stored'], + ['bill_number' => '202508100001', 'client' => 'CJ대한통운', 'amount' => 25000000, 'issue_date' => '2025-08-22', 'maturity_date' => '2025-11-22', 'status' => 'stored'], + ['bill_number' => '202509100001', 'client' => '두산에너빌리티', 'amount' => 60000000, 'issue_date' => '2025-09-10', 'maturity_date' => '2025-12-10', 'status' => 'maturityAlert'], + ['bill_number' => '202510100001', 'client' => '한화솔루션', 'amount' => 45000000, 'issue_date' => '2025-10-08', 'maturity_date' => '2026-01-08', 'status' => 'stored'], + ['bill_number' => '202511100001', 'client' => '포스코', 'amount' => 58000000, 'issue_date' => '2025-11-05', 'maturity_date' => '2026-02-05', 'status' => 'stored'], + ['bill_number' => '202511100002', 'client' => '롯데케미칼', 'amount' => 32000000, 'issue_date' => '2025-11-18', 'maturity_date' => '2026-02-18', 'status' => 'stored'], + ['bill_number' => '202512100001', 'client' => 'GS칼텍스', 'amount' => 28000000, 'issue_date' => '2025-12-05', 'maturity_date' => '2026-03-05', 'status' => 'stored'], + ['bill_number' => '202512100002', 'client' => '현대제철', 'amount' => 42000000, 'issue_date' => '2025-12-15', 'maturity_date' => '2026-03-15', 'status' => 'stored'], + ['bill_number' => '202512100003', 'client' => 'SK이노베이션', 'amount' => 38000000, 'issue_date' => '2025-12-22', 'maturity_date' => '2026-03-22', 'status' => 'stored'], + ]; + + // 차수 관리 데이터 + $installmentsData = [ + '202501000001' => [ + ['date' => '2025-02-15', 'amount' => 25000000, 'note' => '1차 분할 입금'], + ['date' => '2025-03-15', 'amount' => 25000000, 'note' => '2차 분할 입금'], + ], + '202502000001' => [ + ['date' => '2025-03-20', 'amount' => 40000000, 'note' => '1차 분할 입금'], + ['date' => '2025-04-20', 'amount' => 40000000, 'note' => '2차 분할 입금'], + ], + '202507000001' => [ + ['date' => '2025-08-20', 'amount' => 30000000, 'note' => '1차 분할 입금'], + ['date' => '2025-09-20', 'amount' => 35000000, 'note' => '2차 분할 입금'], + ], + '202501100001' => [ + ['date' => '2025-02-20', 'amount' => 20000000, 'note' => '1차 분할 지급'], + ['date' => '2025-03-20', 'amount' => 20000000, 'note' => '2차 분할 지급'], + ], + '202502100001' => [ + ['date' => '2025-03-15', 'amount' => 27500000, 'note' => '1차 분할 지급'], + ['date' => '2025-04-15', 'amount' => 27500000, 'note' => '2차 분할 지급'], + ], + '202506100001' => [ + ['date' => '2025-07-20', 'amount' => 24000000, 'note' => '1차 분할 지급'], + ['date' => '2025-08-20', 'amount' => 24000000, 'note' => '2차 분할 지급'], + ], + ]; + + $billCount = 0; + $installmentCount = 0; + + // 수취 어음 생성 + foreach ($receivedBills as $data) { + $client = $clients->get($data['client']); + $bill = Bill::create([ + 'tenant_id' => $tenantId, + 'bill_number' => $data['bill_number'], + 'bill_type' => 'received', + 'client_id' => $client?->id, + 'client_name' => $client ? null : $data['client'], + 'amount' => $data['amount'], + 'issue_date' => $data['issue_date'], + 'maturity_date' => $data['maturity_date'], + 'status' => $data['status'], + 'is_electronic' => rand(0, 1) === 1, + 'bank_account_id' => $primaryBankId, + 'installment_count' => isset($installmentsData[$data['bill_number']]) ? count($installmentsData[$data['bill_number']]) : 0, + 'created_by' => $userId, + ]); + $billCount++; + + // 차수 관리 데이터 추가 + if (isset($installmentsData[$data['bill_number']])) { + foreach ($installmentsData[$data['bill_number']] as $instData) { + BillInstallment::create([ + 'bill_id' => $bill->id, + 'installment_date' => $instData['date'], + 'amount' => $instData['amount'], + 'note' => $instData['note'], + ]); + $installmentCount++; + } + } + } + + // 발행 어음 생성 + foreach ($issuedBills as $data) { + $client = $clients->get($data['client']); + $bill = Bill::create([ + 'tenant_id' => $tenantId, + 'bill_number' => $data['bill_number'], + 'bill_type' => 'issued', + 'client_id' => $client?->id, + 'client_name' => $client ? null : $data['client'], + 'amount' => $data['amount'], + 'issue_date' => $data['issue_date'], + 'maturity_date' => $data['maturity_date'], + 'status' => $data['status'], + 'is_electronic' => rand(0, 1) === 1, + 'bank_account_id' => $primaryBankId, + 'installment_count' => isset($installmentsData[$data['bill_number']]) ? count($installmentsData[$data['bill_number']]) : 0, + 'created_by' => $userId, + ]); + $billCount++; + + // 차수 관리 데이터 추가 + if (isset($installmentsData[$data['bill_number']])) { + foreach ($installmentsData[$data['bill_number']] as $instData) { + BillInstallment::create([ + 'bill_id' => $bill->id, + 'installment_date' => $instData['date'], + 'amount' => $instData['amount'], + 'note' => $instData['note'], + ]); + $installmentCount++; + } + } + } + + $this->command->info(' ✓ bills: '.$billCount.'건 생성'); + $this->command->info(' ✓ bill_installments: '.$installmentCount.'건 생성'); + } +} diff --git a/routes/api.php b/routes/api.php index c2f2ab5..401c882 100644 --- a/routes/api.php +++ b/routes/api.php @@ -11,6 +11,7 @@ use App\Http\Controllers\Api\V1\AttendanceController; use App\Http\Controllers\Api\V1\BadDebtController; use App\Http\Controllers\Api\V1\BankAccountController; +use App\Http\Controllers\Api\V1\BillController; use App\Http\Controllers\Api\V1\BarobillSettingController; use App\Http\Controllers\Api\V1\BoardController; use App\Http\Controllers\Api\V1\CardController; @@ -551,6 +552,17 @@ Route::delete('/{id}/memos/{memoId}', [BadDebtController::class, 'removeMemo'])->whereNumber(['id', 'memoId'])->name('v1.bad-debts.memos.destroy'); }); + // Bill API (어음관리) + Route::prefix('bills')->group(function () { + Route::get('', [BillController::class, 'index'])->name('v1.bills.index'); + Route::post('', [BillController::class, 'store'])->name('v1.bills.store'); + Route::get('/summary', [BillController::class, 'summary'])->name('v1.bills.summary'); + Route::get('/{id}', [BillController::class, 'show'])->whereNumber('id')->name('v1.bills.show'); + Route::put('/{id}', [BillController::class, 'update'])->whereNumber('id')->name('v1.bills.update'); + Route::delete('/{id}', [BillController::class, 'destroy'])->whereNumber('id')->name('v1.bills.destroy'); + Route::patch('/{id}/status', [BillController::class, 'updateStatus'])->whereNumber('id')->name('v1.bills.update-status'); + }); + // Popup API (팝업관리) Route::prefix('popups')->group(function () { Route::get('', [PopupController::class, 'index'])->name('v1.popups.index');