diff --git a/app/Http/Controllers/Barobill/HometaxController.php b/app/Http/Controllers/Barobill/HometaxController.php index 56f7597e..5844a4eb 100644 --- a/app/Http/Controllers/Barobill/HometaxController.php +++ b/app/Http/Controllers/Barobill/HometaxController.php @@ -7,8 +7,7 @@ use App\Models\Barobill\BarobillMember; use App\Models\Barobill\CardTransaction as BarobillCardTransaction; use App\Models\Barobill\HometaxInvoice; -use App\Models\Finance\JournalEntry; -use App\Models\Finance\JournalEntryLine; +use App\Models\Barobill\HometaxInvoiceJournal; use App\Models\Tenants\Tenant; use App\Services\Barobill\HometaxSyncService; use Illuminate\Http\JsonResponse; @@ -1632,55 +1631,30 @@ public function createJournalEntry(Request $request): JsonResponse ->where('tenant_id', $tenantId) ->firstOrFail(); - $result = DB::transaction(function () use ($tenantId, $invoice, $validated) { - $entryNo = JournalEntry::generateEntryNo($tenantId, $invoice->write_date); + $tradingPartner = $invoice->invoice_type === 'sales' + ? $invoice->invoicee_corp_name + : $invoice->invoicer_corp_name; - $totalDebit = collect($validated['lines'])->sum('debit_amount'); - $totalCredit = collect($validated['lines'])->sum('credit_amount'); - - $tradingPartner = $invoice->invoice_type === 'sales' - ? $invoice->invoicee_corp_name - : $invoice->invoicer_corp_name; - - $entry = JournalEntry::create([ - 'tenant_id' => $tenantId, - 'entry_no' => $entryNo, - 'entry_date' => $invoice->write_date, - 'entry_type' => 'general', - 'description' => "[홈택스-{$invoice->nts_confirm_num}] {$tradingPartner}", - 'total_debit' => $totalDebit, - 'total_credit' => $totalCredit, - 'status' => 'confirmed', - 'created_by_name' => auth()->user()?->name ?? '시스템', - ]); - - foreach ($validated['lines'] as $i => $line) { - JournalEntryLine::create([ - 'tenant_id' => $tenantId, - 'journal_entry_id' => $entry->id, - 'line_no' => $i + 1, - 'dc_type' => $line['dc_type'], - 'account_code' => $line['account_code'], - 'account_name' => $line['account_name'], - 'trading_partner_name' => $tradingPartner, - 'debit_amount' => $line['debit_amount'], - 'credit_amount' => $line['credit_amount'], - 'description' => $line['description'] ?? '', - ]); - } - - return $entry; + DB::transaction(function () use ($tenantId, $invoice, $validated, $tradingPartner) { + HometaxInvoiceJournal::saveJournals($tenantId, $invoice->id, [ + 'nts_confirm_num' => $invoice->nts_confirm_num, + 'invoice_type' => $invoice->invoice_type, + 'write_date' => $invoice->write_date, + 'supply_amount' => $invoice->supply_amount, + 'tax_amount' => $invoice->tax_amount, + 'total_amount' => $invoice->total_amount, + 'trading_partner_name' => $tradingPartner, + ], $validated['lines']); }); return response()->json([ 'success' => true, - 'message' => "전표 {$result->entry_no}가 생성되었습니다.", - 'data' => $result->load('lines'), + 'message' => '분개가 저장되었습니다.', ]); } catch (\Illuminate\Validation\ValidationException $e) { $errors = $e->errors(); $firstError = collect($errors)->flatten()->first() ?? '입력 데이터가 올바르지 않습니다.'; - Log::error('분개 생성 검증 오류', ['errors' => $errors]); + Log::error('분개 저장 검증 오류', ['errors' => $errors]); return response()->json([ 'success' => false, 'error' => $firstError, @@ -1692,10 +1666,75 @@ public function createJournalEntry(Request $request): JsonResponse 'error' => '해당 세금계산서를 찾을 수 없습니다.', ], 404); } catch (\Throwable $e) { - Log::error('분개 생성 오류: ' . $e->getMessage(), ['trace' => $e->getTraceAsString()]); + Log::error('분개 저장 오류: ' . $e->getMessage(), ['trace' => $e->getTraceAsString()]); return response()->json([ 'success' => false, - 'error' => '분개 생성 오류: ' . $e->getMessage(), + 'error' => '분개 저장 오류: ' . $e->getMessage(), + ]); + } + } + + /** + * 특정 인보이스의 분개 조회 + */ + public function getJournals(Request $request): JsonResponse + { + try { + $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID); + $invoiceId = $request->input('invoice_id'); + + if (!$invoiceId) { + return response()->json(['success' => false, 'error' => 'invoice_id는 필수입니다.'], 422); + } + + $journals = HometaxInvoiceJournal::getByInvoiceId($tenantId, (int)$invoiceId); + + return response()->json([ + 'success' => true, + 'data' => $journals->map(function ($j) { + return [ + 'dc_type' => $j->dc_type, + 'account_code' => $j->account_code, + 'account_name' => $j->account_name, + 'debit_amount' => $j->debit_amount, + 'credit_amount' => $j->credit_amount, + 'description' => $j->description, + ]; + }), + ]); + } catch (\Throwable $e) { + Log::error('분개 조회 오류: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'error' => '분개 조회 오류: ' . $e->getMessage(), + ]); + } + } + + /** + * 특정 인보이스의 분개 삭제 + */ + public function deleteJournals(Request $request): JsonResponse + { + try { + $tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID); + $invoiceId = $request->input('invoice_id'); + + if (!$invoiceId) { + return response()->json(['success' => false, 'error' => 'invoice_id는 필수입니다.'], 422); + } + + $deleted = HometaxInvoiceJournal::deleteJournals($tenantId, (int)$invoiceId); + + return response()->json([ + 'success' => true, + 'message' => "분개가 삭제되었습니다. ({$deleted}건)", + ]); + } catch (\Throwable $e) { + Log::error('분개 삭제 오류: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'error' => '분개 삭제 오류: ' . $e->getMessage(), ]); } } diff --git a/app/Models/Barobill/HometaxInvoice.php b/app/Models/Barobill/HometaxInvoice.php index 73dffaa2..afe3a001 100644 --- a/app/Models/Barobill/HometaxInvoice.php +++ b/app/Models/Barobill/HometaxInvoice.php @@ -5,6 +5,7 @@ use App\Models\Tenants\Tenant; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; /** @@ -105,6 +106,14 @@ public function tenant(): BelongsTo return $this->belongsTo(Tenant::class); } + /** + * 분개 관계 + */ + public function journals(): HasMany + { + return $this->hasMany(HometaxInvoiceJournal::class); + } + /** * 매출 스코프 */ diff --git a/app/Models/Barobill/HometaxInvoiceJournal.php b/app/Models/Barobill/HometaxInvoiceJournal.php new file mode 100644 index 00000000..478d1fdd --- /dev/null +++ b/app/Models/Barobill/HometaxInvoiceJournal.php @@ -0,0 +1,125 @@ + 'integer', + 'credit_amount' => 'integer', + 'supply_amount' => 'integer', + 'tax_amount' => 'integer', + 'total_amount' => 'integer', + 'sort_order' => 'integer', + 'write_date' => 'date', + ]; + + public function tenant(): BelongsTo + { + return $this->belongsTo(Tenant::class); + } + + public function invoice(): BelongsTo + { + return $this->belongsTo(HometaxInvoice::class, 'hometax_invoice_id'); + } + + /** + * 특정 인보이스의 분개 저장 (DELETE 후 INSERT) + */ + public static function saveJournals(int $tenantId, int $invoiceId, array $invoiceData, array $lines): void + { + // 기존 분개 삭제 + self::where('tenant_id', $tenantId) + ->where('hometax_invoice_id', $invoiceId) + ->delete(); + + // 새 분개 저장 + foreach ($lines as $index => $line) { + self::create([ + 'tenant_id' => $tenantId, + 'hometax_invoice_id' => $invoiceId, + 'nts_confirm_num' => $invoiceData['nts_confirm_num'] ?? '', + 'dc_type' => $line['dc_type'], + 'account_code' => $line['account_code'], + 'account_name' => $line['account_name'], + 'debit_amount' => (int)($line['debit_amount'] ?? 0), + 'credit_amount' => (int)($line['credit_amount'] ?? 0), + 'description' => $line['description'] ?? '', + 'sort_order' => $index, + 'invoice_type' => $invoiceData['invoice_type'] ?? '', + 'write_date' => $invoiceData['write_date'] ?? null, + 'supply_amount' => (int)($invoiceData['supply_amount'] ?? 0), + 'tax_amount' => (int)($invoiceData['tax_amount'] ?? 0), + 'total_amount' => (int)($invoiceData['total_amount'] ?? 0), + 'trading_partner_name' => $invoiceData['trading_partner_name'] ?? '', + ]); + } + } + + /** + * 특정 인보이스의 분개 조회 + */ + public static function getByInvoiceId(int $tenantId, int $invoiceId): \Illuminate\Database\Eloquent\Collection + { + return self::where('tenant_id', $tenantId) + ->where('hometax_invoice_id', $invoiceId) + ->orderBy('sort_order') + ->get(); + } + + /** + * 특정 인보이스의 분개 삭제 + */ + public static function deleteJournals(int $tenantId, int $invoiceId): int + { + return self::where('tenant_id', $tenantId) + ->where('hometax_invoice_id', $invoiceId) + ->delete(); + } + + /** + * 분개 완료된 인보이스 ID 목록 일괄 조회 + */ + public static function getJournaledInvoiceIds(int $tenantId, array $invoiceIds): array + { + if (empty($invoiceIds)) { + return []; + } + + return self::where('tenant_id', $tenantId) + ->whereIn('hometax_invoice_id', $invoiceIds) + ->distinct() + ->pluck('hometax_invoice_id') + ->toArray(); + } +} diff --git a/app/Services/Barobill/HometaxSyncService.php b/app/Services/Barobill/HometaxSyncService.php index 890ade03..0fd01f37 100644 --- a/app/Services/Barobill/HometaxSyncService.php +++ b/app/Services/Barobill/HometaxSyncService.php @@ -3,6 +3,7 @@ namespace App\Services\Barobill; use App\Models\Barobill\HometaxInvoice; +use App\Models\Barobill\HometaxInvoiceJournal; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; @@ -128,8 +129,12 @@ public function getLocalInvoices( $invoices = $query->orderByDesc('write_date')->get(); + // 분개 완료 인보이스 ID 조회 + $invoiceIds = $invoices->pluck('id')->toArray(); + $journaledIds = HometaxInvoiceJournal::getJournaledInvoiceIds($tenantId, $invoiceIds); + // API 응답 형식에 맞게 변환 - $formattedInvoices = $invoices->map(function ($inv) { + $formattedInvoices = $invoices->map(function ($inv) use ($journaledIds) { return [ 'id' => $inv->id, 'ntsConfirmNum' => $inv->nts_confirm_num, @@ -159,6 +164,7 @@ public function getLocalInvoices( 'memo' => $inv->memo, 'category' => $inv->category, 'isChecked' => $inv->is_checked, + 'hasJournal' => in_array($inv->id, $journaledIds), 'syncedAt' => $inv->synced_at?->format('Y-m-d H:i:s'), ]; })->toArray(); diff --git a/resources/views/barobill/hometax/index.blade.php b/resources/views/barobill/hometax/index.blade.php index ca30cfda..837ce86a 100644 --- a/resources/views/barobill/hometax/index.blade.php +++ b/resources/views/barobill/hometax/index.blade.php @@ -82,6 +82,8 @@ manualUpdate: '{{ route("barobill.hometax.manual-update", ["id" => "__ID__"]) }}', manualDestroy: '{{ route("barobill.hometax.manual-destroy", ["id" => "__ID__"]) }}', createJournalEntry: '{{ route("barobill.hometax.create-journal-entry") }}', + journals: '{{ route("barobill.hometax.journals") }}', + deleteJournals: '{{ route("barobill.hometax.journals.delete") }}', cardTransactions: '{{ route("barobill.hometax.card-transactions") }}', accountCodes: '{{ route("barobill.ecard.account-codes") }}', }; @@ -338,9 +340,9 @@ className="flex items-center gap-2 px-3 py-1.5 bg-blue-100 text-blue-700 rounded
+ className={`px-2 py-1 rounded text-xs transition-colors ${inv.hasJournal ? 'bg-emerald-600 text-white hover:bg-emerald-700' : 'bg-emerald-50 text-emerald-700 hover:bg-emerald-100'}`} + title={inv.hasJournal ? "분개 수정" : "분개 생성"} + >{inv.hasJournal ? '분개완료' : '분개'} {inv.ntsConfirmNum && inv.ntsConfirmNum.startsWith('MAN-') && ( <>
- {/* 분개 라인 테이블 */} -
-

분개 내역

- - - - - - - - - - - {lines.map((line, idx) => ( - - - - - + {loadingJournal ? ( +
+
+ 분개 데이터 로딩중... +
+ ) : ( + /* 분개 라인 테이블 */ +
+

분개 내역

+
차/대계정과목차변금액대변금액
- - - { - setLines(prev => prev.map((l, i) => i === idx ? { ...l, account_code: code, account_name: name } : l)); - }} - accountCodes={accountCodes} - /> - - updateLine(idx, 'debit_amount', parseFloat(e.target.value) || 0)} - className="w-full px-2 py-1 border border-stone-200 rounded text-sm text-right focus:ring-1 focus:ring-violet-500 outline-none" - /> - - updateLine(idx, 'credit_amount', parseFloat(e.target.value) || 0)} - className="w-full px-2 py-1 border border-stone-200 rounded text-sm text-right focus:ring-1 focus:ring-violet-500 outline-none" - /> -
+ + + + + + - ))} - {/* 합계 */} - - - - - - -
차/대계정과목차변금액대변금액
합계{formatCurrency(totalDebit)}{formatCurrency(totalCredit)}
- {!isBalanced && ( -

차변과 대변의 합계가 일치하지 않습니다. (차이: {formatCurrency(Math.abs(totalDebit - totalCredit))})

+ + + {lines.map((line, idx) => ( + + + + + + { + setLines(prev => prev.map((l, i) => i === idx ? { ...l, account_code: code, account_name: name } : l)); + }} + accountCodes={accountCodes} + /> + + + updateLine(idx, 'debit_amount', parseFloat(e.target.value) || 0)} + className="w-full px-2 py-1 border border-stone-200 rounded text-sm text-right focus:ring-1 focus:ring-violet-500 outline-none" + /> + + + updateLine(idx, 'credit_amount', parseFloat(e.target.value) || 0)} + className="w-full px-2 py-1 border border-stone-200 rounded text-sm text-right focus:ring-1 focus:ring-violet-500 outline-none" + /> + + + ))} + {/* 합계 */} + + 합계 + {formatCurrency(totalDebit)} + {formatCurrency(totalCredit)} + + + + {!isBalanced && ( +

차변과 대변의 합계가 일치하지 않습니다. (차이: {formatCurrency(Math.abs(totalDebit - totalCredit))})

+ )} +
+ )} + +
+
+ {isEditMode && ( + )}
-
-
- - +
+ + +
diff --git a/routes/web.php b/routes/web.php index 1200144b..ed861728 100644 --- a/routes/web.php +++ b/routes/web.php @@ -564,8 +564,10 @@ Route::post('/manual-store', [\App\Http\Controllers\Barobill\HometaxController::class, 'manualStore'])->name('manual-store'); Route::put('/manual/{id}', [\App\Http\Controllers\Barobill\HometaxController::class, 'manualUpdate'])->name('manual-update'); Route::delete('/manual/{id}', [\App\Http\Controllers\Barobill\HometaxController::class, 'manualDestroy'])->name('manual-destroy'); - // 분개 생성 + // 분개 Route::post('/create-journal-entry', [\App\Http\Controllers\Barobill\HometaxController::class, 'createJournalEntry'])->name('create-journal-entry'); + Route::get('/journals', [\App\Http\Controllers\Barobill\HometaxController::class, 'getJournals'])->name('journals'); + Route::delete('/journals', [\App\Http\Controllers\Barobill\HometaxController::class, 'deleteJournals'])->name('journals.delete'); // 카드내역 참조 Route::get('/card-transactions', [\App\Http\Controllers\Barobill\HometaxController::class, 'cardTransactions'])->name('card-transactions'); });