feat:홈택스 세금계산서 로컬 저장 및 동기화 기능 구현
- HometaxInvoice 모델 생성 (로컬 DB 조회/저장) - HometaxSyncService 서비스 생성 (API 데이터 동기화) - HometaxController에 로컬 조회/동기화 메서드 추가 - 라우트 추가: local-sales, local-purchases, sync, update-memo, toggle-checked - UI: 데이터소스 선택 (로컬 DB/바로빌 API), 동기화 버튼 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
266
app/Models/Barobill/HometaxInvoice.php
Normal file
266
app/Models/Barobill/HometaxInvoice.php
Normal file
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Barobill;
|
||||
|
||||
use App\Models\Tenants\Tenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* 홈택스 세금계산서 모델
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $tenant_id
|
||||
* @property string $nts_confirm_num 국세청승인번호
|
||||
* @property string $invoice_type 매출/매입 (sales/purchase)
|
||||
* @property \Carbon\Carbon $write_date 작성일자
|
||||
* @property \Carbon\Carbon|null $issue_date 발급일자
|
||||
* @property \Carbon\Carbon|null $send_date 전송일자
|
||||
* @property string $invoicer_corp_num 공급자 사업자번호
|
||||
* @property string $invoicer_corp_name 공급자 상호
|
||||
* @property string|null $invoicer_ceo_name 공급자 대표자
|
||||
* @property string $invoicee_corp_num 공급받는자 사업자번호
|
||||
* @property string $invoicee_corp_name 공급받는자 상호
|
||||
* @property string|null $invoicee_ceo_name 공급받는자 대표자
|
||||
* @property int $supply_amount 공급가액
|
||||
* @property int $tax_amount 세액
|
||||
* @property int $total_amount 합계
|
||||
* @property int $tax_type 과세유형 (1:과세, 2:영세, 3:면세)
|
||||
* @property int $purpose_type 영수/청구 (1:영수, 2:청구)
|
||||
* @property int $issue_type 발급유형 (1:정발행, 2:역발행)
|
||||
* @property string|null $item_name 품목명
|
||||
* @property string|null $remark 비고
|
||||
* @property string|null $memo 내부 메모
|
||||
* @property string|null $category 분류 태그
|
||||
* @property bool $is_checked 확인 여부
|
||||
* @property \Carbon\Carbon|null $synced_at 마지막 동기화 시간
|
||||
*/
|
||||
class HometaxInvoice extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'hometax_invoices';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'nts_confirm_num',
|
||||
'invoice_type',
|
||||
'write_date',
|
||||
'issue_date',
|
||||
'send_date',
|
||||
'invoicer_corp_num',
|
||||
'invoicer_corp_name',
|
||||
'invoicer_ceo_name',
|
||||
'invoicee_corp_num',
|
||||
'invoicee_corp_name',
|
||||
'invoicee_ceo_name',
|
||||
'supply_amount',
|
||||
'tax_amount',
|
||||
'total_amount',
|
||||
'tax_type',
|
||||
'purpose_type',
|
||||
'issue_type',
|
||||
'item_name',
|
||||
'remark',
|
||||
'memo',
|
||||
'category',
|
||||
'is_checked',
|
||||
'synced_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'tenant_id' => 'integer',
|
||||
'write_date' => 'date',
|
||||
'issue_date' => 'date',
|
||||
'send_date' => 'date',
|
||||
'supply_amount' => 'integer',
|
||||
'tax_amount' => 'integer',
|
||||
'total_amount' => 'integer',
|
||||
'tax_type' => 'integer',
|
||||
'purpose_type' => 'integer',
|
||||
'issue_type' => 'integer',
|
||||
'is_checked' => 'boolean',
|
||||
'synced_at' => 'datetime',
|
||||
];
|
||||
|
||||
// 과세유형 상수
|
||||
public const TAX_TYPE_TAXABLE = 1; // 과세
|
||||
public const TAX_TYPE_ZERO_RATE = 2; // 영세
|
||||
public const TAX_TYPE_EXEMPT = 3; // 면세
|
||||
|
||||
// 영수/청구 상수
|
||||
public const PURPOSE_TYPE_RECEIPT = 1; // 영수
|
||||
public const PURPOSE_TYPE_CLAIM = 2; // 청구
|
||||
|
||||
// 발급유형 상수
|
||||
public const ISSUE_TYPE_NORMAL = 1; // 정발행
|
||||
public const ISSUE_TYPE_REVERSE = 2; // 역발행
|
||||
|
||||
/**
|
||||
* 테넌트 관계
|
||||
*/
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Tenant::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 매출 스코프
|
||||
*/
|
||||
public function scopeSales($query)
|
||||
{
|
||||
return $query->where('invoice_type', 'sales');
|
||||
}
|
||||
|
||||
/**
|
||||
* 매입 스코프
|
||||
*/
|
||||
public function scopePurchase($query)
|
||||
{
|
||||
return $query->where('invoice_type', 'purchase');
|
||||
}
|
||||
|
||||
/**
|
||||
* 기간 스코프
|
||||
*/
|
||||
public function scopePeriod($query, string $startDate, string $endDate, string $dateType = 'write')
|
||||
{
|
||||
$column = match($dateType) {
|
||||
'issue' => 'issue_date',
|
||||
'send' => 'send_date',
|
||||
default => 'write_date',
|
||||
};
|
||||
|
||||
return $query->whereBetween($column, [$startDate, $endDate]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 거래처 검색 스코프
|
||||
*/
|
||||
public function scopeSearchCorp($query, string $keyword, string $invoiceType = 'sales')
|
||||
{
|
||||
if (empty($keyword)) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
// 매출이면 공급받는자, 매입이면 공급자 검색
|
||||
if ($invoiceType === 'sales') {
|
||||
return $query->where(function ($q) use ($keyword) {
|
||||
$q->where('invoicee_corp_name', 'like', "%{$keyword}%")
|
||||
->orWhere('invoicee_corp_num', 'like', "%{$keyword}%");
|
||||
});
|
||||
} else {
|
||||
return $query->where(function ($q) use ($keyword) {
|
||||
$q->where('invoicer_corp_name', 'like', "%{$keyword}%")
|
||||
->orWhere('invoicer_corp_num', 'like', "%{$keyword}%");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 과세유형 라벨
|
||||
*/
|
||||
public function getTaxTypeNameAttribute(): string
|
||||
{
|
||||
return match($this->tax_type) {
|
||||
self::TAX_TYPE_TAXABLE => '과세',
|
||||
self::TAX_TYPE_ZERO_RATE => '영세',
|
||||
self::TAX_TYPE_EXEMPT => '면세',
|
||||
default => '-',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 영수/청구 라벨
|
||||
*/
|
||||
public function getPurposeTypeNameAttribute(): string
|
||||
{
|
||||
return match($this->purpose_type) {
|
||||
self::PURPOSE_TYPE_RECEIPT => '영수',
|
||||
self::PURPOSE_TYPE_CLAIM => '청구',
|
||||
default => '-',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 발급유형 라벨
|
||||
*/
|
||||
public function getIssueTypeNameAttribute(): string
|
||||
{
|
||||
return match($this->issue_type) {
|
||||
self::ISSUE_TYPE_NORMAL => '정발급',
|
||||
self::ISSUE_TYPE_REVERSE => '역발급',
|
||||
default => '-',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 포맷된 공급가액
|
||||
*/
|
||||
public function getFormattedSupplyAmountAttribute(): string
|
||||
{
|
||||
return number_format($this->supply_amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 포맷된 세액
|
||||
*/
|
||||
public function getFormattedTaxAmountAttribute(): string
|
||||
{
|
||||
return number_format($this->tax_amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 포맷된 합계
|
||||
*/
|
||||
public function getFormattedTotalAmountAttribute(): string
|
||||
{
|
||||
return number_format($this->total_amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* API 응답 데이터를 모델 데이터로 변환
|
||||
*/
|
||||
public static function fromApiData(array $apiData, int $tenantId, string $invoiceType): array
|
||||
{
|
||||
// 작성일자 파싱
|
||||
$writeDate = null;
|
||||
if (!empty($apiData['writeDate']) && strlen($apiData['writeDate']) >= 8) {
|
||||
$writeDate = substr($apiData['writeDate'], 0, 4) . '-' .
|
||||
substr($apiData['writeDate'], 4, 2) . '-' .
|
||||
substr($apiData['writeDate'], 6, 2);
|
||||
}
|
||||
|
||||
// 발급일자 파싱
|
||||
$issueDate = null;
|
||||
if (!empty($apiData['issueDT']) && strlen($apiData['issueDT']) >= 8) {
|
||||
$issueDate = substr($apiData['issueDT'], 0, 4) . '-' .
|
||||
substr($apiData['issueDT'], 4, 2) . '-' .
|
||||
substr($apiData['issueDT'], 6, 2);
|
||||
}
|
||||
|
||||
return [
|
||||
'tenant_id' => $tenantId,
|
||||
'nts_confirm_num' => $apiData['ntsConfirmNum'] ?? '',
|
||||
'invoice_type' => $invoiceType,
|
||||
'write_date' => $writeDate,
|
||||
'issue_date' => $issueDate,
|
||||
'invoicer_corp_num' => $apiData['invoicerCorpNum'] ?? '',
|
||||
'invoicer_corp_name' => $apiData['invoicerCorpName'] ?? '',
|
||||
'invoicer_ceo_name' => $apiData['invoicerCEOName'] ?? null,
|
||||
'invoicee_corp_num' => $apiData['invoiceeCorpNum'] ?? '',
|
||||
'invoicee_corp_name' => $apiData['invoiceeCorpName'] ?? '',
|
||||
'invoicee_ceo_name' => $apiData['invoiceeCEOName'] ?? null,
|
||||
'supply_amount' => (int)($apiData['supplyAmount'] ?? 0),
|
||||
'tax_amount' => (int)($apiData['taxAmount'] ?? 0),
|
||||
'total_amount' => (int)($apiData['totalAmount'] ?? 0),
|
||||
'tax_type' => (int)($apiData['taxType'] ?? 1),
|
||||
'purpose_type' => (int)($apiData['purposeType'] ?? 1),
|
||||
'issue_type' => 1, // 기본값: 정발행
|
||||
'item_name' => $apiData['itemName'] ?? null,
|
||||
'remark' => $apiData['remark'] ?? null,
|
||||
'synced_at' => now(),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user