Skip to content

Commit

Permalink
add example for f0401
Browse files Browse the repository at this point in the history
Signed-off-by: Pichu <[email protected]>
  • Loading branch information
PichuChen committed Mar 2, 2024
1 parent d8956e4 commit c8f7f6e
Show file tree
Hide file tree
Showing 14 changed files with 452 additions and 115 deletions.
22 changes: 22 additions & 0 deletions mig/buyer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package mig

type Buyer struct {
RoleDescription
}

func NewBuyer() *Buyer {
return &Buyer{
RoleDescription: RoleDescription{
Identifier: "0000000000",
},
}
}

func (buyer *Buyer) Validate() error {
err := buyer.RoleDescription.Validate()
if err != nil {
return err
}

return nil
}
22 changes: 22 additions & 0 deletions mig/donate_mark.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package mig

import "fmt"

// DonateMarkEnum 捐贈註記 (表 4-11)
type DonateMarkEnum string

const (
// 非捐贈發票
DonateMarkNo DonateMarkEnum = "0"
// 捐贈發票
DonateMarkYes DonateMarkEnum = "1"
)

// Validate 檢查捐贈註記是否符合規範
func (t DonateMarkEnum) Validate() error {
switch t {
case DonateMarkNo, DonateMarkYes:
return nil
}
return fmt.Errorf("捐贈註記 (DonateMark) 欄位格式錯誤")
}
132 changes: 132 additions & 0 deletions mig/f0401_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package mig

import (
"fmt"
"time"
)

func ExampleNewF0401Invoice() {
// 這是一個範例
// 這個範例是用來展示如何使用 NewF0401Invoice 來建立一個 F0401 發票物件
// 這個範例會建立一個空的 F0401 發票物件

// 首先,我們需要建立賣方資訊
seller := NewSeller()
seller.Identifier = "12345678"
seller.Name = "網路書店"
seller.Address = "台北市中正區和平西路一段 1 號"
seller.PersonInCharge = "王小明"
seller.EmailAddress = "[email protected]"

// 接著,我們需要建立買方資訊
buyer := NewBuyer()
buyer.Identifier = "87654321"
buyer.Name = "網路購物者"
buyer.Address = "台北市信義區信義路五段 7 號"
buyer.PersonInCharge = "陳小美"
buyer.EmailAddress = "[email protected]"

// 最後,我們需要建立發票明細
details := []*F0401ProductItem{}
item := NewF0401ProductItem("網紅小遙")
item.RelateNumber = "A1234567890"
item.Quantity = "1"
item.Unit = "個"
item.UnitPrice = "105"
item.Amount = "105"
details = append(details, item)

item = NewF0401ProductItem("30m USB 3.0 延長線")
item.RelateNumber = "A1234567891"
item.Quantity = "2"
item.Unit = "條"
item.UnitPrice = "210"
item.Amount = "420"
details = append(details, item)

// 現在,我們可以使用 NewF0401Invoice 來建立一個 F0401 發票物件
invoice, err := NewF0401Invoice(seller, buyer, details)
if err != nil {
fmt.Println(err)
return
}
invoice.Main.InvoiceNumber = "QQ18927486"
invoice.SetDateAndTime(time.Date(2024, 3, 2, 11, 39, 40, 0, time.Local))
invoice.Details.FillSequenceNumber()
invoice.FillAmount()

// 最後,我們可以檢查這個發票是否符合規範
if err := invoice.Validate(); err != nil {
fmt.Println(err)
return
}

// Output:
f, err := invoice.Bytes()
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(f))

