forked from hswaw/hscloud
go/svc/invoice: refactor
We unify calculation logic, move the existing Invoice proto message into InvoiceData, and create other messages/fields around it to hold denormalized data.
This commit is contained in:
parent
57ef6b0d7f
commit
3976e3cee8
7 changed files with 190 additions and 158 deletions
|
@ -3,6 +3,7 @@ load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||||
go_library(
|
go_library(
|
||||||
name = "go_default_library",
|
name = "go_default_library",
|
||||||
srcs = [
|
srcs = [
|
||||||
|
"calc.go",
|
||||||
"main.go",
|
"main.go",
|
||||||
"model.go",
|
"model.go",
|
||||||
"render.go",
|
"render.go",
|
||||||
|
|
29
go/svc/invoice/calc.go
Normal file
29
go/svc/invoice/calc.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
pb "code.hackerspace.pl/hscloud/proto/invoice"
|
||||||
|
)
|
||||||
|
|
||||||
|
func calculateInvoiceData(p *pb.Invoice) {
|
||||||
|
p.Unit = p.Data.Unit
|
||||||
|
if p.Unit == "" {
|
||||||
|
p.Unit = "€"
|
||||||
|
}
|
||||||
|
|
||||||
|
p.TotalNet = 0
|
||||||
|
p.Total = 0
|
||||||
|
for _, i := range p.Data.Item {
|
||||||
|
rowTotalNet := uint64(i.UnitPrice * i.Count)
|
||||||
|
rowTotal := uint64(float64(rowTotalNet) * (float64(1) + float64(i.Vat)/100000))
|
||||||
|
|
||||||
|
p.TotalNet += rowTotalNet
|
||||||
|
p.Total += rowTotal
|
||||||
|
i.TotalNet = rowTotalNet
|
||||||
|
i.Total = rowTotal
|
||||||
|
}
|
||||||
|
|
||||||
|
due := int64(time.Hour*24) * p.Data.DaysDue
|
||||||
|
p.DueDate = time.Unix(0, p.Date).Add(time.Duration(due)).UnixNano()
|
||||||
|
}
|
|
@ -23,33 +23,33 @@ type service struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) CreateInvoice(ctx context.Context, req *pb.CreateInvoiceRequest) (*pb.CreateInvoiceResponse, error) {
|
func (s *service) CreateInvoice(ctx context.Context, req *pb.CreateInvoiceRequest) (*pb.CreateInvoiceResponse, error) {
|
||||||
if req.Invoice == nil {
|
if req.InvoiceData == nil {
|
||||||
return nil, status.Error(codes.InvalidArgument, "invoice must be given")
|
return nil, status.Error(codes.InvalidArgument, "invoice data must be given")
|
||||||
}
|
}
|
||||||
if len(req.Invoice.Item) < 1 {
|
if len(req.InvoiceData.Item) < 1 {
|
||||||
return nil, status.Error(codes.InvalidArgument, "invoice must contain at least one item")
|
return nil, status.Error(codes.InvalidArgument, "invoice data must contain at least one item")
|
||||||
}
|
}
|
||||||
for i, item := range req.Invoice.Item {
|
for i, item := range req.InvoiceData.Item {
|
||||||
if item.Title == "" {
|
if item.Title == "" {
|
||||||
return nil, status.Errorf(codes.InvalidArgument, "invoice item %d must have title set", i)
|
return nil, status.Errorf(codes.InvalidArgument, "invoice data item %d must have title set", i)
|
||||||
}
|
}
|
||||||
if item.Count == 0 || item.Count > 1000000 {
|
if item.Count == 0 || item.Count > 1000000 {
|
||||||
return nil, status.Errorf(codes.InvalidArgument, "invoice item %d must have correct count", i)
|
return nil, status.Errorf(codes.InvalidArgument, "invoice data item %d must have correct count", i)
|
||||||
}
|
}
|
||||||
if item.UnitPrice == 0 {
|
if item.UnitPrice == 0 {
|
||||||
return nil, status.Errorf(codes.InvalidArgument, "invoice item %d must have correct unit price", i)
|
return nil, status.Errorf(codes.InvalidArgument, "invoice data item %d must have correct unit price", i)
|
||||||
}
|
}
|
||||||
if item.Vat > 100000 {
|
if item.Vat > 100000 {
|
||||||
return nil, status.Errorf(codes.InvalidArgument, "invoice item %d must have correct vat set", i)
|
return nil, status.Errorf(codes.InvalidArgument, "invoice data item %d must have correct vat set", i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(req.Invoice.CustomerBilling) < 1 {
|
if len(req.InvoiceData.CustomerBilling) < 1 {
|
||||||
return nil, status.Error(codes.InvalidArgument, "invoice must contain at least one line of the customer's billing address")
|
return nil, status.Error(codes.InvalidArgument, "invoice data must contain at least one line of the customer's billing address")
|
||||||
}
|
}
|
||||||
if len(req.Invoice.InvoicerBilling) < 1 {
|
if len(req.InvoiceData.InvoicerBilling) < 1 {
|
||||||
return nil, status.Error(codes.InvalidArgument, "invoice must contain at least one line of the invoicer's billing address")
|
return nil, status.Error(codes.InvalidArgument, "invoice data must contain at least one line of the invoicer's billing address")
|
||||||
}
|
}
|
||||||
for i, c := range req.Invoice.InvoicerContact {
|
for i, c := range req.InvoiceData.InvoicerContact {
|
||||||
if c.Medium == "" {
|
if c.Medium == "" {
|
||||||
return nil, status.Errorf(codes.InvalidArgument, "contact point %d must have medium set", i)
|
return nil, status.Errorf(codes.InvalidArgument, "contact point %d must have medium set", i)
|
||||||
}
|
}
|
||||||
|
@ -57,11 +57,11 @@ func (s *service) CreateInvoice(ctx context.Context, req *pb.CreateInvoiceReques
|
||||||
return nil, status.Errorf(codes.InvalidArgument, "contact point %d must have contact set", i)
|
return nil, status.Errorf(codes.InvalidArgument, "contact point %d must have contact set", i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if req.Invoice.InvoicerVatId == "" {
|
if req.InvoiceData.InvoicerVatId == "" {
|
||||||
return nil, status.Error(codes.InvalidArgument, "invoice must contain invoicer's vat id")
|
return nil, status.Error(codes.InvalidArgument, "invoice data must contain invoicer's vat id")
|
||||||
}
|
}
|
||||||
|
|
||||||
uid, err := s.m.createInvoice(ctx, req.Invoice)
|
uid, err := s.m.createInvoice(ctx, req.InvoiceData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := status.FromError(err); ok {
|
if _, ok := status.FromError(err); ok {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -83,25 +83,9 @@ func (s *service) GetInvoice(ctx context.Context, req *pb.GetInvoiceRequest) (*p
|
||||||
glog.Errorf("getInvoice(_, %q): %v", req.Uid, err)
|
glog.Errorf("getInvoice(_, %q): %v", req.Uid, err)
|
||||||
return nil, status.Error(codes.Unavailable, "internal server error")
|
return nil, status.Error(codes.Unavailable, "internal server error")
|
||||||
}
|
}
|
||||||
sealedUid, err := s.m.getSealedUid(ctx, req.Uid)
|
|
||||||
if err != nil {
|
|
||||||
if _, ok := status.FromError(err); ok {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
glog.Errorf("getSealedUid(_, %q): %v", req.Uid, err)
|
|
||||||
return nil, status.Error(codes.Unavailable, "internal server error")
|
|
||||||
}
|
|
||||||
|
|
||||||
res := &pb.GetInvoiceResponse{
|
res := &pb.GetInvoiceResponse{
|
||||||
Invoice: invoice,
|
Invoice: invoice,
|
||||||
}
|
}
|
||||||
if sealedUid == "" {
|
|
||||||
res.State = pb.GetInvoiceResponse_STATE_PROFORMA
|
|
||||||
} else {
|
|
||||||
res.State = pb.GetInvoiceResponse_STATE_SEALED
|
|
||||||
res.FinalUid = sealedUid
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,7 +115,7 @@ func (s *service) invoicePDF(ctx context.Context, uid string) ([]byte, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
rendered, err = renderInvoicePDF(invoice, "xxxx", true)
|
rendered, err = renderInvoicePDF(invoice)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/golang/glog"
|
"github.com/golang/glog"
|
||||||
"github.com/golang/protobuf/proto"
|
"github.com/golang/protobuf/proto"
|
||||||
|
@ -33,12 +34,14 @@ func (m *model) init() error {
|
||||||
_, err := m.db.Exec(`
|
_, err := m.db.Exec(`
|
||||||
create table invoice (
|
create table invoice (
|
||||||
id integer primary key not null,
|
id integer primary key not null,
|
||||||
|
created_time integer not null,
|
||||||
proto blob not null
|
proto blob not null
|
||||||
);
|
);
|
||||||
create table invoice_seal (
|
create table invoice_seal (
|
||||||
id integer primary key not null,
|
id integer primary key not null,
|
||||||
invoice_id integer not null,
|
invoice_id integer not null,
|
||||||
final_uid text not null unique,
|
final_uid text not null unique,
|
||||||
|
sealed_time integer not null,
|
||||||
foreign key (invoice_id) references invoice(id)
|
foreign key (invoice_id) references invoice(id)
|
||||||
);
|
);
|
||||||
create table invoice_blob (
|
create table invoice_blob (
|
||||||
|
@ -69,14 +72,16 @@ func (m *model) sealInvoice(ctx context.Context, uid string) error {
|
||||||
|
|
||||||
q := `
|
q := `
|
||||||
insert into invoice_seal (
|
insert into invoice_seal (
|
||||||
invoice_id, final_uid
|
invoice_id, final_uid, sealed_time
|
||||||
) values (
|
) values (
|
||||||
?,
|
?,
|
||||||
( select printf("%04d", ifnull( (select final_uid as v from invoice_seal order by final_uid desc limit 1), 19000) + 1 ))
|
( select printf("%04d", ifnull( (select final_uid as v from invoice_seal order by final_uid desc limit 1), 19000) + 1 )),
|
||||||
|
?
|
||||||
)
|
)
|
||||||
|
|
||||||
`
|
`
|
||||||
res, err := tx.Exec(q, id)
|
sealTime := time.Now()
|
||||||
|
res, err := tx.Exec(q, id, sealTime.UnixNano())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -95,20 +100,24 @@ func (m *model) sealInvoice(ctx context.Context, uid string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
invoice.State = pb.Invoice_STATE_SEALED
|
||||||
|
invoice.FinalUid = fmt.Sprintf("FV/%s", finalUid)
|
||||||
|
invoice.Date = sealTime.UnixNano()
|
||||||
|
calculateInvoiceData(invoice)
|
||||||
|
|
||||||
|
pdfBlob, err := renderInvoicePDF(invoice)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
q = `
|
q = `
|
||||||
insert into invoice_blob (
|
insert into invoice_blob (
|
||||||
invoice_id, pdf
|
invoice_id, pdf
|
||||||
) values (
|
) values (
|
||||||
?,
|
?, ?
|
||||||
?
|
|
||||||
)
|
)
|
||||||
`
|
`
|
||||||
|
|
||||||
pdfBlob, err := renderInvoicePDF(invoice, finalUid, false)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := tx.Exec(q, id, pdfBlob); err != nil {
|
if _, err := tx.Exec(q, id, pdfBlob); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -120,30 +129,30 @@ func (m *model) sealInvoice(ctx context.Context, uid string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *model) createInvoice(ctx context.Context, i *pb.Invoice) (string, error) {
|
func (m *model) createInvoice(ctx context.Context, id *pb.InvoiceData) (string, error) {
|
||||||
data, err := proto.Marshal(i)
|
data, err := proto.Marshal(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
sql := `
|
sql := `
|
||||||
insert into invoice (
|
insert into invoice (
|
||||||
proto
|
proto, created_time
|
||||||
) values (
|
) values (
|
||||||
?
|
?, ?
|
||||||
)
|
)
|
||||||
`
|
`
|
||||||
res, err := m.db.Exec(sql, data)
|
res, err := m.db.Exec(sql, data, time.Now().UnixNano())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
id, err := res.LastInsertId()
|
uid, err := res.LastInsertId()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
glog.Infof("%+v", id)
|
glog.Infof("%+v", uid)
|
||||||
return fmt.Sprintf("%d", id), nil
|
return fmt.Sprintf("%d", uid), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *model) getRendered(ctx context.Context, uid string) ([]byte, error) {
|
func (m *model) getRendered(ctx context.Context, uid string) ([]byte, error) {
|
||||||
|
@ -187,6 +196,37 @@ func (m *model) getSealedUid(ctx context.Context, uid string) (string, error) {
|
||||||
return finalUid, nil
|
return finalUid, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type sqlInvoiceSealRow struct {
|
||||||
|
proto []byte
|
||||||
|
createdTime int64
|
||||||
|
sealedTime sql.NullInt64
|
||||||
|
finalUid sql.NullString
|
||||||
|
uid int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sqlInvoiceSealRow) Proto() (*pb.Invoice, error) {
|
||||||
|
data := &pb.InvoiceData{}
|
||||||
|
if err := proto.Unmarshal(s.proto, data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
p := &pb.Invoice{
|
||||||
|
Uid: fmt.Sprintf("%d", s.uid),
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
if s.finalUid.Valid {
|
||||||
|
p.State = pb.Invoice_STATE_SEALED
|
||||||
|
p.FinalUid = fmt.Sprintf("FV/%s", s.finalUid.String)
|
||||||
|
p.Date = s.sealedTime.Int64
|
||||||
|
} else {
|
||||||
|
p.State = pb.Invoice_STATE_PROFORMA
|
||||||
|
p.FinalUid = fmt.Sprintf("PROFORMA/%d", s.uid)
|
||||||
|
p.Date = s.createdTime
|
||||||
|
}
|
||||||
|
calculateInvoiceData(p)
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *model) getInvoice(ctx context.Context, uid string) (*pb.Invoice, error) {
|
func (m *model) getInvoice(ctx context.Context, uid string) (*pb.Invoice, error) {
|
||||||
id, err := strconv.Atoi(uid)
|
id, err := strconv.Atoi(uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -194,78 +234,51 @@ func (m *model) getInvoice(ctx context.Context, uid string) (*pb.Invoice, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
q := `
|
q := `
|
||||||
select invoice.proto from invoice where invoice.id = ?
|
select
|
||||||
|
invoice.id, invoice.proto, invoice.created_time, invoice_seal.sealed_time, invoice_seal.final_uid
|
||||||
|
from invoice
|
||||||
|
left join invoice_seal
|
||||||
|
on invoice_seal.invoice_id = invoice.id
|
||||||
|
where invoice.id = ?
|
||||||
`
|
`
|
||||||
res := m.db.QueryRow(q, id)
|
res := m.db.QueryRow(q, id)
|
||||||
data := []byte{}
|
row := sqlInvoiceSealRow{}
|
||||||
if err := res.Scan(&data); err != nil {
|
if err := res.Scan(&row.uid, &row.proto, &row.createdTime, &row.sealedTime, &row.finalUid); err != nil {
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return nil, status.Error(codes.NotFound, "no such invoice")
|
return nil, status.Error(codes.NotFound, "no such invoice")
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
p := &pb.Invoice{}
|
return row.Proto()
|
||||||
if err := proto.Unmarshal(data, p); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return p, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type invoice struct {
|
func (m *model) getInvoices(ctx context.Context) ([]*pb.Invoice, error) {
|
||||||
ID int64
|
|
||||||
Number string
|
|
||||||
Sealed bool
|
|
||||||
Proto *pb.Invoice
|
|
||||||
TotalNet int
|
|
||||||
Total int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *model) getInvoices(ctx context.Context) ([]invoice, error) {
|
|
||||||
q := `
|
q := `
|
||||||
select invoice_seal.final_uid, invoice.id, invoice.proto from invoice
|
select
|
||||||
|
invoice.id, invoice.proto, invoice.created_time, invoice_seal.sealed_time, invoice_seal.final_uid
|
||||||
|
from invoice
|
||||||
left join invoice_seal
|
left join invoice_seal
|
||||||
on invoice_seal.invoice_id = invoice.id
|
on invoice_seal.invoice_id = invoice.id
|
||||||
`
|
`
|
||||||
rows, err := m.db.QueryContext(ctx, q)
|
rows, err := m.db.QueryContext(ctx, q)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []invoice{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
res := []invoice{}
|
res := []*pb.Invoice{}
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
i := invoice{
|
row := sqlInvoiceSealRow{}
|
||||||
Proto: &pb.Invoice{},
|
if err := rows.Scan(&row.uid, &row.proto, &row.createdTime, &row.sealedTime, &row.finalUid); err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
buf := []byte{}
|
p, err := row.Proto()
|
||||||
|
if err != nil {
|
||||||
number := sql.NullString{}
|
return nil, err
|
||||||
if err := rows.Scan(&number, &i.ID, &buf); err != nil {
|
|
||||||
return []invoice{}, err
|
|
||||||
}
|
}
|
||||||
|
res = append(res, p)
|
||||||
if err := proto.Unmarshal(buf, i.Proto); err != nil {
|
|
||||||
return []invoice{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if number.Valid {
|
|
||||||
i.Sealed = true
|
|
||||||
i.Number = number.String
|
|
||||||
} else {
|
|
||||||
i.Number = "proforma"
|
|
||||||
}
|
|
||||||
|
|
||||||
i.Total = 0
|
|
||||||
i.TotalNet = 0
|
|
||||||
for _, it := range i.Proto.Item {
|
|
||||||
rowTotalNet := int(it.UnitPrice * it.Count)
|
|
||||||
rowTotal := int(float64(rowTotalNet) * (float64(1) + float64(it.Vat)/100000))
|
|
||||||
i.TotalNet += rowTotalNet
|
|
||||||
i.Total += rowTotal
|
|
||||||
}
|
|
||||||
|
|
||||||
res = append(res, i)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
|
|
|
@ -24,9 +24,7 @@ func init() {
|
||||||
invTmpl = template.Must(template.New("invoice.html").Parse(string(a)))
|
invTmpl = template.Must(template.New("invoice.html").Parse(string(a)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderInvoicePDF(i *pb.Invoice, number string, proforma bool) ([]byte, error) {
|
func renderInvoicePDF(i *pb.Invoice) ([]byte, error) {
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
type item struct {
|
type item struct {
|
||||||
Title string
|
Title string
|
||||||
UnitPrice string
|
UnitPrice string
|
||||||
|
@ -56,55 +54,45 @@ func renderInvoicePDF(i *pb.Invoice, number string, proforma bool) ([]byte, erro
|
||||||
Total string
|
Total string
|
||||||
DeliveryCharge string
|
DeliveryCharge string
|
||||||
}{
|
}{
|
||||||
InvoiceNumber: number,
|
InvoiceNumber: i.FinalUid,
|
||||||
Date: now,
|
Date: time.Unix(0, i.Date),
|
||||||
DueDate: now.AddDate(0, 0, int(i.DaysDue)),
|
DueDate: time.Unix(0, i.DueDate),
|
||||||
IBAN: i.Iban,
|
IBAN: i.Data.Iban,
|
||||||
SWIFT: i.Swift,
|
SWIFT: i.Data.Swift,
|
||||||
InvoicerVAT: i.InvoicerVatId,
|
InvoicerVAT: i.Data.InvoicerVatId,
|
||||||
InvoicerCompanyNumber: i.InvoicerCompanyNumber,
|
InvoicerCompanyNumber: i.Data.InvoicerCompanyNumber,
|
||||||
InvoiceeVAT: i.CustomerVatId,
|
InvoiceeVAT: i.Data.CustomerVatId,
|
||||||
Proforma: proforma,
|
Proforma: i.State == pb.Invoice_STATE_PROFORMA,
|
||||||
ReverseVAT: i.ReverseVat,
|
ReverseVAT: i.Data.ReverseVat,
|
||||||
USCustomer: i.UsCustomer,
|
USCustomer: i.Data.UsCustomer,
|
||||||
|
|
||||||
InvoicerBilling: make([]string, len(i.InvoicerBilling)),
|
InvoicerBilling: make([]string, len(i.Data.InvoicerBilling)),
|
||||||
InvoiceeBilling: make([]string, len(i.CustomerBilling)),
|
InvoiceeBilling: make([]string, len(i.Data.CustomerBilling)),
|
||||||
}
|
}
|
||||||
|
|
||||||
unit := i.Unit
|
for d, s := range i.Data.InvoicerBilling {
|
||||||
if unit == "" {
|
|
||||||
unit = "€"
|
|
||||||
}
|
|
||||||
|
|
||||||
for d, s := range i.InvoicerBilling {
|
|
||||||
data.InvoicerBilling[d] = s
|
data.InvoicerBilling[d] = s
|
||||||
}
|
}
|
||||||
for d, s := range i.CustomerBilling {
|
for d, s := range i.Data.CustomerBilling {
|
||||||
data.InvoiceeBilling[d] = s
|
data.InvoiceeBilling[d] = s
|
||||||
}
|
}
|
||||||
|
|
||||||
totalNet := 0
|
unit := i.Unit
|
||||||
total := 0
|
|
||||||
for _, i := range i.Item {
|
|
||||||
rowTotalNet := int(i.UnitPrice * i.Count)
|
|
||||||
rowTotal := int(float64(rowTotalNet) * (float64(1) + float64(i.Vat)/100000))
|
|
||||||
|
|
||||||
totalNet += rowTotalNet
|
for _, it := range i.Data.Item {
|
||||||
total += rowTotal
|
|
||||||
data.Items = append(data.Items, item{
|
data.Items = append(data.Items, item{
|
||||||
Title: i.Title,
|
Title: it.Title,
|
||||||
Qty: fmt.Sprintf("%d", i.Count),
|
Qty: fmt.Sprintf("%d", it.Count),
|
||||||
UnitPrice: fmt.Sprintf(unit+"%.2f", float64(i.UnitPrice)/100),
|
UnitPrice: fmt.Sprintf(unit+"%.2f", float64(it.UnitPrice)/100),
|
||||||
VATRate: fmt.Sprintf("%.2f%%", float64(i.Vat)/1000),
|
VATRate: fmt.Sprintf("%.2f%%", float64(it.Vat)/1000),
|
||||||
TotalNet: fmt.Sprintf(unit+"%.2f", float64(rowTotalNet)/100),
|
TotalNet: fmt.Sprintf(unit+"%.2f", float64(it.TotalNet)/100),
|
||||||
Total: fmt.Sprintf(unit+"%.2f", float64(rowTotal)/100),
|
Total: fmt.Sprintf(unit+"%.2f", float64(it.Total)/100),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
data.TotalNet = fmt.Sprintf(unit+"%.2f", float64(totalNet)/100)
|
data.TotalNet = fmt.Sprintf(unit+"%.2f", float64(i.TotalNet)/100)
|
||||||
data.VATTotal = fmt.Sprintf(unit+"%.2f", float64(total-totalNet)/100)
|
data.VATTotal = fmt.Sprintf(unit+"%.2f", float64(i.Total-i.TotalNet)/100)
|
||||||
data.Total = fmt.Sprintf(unit+"%.2f", float64(total)/100)
|
data.Total = fmt.Sprintf(unit+"%.2f", float64(i.Total)/100)
|
||||||
data.DeliveryCharge = fmt.Sprintf(unit+"%.2f", float64(0))
|
data.DeliveryCharge = fmt.Sprintf(unit+"%.2f", float64(0))
|
||||||
|
|
||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
"code.hackerspace.pl/hscloud/go/mirko"
|
"code.hackerspace.pl/hscloud/go/mirko"
|
||||||
"code.hackerspace.pl/hscloud/go/statusz"
|
"code.hackerspace.pl/hscloud/go/statusz"
|
||||||
|
pb "code.hackerspace.pl/hscloud/proto/invoice"
|
||||||
"github.com/golang/glog"
|
"github.com/golang/glog"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -37,17 +38,17 @@ const invoicesFragment = `
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
{{ range .Invoices }}
|
{{ range .Invoices }}
|
||||||
{{ if .Sealed }}
|
{{ if eq .State 2 }}
|
||||||
<tr>
|
<tr>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<tr style="opacity: 0.5">
|
<tr style="opacity: 0.5">
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<td>{{ .ID }}</td>
|
<td>{{ .Uid }}</td>
|
||||||
<td>{{ .Number }}</td>
|
<td>{{ .FinalUid }}</td>
|
||||||
<td>{{ index .Proto.CustomerBilling 0 }}</td>
|
<td>{{ index .Data.CustomerBilling 0 }}</td>
|
||||||
<td>{{ .TotalNetPretty }}</td>
|
<td>{{ .TotalNetPretty }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="/debug/view?id={{ .ID }}">View</a>
|
<a href="/debug/view?id={{ .Uid }}">View</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
@ -56,7 +57,7 @@ const invoicesFragment = `
|
||||||
`
|
`
|
||||||
|
|
||||||
type templateInvoice struct {
|
type templateInvoice struct {
|
||||||
invoice
|
*pb.Invoice
|
||||||
TotalNetPretty string
|
TotalNetPretty string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,8 +71,8 @@ func (s *service) setupStatusz(m *mirko.Mirko) {
|
||||||
res.Invoices = make([]templateInvoice, len(invoices))
|
res.Invoices = make([]templateInvoice, len(invoices))
|
||||||
for i, inv := range invoices {
|
for i, inv := range invoices {
|
||||||
res.Invoices[i] = templateInvoice{
|
res.Invoices[i] = templateInvoice{
|
||||||
invoice: inv,
|
Invoice: inv,
|
||||||
TotalNetPretty: fmt.Sprintf("%.2f %s", float64(inv.TotalNet)/100, inv.Proto.Unit),
|
TotalNetPretty: fmt.Sprintf("%.2f %s", float64(inv.TotalNet)/100, inv.Unit),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,9 @@ message Item {
|
||||||
// in thousands of percent points
|
// in thousands of percent points
|
||||||
// (ie 23% == 23000)
|
// (ie 23% == 23000)
|
||||||
uint64 vat = 4;
|
uint64 vat = 4;
|
||||||
|
// Denormalized fields follow.
|
||||||
|
uint64 total_net = 5;
|
||||||
|
uint64 total = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ContactPoint {
|
message ContactPoint {
|
||||||
|
@ -16,7 +19,7 @@ message ContactPoint {
|
||||||
string contact = 2;
|
string contact = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Invoice {
|
message InvoiceData {
|
||||||
repeated Item item = 1;
|
repeated Item item = 1;
|
||||||
repeated string invoicer_billing = 2;
|
repeated string invoicer_billing = 2;
|
||||||
repeated ContactPoint invoicer_contact = 3;
|
repeated ContactPoint invoicer_contact = 3;
|
||||||
|
@ -32,8 +35,28 @@ message Invoice {
|
||||||
string unit = 13;
|
string unit = 13;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message Invoice {
|
||||||
|
// Original invoice parameters/data.
|
||||||
|
InvoiceData data = 1;
|
||||||
|
enum State {
|
||||||
|
STATE_INVALID = 0;
|
||||||
|
STATE_PROFORMA = 1;
|
||||||
|
STATE_SEALED = 2;
|
||||||
|
};
|
||||||
|
State state = 2;
|
||||||
|
string uid = 9;
|
||||||
|
// If sealed, otherwise 'proforma'.
|
||||||
|
string final_uid = 3;
|
||||||
|
int64 date = 4;
|
||||||
|
int64 due_date = 5;
|
||||||
|
// Denormalized fields follow.
|
||||||
|
uint64 total_net = 6;
|
||||||
|
uint64 total = 7;
|
||||||
|
string unit = 8;
|
||||||
|
}
|
||||||
|
|
||||||
message CreateInvoiceRequest {
|
message CreateInvoiceRequest {
|
||||||
Invoice invoice = 1;
|
InvoiceData invoice_data = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message CreateInvoiceResponse {
|
message CreateInvoiceResponse {
|
||||||
|
@ -47,13 +70,6 @@ message GetInvoiceRequest {
|
||||||
|
|
||||||
message GetInvoiceResponse {
|
message GetInvoiceResponse {
|
||||||
Invoice invoice = 1;
|
Invoice invoice = 1;
|
||||||
enum State {
|
|
||||||
STATE_INVALID = 0;
|
|
||||||
STATE_PROFORMA = 1;
|
|
||||||
STATE_SEALED = 2;
|
|
||||||
};
|
|
||||||
State state = 2;
|
|
||||||
string final_uid = 3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message RenderInvoiceRequest {
|
message RenderInvoiceRequest {
|
||||||
|
|
Loading…
Reference in a new issue