feat:홈택스 매출/매입 수동입력, 분개, 카드내역 참조 기능 추가
- 수동입력: MAN-YYYYMMDD-NNN 형식 자동채번, 생성/수정/삭제 - 분개: 세금계산서에서 일반전표 자동 생성 (매출/매입 패턴) - 카드내역 참조: 수동입력 시 카드사용내역에서 금액/거래처 자동채움 - 테이블에 액션 컬럼 추가 (분개/수정/삭제 버튼) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,11 +5,16 @@
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Barobill\BarobillConfig;
|
||||
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\Tenants\Tenant;
|
||||
use App\Services\Barobill\HometaxSyncService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\View\View;
|
||||
@@ -1401,4 +1406,319 @@ public function toggleChecked(Request $request, HometaxSyncService $syncService)
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 수동 세금계산서 저장
|
||||
*/
|
||||
public function manualStore(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
||||
|
||||
$validated = $request->validate([
|
||||
'invoice_type' => 'required|in:sales,purchase',
|
||||
'write_date' => 'required|date',
|
||||
'invoicer_corp_name' => 'required|string|max:200',
|
||||
'invoicer_corp_num' => 'nullable|string|max:20',
|
||||
'invoicee_corp_name' => 'required|string|max:200',
|
||||
'invoicee_corp_num' => 'nullable|string|max:20',
|
||||
'supply_amount' => 'required|numeric|min:0',
|
||||
'tax_amount' => 'required|numeric|min:0',
|
||||
'item_name' => 'nullable|string|max:200',
|
||||
'remark' => 'nullable|string|max:500',
|
||||
'tax_type' => 'nullable|integer|in:1,2,3',
|
||||
'purpose_type' => 'nullable|integer|in:1,2',
|
||||
]);
|
||||
|
||||
// MAN-YYYYMMDD-NNN 형식 자동채번
|
||||
$dateStr = date('Ymd', strtotime($validated['write_date']));
|
||||
$lastNum = HometaxInvoice::where('tenant_id', $tenantId)
|
||||
->where('nts_confirm_num', 'like', "MAN-{$dateStr}-%")
|
||||
->orderByRaw('CAST(SUBSTRING_INDEX(nts_confirm_num, "-", -1) AS UNSIGNED) DESC')
|
||||
->value('nts_confirm_num');
|
||||
|
||||
$seq = 1;
|
||||
if ($lastNum) {
|
||||
$parts = explode('-', $lastNum);
|
||||
$seq = (int)end($parts) + 1;
|
||||
}
|
||||
$ntsConfirmNum = sprintf('MAN-%s-%03d', $dateStr, $seq);
|
||||
|
||||
$totalAmount = (float)$validated['supply_amount'] + (float)$validated['tax_amount'];
|
||||
|
||||
$invoice = HometaxInvoice::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'nts_confirm_num' => $ntsConfirmNum,
|
||||
'invoice_type' => $validated['invoice_type'],
|
||||
'write_date' => $validated['write_date'],
|
||||
'issue_date' => $validated['write_date'],
|
||||
'invoicer_corp_name' => $validated['invoicer_corp_name'],
|
||||
'invoicer_corp_num' => $validated['invoicer_corp_num'] ?? '',
|
||||
'invoicee_corp_name' => $validated['invoicee_corp_name'],
|
||||
'invoicee_corp_num' => $validated['invoicee_corp_num'] ?? '',
|
||||
'supply_amount' => $validated['supply_amount'],
|
||||
'tax_amount' => $validated['tax_amount'],
|
||||
'total_amount' => $totalAmount,
|
||||
'item_name' => $validated['item_name'] ?? '',
|
||||
'remark' => $validated['remark'] ?? '',
|
||||
'tax_type' => $validated['tax_type'] ?? 1,
|
||||
'purpose_type' => $validated['purpose_type'] ?? 1,
|
||||
'synced_at' => now(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '수동 세금계산서가 등록되었습니다.',
|
||||
'data' => $invoice,
|
||||
]);
|
||||
} catch (\Illuminate\Validation\ValidationException $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => '입력값 오류: ' . implode(', ', $e->validator->errors()->all()),
|
||||
], 422);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('수동 세금계산서 저장 오류: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => '저장 오류: ' . $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 수동 세금계산서 수정 (MAN- 건만 가능)
|
||||
*/
|
||||
public function manualUpdate(Request $request, int $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
||||
|
||||
$invoice = HometaxInvoice::where('id', $id)
|
||||
->where('tenant_id', $tenantId)
|
||||
->firstOrFail();
|
||||
|
||||
if (!str_starts_with($invoice->nts_confirm_num, 'MAN-')) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => '수동 입력 건만 수정할 수 있습니다.',
|
||||
], 403);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'invoice_type' => 'sometimes|in:sales,purchase',
|
||||
'write_date' => 'sometimes|date',
|
||||
'invoicer_corp_name' => 'sometimes|string|max:200',
|
||||
'invoicer_corp_num' => 'nullable|string|max:20',
|
||||
'invoicee_corp_name' => 'sometimes|string|max:200',
|
||||
'invoicee_corp_num' => 'nullable|string|max:20',
|
||||
'supply_amount' => 'sometimes|numeric|min:0',
|
||||
'tax_amount' => 'sometimes|numeric|min:0',
|
||||
'item_name' => 'nullable|string|max:200',
|
||||
'remark' => 'nullable|string|max:500',
|
||||
'tax_type' => 'nullable|integer|in:1,2,3',
|
||||
'purpose_type' => 'nullable|integer|in:1,2',
|
||||
]);
|
||||
|
||||
if (isset($validated['supply_amount']) || isset($validated['tax_amount'])) {
|
||||
$supply = $validated['supply_amount'] ?? $invoice->supply_amount;
|
||||
$tax = $validated['tax_amount'] ?? $invoice->tax_amount;
|
||||
$validated['total_amount'] = (float)$supply + (float)$tax;
|
||||
}
|
||||
|
||||
$invoice->update($validated);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '수정되었습니다.',
|
||||
'data' => $invoice->fresh(),
|
||||
]);
|
||||
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => '해당 세금계산서를 찾을 수 없습니다.',
|
||||
], 404);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('수동 세금계산서 수정 오류: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => '수정 오류: ' . $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 수동 세금계산서 삭제 (MAN- 건만 가능)
|
||||
*/
|
||||
public function manualDestroy(int $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
||||
|
||||
$invoice = HometaxInvoice::where('id', $id)
|
||||
->where('tenant_id', $tenantId)
|
||||
->firstOrFail();
|
||||
|
||||
if (!str_starts_with($invoice->nts_confirm_num, 'MAN-')) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => '수동 입력 건만 삭제할 수 있습니다.',
|
||||
], 403);
|
||||
}
|
||||
|
||||
$invoice->delete();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '삭제되었습니다.',
|
||||
]);
|
||||
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => '해당 세금계산서를 찾을 수 없습니다.',
|
||||
], 404);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('수동 세금계산서 삭제 오류: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => '삭제 오류: ' . $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 세금계산서에서 분개(일반전표) 생성
|
||||
*/
|
||||
public function createJournalEntry(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
||||
|
||||
$validated = $request->validate([
|
||||
'invoice_id' => 'required|integer',
|
||||
'lines' => 'required|array|min:1',
|
||||
'lines.*.dc_type' => 'required|in:debit,credit',
|
||||
'lines.*.account_code' => 'required|string',
|
||||
'lines.*.account_name' => 'required|string',
|
||||
'lines.*.debit_amount' => 'required|numeric|min:0',
|
||||
'lines.*.credit_amount' => 'required|numeric|min:0',
|
||||
'lines.*.description' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$invoice = HometaxInvoice::where('id', $validated['invoice_id'])
|
||||
->where('tenant_id', $tenantId)
|
||||
->firstOrFail();
|
||||
|
||||
$result = DB::transaction(function () use ($tenantId, $invoice, $validated) {
|
||||
$entryNo = JournalEntry::generateEntryNo($tenantId, $invoice->write_date);
|
||||
|
||||
$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;
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => "전표 {$result->entry_no}가 생성되었습니다.",
|
||||
'data' => $result->load('lines'),
|
||||
]);
|
||||
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => '해당 세금계산서를 찾을 수 없습니다.',
|
||||
], 404);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('분개 생성 오류: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => '분개 생성 오류: ' . $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드내역 조회 (수동입력 참조용)
|
||||
*/
|
||||
public function cardTransactions(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
||||
|
||||
$startDate = $request->input('startDate', date('Y-m-d', strtotime('-1 month')));
|
||||
$endDate = $request->input('endDate', date('Y-m-d'));
|
||||
$search = $request->input('search', '');
|
||||
|
||||
$query = BarobillCardTransaction::where('tenant_id', $tenantId)
|
||||
->whereBetween('use_date', [$startDate, $endDate])
|
||||
->orderByDesc('use_date')
|
||||
->orderByDesc('use_time');
|
||||
|
||||
if (!empty($search)) {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('merchant_name', 'like', "%{$search}%")
|
||||
->orWhere('merchant_biz_num', 'like', "%{$search}%")
|
||||
->orWhere('approval_num', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
$transactions = $query->limit(100)->get()->map(function ($t) {
|
||||
return [
|
||||
'id' => $t->id,
|
||||
'useDate' => $t->use_date,
|
||||
'useTime' => $t->use_time,
|
||||
'merchantName' => $t->merchant_name,
|
||||
'merchantBizNum' => $t->merchant_biz_num,
|
||||
'approvalNum' => $t->approval_num,
|
||||
'approvalAmount' => (float)$t->approval_amount,
|
||||
'approvalAmountFormatted' => number_format($t->approval_amount),
|
||||
'tax' => (float)($t->tax ?? 0),
|
||||
'supplyAmount' => (float)($t->modified_supply_amount ?: ($t->approval_amount - ($t->tax ?? 0))),
|
||||
'cardNum' => $t->card_num ? substr($t->card_num, -4) : '',
|
||||
'cardCompanyName' => $t->card_company_name ?? '',
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $transactions,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('카드내역 조회 오류: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => '조회 오류: ' . $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user