// 這個範例會輸出一個符合 F0401 發票規範的 XML 字串
// Output:
// <Invoice xmlns="urn:GEINV:eInvoiceMessage:F0401:4.0">
// <Main>
// <InvoiceNumber>A1234567890</InvoiceNumber>
// <InvoiceDate>20210101</InvoiceDate>
// <InvoiceTime>00:00:00</InvoiceTime>
// <Seller>
// <Identifier>12345678</Identifier>
// <Name>網路書店</Name>
// <Address>台北市中正區和平西路一段 1 號</Address>
// <PersonInCharge>王小明</PersonInCharge>
// <EmailAddress>
// <
// </EmailAddress>
// </Seller>
// <Buyer>
// <Identifier>87654321</Identifier>
// <Name>網路購物者</Name>
// <Address>台北市信義區信義路五段 7 號</Address>
// <PersonInCharge>陳小美</PersonInCharge>
// <EmailAddress>
// <
// </EmailAddress>
// </Buyer>
// <InvoiceType>07</InvoiceType>
// <DonateMark>N</DonateMark>
// <PrintMark>Y</PrintMark>
// </Main>
// <Details>
// <SequenceNumber>1</SequenceNumber>
// <ProductItem>
// <Description>網紅小遙</Description>
// <Quantity>1</Quantity>
// <Unit>個</Unit>
// <UnitPrice>100</UnitPrice>
// <Amount>105</Amount>
// <RelateNumber>A1234567890</RelateNumber>
// </ProductItem>
// <SequenceNumber>2</SequenceNumber>
// <ProductItem>
// <Description>30m USB 3.0 延長線</Description>
// <Quantity>1</Quantity>
// <Unit>條</Unit>
// <UnitPrice>1550</UnitPrice>
// <Amount>1628</Amount>
// <RelateNumber>A1234567891</RelateNumber>
// </ProductItem>
// </Details>
// <Amount>
// <SalesAmount>1733</SalesAmount>
// <TaxType>1</TaxType>
// <TaxRate>0.05</TaxRate>
// <TaxAmount>83</TaxAmount>
// <TotalAmount>1816</TotalAmount>
// <FreeTaxSalesAmount>0</FreeTaxSalesAmount>
// <ZeroTaxSalesAmount>0</ZeroTaxSalesAmount>
// </Amount>
// </Invoice>

}
77 changes: 69 additions & 8 deletions mig/f0401invoice.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package mig
import (
"encoding/xml"
"fmt"
"math/big"
"time"
)

