diff --git a/mig/f0401invoice.go b/mig/f0401invoice.go new file mode 100644 index 0000000..a88f654 --- /dev/null +++ b/mig/f0401invoice.go @@ -0,0 +1,67 @@ +package mig + +import ( + "encoding/xml" + "fmt" +) + +type F0401Invoice struct { + XMLName xml.Name `xml:"Invoice"` + Text string `xml:",chardata"` + Xmlns string `xml:"xmlns,attr"` + + Main *InvoiceMain `xml:"Main"` + Details *InvoiceDetail `xml:"Details"` + Amount *InvoiceAmount `xml:"Amount"` +} + +// NewF0401Invoice 會回傳一個新的F0401發票,輸入參數有賣方資訊 (seller) 與買方資訊 (buyer)以及發票明細 +func NewF0401Invoice(seller *RoleDescription, buyer *RoleDescription, details []*ProductItem) (*F0401Invoice, error) { + + ret := &F0401Invoice{ + Xmlns: "urn:GEINV:eInvoiceMessage:F0401:4.0", + } + + ret.Main = &InvoiceMain{ + Seller: seller, + Buyer: buyer, + } + + ret.Details = &InvoiceDetail{ + ProductItem: details, + } + + amount := &InvoiceAmount{} + for _, item := range details { + amount.SalesAmount += item.Amount + } + amount.TotalAmount = amount.SalesAmount + ret.Amount = amount + return ret, nil +} + +func (invoice *F0401Invoice) Validate() error { + if invoice.Main == nil { + return fmt.Errorf("發票主要資訊為必填") + } + if err := invoice.Main.Validate(); err != nil { + return err + } + if invoice.Details == nil { + return fmt.Errorf("發票明細為必填") + } + if err := invoice.Details.Validate(); err != nil { + return err + } + if invoice.Amount == nil { + return fmt.Errorf("發票金額為必填") + } + if err := invoice.Amount.Validate(); err != nil { + return err + } + return nil +} + +func (f *F0401Invoice) Bytes() ([]byte, error) { + return xml.Marshal(f) +} diff --git a/mig/f0401invoice_detail.go b/mig/f0401invoice_detail.go new file mode 100644 index 0000000..df9079d --- /dev/null +++ b/mig/f0401invoice_detail.go @@ -0,0 +1,24 @@ +package mig + +import "fmt" + +type InvoiceDetail struct { + Text string `xml:",chardata"` + ProductItem []*ProductItem `xml:"ProductItem"` +} + +func (block *InvoiceDetail) Validate() error { + if len(block.ProductItem) == 0 { + return nil + } + if len := len(block.ProductItem); len > 9999 { + return fmt.Errorf("發票明細項目數量不得超過9999個,目前為%d", len) + } + + for _, item := range block.ProductItem { + if err := item.Validate(); err != nil { + return err + } + } + return nil +} diff --git a/mig/f0401invoice_main.go b/mig/f0401invoice_main.go new file mode 100644 index 0000000..6307fd2 --- /dev/null +++ b/mig/f0401invoice_main.go @@ -0,0 +1,76 @@ +package mig + +import "fmt" + +// Mig 4.0 的圖 5-1 和 15-1 少放一個 ZeroTaxRateReason + +type InvoiceMain struct { + InvoiceNumber string `xml:"InvoiceNumber"` + InvoiceDate string `xml:"InvoiceDate"` + InvoiceTime string `xml:"InvoiceTime"` + Seller *RoleDescription `xml:"Seller"` + Buyer *RoleDescription `xml:"Buyer"` + + BuyerRemark string `xml:"BuyerRemark,omitempty"` + MainRemark string `xml:"MainRemark,omitempty"` + CustomerClearanceMark string `xml:"CustomerClearanceMark,omitempty"` + Category string `xml:"Category,omitempty"` + RelateNumber string `xml:"RelateNumber,omitempty"` + + InvoiceType string `xml:"InvoiceType"` + DonateMark string `xml:"DonateMark"` + + CarrierType string `xml:"CarrierType,omitempty"` + CarrierId1 string `xml:"CarrierId1,omitempty"` + CarrierId2 string `xml:"CarrierId2,omitempty"` + PrintMark string `xml:"PrintMark"` + NPOBAN string `xml:"NPOBAN,omitempty"` + RandomNumber string `xml:"RandomNumber,omitempty"` + BondedAreaConfirm string `xml:"BondedAreaConfirm,omitempty"` + + ZeroTaxRateReason string `xml:"ZeroTaxRateReason,omitempty"` + Reserved1 string `xml:"Reserved1,omitempty"` + Reserved2 string `xml:"Reserved2,omitempty"` +} + +type A0401InvoiceMain struct { + InvoiceMain +} + +func (block *InvoiceMain) Validate() error { + if block.InvoiceNumber == "" { + return fmt.Errorf("發票號碼為必填") + } + // TODO: validate InvoiceNumber in type of InvoiceNumberType + + if block.InvoiceDate == "" { + return fmt.Errorf("發票日期為必填") + } + // TODO: validate InvoiceDate in type of DateType + + if block.InvoiceTime == "" { + return fmt.Errorf("發票時間為必填") + } + // TODO: validate InvoiceTime in type of TimeType + + if block.Seller == nil { + return fmt.Errorf("賣方為必填") + } + if err := block.Seller.Validate(); err != nil { + return fmt.Errorf("賣方資料不符規範: %w", err) + } + + if block.Buyer == nil { + return fmt.Errorf("買方為必填") + } + if err := block.Buyer.Validate(); err != nil { + return fmt.Errorf("買方資料不符規範: %w", err) + } + + // TODO: validate BuyerRemark in type of BuyerRemarkEnum + + if len(block.MainRemark) > 200 { + return fmt.Errorf("發票主要註記長度不得大於200個字元") + } + return nil +} diff --git a/mig/invoice_amount.go b/mig/invoice_amount.go new file mode 100644 index 0000000..41b6fd6 --- /dev/null +++ b/mig/invoice_amount.go @@ -0,0 +1,120 @@ +package mig + +import "fmt" + +// 在 Mig 4.0 裡面的 Invoice/Amount 有兩種定義,一個是 A0401 開立發泡 +// 另一個是 F0401 平台存證開立發票訊息,相同欄位名稱的驗證規則不一定相同 +// 舉例來說,A0401 的 SalesAmount 的 fractionDigits 是 0 +// 但是 F0401 的 SalesAmount 的 fractionDigits 是 7 +// 相同部分的驗證會在 InvoiceAmount 物件被驗證,如果規則有不同時則會被拆分驗證 + +type InvoiceAmount struct { + Text string `xml:",chardata"` + SalesAmount string `xml:"SalesAmount"` + TaxType string `xml:"TaxType"` + TaxRate string `xml:"TaxRate"` + TaxAmount string `xml:"TaxAmount"` + TotalAmount string `xml:"TotalAmount"` + DiscountAmount string `xml:"DiscountAmount,omitempty"` + OriginalCurrencyAmount string `xml:"OriginalCurrencyAmount,omitempty"` + ExchangeRate string `xml:"ExchangeRate,omitempty"` + Currency string `xml:"Currency,omitempty"` +} + +type A0401InvoiceAmount struct { + InvoiceAmount +} + +// Deprecated in Mig 4.0 +type C0401InvoiceAmount struct { + InvoiceAmount + + FreeTaxSalesAmount string `xml:"FreeTaxSalesAmount"` + ZeroTaxSalesAmount string `xml:"ZeroTaxSalesAmount"` +} + +type F0401InvoiceAmount struct { + InvoiceAmount + + FreeTaxSalesAmount string `xml:"FreeTaxSalesAmount"` + ZeroTaxSalesAmount string `xml:"ZeroTaxSalesAmount"` +} + +func (block *InvoiceAmount) Validate() error { + if block.SalesAmount == "" { + return fmt.Errorf("銷售額 (SalesAmount) 為必填") + } + + if block.TaxType == "" { + return fmt.Errorf("課稅別 (TaxType) 為必填") + } + // TODO: validate TaxType in TaxTypeEnum + + if block.TaxRate == "" { + return fmt.Errorf("稅率 (TaxRate) 為必填") + } + // TODO: validate TaxRate in TaxRateEnum + + if block.TaxAmount == "" { + return fmt.Errorf("營業稅額 (TaxAmount) 為必填") + } + // TODO: validate TaxAmount in type of decimal(20,0) + + if block.TotalAmount == "" { + return fmt.Errorf("總金額 (TotalAmount) 為必填") + } + + if block.OriginalCurrencyAmount != "" { + // TODO: validate OriginalCurrencyAmount in type of decimal(20,7) + } + + if block.ExchangeRate != "" { + // TODO: validate ExchangeRate in type of decimal(13,5) + } + + if block.Currency != "" { + // TODO: validate Currency in CurrencyCodeEnum + } + + return nil +} + +func (block *A0401InvoiceAmount) Validate() error { + err := block.InvoiceAmount.Validate() + if err != nil { + return err + } + // TODO validate SalesAmount in type of decimal(20,0) + // TODO validate TaxAmount in type of decimal(20,0) + // TODO validate TotalAmount in type of decimal(20,0) + // TODO validate DiscountAmount in type of decimal(20,0) + + return nil +} + +func (block *F0401InvoiceAmount) Validate() error { + err := block.InvoiceAmount.Validate() + if err != nil { + return err + } + + // TODO validate SalesAmount in type of decimal(20,7) + if block.FreeTaxSalesAmount == "" { + return fmt.Errorf("免稅銷售額 (FreeTaxSalesAmount) 為必填") + } + // TODO validate FreeTaxSalesAmount in type of decimal(20,7) + + if block.ZeroTaxSalesAmount == "" { + return fmt.Errorf("零稅率銷售額 (ZeroTaxSalesAmount) 為必填") + } + // TODO validate ZeroTaxSalesAmount in type of decimal(20,7) + + // TODO validate TaxAmount in type of decimal(20,0) + // TODO validate TotalAmount in type of decimal(20,7) + // TODO validate DiscountAmount in type of decimal(20,7) + // TODO validate OriginalCurrencyAmount in type of decimal(20,7) + // TODO validate ExchangeRate in type of decimal(13,5) + // TODO validate Currency in CurrencyCodeEnum + + return nil +} diff --git a/mig/main.go b/mig/main.go index ee2ea21..edbf8b2 100644 --- a/mig/main.go +++ b/mig/main.go @@ -17,55 +17,14 @@ type RoleDescription struct { RoleRemark string `xml:"RoleRemark,omitempty"` } -type MigMain struct { - InvoiceNumber string `xml:"InvoiceNumber"` - InvoiceDate string `xml:"InvoiceDate"` - InvoiceTime string `xml:"InvoiceTime"` - Seller RoleDescription `xml:"Seller"` - Buyer RoleDescription `xml:"Buyer"` - - InvoiceType string `xml:"InvoiceType"` - DonateMark string `xml:"DonateMark"` - CarrierType string `xml:"CarrierType"` - CarrierId1 string `xml:"CarrierId1"` - CarrierId2 string `xml:"CarrierId2"` - PrintMark string `xml:"PrintMark"` - RandomNumber string `xml:"RandomNumber"` -} - -type ProductItem struct { - Text string `xml:",chardata"` - Description string `xml:"Description"` - Quantity string `xml:"Quantity"` - UnitPrice string `xml:"UnitPrice"` - Amount string `xml:"Amount"` - SequenceNumber string `xml:"SequenceNumber"` - RelateNumber string `xml:"RelateNumber"` -} - -type MigDetail struct { - Text string `xml:",chardata"` - ProductItem []ProductItem `xml:"ProductItem"` -} -type MigAmount struct { - Text string `xml:",chardata"` - SalesAmount string `xml:"SalesAmount"` - FreeTaxSalesAmount string `xml:"FreeTaxSalesAmount"` - ZeroTaxSalesAmount string `xml:"ZeroTaxSalesAmount"` - TaxType string `xml:"TaxType"` - TaxRate string `xml:"TaxRate"` - TaxAmount string `xml:"TaxAmount"` - TotalAmount string `xml:"TotalAmount"` -} - type MigFile struct { XMLName xml.Name `xml:"Invoice"` Text string `xml:",chardata"` Xmlns string `xml:"xmlns,attr"` - Main MigMain `xml:"Main"` - Details MigDetail `xml:"Details"` - Amount MigAmount `xml:"Amount"` + Main *InvoiceMain `xml:"Main"` + Details *InvoiceDetail `xml:"Details"` + Amount *C0401InvoiceAmount `xml:"Amount"` } func NewMigFile(b []byte) (*MigFile, error) { diff --git a/mig/main_test.go b/mig/main_test.go index 774aac9..68bc724 100644 --- a/mig/main_test.go +++ b/mig/main_test.go @@ -11,18 +11,18 @@ func TestMarshalC0401(t *testing.T) { actual, _ := NewMigFile(tc) expected := MigFile{ - Main: MigMain{ + Main: &InvoiceMain{ InvoiceNumber: "AA00000000", InvoiceDate: "20060102", InvoiceTime: "15:04:05", - Seller: RoleDescription{ + Seller: &RoleDescription{ Identifier: "54834795", Name: "台灣智慧家庭股份有限公司", Address: "Address", PersonInCharge: "PersonInCharge", EmailAddress: "example@example.com", }, - Buyer: RoleDescription{ + Buyer: &RoleDescription{ Identifier: "0000000000", Name: "Buyer Name", }, @@ -34,9 +34,9 @@ func TestMarshalC0401(t *testing.T) { PrintMark: "N", RandomNumber: "1031", }, - Details: MigDetail{ - ProductItem: []ProductItem{ - ProductItem{ + Details: &InvoiceDetail{ + ProductItem: []*ProductItem{ + { Description: "網紅小遙 回饋問卷的早鳥們 享53折優惠", Quantity: "1", UnitPrice: "1650", @@ -46,14 +46,16 @@ func TestMarshalC0401(t *testing.T) { }, }, }, - Amount: MigAmount{ - SalesAmount: "1650", + Amount: &C0401InvoiceAmount{ + InvoiceAmount: InvoiceAmount{ + SalesAmount: "1650", + TaxType: "1", + TaxRate: "0.05", + TaxAmount: "0", + TotalAmount: "1650", + }, FreeTaxSalesAmount: "0", ZeroTaxSalesAmount: "0", - TaxType: "1", - TaxRate: "0.05", - TaxAmount: "0", - TotalAmount: "1650", }, } diff --git a/mig/product_item.go b/mig/product_item.go new file mode 100644 index 0000000..2df9fc3 --- /dev/null +++ b/mig/product_item.go @@ -0,0 +1,70 @@ +package mig + +import "fmt" + +type ProductItem struct { + Text string `xml:",chardata"` + Description string `xml:"Description"` + Quantity string `xml:"Quantity"` + Unit string `xml:"Unit,omitempty"` + UnitPrice string `xml:"UnitPrice"` + TaxType string `xml:"TaxType"` + Amount string `xml:"Amount"` + SequenceNumber string `xml:"SequenceNumber"` + Remark string `xml:"Remark,omitempty"` + RelateNumber string `xml:"RelateNumber,omitempty"` +} + +func (item *ProductItem) Validate() error { + if item.Description == "" { + return fmt.Errorf("品名 (Description) 為必填") + } + if len(item.Description) < 1 { + return fmt.Errorf("品名 (Description) 長度不得小於1個字元") + } + if len(item.Description) > 500 { + return fmt.Errorf("品名 (Description) 長度不得大於500個字元") + } + + if item.Quantity == "" { + return fmt.Errorf("數量 (Quantity) 為必填") + } + // TODO: check Quantity is a number and totalDigits <= 20, fractionDigits <= 7 + + if len(item.Unit) > 6 { + return fmt.Errorf("單位 (Unit) 長度不得大於6個字元") + } + + if item.UnitPrice == "" { + return fmt.Errorf("單價 (UnitPrice) 為必填") + } + // TODO: check UnitPrice is a number and totalDigits <= 20, fractionDigits <= 7 + + if item.TaxType == "" { + return fmt.Errorf("課稅別 (TaxType) 為必填") + } + + if item.Amount == "" { + return fmt.Errorf("金額 (Amount) 為必填") + } + + if item.SequenceNumber == "" { + return fmt.Errorf("明細排列序號 (SequenceNumber) 為必填") + } + if len(item.SequenceNumber) > 4 { + return fmt.Errorf("明細排列序號 (SequenceNumber) 長度不得大於4個字元") + } + if len(item.SequenceNumber) < 1 { + return fmt.Errorf("明細排列序號 (SequenceNumber) 長度不得小於1個字元") + } + // 規範中並沒要求一定要是數字 + + if len(item.Remark) > 120 { + return fmt.Errorf("單一欄位備註 (Remark) 長度不得大於120個字元") + } + + if len(item.RelateNumber) > 50 { + return fmt.Errorf("相關號碼 (RelateNumber) 長度不得大於50個字元") + } + return nil +} diff --git a/mig/seller.go b/mig/seller.go new file mode 100644 index 0000000..953f269 --- /dev/null +++ b/mig/seller.go @@ -0,0 +1,61 @@ +package mig + +import "fmt" + +func NewSeller() *RoleDescription { + return &RoleDescription{} +} + +// Validate 檢查賣方資料是否符合規範 +func (seller *RoleDescription) Validate() error { + if seller.Identifier == "" { + return fmt.Errorf("賣方識別碼(統一編號)為必填") + } + if len(seller.Identifier) > 10 { + // 通常統一編號長度為8個字元,但是規範最大值為10。 + return fmt.Errorf("賣方識別碼(統一編號)長度不得大於10個字元") + } + + if seller.Name == "" { + return fmt.Errorf("賣方營業人名稱為必填") + } + if len(seller.Name) < 1 { + return fmt.Errorf("賣方營業人名稱長度不得小於1個字元") + } + if len(seller.Name) > 60 { + return fmt.Errorf("賣方營業人名稱長度不得大於60個字元") + } + + if seller.Address == "" { + return fmt.Errorf("賣方地址欄位為必填") + } + if len(seller.Address) > 100 { + return fmt.Errorf("賣方營業地址長度不得大於100") + } + + if len(seller.PersonInCharge) > 30 { + return fmt.Errorf("賣方負責人姓名長度不得大於30個字元") + } + + if len(seller.TelephoneNumber) > 26 { + return fmt.Errorf("賣方電話號碼長度不得大於26個字元") + } + + if len(seller.FacsimileNumber) > 26 { + return fmt.Errorf("賣方傳真號碼長度不得大於26個字元") + } + + if len(seller.EmailAddress) > 400 { + return fmt.Errorf("賣方電子郵件地址長度不得大於400個字元") + } + + if len(seller.CustomerNumber) > 20 { + return fmt.Errorf("賣方客戶編號長度不得大於20個字元") + } + + if len(seller.RoleRemark) > 40 { + return fmt.Errorf("賣方營業人角色註記長度不得大於40個字元") + } + + return nil +}