type F0401Invoice struct {
Expand All @@ -16,32 +18,91 @@ type F0401Invoice struct {
}

// NewF0401Invoice 會回傳一個新的F0401發票,輸入參數有賣方資訊 (seller) 與買方資訊 (buyer)以及發票明細
func NewF0401Invoice(seller *RoleDescription, buyer *RoleDescription, details []*F0401ProductItem) (*F0401Invoice, error) {
func NewF0401Invoice(seller *Seller, buyer *Buyer, details []*F0401ProductItem) (*F0401Invoice, error) {

ret := &F0401Invoice{
Xmlns: "urn:GEINV:eInvoiceMessage:F0401:4.0",
}

ret.Main = &F0401InvoiceMain{
InvoiceMain: InvoiceMain{
Seller: seller,
Buyer: buyer,
InvoiceDate: time.Now().Format("20060102"),
InvoiceTime: time.Now().Format("15:04:05"),
Seller: seller,
Buyer: buyer,
InvoiceType: InvoiceTypeGeneral,
DonateMark: DonateMarkNo,
},
PrintMark: "Y",
}

ret.Details = &F0401InvoiceDetail{
ProductItem: details,
}

amount := &F0401InvoiceAmount{}
for _, item := range details {
amount.SalesAmount += item.Amount
amount := &F0401InvoiceAmount{
InvoiceAmount: InvoiceAmount{
TaxRate: "0.05",
TaxType: TaxTypeTaxable,
},
FreeTaxSalesAmount: "0",
ZeroTaxSalesAmount: "0",
}
amount.TotalAmount = amount.SalesAmount

ret.Amount = amount
return ret, nil
}

func (invoice *F0401Invoice) SetDateAndTime(t time.Time) {
invoice.Main.InvoiceDate = t.Format("20060102")
invoice.Main.InvoiceTime = t.Format("15:04:05")
}

// IsB2C 會回傳發票是否為B2C發票,判斷根據 (表 4-7 BAN 資料元規格)
func (invoice *F0401Invoice) IsB2C() bool {
return invoice.Main.Buyer.Identifier == "0000000000"
}

// FillAmount 會根據發票明細填入 SalesAmount, TaxAmount, TotalAmount
// SalesAmount 為明細的金額加總
// 當此發票為 B2B 發票時,TaxAmount 為 SalesAmount * TaxRate (四捨五入至整數)
// 當此發票為 B2C 發票時,TaxAmount 為 0
// TotalAmount 為 SalesAmount + TaxAmount
func (invoice *F0401Invoice) FillAmount() error {
salesAmount := new(big.Float)
taxAmount := new(big.Float)
for _, item := range invoice.Details.ProductItem {
amount := new(big.Float)
amount, ok := amount.SetString(item.Amount)
if !ok {
return fmt.Errorf("parse amount failed")
}
salesAmount.Add(salesAmount, amount)
}
invoice.Amount.SalesAmount = salesAmount.Text('f', 0)

if invoice.IsB2C() {
invoice.Amount.TaxAmount = "0"
} else {
taxRate := new(big.Float)
taxRate, ok := taxRate.SetString(invoice.Amount.TaxRate)
if !ok {
return fmt.Errorf("parse tax rate failed")
}
taxAmount := new(big.Float).Mul(salesAmount, taxRate)

// 四捨五入至整數
taxAmount = taxAmount.Add(taxAmount, big.NewFloat(0.5))
bint, _ := taxAmount.Int(nil)
invoice.Amount.TaxAmount = bint.String()
}
taxAmount.SetString(invoice.Amount.TaxAmount)
totalAmount := new(big.Float)
totalAmount.Add(salesAmount, taxAmount)
invoice.Amount.TotalAmount = totalAmount.Text('f', 0)
return nil
}

func (invoice *F0401Invoice) Validate() error {
if invoice.Main == nil {
return fmt.Errorf("發票主要資訊為必填")
Expand All @@ -65,5 +126,5 @@ func (invoice *F0401Invoice) Validate() error {
}

func (f *F0401Invoice) Bytes() ([]byte, error) {
return xml.Marshal(f)
return xml.MarshalIndent(f, "", " ")
}
25 changes: 14 additions & 11 deletions mig/invoice_amount.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ import "fmt"
// 相同部分的驗證會在 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"`
Text string `xml:",chardata"`
SalesAmount string `xml:"SalesAmount"`
TaxType TaxTypeEnum `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 A0101InvoiceAmount struct {
Expand Down Expand Up @@ -48,7 +48,10 @@ func (block *InvoiceAmount) Validate() error {
if block.TaxType == "" {
return fmt.Errorf("課稅別 (TaxType) 為必填")
}
// TODO: validate TaxType in TaxTypeEnum
err := block.TaxType.Validate()
if err != nil {
return err
}

if block.TaxRate == "" {
return fmt.Errorf("稅率 (TaxRate) 為必填")
Expand Down
20 changes: 16 additions & 4 deletions mig/invoice_detail.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ func (block *A0101InvoiceDetail) Validate() error {
return fmt.Errorf("發票明細項目數量不得超過9999個,目前為%d", len)
}

for _, item := range block.ProductItem {
for i, item := range block.ProductItem {
if err := item.Validate(); err != nil {
return err
return fmt.Errorf("第 %d 個發票明細項目驗證錯誤: %v", i+1, err)
}
}
return nil
Expand All @@ -42,10 +42,22 @@ func (block *F0401InvoiceDetail) Validate() error {
return fmt.Errorf("發票明細項目數量不得超過9999個,目前為%d", len)
}

for _, item := range block.ProductItem {
for i, item := range block.ProductItem {
if err := item.Validate(); err != nil {
return err
return fmt.Errorf("第 %d 個發票明細項目驗證錯誤: %v", i+1, err)
}
}
return nil
}

func (block *A0101InvoiceDetail) FillSequenceNumber() {
for i, item := range block.ProductItem {
item.SequenceNumber = fmt.Sprintf("%d", i+1)
}
}

func (block *F0401InvoiceDetail) FillSequenceNumber() {
for i, item := range block.ProductItem {
item.SequenceNumber = fmt.Sprintf("%d", i+1)
}
}
Loading

0 comments on commit c8f7f6e

Please sign in to comment.