From 9db74965087cf7b0e4baa4346ba3e7256bf8d8ce Mon Sep 17 00:00:00 2001 From: Adrian-George Bostan Date: Mon, 22 Oct 2018 22:05:45 +0300 Subject: [PATCH 01/13] Allow table as a table cell component --- pdf/creator/table.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pdf/creator/table.go b/pdf/creator/table.go index e6ff5dab..08951387 100644 --- a/pdf/creator/table.go +++ b/pdf/creator/table.go @@ -45,6 +45,9 @@ type Table struct { // Margins to be applied around the block when drawing on Page. margins margins + + // Table width. + width float64 } // newTable create a new Table with a specified number of columns. @@ -97,6 +100,12 @@ func (table *Table) Height() float64 { return sum } +// Width is not used. Not used as a Table element is designed to fill into +// available width depending on the context. Returns 0. +func (table *Table) Width() float64 { + return 0 +} + // SetMargins sets the Table's left, right, top, bottom margins. func (table *Table) SetMargins(left, right, top, bottom float64) { table.margins.left = left @@ -235,6 +244,14 @@ func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, // Add diff to last row. table.rowHeights[cell.row+cell.rowspan-2] += diffh } + case *Table: + tbl := t + newh := tbl.Height() + tbl.margins.top + tbl.margins.bottom + if newh > h { + diffh := newh - h + // Add diff to last row. + table.rowHeights[cell.row+cell.rowspan-2] += diffh + } case *Division: div := t @@ -373,6 +390,8 @@ func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, if t.enableWrap { cw = t.getMaxLineWidth() / 1000.0 } + case *Table: + cw = w } // Account for horizontal alignment: @@ -724,6 +743,8 @@ func (cell *TableCell) SetContent(vd VectorDrawable) error { cell.content = vd case *Image: cell.content = vd + case *Table: + cell.content = vd case *Division: cell.content = vd default: From 8cbb57960dcb46f88dbb494a73ec226f0565cc7f Mon Sep 17 00:00:00 2001 From: Adrian-George Bostan Date: Mon, 22 Oct 2018 22:07:08 +0300 Subject: [PATCH 02/13] Add SetText method to the styled paragraph component --- pdf/creator/styled_paragraph.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pdf/creator/styled_paragraph.go b/pdf/creator/styled_paragraph.go index e79f37cd..68604732 100644 --- a/pdf/creator/styled_paragraph.go +++ b/pdf/creator/styled_paragraph.go @@ -119,6 +119,12 @@ func (p *StyledParagraph) Reset() { p.chunks = []*TextChunk{} } +// SetText replaces all the of the paragraph with the specified one. +func (p *StyledParagraph) SetText(text string) *TextChunk { + p.Reset() + return p.Append(text) +} + // SetTextAlignment sets the horizontal alignment of the text within the space provided. func (p *StyledParagraph) SetTextAlignment(align TextAlignment) { p.alignment = align From b8a0299ca7500398bc01442fd9e2c48aa0a7808c Mon Sep 17 00:00:00 2001 From: Adrian-George Bostan Date: Mon, 22 Oct 2018 22:09:56 +0300 Subject: [PATCH 03/13] Remove unnecessary table member --- pdf/creator/styled_paragraph.go | 2 +- pdf/creator/table.go | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/pdf/creator/styled_paragraph.go b/pdf/creator/styled_paragraph.go index 68604732..ebdd12bb 100644 --- a/pdf/creator/styled_paragraph.go +++ b/pdf/creator/styled_paragraph.go @@ -119,7 +119,7 @@ func (p *StyledParagraph) Reset() { p.chunks = []*TextChunk{} } -// SetText replaces all the of the paragraph with the specified one. +// SetText replaces all the text of the paragraph with the specified one. func (p *StyledParagraph) SetText(text string) *TextChunk { p.Reset() return p.Append(text) diff --git a/pdf/creator/table.go b/pdf/creator/table.go index 08951387..29ca4d35 100644 --- a/pdf/creator/table.go +++ b/pdf/creator/table.go @@ -45,9 +45,6 @@ type Table struct { // Margins to be applied around the block when drawing on Page. margins margins - - // Table width. - width float64 } // newTable create a new Table with a specified number of columns. From b98f1fe6cef95861b951094894512e9e2de1dc7d Mon Sep 17 00:00:00 2001 From: Adrian-George Bostan Date: Fri, 26 Oct 2018 12:29:40 +0300 Subject: [PATCH 04/13] Add initial invoice implementation --- pdf/creator/creator.go | 8 + pdf/creator/division.go | 4 + pdf/creator/invoice.go | 509 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 521 insertions(+) create mode 100644 pdf/creator/invoice.go diff --git a/pdf/creator/creator.go b/pdf/creator/creator.go index a3108abb..bf9dff7a 100644 --- a/pdf/creator/creator.go +++ b/pdf/creator/creator.go @@ -672,6 +672,14 @@ func (c *Creator) NewSubchapter(ch *Chapter, title string) *Subchapter { return newSubchapter(ch, title, c.NewTextStyle()) } +// NewInvoice returns an instance of an empty invoice. +func (c *Creator) NewInvoice() *Invoice { + headingStyle := c.NewTextStyle() + headingStyle.Font = c.defaultFontBold + + return newInvoice(c.NewTextStyle(), headingStyle) +} + // NewRectangle creates a new Rectangle with default parameters // with left corner at (x,y) and width, height as specified. func (c *Creator) NewRectangle(x, y, width, height float64) *Rectangle { diff --git a/pdf/creator/division.go b/pdf/creator/division.go index 58fb0873..9398d85e 100644 --- a/pdf/creator/division.go +++ b/pdf/creator/division.go @@ -80,6 +80,10 @@ func (div *Division) Height() float64 { p := t compWidth += p.margins.left + p.margins.right compHeight += p.margins.top + p.margins.bottom + case *StyledParagraph: + p := t + compWidth += p.margins.left + p.margins.right + compHeight += p.margins.top + p.margins.bottom } // Vertical stacking. diff --git a/pdf/creator/invoice.go b/pdf/creator/invoice.go new file mode 100644 index 00000000..4b41fbca --- /dev/null +++ b/pdf/creator/invoice.go @@ -0,0 +1,509 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +package creator + +import "fmt" + +// InvoiceAddress +type InvoiceAddress struct { + Name string + Street string + Zip string + City string + Country string + Phone string + Email string +} + +// InvoiceCellProps +type InvoiceCellProps struct { + Alignment CellHorizontalAlignment + BackgroundColor Color + BorderColor Color + Style TextStyle +} + +// InvoiceColumn +type InvoiceColumn struct { + InvoiceCellProps + + Description string +} + +// InvoiceCell +type InvoiceCell struct { + InvoiceCellProps + + Value string +} + +// Invoice represents a configurable template for an invoice. +type Invoice struct { + // Invoice styles. + defaultStyle TextStyle + headingStyle TextStyle + titleStyle TextStyle + + infoDescProps InvoiceCellProps + infoValProps InvoiceCellProps + colProps InvoiceCellProps + itemProps InvoiceCellProps + + // The title of the invoice. + title string + + // The logo of the invoice. + logo *Image + + // Buyer address. + buyerAddress InvoiceAddress + + // Seller address. + sellerAddress InvoiceAddress + + // Invoice information. + number string + date string + paymentTerms string + dueDate string + + additionalInfo [][2]string + + // Invoice columns. + columns []*InvoiceColumn + + // Invoice lines. + lines [][]*InvoiceCell + + // Invoice totals. + totals [][2]*InvoiceCell + + // Positioning: relative/absolute. + positioning positioning +} + +// newInvoice returns an instance of an empty invoice. +func newInvoice(defaultStyle, headingStyle TextStyle) *Invoice { + i := &Invoice{ + // Styles. + defaultStyle: defaultStyle, + headingStyle: headingStyle, + + // Title. + title: "INVOICE", + + // Information. + additionalInfo: [][2]string{}, + + // Addresses. + sellerAddress: InvoiceAddress{}, + buyerAddress: InvoiceAddress{}, + } + + // Default style properties. + lightBlue := ColorRGBFrom8bit(217, 240, 250) + lightGrey := ColorRGBFrom8bit(245, 245, 245) + mediumGrey := ColorRGBFrom8bit(155, 155, 155) + + i.titleStyle = headingStyle + i.titleStyle.Color = mediumGrey + i.titleStyle.FontSize = 20 + + i.infoDescProps = i.newCellProps() + i.infoDescProps.BackgroundColor = lightBlue + i.infoDescProps.Style = headingStyle + + i.infoValProps = i.newCellProps() + i.infoValProps.BackgroundColor = lightGrey + + i.colProps = i.newCellProps() + i.colProps.BackgroundColor = lightBlue + i.colProps.Style = headingStyle + + i.itemProps = i.newCellProps() + + // Default item columns. + quantityCol := i.NewColumn("Quantity") + quantityCol.Alignment = CellHorizontalAlignmentRight + + unitPriceCol := i.NewColumn("Unit price") + unitPriceCol.Alignment = CellHorizontalAlignmentRight + + amountCol := i.NewColumn("Amount") + amountCol.Alignment = CellHorizontalAlignmentRight + + i.columns = []*InvoiceColumn{ + i.NewColumn("Description"), + quantityCol, + unitPriceCol, + amountCol, + } + + return i +} + +func (i *Invoice) newCellProps() InvoiceCellProps { + white := ColorRGBFrom8bit(255, 255, 255) + + return InvoiceCellProps{ + Alignment: CellHorizontalAlignmentLeft, + BackgroundColor: white, + BorderColor: white, + Style: i.defaultStyle, + } +} + +func (i *Invoice) Title() string { + return i.title +} + +func (i *Invoice) SetTitle(title string) { + i.title = title +} + +func (i *Invoice) Logo() *Image { + return i.logo +} + +func (i *Invoice) SetLogo(logo *Image) { + i.logo = logo +} + +func (i *Invoice) SetSellerAddress(address InvoiceAddress) { + i.sellerAddress = address +} + +func (i *Invoice) SetBuyerAddress(address InvoiceAddress) { + i.buyerAddress = address +} + +func (i *Invoice) Number() string { + return i.number +} + +func (i *Invoice) SetNumber(number string) { + i.number = number +} + +func (i *Invoice) Date() string { + return i.date +} + +func (i *Invoice) SetDate(date string) { + i.date = date +} + +func (i *Invoice) PaymentTerms() string { + return i.paymentTerms +} + +func (i *Invoice) SetPaymentTerms(paymentTerms string) { + i.paymentTerms = paymentTerms +} + +func (i *Invoice) DueDate() string { + return i.dueDate +} + +func (i *Invoice) SetDueDate(dueDate string) { + i.dueDate = dueDate +} + +func (i *Invoice) AddInvoiceInfo(description, value string) { + i.additionalInfo = append(i.additionalInfo, [2]string{description, value}) +} + +func (i *Invoice) AppendColumn(description string) *InvoiceColumn { + return nil +} + +func (i *Invoice) InsertColumn(description string) *InvoiceColumn { + return nil +} + +func (i *Invoice) Lines() [][]*InvoiceCell { + return i.lines +} + +func (i *Invoice) AddLine(values ...string) { + lenCols := len(i.columns) + + var line []*InvoiceCell + for j, value := range values { + itemCell := i.NewCell(value) + if j < lenCols { + itemCell.Alignment = i.columns[j].Alignment + } + + line = append(line, itemCell) + } + + i.lines = append(i.lines, line) +} + +func (i *Invoice) AddSubtotal(value string) *InvoiceCell { + return nil +} + +func (i *Invoice) AddTotal(value string) *InvoiceCell { + return nil +} + +func (i *Invoice) AddTotalsLine(desc, value string) *InvoiceCell { + return nil +} + +func (i *Invoice) NewCell(value string) *InvoiceCell { + return &InvoiceCell{ + i.itemProps, + value, + } +} + +func (i *Invoice) NewColumn(description string) *InvoiceColumn { + return &InvoiceColumn{ + i.colProps, + description, + } +} + +func (i *Invoice) drawAddress(title, name string, addr *InvoiceAddress) []*StyledParagraph { + var paragraphs []*StyledParagraph + + // Address title. + if title != "" { + titleParagraph := newStyledParagraph(i.headingStyle) + titleParagraph.SetMargins(0, 0, 0, 7) + titleParagraph.Append(title) + + paragraphs = append(paragraphs, titleParagraph) + } + + // Address information. + addressParagraph := newStyledParagraph(i.defaultStyle) + addressParagraph.SetLineHeight(1.2) + + city := addr.City + if addr.Zip != "" { + if city != "" { + city += ", " + } + + city += addr.Zip + } + + if name != "" { + addressParagraph.Append(addr.Name + "\n") + } + if addr.Street != "" { + addressParagraph.Append(addr.Street + "\n") + } + if city != "" { + addressParagraph.Append(city + "\n") + } + if addr.Country != "" { + addressParagraph.Append(addr.Country + "\n") + } + + // Contact information + contactParagraph := newStyledParagraph(i.defaultStyle) + contactParagraph.SetLineHeight(1.2) + contactParagraph.SetMargins(0, 0, 7, 0) + + if addr.Phone != "" { + contactParagraph.Append(fmt.Sprintf("Phone: %s\n", addr.Phone)) + } + if addr.Email != "" { + contactParagraph.Append(fmt.Sprintf("Email: %s\n", addr.Email)) + } + + paragraphs = append(paragraphs, addressParagraph, contactParagraph) + return paragraphs +} + +func (i *Invoice) drawInformation() *Table { + table := newTable(2) + + info := [][2]string{ + [2]string{"Invoice number", i.number}, + [2]string{"Invoice date", i.date}, + [2]string{"Payment terms", i.paymentTerms}, + [2]string{"Due date", i.dueDate}, + } + info = append(info, i.additionalInfo...) + + for _, v := range info { + description, value := v[0], v[1] + if len(value) == 0 { + continue + } + + // Add description. + cell := table.NewCell() + cell.SetBackgroundColor(i.infoDescProps.BackgroundColor) + cell.SetBorder(CellBorderSideAll, CellBorderStyleSingle, 1) + cell.SetBorderColor(i.infoDescProps.BorderColor) + + p := newStyledParagraph(i.infoDescProps.Style) + p.Append(description) + p.SetMargins(0, 0, 2, 0) + cell.SetContent(p) + + // Add value. + cell = table.NewCell() + cell.SetBackgroundColor(i.infoValProps.BackgroundColor) + cell.SetBorder(CellBorderSideAll, CellBorderStyleSingle, 1) + cell.SetBorderColor(i.infoValProps.BorderColor) + + p = newStyledParagraph(i.infoValProps.Style) + p.Append(value) + p.SetMargins(0, 0, 2, 0) + cell.SetContent(p) + } + + return table +} + +func (i *Invoice) generateHeaderBlocks(ctx DrawContext) ([]*Block, DrawContext, error) { + // Create title paragraph. + titleParagraph := newStyledParagraph(i.titleStyle) + titleParagraph.SetEnableWrap(true) + titleParagraph.Append(i.title) + + // Add invoice logo. + table := newTable(2) + + if i.logo != nil { + cell := table.NewCell() + cell.SetHorizontalAlignment(CellHorizontalAlignmentLeft) + cell.SetVerticalAlignment(CellVerticalAlignmentMiddle) + cell.SetContent(i.logo) + + i.logo.ScaleToHeight(titleParagraph.Height() + 20) + } else { + table.SkipCells(1) + } + + // Add invoice title. + cell := table.NewCell() + cell.SetHorizontalAlignment(CellHorizontalAlignmentRight) + cell.SetVerticalAlignment(CellVerticalAlignmentMiddle) + cell.SetContent(titleParagraph) + + // Generate blocks. + return table.GeneratePageBlocks(ctx) +} + +func (i *Invoice) generateInformationBlocks(ctx DrawContext) ([]*Block, DrawContext, error) { + // Draw addresses. + separatorParagraph := newStyledParagraph(i.defaultStyle) + separatorParagraph.SetMargins(0, 0, 0, 20) + + addrParagraphs := i.drawAddress(i.sellerAddress.Name, "", &i.sellerAddress) + addrParagraphs = append(addrParagraphs, separatorParagraph) + addrParagraphs = append(addrParagraphs, + i.drawAddress("Bill to", i.buyerAddress.Name, &i.buyerAddress)...) + + addrDivision := newDivision() + for _, addrParagraph := range addrParagraphs { + addrDivision.Add(addrParagraph) + } + + // Draw invoice information. + information := i.drawInformation() + + // Generate blocks. + table := newTable(2) + table.SetMargins(0, 0, 25, 0) + + cell := table.NewCell() + cell.SetContent(addrDivision) + + cell = table.NewCell() + cell.SetContent(information) + + return table.GeneratePageBlocks(ctx) +} + +func (i *Invoice) generateLineBlocks(ctx DrawContext) ([]*Block, DrawContext, error) { + table := newTable(4) + table.SetMargins(0, 0, 25, 0) + + // Draw item columns + for _, col := range i.columns { + paragraph := newStyledParagraph(col.Style) + paragraph.Append(col.Description) + + cell := table.NewCell() + cell.SetHorizontalAlignment(col.Alignment) + cell.SetBackgroundColor(col.BackgroundColor) + cell.SetBorder(CellBorderSideAll, CellBorderStyleSingle, 1) + cell.SetBorderColor(col.BorderColor) + cell.SetContent(paragraph) + } + + // Draw item lines + for _, line := range i.lines { + for _, itemCell := range line { + paragraph := newStyledParagraph(itemCell.Style) + paragraph.Append(itemCell.Value) + + cell := table.NewCell() + cell.SetHorizontalAlignment(itemCell.Alignment) + cell.SetBackgroundColor(itemCell.BackgroundColor) + cell.SetBorder(CellBorderSideAll, CellBorderStyleSingle, 1) + cell.SetBorderColor(itemCell.BorderColor) + cell.SetContent(paragraph) + } + } + + return table.GeneratePageBlocks(ctx) +} + +// GeneratePageBlocks generate the Page blocks. Multiple blocks are generated +// if the contents wrap over multiple pages. +func (i *Invoice) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, error) { + origCtx := ctx + + // Generate title blocks. + blocks, ctx, err := i.generateHeaderBlocks(ctx) + if err != nil { + return blocks, ctx, err + } + + // Generate address and information blocks. + newBlocks, c, err := i.generateInformationBlocks(ctx) + if err != nil { + return blocks, ctx, err + } + + blocks[len(blocks)-1].mergeBlocks(newBlocks[0]) + blocks = append(blocks, newBlocks[1:]...) + ctx = c + + // Generate line items blocks. + newBlocks, c, err = i.generateLineBlocks(ctx) + if err != nil { + return blocks, ctx, err + } + + blocks[len(blocks)-1].mergeBlocks(newBlocks[0]) + blocks = append(blocks, newBlocks[1:]...) + ctx = c + + if i.positioning.isRelative() { + // Move back X to same start of line. + ctx.X = origCtx.X + } + + if i.positioning.isAbsolute() { + // If absolute: return original context. + return blocks, origCtx, nil + + } + + return blocks, ctx, nil +} From e7a6b1825f2743bd4640c7d4bbb42fcfe8ffab7d Mon Sep 17 00:00:00 2001 From: Adrian-George Bostan Date: Mon, 29 Oct 2018 20:18:32 +0200 Subject: [PATCH 05/13] Add totals and notes sections --- pdf/creator/invoice.go | 242 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 212 insertions(+), 30 deletions(-) diff --git a/pdf/creator/invoice.go b/pdf/creator/invoice.go index 4b41fbca..11777797 100644 --- a/pdf/creator/invoice.go +++ b/pdf/creator/invoice.go @@ -51,6 +51,7 @@ type Invoice struct { infoValProps InvoiceCellProps colProps InvoiceCellProps itemProps InvoiceCellProps + totalProps InvoiceCellProps // The title of the invoice. title string @@ -79,7 +80,13 @@ type Invoice struct { lines [][]*InvoiceCell // Invoice totals. - totals [][2]*InvoiceCell + subtotal *InvoiceCell + total *InvoiceCell + totals [][2]*InvoiceCell + + // Invoice notes. + notes string + terms string // Positioning: relative/absolute. positioning positioning @@ -121,9 +128,23 @@ func newInvoice(defaultStyle, headingStyle TextStyle) *Invoice { i.colProps = i.newCellProps() i.colProps.BackgroundColor = lightBlue + i.colProps.BorderColor = lightBlue i.colProps.Style = headingStyle i.itemProps = i.newCellProps() + i.itemProps.Alignment = CellHorizontalAlignmentRight + + i.totalProps = i.newCellProps() + i.totalProps.Alignment = CellHorizontalAlignmentRight + + i.subtotal = i.NewCell("") + i.subtotal.Alignment = CellHorizontalAlignmentRight + + i.total = i.NewCell("") + i.total.BackgroundColor = lightBlue + i.total.Style = headingStyle + i.total.Alignment = CellHorizontalAlignmentRight + i.total.BorderColor = lightBlue // Default item columns. quantityCol := i.NewColumn("Quantity") @@ -244,16 +265,56 @@ func (i *Invoice) AddLine(values ...string) { i.lines = append(i.lines, line) } -func (i *Invoice) AddSubtotal(value string) *InvoiceCell { - return nil +func (i *Invoice) Subtotal() *InvoiceCell { + return i.subtotal } -func (i *Invoice) AddTotal(value string) *InvoiceCell { - return nil +func (i *Invoice) SetSubtotal(value string) *InvoiceCell { + i.subtotal.Value = value + return i.subtotal } -func (i *Invoice) AddTotalsLine(desc, value string) *InvoiceCell { - return nil +func (i *Invoice) Total() *InvoiceCell { + return i.total +} + +func (i *Invoice) SetTotal(value string) *InvoiceCell { + i.total.Value = value + return i.total +} + +func (i *Invoice) TotalLines() [][2]*InvoiceCell { + return i.totals +} + +func (i *Invoice) AddTotalLine(desc, value string) (*InvoiceCell, *InvoiceCell) { + descCell := &InvoiceCell{ + i.totalProps, + desc, + } + valueCell := &InvoiceCell{ + i.totalProps, + value, + } + + i.totals = append(i.totals, [2]*InvoiceCell{descCell, valueCell}) + return descCell, valueCell +} + +func (i *Invoice) Notes() string { + return i.notes +} + +func (i *Invoice) SetNotes(notes string) { + i.notes = notes +} + +func (i *Invoice) Terms() string { + return i.terms +} + +func (i *Invoice) SetTerms(terms string) { + i.terms = terms } func (i *Invoice) NewCell(value string) *InvoiceCell { @@ -324,6 +385,29 @@ func (i *Invoice) drawAddress(title, name string, addr *InvoiceAddress) []*Style return paragraphs } +func (i *Invoice) drawSection(title, content string) []*StyledParagraph { + var paragraphs []*StyledParagraph + + // Title paragraph. + if title != "" { + titleParagraph := newStyledParagraph(i.headingStyle) + titleParagraph.SetMargins(0, 0, 0, 5) + titleParagraph.Append(title) + + paragraphs = append(paragraphs, titleParagraph) + } + + // Content paragraph. + if content != "" { + contentParagraph := newStyledParagraph(i.defaultStyle) + contentParagraph.Append(content) + + paragraphs = append(paragraphs, contentParagraph) + } + + return paragraphs +} + func (i *Invoice) drawInformation() *Table { table := newTable(2) @@ -367,6 +451,57 @@ func (i *Invoice) drawInformation() *Table { return table } +func (i *Invoice) drawTotals() *Table { + table := newTable(2) + + totals := [][2]*InvoiceCell{} + if i.subtotal.Value != "" { + subtotalDesc := *i.subtotal + subtotalDesc.Value = "Subtotal" + + totals = append(totals, [2]*InvoiceCell{&subtotalDesc, i.subtotal}) + } + + totals = append(totals, i.totals...) + + if i.total.Value != "" { + totalDesc := *i.total + totalDesc.Value = "Total" + + totals = append(totals, [2]*InvoiceCell{&totalDesc, i.total}) + } + + for _, total := range totals { + description, value := total[0], total[1] + + // Add description. + cell := table.NewCell() + cell.SetBackgroundColor(description.BackgroundColor) + cell.SetHorizontalAlignment(value.Alignment) + cell.SetBorder(CellBorderSideAll, CellBorderStyleSingle, 1) + cell.SetBorderColor(value.BorderColor) + + p := newStyledParagraph(description.Style) + p.SetMargins(0, 0, 1, 1) + p.Append(description.Value) + cell.SetContent(p) + + // Add value. + cell = table.NewCell() + cell.SetBackgroundColor(value.BackgroundColor) + cell.SetHorizontalAlignment(value.Alignment) + cell.SetBorder(CellBorderSideAll, CellBorderStyleSingle, 1) + cell.SetBorderColor(value.BorderColor) + + p = newStyledParagraph(value.Style) + p.SetMargins(0, 0, 1, 1) + p.Append(value.Value) + cell.SetContent(p) + } + + return table +} + func (i *Invoice) generateHeaderBlocks(ctx DrawContext) ([]*Block, DrawContext, error) { // Create title paragraph. titleParagraph := newStyledParagraph(i.titleStyle) @@ -435,6 +570,7 @@ func (i *Invoice) generateLineBlocks(ctx DrawContext) ([]*Block, DrawContext, er // Draw item columns for _, col := range i.columns { paragraph := newStyledParagraph(col.Style) + paragraph.SetMargins(0, 0, 1, 0) paragraph.Append(col.Description) cell := table.NewCell() @@ -449,6 +585,7 @@ func (i *Invoice) generateLineBlocks(ctx DrawContext) ([]*Block, DrawContext, er for _, line := range i.lines { for _, itemCell := range line { paragraph := newStyledParagraph(itemCell.Style) + paragraph.SetMargins(0, 0, 1, 0) paragraph.Append(itemCell.Value) cell := table.NewCell() @@ -463,37 +600,83 @@ func (i *Invoice) generateLineBlocks(ctx DrawContext) ([]*Block, DrawContext, er return table.GeneratePageBlocks(ctx) } +func (i *Invoice) generateTotalBlocks(ctx DrawContext) ([]*Block, DrawContext, error) { + table := newTable(2) + table.SetMargins(0, 0, 5, 35) + + mediumGrey := ColorRGBFrom8bit(195, 195, 195) + + cell := table.NewCell() + cell.SetBorder(CellBorderSideTop, CellBorderStyleSingle, 1) + cell.SetBorderColor(mediumGrey) + + if i.notes != "" { + noteParagraphs := i.drawSection("Notes", i.notes) + + noteDivision := newDivision() + for _, noteParagraph := range noteParagraphs { + noteParagraph.SetMargins(0, 0, 5, 0) + noteDivision.Add(noteParagraph) + } + + cell.SetContent(noteDivision) + } + + totalsTable := i.drawTotals() + totalsTable.SetMargins(0, 0, 5, 0) + + cell = table.NewCell() + cell.SetBorder(CellBorderSideTop, CellBorderStyleSingle, 1) + cell.SetBorderColor(mediumGrey) + cell.SetContent(totalsTable) + + return table.GeneratePageBlocks(ctx) +} + +func (i *Invoice) generateNoteBlocks(ctx DrawContext) ([]*Block, DrawContext, error) { + division := newDivision() + + if i.terms != "" { + termParagraphs := i.drawSection("Terms and conditions", i.terms) + for _, termParagraph := range termParagraphs { + termParagraph.SetMargins(0, 0, 5, 0) + division.Add(termParagraph) + } + } + + return division.GeneratePageBlocks(ctx) +} + // GeneratePageBlocks generate the Page blocks. Multiple blocks are generated // if the contents wrap over multiple pages. func (i *Invoice) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, error) { origCtx := ctx - // Generate title blocks. - blocks, ctx, err := i.generateHeaderBlocks(ctx) - if err != nil { - return blocks, ctx, err + blockFuncs := []func(ctx DrawContext) ([]*Block, DrawContext, error){ + i.generateHeaderBlocks, + i.generateInformationBlocks, + i.generateLineBlocks, + i.generateTotalBlocks, + i.generateNoteBlocks, } - // Generate address and information blocks. - newBlocks, c, err := i.generateInformationBlocks(ctx) - if err != nil { - return blocks, ctx, err + var blocks []*Block + for _, blockFunc := range blockFuncs { + newBlocks, c, err := blockFunc(ctx) + if err != nil { + return blocks, ctx, err + } + + if len(blocks) == 0 { + blocks = newBlocks + } else if len(newBlocks) > 0 { + blocks[len(blocks)-1].mergeBlocks(newBlocks[0]) + blocks = append(blocks, newBlocks[1:]...) + } + + ctx = c } - blocks[len(blocks)-1].mergeBlocks(newBlocks[0]) - blocks = append(blocks, newBlocks[1:]...) - ctx = c - - // Generate line items blocks. - newBlocks, c, err = i.generateLineBlocks(ctx) - if err != nil { - return blocks, ctx, err - } - - blocks[len(blocks)-1].mergeBlocks(newBlocks[0]) - blocks = append(blocks, newBlocks[1:]...) - ctx = c - if i.positioning.isRelative() { // Move back X to same start of line. ctx.X = origCtx.X @@ -502,7 +685,6 @@ func (i *Invoice) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, er if i.positioning.isAbsolute() { // If absolute: return original context. return blocks, origCtx, nil - } return blocks, ctx, nil From 7a6b16b614b4dd06468f952cf23aa83dc772615b Mon Sep 17 00:00:00 2001 From: Adrian-George Bostan Date: Tue, 30 Oct 2018 20:43:10 +0200 Subject: [PATCH 06/13] Make invoice more customizable --- pdf/creator/invoice.go | 462 +++++++++++++++++++++-------------------- 1 file changed, 239 insertions(+), 223 deletions(-) diff --git a/pdf/creator/invoice.go b/pdf/creator/invoice.go index 11777797..ce8330e9 100644 --- a/pdf/creator/invoice.go +++ b/pdf/creator/invoice.go @@ -7,7 +7,7 @@ package creator import "fmt" -// InvoiceAddress +// InvoiceAddress. type InvoiceAddress struct { Name string Street string @@ -18,75 +18,65 @@ type InvoiceAddress struct { Email string } -// InvoiceCellProps +// InvoiceCellProps. type InvoiceCellProps struct { + TextStyle TextStyle Alignment CellHorizontalAlignment BackgroundColor Color - BorderColor Color - Style TextStyle + + BorderColor Color + BorderWidth int + BorderStyle int } -// InvoiceColumn -type InvoiceColumn struct { - InvoiceCellProps - - Description string -} - -// InvoiceCell +// InvoiceCell. type InvoiceCell struct { InvoiceCellProps Value string } -// Invoice represents a configurable template for an invoice. +// Invoice represents a configurable invoice template. type Invoice struct { + // Invoice title. + title string + + // Invoice logo. + logo *Image + + // Invoice addresses. + buyerAddress *InvoiceAddress + sellerAddress *InvoiceAddress + + // Invoice information. + number [2]*InvoiceCell + date [2]*InvoiceCell + dueDate [2]*InvoiceCell + info [][2]*InvoiceCell + + // Invoice lines. + columns []*InvoiceCell + lines [][]*InvoiceCell + + // Invoice totals. + subtotal [2]*InvoiceCell + total [2]*InvoiceCell + totals [][2]*InvoiceCell + + // Invoice note sections. + notes [2]string + terms [2]string + sections [][2]string + // Invoice styles. defaultStyle TextStyle headingStyle TextStyle titleStyle TextStyle - infoDescProps InvoiceCellProps - infoValProps InvoiceCellProps - colProps InvoiceCellProps - itemProps InvoiceCellProps - totalProps InvoiceCellProps - - // The title of the invoice. - title string - - // The logo of the invoice. - logo *Image - - // Buyer address. - buyerAddress InvoiceAddress - - // Seller address. - sellerAddress InvoiceAddress - - // Invoice information. - number string - date string - paymentTerms string - dueDate string - - additionalInfo [][2]string - - // Invoice columns. - columns []*InvoiceColumn - - // Invoice lines. - lines [][]*InvoiceCell - - // Invoice totals. - subtotal *InvoiceCell - total *InvoiceCell - totals [][2]*InvoiceCell - - // Invoice notes. - notes string - terms string + infoProps InvoiceCellProps + colProps InvoiceCellProps + itemProps InvoiceCellProps + totalProps InvoiceCellProps // Positioning: relative/absolute. positioning positioning @@ -95,88 +85,92 @@ type Invoice struct { // newInvoice returns an instance of an empty invoice. func newInvoice(defaultStyle, headingStyle TextStyle) *Invoice { i := &Invoice{ - // Styles. - defaultStyle: defaultStyle, - headingStyle: headingStyle, - // Title. title: "INVOICE", - // Information. - additionalInfo: [][2]string{}, - // Addresses. - sellerAddress: InvoiceAddress{}, - buyerAddress: InvoiceAddress{}, + sellerAddress: &InvoiceAddress{}, + buyerAddress: &InvoiceAddress{}, + + // Styles. + defaultStyle: defaultStyle, + headingStyle: headingStyle, } - // Default style properties. - lightBlue := ColorRGBFrom8bit(217, 240, 250) + // Default colors. lightGrey := ColorRGBFrom8bit(245, 245, 245) mediumGrey := ColorRGBFrom8bit(155, 155, 155) + // Default title style. i.titleStyle = headingStyle i.titleStyle.Color = mediumGrey i.titleStyle.FontSize = 20 - i.infoDescProps = i.newCellProps() - i.infoDescProps.BackgroundColor = lightBlue - i.infoDescProps.Style = headingStyle + // Invoice information default properties. + i.infoProps = i.NewCellProps() + i.infoProps.BackgroundColor = lightGrey + i.infoProps.TextStyle = headingStyle - i.infoValProps = i.newCellProps() - i.infoValProps.BackgroundColor = lightGrey + // Invoice line items default properties. + i.colProps = i.NewCellProps() + i.colProps.TextStyle = headingStyle + i.colProps.BackgroundColor = lightGrey + i.colProps.BorderColor = lightGrey - i.colProps = i.newCellProps() - i.colProps.BackgroundColor = lightBlue - i.colProps.BorderColor = lightBlue - i.colProps.Style = headingStyle - - i.itemProps = i.newCellProps() + i.itemProps = i.NewCellProps() i.itemProps.Alignment = CellHorizontalAlignmentRight - i.totalProps = i.newCellProps() + // Invoice totals default properties. + i.totalProps = i.NewCellProps() i.totalProps.Alignment = CellHorizontalAlignmentRight - i.subtotal = i.NewCell("") - i.subtotal.Alignment = CellHorizontalAlignmentRight + // Invoice information fields. + i.number = [2]*InvoiceCell{ + i.newCell("Invoice number", i.infoProps), + i.newCell("", i.infoProps), + } - i.total = i.NewCell("") - i.total.BackgroundColor = lightBlue - i.total.Style = headingStyle - i.total.Alignment = CellHorizontalAlignmentRight - i.total.BorderColor = lightBlue + i.date = [2]*InvoiceCell{ + i.newCell("Date", i.infoProps), + i.newCell("", i.infoProps), + } + + i.dueDate = [2]*InvoiceCell{ + i.newCell("Date", i.infoProps), + i.newCell("", i.infoProps), + } + + // Invoice totals fields. + i.subtotal = [2]*InvoiceCell{ + i.newCell("Subtotal", i.totalProps), + i.newCell("", i.totalProps), + } + + totalProps := i.totalProps + totalProps.TextStyle = headingStyle + totalProps.BackgroundColor = lightGrey + totalProps.BorderColor = lightGrey + + i.total = [2]*InvoiceCell{ + i.newCell("Total", totalProps), + i.newCell("", totalProps), + } + + // Invoice notes fields. + i.notes = [2]string{"Notes", ""} + i.terms = [2]string{"Terms and conditions", ""} // Default item columns. - quantityCol := i.NewColumn("Quantity") - quantityCol.Alignment = CellHorizontalAlignmentRight - - unitPriceCol := i.NewColumn("Unit price") - unitPriceCol.Alignment = CellHorizontalAlignmentRight - - amountCol := i.NewColumn("Amount") - amountCol.Alignment = CellHorizontalAlignmentRight - - i.columns = []*InvoiceColumn{ - i.NewColumn("Description"), - quantityCol, - unitPriceCol, - amountCol, + i.columns = []*InvoiceCell{ + i.newColumn("Description", CellHorizontalAlignmentLeft), + i.newColumn("Quantity", CellHorizontalAlignmentRight), + i.newColumn("Unit price", CellHorizontalAlignmentRight), + i.newColumn("Amount", CellHorizontalAlignmentRight), } return i } -func (i *Invoice) newCellProps() InvoiceCellProps { - white := ColorRGBFrom8bit(255, 255, 255) - - return InvoiceCellProps{ - Alignment: CellHorizontalAlignmentLeft, - BackgroundColor: white, - BorderColor: white, - Style: i.defaultStyle, - } -} - func (i *Invoice) Title() string { return i.title } @@ -193,55 +187,61 @@ func (i *Invoice) SetLogo(logo *Image) { i.logo = logo } -func (i *Invoice) SetSellerAddress(address InvoiceAddress) { +func (i *Invoice) SellerAddress() *InvoiceAddress { + return i.sellerAddress +} + +func (i *Invoice) SetSellerAddress(address *InvoiceAddress) { i.sellerAddress = address } -func (i *Invoice) SetBuyerAddress(address InvoiceAddress) { +func (i *Invoice) BuyerAddress() *InvoiceAddress { + return i.buyerAddress +} + +func (i *Invoice) SetBuyerAddress(address *InvoiceAddress) { i.buyerAddress = address } -func (i *Invoice) Number() string { - return i.number +func (i *Invoice) Number() (*InvoiceCell, *InvoiceCell) { + return i.number[0], i.number[1] } func (i *Invoice) SetNumber(number string) { - i.number = number + i.number[1].Value = number } -func (i *Invoice) Date() string { - return i.date +func (i *Invoice) Date() (*InvoiceCell, *InvoiceCell) { + return i.date[0], i.date[1] } func (i *Invoice) SetDate(date string) { - i.date = date + i.date[1].Value = date } -func (i *Invoice) PaymentTerms() string { - return i.paymentTerms -} - -func (i *Invoice) SetPaymentTerms(paymentTerms string) { - i.paymentTerms = paymentTerms -} - -func (i *Invoice) DueDate() string { - return i.dueDate +func (i *Invoice) DueDate() (*InvoiceCell, *InvoiceCell) { + return i.dueDate[0], i.dueDate[1] } func (i *Invoice) SetDueDate(dueDate string) { - i.dueDate = dueDate + i.dueDate[1].Value = dueDate } -func (i *Invoice) AddInvoiceInfo(description, value string) { - i.additionalInfo = append(i.additionalInfo, [2]string{description, value}) +func (i *Invoice) AddInvoiceInfo(description, value string) (*InvoiceCell, *InvoiceCell) { + info := [2]*InvoiceCell{ + i.newCell(description, i.infoProps), + i.newCell(value, i.infoProps), + } + + i.info = append(i.info, info) + return info[0], info[1] } -func (i *Invoice) AppendColumn(description string) *InvoiceColumn { +func (i *Invoice) AppendColumn(description string) *InvoiceCell { return nil } -func (i *Invoice) InsertColumn(description string) *InvoiceColumn { +func (i *Invoice) InsertColumn(description string) *InvoiceCell { return nil } @@ -254,7 +254,7 @@ func (i *Invoice) AddLine(values ...string) { var line []*InvoiceCell for j, value := range values { - itemCell := i.NewCell(value) + itemCell := i.newCell(value, i.itemProps) if j < lenCols { itemCell.Alignment = i.columns[j].Alignment } @@ -265,22 +265,20 @@ func (i *Invoice) AddLine(values ...string) { i.lines = append(i.lines, line) } -func (i *Invoice) Subtotal() *InvoiceCell { - return i.subtotal +func (i *Invoice) Subtotal() (*InvoiceCell, *InvoiceCell) { + return i.subtotal[0], i.subtotal[1] } -func (i *Invoice) SetSubtotal(value string) *InvoiceCell { - i.subtotal.Value = value - return i.subtotal +func (i *Invoice) SetSubtotal(value string) { + i.subtotal[1].Value = value } -func (i *Invoice) Total() *InvoiceCell { - return i.total +func (i *Invoice) Total() (*InvoiceCell, *InvoiceCell) { + return i.total[0], i.total[1] } -func (i *Invoice) SetTotal(value string) *InvoiceCell { - i.total.Value = value - return i.total +func (i *Invoice) SetTotal(value string) { + i.total[1].Value = value } func (i *Invoice) TotalLines() [][2]*InvoiceCell { @@ -301,34 +299,72 @@ func (i *Invoice) AddTotalLine(desc, value string) (*InvoiceCell, *InvoiceCell) return descCell, valueCell } -func (i *Invoice) Notes() string { - return i.notes +func (i *Invoice) Notes() (string, string) { + return i.notes[0], i.notes[1] } -func (i *Invoice) SetNotes(notes string) { - i.notes = notes +func (i *Invoice) SetNotes(title, content string) { + i.notes = [2]string{ + title, + content, + } } -func (i *Invoice) Terms() string { - return i.terms +func (i *Invoice) Terms() (string, string) { + return i.terms[0], i.terms[1] } -func (i *Invoice) SetTerms(terms string) { - i.terms = terms +func (i *Invoice) SetTerms(title, content string) { + i.terms = [2]string{ + title, + content, + } +} + +func (i *Invoice) Sections() [][2]string { + return i.sections +} + +func (i *Invoice) AddSection(title, content string) { + i.sections = append(i.sections, [2]string{ + title, + content, + }) +} + +func (i *Invoice) NewCellProps() InvoiceCellProps { + white := ColorRGBFrom8bit(255, 255, 255) + + return InvoiceCellProps{ + TextStyle: i.defaultStyle, + Alignment: CellHorizontalAlignmentLeft, + BackgroundColor: white, + BorderColor: white, + BorderWidth: 1, + BorderStyle: int(CellBorderSideAll), + } } func (i *Invoice) NewCell(value string) *InvoiceCell { + return i.newCell(value, i.NewCellProps()) +} + +func (i *Invoice) newCell(value string, props InvoiceCellProps) *InvoiceCell { return &InvoiceCell{ - i.itemProps, + props, value, } } -func (i *Invoice) NewColumn(description string) *InvoiceColumn { - return &InvoiceColumn{ - i.colProps, - description, - } +func (i *Invoice) NewColumn(description string) *InvoiceCell { + return i.newColumn(description, CellHorizontalAlignmentLeft) +} + +func (i *Invoice) newColumn(description string, alignment CellHorizontalAlignment) *InvoiceCell { + col := &InvoiceCell{i.colProps, description} + col.Alignment = alignment + + return col } func (i *Invoice) drawAddress(title, name string, addr *InvoiceAddress) []*StyledParagraph { @@ -369,7 +405,7 @@ func (i *Invoice) drawAddress(title, name string, addr *InvoiceAddress) []*Style addressParagraph.Append(addr.Country + "\n") } - // Contact information + // Contact information. contactParagraph := newStyledParagraph(i.defaultStyle) contactParagraph.SetLineHeight(1.2) contactParagraph.SetMargins(0, 0, 7, 0) @@ -411,39 +447,37 @@ func (i *Invoice) drawSection(title, content string) []*StyledParagraph { func (i *Invoice) drawInformation() *Table { table := newTable(2) - info := [][2]string{ - [2]string{"Invoice number", i.number}, - [2]string{"Invoice date", i.date}, - [2]string{"Payment terms", i.paymentTerms}, - [2]string{"Due date", i.dueDate}, - } - info = append(info, i.additionalInfo...) + info := append([][2]*InvoiceCell{ + i.number, + i.date, + i.dueDate, + }, i.info...) for _, v := range info { description, value := v[0], v[1] - if len(value) == 0 { + if value.Value == "" { continue } // Add description. cell := table.NewCell() - cell.SetBackgroundColor(i.infoDescProps.BackgroundColor) + cell.SetBackgroundColor(description.BackgroundColor) cell.SetBorder(CellBorderSideAll, CellBorderStyleSingle, 1) - cell.SetBorderColor(i.infoDescProps.BorderColor) + cell.SetBorderColor(description.BorderColor) - p := newStyledParagraph(i.infoDescProps.Style) - p.Append(description) + p := newStyledParagraph(description.TextStyle) + p.Append(description.Value) p.SetMargins(0, 0, 2, 0) cell.SetContent(p) // Add value. cell = table.NewCell() - cell.SetBackgroundColor(i.infoValProps.BackgroundColor) + cell.SetBackgroundColor(value.BackgroundColor) cell.SetBorder(CellBorderSideAll, CellBorderStyleSingle, 1) - cell.SetBorderColor(i.infoValProps.BorderColor) + cell.SetBorderColor(value.BorderColor) - p = newStyledParagraph(i.infoValProps.Style) - p.Append(value) + p = newStyledParagraph(value.TextStyle) + p.Append(value.Value) p.SetMargins(0, 0, 2, 0) cell.SetContent(p) } @@ -454,25 +488,15 @@ func (i *Invoice) drawInformation() *Table { func (i *Invoice) drawTotals() *Table { table := newTable(2) - totals := [][2]*InvoiceCell{} - if i.subtotal.Value != "" { - subtotalDesc := *i.subtotal - subtotalDesc.Value = "Subtotal" - - totals = append(totals, [2]*InvoiceCell{&subtotalDesc, i.subtotal}) - } - + totals := [][2]*InvoiceCell{i.subtotal} totals = append(totals, i.totals...) - - if i.total.Value != "" { - totalDesc := *i.total - totalDesc.Value = "Total" - - totals = append(totals, [2]*InvoiceCell{&totalDesc, i.total}) - } + totals = append(totals, i.total) for _, total := range totals { description, value := total[0], total[1] + if value.Value == "" { + continue + } // Add description. cell := table.NewCell() @@ -481,7 +505,7 @@ func (i *Invoice) drawTotals() *Table { cell.SetBorder(CellBorderSideAll, CellBorderStyleSingle, 1) cell.SetBorderColor(value.BorderColor) - p := newStyledParagraph(description.Style) + p := newStyledParagraph(description.TextStyle) p.SetMargins(0, 0, 1, 1) p.Append(description.Value) cell.SetContent(p) @@ -493,7 +517,7 @@ func (i *Invoice) drawTotals() *Table { cell.SetBorder(CellBorderSideAll, CellBorderStyleSingle, 1) cell.SetBorderColor(value.BorderColor) - p = newStyledParagraph(value.Style) + p = newStyledParagraph(value.TextStyle) p.SetMargins(0, 0, 1, 1) p.Append(value.Value) cell.SetContent(p) @@ -537,10 +561,10 @@ func (i *Invoice) generateInformationBlocks(ctx DrawContext) ([]*Block, DrawCont separatorParagraph := newStyledParagraph(i.defaultStyle) separatorParagraph.SetMargins(0, 0, 0, 20) - addrParagraphs := i.drawAddress(i.sellerAddress.Name, "", &i.sellerAddress) + addrParagraphs := i.drawAddress(i.sellerAddress.Name, "", i.sellerAddress) addrParagraphs = append(addrParagraphs, separatorParagraph) addrParagraphs = append(addrParagraphs, - i.drawAddress("Bill to", i.buyerAddress.Name, &i.buyerAddress)...) + i.drawAddress("Bill to", i.buyerAddress.Name, i.buyerAddress)...) addrDivision := newDivision() for _, addrParagraph := range addrParagraphs { @@ -567,11 +591,11 @@ func (i *Invoice) generateLineBlocks(ctx DrawContext) ([]*Block, DrawContext, er table := newTable(4) table.SetMargins(0, 0, 25, 0) - // Draw item columns + // Draw item columns. for _, col := range i.columns { - paragraph := newStyledParagraph(col.Style) + paragraph := newStyledParagraph(col.TextStyle) paragraph.SetMargins(0, 0, 1, 0) - paragraph.Append(col.Description) + paragraph.Append(col.Value) cell := table.NewCell() cell.SetHorizontalAlignment(col.Alignment) @@ -581,10 +605,10 @@ func (i *Invoice) generateLineBlocks(ctx DrawContext) ([]*Block, DrawContext, er cell.SetContent(paragraph) } - // Draw item lines + // Draw item lines. for _, line := range i.lines { for _, itemCell := range line { - paragraph := newStyledParagraph(itemCell.Style) + paragraph := newStyledParagraph(itemCell.TextStyle) paragraph.SetMargins(0, 0, 1, 0) paragraph.Append(itemCell.Value) @@ -603,31 +627,12 @@ func (i *Invoice) generateLineBlocks(ctx DrawContext) ([]*Block, DrawContext, er func (i *Invoice) generateTotalBlocks(ctx DrawContext) ([]*Block, DrawContext, error) { table := newTable(2) table.SetMargins(0, 0, 5, 35) - - mediumGrey := ColorRGBFrom8bit(195, 195, 195) - - cell := table.NewCell() - cell.SetBorder(CellBorderSideTop, CellBorderStyleSingle, 1) - cell.SetBorderColor(mediumGrey) - - if i.notes != "" { - noteParagraphs := i.drawSection("Notes", i.notes) - - noteDivision := newDivision() - for _, noteParagraph := range noteParagraphs { - noteParagraph.SetMargins(0, 0, 5, 0) - noteDivision.Add(noteParagraph) - } - - cell.SetContent(noteDivision) - } + table.SkipCells(1) totalsTable := i.drawTotals() totalsTable.SetMargins(0, 0, 5, 0) - cell = table.NewCell() - cell.SetBorder(CellBorderSideTop, CellBorderStyleSingle, 1) - cell.SetBorderColor(mediumGrey) + cell := table.NewCell() cell.SetContent(totalsTable) return table.GeneratePageBlocks(ctx) @@ -636,11 +641,22 @@ func (i *Invoice) generateTotalBlocks(ctx DrawContext) ([]*Block, DrawContext, e func (i *Invoice) generateNoteBlocks(ctx DrawContext) ([]*Block, DrawContext, error) { division := newDivision() - if i.terms != "" { - termParagraphs := i.drawSection("Terms and conditions", i.terms) - for _, termParagraph := range termParagraphs { - termParagraph.SetMargins(0, 0, 5, 0) - division.Add(termParagraph) + sections := [][2]string{ + i.notes, + i.terms, + } + sections = append(sections, i.sections...) + + for _, section := range sections { + if section[1] != "" { + paragraphs := i.drawSection(section[0], section[1]) + for _, paragraph := range paragraphs { + division.Add(paragraph) + } + + sepParagraph := newStyledParagraph(i.defaultStyle) + sepParagraph.SetMargins(0, 0, 20, 0) + division.Add(sepParagraph) } } From 564877a23b630cbe588a44222feb9a0a59b332ed Mon Sep 17 00:00:00 2001 From: Adrian-George Bostan Date: Tue, 30 Oct 2018 20:51:27 +0200 Subject: [PATCH 07/13] Add invoice test --- pdf/creator/invoice_test.go | 71 +++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 pdf/creator/invoice_test.go diff --git a/pdf/creator/invoice_test.go b/pdf/creator/invoice_test.go new file mode 100644 index 00000000..bbcf294e --- /dev/null +++ b/pdf/creator/invoice_test.go @@ -0,0 +1,71 @@ +package creator + +import ( + "fmt" + "testing" +) + +func TestInvoiceSimple(t *testing.T) { + c := New() + c.NewPage() + + logo, err := c.NewImageFromFile(testImageFile1) + if err != nil { + t.Errorf("Fail: %v\n", err) + } + + invoice := c.NewInvoice() + + invoice.SetLogo(logo) + invoice.SetNumber("0001") + invoice.SetDate("28/07/2016") + invoice.SetDueDate("28/07/2016") + invoice.AddInvoiceInfo("Payment terms", "Due on receipt") + invoice.AddInvoiceInfo("Paid", "No") + + invoice.SetSellerAddress(&InvoiceAddress{ + Name: "John Doe", + Street: "8 Elm Street", + City: "Cambridge", + Zip: "CB14DH", + Country: "United Kingdom", + Phone: "xxx-xxx-xxxx", + Email: "johndoe@email.com", + }) + + invoice.SetBuyerAddress(&InvoiceAddress{ + Name: "Jane Doe", + Street: "9 Elm Street", + City: "London", + Zip: "LB15FH", + Country: "United Kingdom", + Phone: "xxx-xxx-xxxx", + Email: "janedoe@email.com", + }) + + for i := 0; i < 10; i++ { + invoice.AddLine( + fmt.Sprintf("Test product #%d", i+1), + "1", + "$10", + "$10", + ) + } + + invoice.SetSubtotal("$100.00") + invoice.AddTotalLine("Tax (10%)", "$10.00") + invoice.AddTotalLine("Shipping", "$5.00") + invoice.SetTotal("$115.00") + invoice.SetNotes("Notes", "Thank you for your business") + invoice.SetTerms("Terms and conditions", "Full refund for 60 days after purchase.") + + if err = c.Draw(invoice); err != nil { + t.Fatalf("Error drawing: %v", err) + } + + // Write output file. + err = c.WriteToFile("/tmp/invoice_simple.pdf") + if err != nil { + t.Fatalf("Fail: %v\n", err) + } +} From 389083bd9f4623a01e1ee9abde662752c7f587cd Mon Sep 17 00:00:00 2001 From: Adrian-George Bostan Date: Tue, 30 Oct 2018 21:27:08 +0200 Subject: [PATCH 08/13] Improve invoice component style --- pdf/creator/invoice.go | 46 +++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/pdf/creator/invoice.go b/pdf/creator/invoice.go index ce8330e9..52e46f49 100644 --- a/pdf/creator/invoice.go +++ b/pdf/creator/invoice.go @@ -25,8 +25,8 @@ type InvoiceCellProps struct { BackgroundColor Color BorderColor Color - BorderWidth int - BorderStyle int + BorderWidth float64 + BorderSides []CellBorderSide } // InvoiceCell. @@ -118,6 +118,8 @@ func newInvoice(defaultStyle, headingStyle TextStyle) *Invoice { i.colProps.BorderColor = lightGrey i.itemProps = i.NewCellProps() + i.itemProps.BorderColor = lightGrey + i.itemProps.BorderSides = []CellBorderSide{CellBorderSideBottom} i.itemProps.Alignment = CellHorizontalAlignmentRight // Invoice totals default properties. @@ -341,7 +343,7 @@ func (i *Invoice) NewCellProps() InvoiceCellProps { BackgroundColor: white, BorderColor: white, BorderWidth: 1, - BorderStyle: int(CellBorderSideAll), + BorderSides: []CellBorderSide{CellBorderSideAll}, } } @@ -367,6 +369,14 @@ func (i *Invoice) newColumn(description string, alignment CellHorizontalAlignmen return col } +func (i *Invoice) setCellBorder(cell *TableCell, invoiceCell *InvoiceCell) { + for _, side := range invoiceCell.BorderSides { + cell.SetBorder(side, CellBorderStyleSingle, invoiceCell.BorderWidth) + } + + cell.SetBorderColor(invoiceCell.BorderColor) +} + func (i *Invoice) drawAddress(title, name string, addr *InvoiceAddress) []*StyledParagraph { var paragraphs []*StyledParagraph @@ -462,23 +472,21 @@ func (i *Invoice) drawInformation() *Table { // Add description. cell := table.NewCell() cell.SetBackgroundColor(description.BackgroundColor) - cell.SetBorder(CellBorderSideAll, CellBorderStyleSingle, 1) - cell.SetBorderColor(description.BorderColor) + i.setCellBorder(cell, description) p := newStyledParagraph(description.TextStyle) p.Append(description.Value) - p.SetMargins(0, 0, 2, 0) + p.SetMargins(0, 0, 2, 1) cell.SetContent(p) // Add value. cell = table.NewCell() cell.SetBackgroundColor(value.BackgroundColor) - cell.SetBorder(CellBorderSideAll, CellBorderStyleSingle, 1) - cell.SetBorderColor(value.BorderColor) + i.setCellBorder(cell, value) p = newStyledParagraph(value.TextStyle) p.Append(value.Value) - p.SetMargins(0, 0, 2, 0) + p.SetMargins(0, 0, 2, 1) cell.SetContent(p) } @@ -502,11 +510,10 @@ func (i *Invoice) drawTotals() *Table { cell := table.NewCell() cell.SetBackgroundColor(description.BackgroundColor) cell.SetHorizontalAlignment(value.Alignment) - cell.SetBorder(CellBorderSideAll, CellBorderStyleSingle, 1) - cell.SetBorderColor(value.BorderColor) + i.setCellBorder(cell, description) p := newStyledParagraph(description.TextStyle) - p.SetMargins(0, 0, 1, 1) + p.SetMargins(0, 0, 2, 1) p.Append(description.Value) cell.SetContent(p) @@ -514,11 +521,10 @@ func (i *Invoice) drawTotals() *Table { cell = table.NewCell() cell.SetBackgroundColor(value.BackgroundColor) cell.SetHorizontalAlignment(value.Alignment) - cell.SetBorder(CellBorderSideAll, CellBorderStyleSingle, 1) - cell.SetBorderColor(value.BorderColor) + i.setCellBorder(cell, description) p = newStyledParagraph(value.TextStyle) - p.SetMargins(0, 0, 1, 1) + p.SetMargins(0, 0, 2, 1) p.Append(value.Value) cell.SetContent(p) } @@ -600,8 +606,7 @@ func (i *Invoice) generateLineBlocks(ctx DrawContext) ([]*Block, DrawContext, er cell := table.NewCell() cell.SetHorizontalAlignment(col.Alignment) cell.SetBackgroundColor(col.BackgroundColor) - cell.SetBorder(CellBorderSideAll, CellBorderStyleSingle, 1) - cell.SetBorderColor(col.BorderColor) + i.setCellBorder(cell, col) cell.SetContent(paragraph) } @@ -609,14 +614,13 @@ func (i *Invoice) generateLineBlocks(ctx DrawContext) ([]*Block, DrawContext, er for _, line := range i.lines { for _, itemCell := range line { paragraph := newStyledParagraph(itemCell.TextStyle) - paragraph.SetMargins(0, 0, 1, 0) + paragraph.SetMargins(0, 0, 3, 2) paragraph.Append(itemCell.Value) cell := table.NewCell() cell.SetHorizontalAlignment(itemCell.Alignment) cell.SetBackgroundColor(itemCell.BackgroundColor) - cell.SetBorder(CellBorderSideAll, CellBorderStyleSingle, 1) - cell.SetBorderColor(itemCell.BorderColor) + i.setCellBorder(cell, itemCell) cell.SetContent(paragraph) } } @@ -626,7 +630,7 @@ func (i *Invoice) generateLineBlocks(ctx DrawContext) ([]*Block, DrawContext, er func (i *Invoice) generateTotalBlocks(ctx DrawContext) ([]*Block, DrawContext, error) { table := newTable(2) - table.SetMargins(0, 0, 5, 35) + table.SetMargins(0, 0, 5, 40) table.SkipCells(1) totalsTable := i.drawTotals() From 74e89e6a2a8b016e6da12ac40a3d3e44cfdbcd02 Mon Sep 17 00:00:00 2001 From: Adrian-George Bostan Date: Tue, 30 Oct 2018 22:13:47 +0200 Subject: [PATCH 09/13] Improve invoice component interface --- pdf/creator/invoice.go | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/pdf/creator/invoice.go b/pdf/creator/invoice.go index 52e46f49..56f81645 100644 --- a/pdf/creator/invoice.go +++ b/pdf/creator/invoice.go @@ -209,24 +209,27 @@ func (i *Invoice) Number() (*InvoiceCell, *InvoiceCell) { return i.number[0], i.number[1] } -func (i *Invoice) SetNumber(number string) { +func (i *Invoice) SetNumber(number string) (*InvoiceCell, *InvoiceCell) { i.number[1].Value = number + return i.number[0], i.number[1] } func (i *Invoice) Date() (*InvoiceCell, *InvoiceCell) { return i.date[0], i.date[1] } -func (i *Invoice) SetDate(date string) { +func (i *Invoice) SetDate(date string) (*InvoiceCell, *InvoiceCell) { i.date[1].Value = date + return i.date[0], i.date[1] } func (i *Invoice) DueDate() (*InvoiceCell, *InvoiceCell) { return i.dueDate[0], i.dueDate[1] } -func (i *Invoice) SetDueDate(dueDate string) { +func (i *Invoice) SetDueDate(dueDate string) (*InvoiceCell, *InvoiceCell) { i.dueDate[1].Value = dueDate + return i.dueDate[0], i.dueDate[1] } func (i *Invoice) AddInvoiceInfo(description, value string) (*InvoiceCell, *InvoiceCell) { @@ -239,19 +242,32 @@ func (i *Invoice) AddInvoiceInfo(description, value string) (*InvoiceCell, *Invo return info[0], info[1] } -func (i *Invoice) AppendColumn(description string) *InvoiceCell { - return nil +func (i *Invoice) Columns() []*InvoiceCell { + return i.columns } -func (i *Invoice) InsertColumn(description string) *InvoiceCell { - return nil +func (i *Invoice) AppendColumn(description string) *InvoiceCell { + col := i.NewColumn(description) + i.columns = append(i.columns, col) + return col +} + +func (i *Invoice) InsertColumn(index uint, description string) *InvoiceCell { + l := uint(len(i.columns)) + if index > l { + index = l + } + + col := i.NewColumn(description) + i.columns = append(i.columns[:index], append([]*InvoiceCell{col}, i.columns[index:]...)...) + return col } func (i *Invoice) Lines() [][]*InvoiceCell { return i.lines } -func (i *Invoice) AddLine(values ...string) { +func (i *Invoice) AddLine(values ...string) []*InvoiceCell { lenCols := len(i.columns) var line []*InvoiceCell @@ -265,6 +281,7 @@ func (i *Invoice) AddLine(values ...string) { } i.lines = append(i.lines, line) + return line } func (i *Invoice) Subtotal() (*InvoiceCell, *InvoiceCell) { @@ -594,7 +611,7 @@ func (i *Invoice) generateInformationBlocks(ctx DrawContext) ([]*Block, DrawCont } func (i *Invoice) generateLineBlocks(ctx DrawContext) ([]*Block, DrawContext, error) { - table := newTable(4) + table := newTable(len(i.columns)) table.SetMargins(0, 0, 25, 0) // Draw item columns. @@ -659,7 +676,7 @@ func (i *Invoice) generateNoteBlocks(ctx DrawContext) ([]*Block, DrawContext, er } sepParagraph := newStyledParagraph(i.defaultStyle) - sepParagraph.SetMargins(0, 0, 20, 0) + sepParagraph.SetMargins(0, 0, 10, 0) division.Add(sepParagraph) } } From b8f9f07175dd45d4f9e7894b60e24ecf245ee73c Mon Sep 17 00:00:00 2001 From: Adrian-George Bostan Date: Tue, 30 Oct 2018 22:14:04 +0200 Subject: [PATCH 10/13] Add invoice advanced test case --- pdf/creator/invoice_test.go | 114 +++++++++++++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 1 deletion(-) diff --git a/pdf/creator/invoice_test.go b/pdf/creator/invoice_test.go index bbcf294e..4522436d 100644 --- a/pdf/creator/invoice_test.go +++ b/pdf/creator/invoice_test.go @@ -3,6 +3,8 @@ package creator import ( "fmt" "testing" + + "github.com/unidoc/unidoc/pdf/model" ) func TestInvoiceSimple(t *testing.T) { @@ -56,7 +58,7 @@ func TestInvoiceSimple(t *testing.T) { invoice.AddTotalLine("Tax (10%)", "$10.00") invoice.AddTotalLine("Shipping", "$5.00") invoice.SetTotal("$115.00") - invoice.SetNotes("Notes", "Thank you for your business") + invoice.SetNotes("Notes", "Thank you for your business.") invoice.SetTerms("Terms and conditions", "Full refund for 60 days after purchase.") if err = c.Draw(invoice); err != nil { @@ -69,3 +71,113 @@ func TestInvoiceSimple(t *testing.T) { t.Fatalf("Fail: %v\n", err) } } + +func TestInvoiceAdvanced(t *testing.T) { + fontHelvetica := model.NewStandard14FontMustCompile(model.Helvetica) + + c := New() + c.NewPage() + + logo, err := c.NewImageFromFile(testImageFile1) + if err != nil { + t.Errorf("Fail: %v\n", err) + } + + white := ColorRGBFrom8bit(255, 255, 255) + lightBlue := ColorRGBFrom8bit(217, 240, 250) + + invoice := c.NewInvoice() + + invoice.SetLogo(logo) + + // Customize invoice info + descCell, contentCell := invoice.SetNumber("0001") + descCell.BackgroundColor = lightBlue + contentCell.TextStyle.Font = fontHelvetica + + descCell, contentCell = invoice.SetDate("28/07/2016") + descCell.BackgroundColor = lightBlue + contentCell.TextStyle.Font = fontHelvetica + + descCell, contentCell = invoice.SetDueDate("28/07/2016") + descCell.BackgroundColor = lightBlue + contentCell.TextStyle.Font = fontHelvetica + + descCell, contentCell = invoice.AddInvoiceInfo("Payment terms", "Due on receipt") + descCell.BackgroundColor = lightBlue + contentCell.TextStyle.Font = fontHelvetica + + descCell, contentCell = invoice.AddInvoiceInfo("Paid", "No") + descCell.BackgroundColor = lightBlue + contentCell.TextStyle.Font = fontHelvetica + + invoice.SetSellerAddress(&InvoiceAddress{ + Name: "John Doe", + Street: "8 Elm Street", + City: "Cambridge", + Zip: "CB14DH", + Country: "United Kingdom", + Phone: "xxx-xxx-xxxx", + Email: "johndoe@email.com", + }) + + invoice.SetBuyerAddress(&InvoiceAddress{ + Name: "Jane Doe", + Street: "9 Elm Street", + City: "London", + Zip: "LB15FH", + Country: "United Kingdom", + Phone: "xxx-xxx-xxxx", + Email: "janedoe@email.com", + }) + + // Insert new column + col := invoice.InsertColumn(2, "Discount") + col.Alignment = CellHorizontalAlignmentRight + + // Customize column styles. + for _, column := range invoice.Columns() { + column.BackgroundColor = lightBlue + column.BorderColor = lightBlue + } + + for i := 0; i < 7; i++ { + cells := invoice.AddLine( + fmt.Sprintf("Test product #%d", i+1), + "1", + "0%", + "$10", + "$10", + ) + + for _, cell := range cells { + cell.BorderColor = white + } + } + + // Customize total line style. + titleCell, contentCell := invoice.Total() + titleCell.BackgroundColor = lightBlue + titleCell.BorderColor = lightBlue + contentCell.BackgroundColor = lightBlue + contentCell.BorderColor = lightBlue + + invoice.SetSubtotal("$100.00") + invoice.AddTotalLine("Tax (10%)", "$10.00") + invoice.AddTotalLine("Shipping", "$5.00") + invoice.SetTotal("$85.00") + + invoice.SetNotes("Notes", "Thank you for your business.") + invoice.SetTerms("Terms and conditions", "Full refund for 60 days after purchase.") + invoice.AddSection("Custom section", "This is a custom section.") + + if err = c.Draw(invoice); err != nil { + t.Fatalf("Error drawing: %v", err) + } + + // Write output file. + err = c.WriteToFile("/tmp/invoice_advanced.pdf") + if err != nil { + t.Fatalf("Fail: %v\n", err) + } +} From 7ca49f50f254a04f26c12a63a6cceabe5c33d6c9 Mon Sep 17 00:00:00 2001 From: Adrian-George Bostan Date: Tue, 30 Oct 2018 22:36:13 +0200 Subject: [PATCH 11/13] Improve invoice test --- pdf/creator/invoice.go | 20 ++++++++++++++++++-- pdf/creator/invoice_test.go | 36 +++++++++++++++--------------------- 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/pdf/creator/invoice.go b/pdf/creator/invoice.go index 56f81645..fd496d42 100644 --- a/pdf/creator/invoice.go +++ b/pdf/creator/invoice.go @@ -232,7 +232,17 @@ func (i *Invoice) SetDueDate(dueDate string) (*InvoiceCell, *InvoiceCell) { return i.dueDate[0], i.dueDate[1] } -func (i *Invoice) AddInvoiceInfo(description, value string) (*InvoiceCell, *InvoiceCell) { +func (i *Invoice) InfoLines() [][2]*InvoiceCell { + info := [][2]*InvoiceCell{ + i.number, + i.date, + i.dueDate, + } + + return append(info, i.info...) +} + +func (i *Invoice) AddInfo(description, value string) (*InvoiceCell, *InvoiceCell) { info := [2]*InvoiceCell{ i.newCell(description, i.infoProps), i.newCell(value, i.infoProps), @@ -301,7 +311,9 @@ func (i *Invoice) SetTotal(value string) { } func (i *Invoice) TotalLines() [][2]*InvoiceCell { - return i.totals + totals := [][2]*InvoiceCell{i.subtotal} + totals = append(totals, i.totals...) + return append(totals, i.total) } func (i *Invoice) AddTotalLine(desc, value string) (*InvoiceCell, *InvoiceCell) { @@ -386,6 +398,10 @@ func (i *Invoice) newColumn(description string, alignment CellHorizontalAlignmen return col } +func (i *Invoice) setTitleStyle(style TextStyle) { + i.titleStyle = style +} + func (i *Invoice) setCellBorder(cell *TableCell, invoiceCell *InvoiceCell) { for _, side := range invoiceCell.BorderSides { cell.SetBorder(side, CellBorderStyleSingle, invoiceCell.BorderWidth) diff --git a/pdf/creator/invoice_test.go b/pdf/creator/invoice_test.go index 4522436d..9e34c07e 100644 --- a/pdf/creator/invoice_test.go +++ b/pdf/creator/invoice_test.go @@ -22,8 +22,8 @@ func TestInvoiceSimple(t *testing.T) { invoice.SetNumber("0001") invoice.SetDate("28/07/2016") invoice.SetDueDate("28/07/2016") - invoice.AddInvoiceInfo("Payment terms", "Due on receipt") - invoice.AddInvoiceInfo("Paid", "No") + invoice.AddInfo("Payment terms", "Due on receipt") + invoice.AddInfo("Paid", "No") invoice.SetSellerAddress(&InvoiceAddress{ Name: "John Doe", @@ -90,26 +90,18 @@ func TestInvoiceAdvanced(t *testing.T) { invoice.SetLogo(logo) + invoice.SetNumber("0001") + invoice.SetDate("28/07/2016") + invoice.SetDueDate("28/07/2016") + invoice.AddInfo("Payment terms", "Due on receipt") + invoice.AddInfo("Paid", "No") + // Customize invoice info - descCell, contentCell := invoice.SetNumber("0001") - descCell.BackgroundColor = lightBlue - contentCell.TextStyle.Font = fontHelvetica - - descCell, contentCell = invoice.SetDate("28/07/2016") - descCell.BackgroundColor = lightBlue - contentCell.TextStyle.Font = fontHelvetica - - descCell, contentCell = invoice.SetDueDate("28/07/2016") - descCell.BackgroundColor = lightBlue - contentCell.TextStyle.Font = fontHelvetica - - descCell, contentCell = invoice.AddInvoiceInfo("Payment terms", "Due on receipt") - descCell.BackgroundColor = lightBlue - contentCell.TextStyle.Font = fontHelvetica - - descCell, contentCell = invoice.AddInvoiceInfo("Paid", "No") - descCell.BackgroundColor = lightBlue - contentCell.TextStyle.Font = fontHelvetica + for _, info := range invoice.InfoLines() { + descCell, contentCell := info[0], info[1] + descCell.BackgroundColor = lightBlue + contentCell.TextStyle.Font = fontHelvetica + } invoice.SetSellerAddress(&InvoiceAddress{ Name: "John Doe", @@ -139,6 +131,7 @@ func TestInvoiceAdvanced(t *testing.T) { for _, column := range invoice.Columns() { column.BackgroundColor = lightBlue column.BorderColor = lightBlue + column.TextStyle.FontSize = 9 } for i := 0; i < 7; i++ { @@ -152,6 +145,7 @@ func TestInvoiceAdvanced(t *testing.T) { for _, cell := range cells { cell.BorderColor = white + cell.TextStyle.FontSize = 9 } } From 1508b923e2852287393d579c38ae1934aa52d940 Mon Sep 17 00:00:00 2001 From: Adrian-George Bostan Date: Thu, 1 Nov 2018 20:44:50 +0200 Subject: [PATCH 12/13] Make invoice component styles customizable --- pdf/creator/invoice.go | 149 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 134 insertions(+), 15 deletions(-) diff --git a/pdf/creator/invoice.go b/pdf/creator/invoice.go index fd496d42..4a3d78ff 100644 --- a/pdf/creator/invoice.go +++ b/pdf/creator/invoice.go @@ -7,7 +7,9 @@ package creator import "fmt" -// InvoiceAddress. +// InvoiceAddress contains contact information that can be displayed +// in an invoice. It is used for the seller and buyer information in the +// invoice template. type InvoiceAddress struct { Name string Street string @@ -18,7 +20,7 @@ type InvoiceAddress struct { Email string } -// InvoiceCellProps. +// InvoiceCellProps holds all style properties for an invoice cell. type InvoiceCellProps struct { TextStyle TextStyle Alignment CellHorizontalAlignment @@ -29,7 +31,10 @@ type InvoiceCellProps struct { BorderSides []CellBorderSide } -// InvoiceCell. +// InvoiceCell represents any cell belonging to a table from the invoice +// template. The main tables are the invoice information table, the line +// items table and totals table. Contains the text value of the cell and +// the style properties of the cell. type InvoiceCell struct { InvoiceCellProps @@ -73,6 +78,13 @@ type Invoice struct { headingStyle TextStyle titleStyle TextStyle + addressStyle TextStyle + addressHeadingStyle TextStyle + + noteStyle TextStyle + noteHeadingStyle TextStyle + + // Invoice style properties. infoProps InvoiceCellProps colProps InvoiceCellProps itemProps InvoiceCellProps @@ -106,6 +118,14 @@ func newInvoice(defaultStyle, headingStyle TextStyle) *Invoice { i.titleStyle.Color = mediumGrey i.titleStyle.FontSize = 20 + // Default address styles. + i.addressStyle = defaultStyle + i.addressHeadingStyle = headingStyle + + // Default note styles. + i.noteStyle = defaultStyle + i.noteHeadingStyle = headingStyle + // Invoice information default properties. i.infoProps = i.NewCellProps() i.infoProps.BackgroundColor = lightGrey @@ -173,65 +193,84 @@ func newInvoice(defaultStyle, headingStyle TextStyle) *Invoice { return i } +// Title returns the title of the invoice. func (i *Invoice) Title() string { return i.title } +// SetTitle sets the title of the invoice. func (i *Invoice) SetTitle(title string) { i.title = title } +// Logo returns the logo of the invoice. func (i *Invoice) Logo() *Image { return i.logo } +// SetLogo sets the logo of the invoice. func (i *Invoice) SetLogo(logo *Image) { i.logo = logo } +// SellerAddress returns the seller address used in the invoice template. func (i *Invoice) SellerAddress() *InvoiceAddress { return i.sellerAddress } +// SetSellerAddress sets the seller address of the invoice. func (i *Invoice) SetSellerAddress(address *InvoiceAddress) { i.sellerAddress = address } +// BuyerAddress returns the buyer address used in the invoice template. func (i *Invoice) BuyerAddress() *InvoiceAddress { return i.buyerAddress } +// SetBuyerAddress sets the buyer address of the invoice. func (i *Invoice) SetBuyerAddress(address *InvoiceAddress) { i.buyerAddress = address } +// Number returns the invoice number description and value cells. +// The returned values can be used to customize the styles of the cells. func (i *Invoice) Number() (*InvoiceCell, *InvoiceCell) { return i.number[0], i.number[1] } +// SetNumber sets the number of the invoice. func (i *Invoice) SetNumber(number string) (*InvoiceCell, *InvoiceCell) { i.number[1].Value = number return i.number[0], i.number[1] } +// Date returns the invoice date description and value cells. +// The returned values can be used to customize the styles of the cells. func (i *Invoice) Date() (*InvoiceCell, *InvoiceCell) { return i.date[0], i.date[1] } +// SetDate sets the date of the invoice. func (i *Invoice) SetDate(date string) (*InvoiceCell, *InvoiceCell) { i.date[1].Value = date return i.date[0], i.date[1] } +// DueDate returns the invoice due date description and value cells. +// The returned values can be used to customize the styles of the cells. func (i *Invoice) DueDate() (*InvoiceCell, *InvoiceCell) { return i.dueDate[0], i.dueDate[1] } +// SetDueDate sets the due date of the invoice. func (i *Invoice) SetDueDate(dueDate string) (*InvoiceCell, *InvoiceCell) { i.dueDate[1].Value = dueDate return i.dueDate[0], i.dueDate[1] } +// InfoLines returns all the rows in the invoice information table as +// description-value cell pairs. func (i *Invoice) InfoLines() [][2]*InvoiceCell { info := [][2]*InvoiceCell{ i.number, @@ -242,6 +281,8 @@ func (i *Invoice) InfoLines() [][2]*InvoiceCell { return append(info, i.info...) } +// AddInfo is used to append a piece of invoice information in the template +// information table. func (i *Invoice) AddInfo(description, value string) (*InvoiceCell, *InvoiceCell) { info := [2]*InvoiceCell{ i.newCell(description, i.infoProps), @@ -252,16 +293,19 @@ func (i *Invoice) AddInfo(description, value string) (*InvoiceCell, *InvoiceCell return info[0], info[1] } +// Columns returns all the columns in the invoice line items table. func (i *Invoice) Columns() []*InvoiceCell { return i.columns } +// AppendColumn appends a column to the line items table. func (i *Invoice) AppendColumn(description string) *InvoiceCell { col := i.NewColumn(description) i.columns = append(i.columns, col) return col } +// InsertColumn inserts a column in the line items table at the specified index. func (i *Invoice) InsertColumn(index uint, description string) *InvoiceCell { l := uint(len(i.columns)) if index > l { @@ -273,10 +317,12 @@ func (i *Invoice) InsertColumn(index uint, description string) *InvoiceCell { return col } +// Lines returns all the rows of the invoice line items table. func (i *Invoice) Lines() [][]*InvoiceCell { return i.lines } +// AddLine appends a new line to the invoice line items table. func (i *Invoice) AddLine(values ...string) []*InvoiceCell { lenCols := len(i.columns) @@ -294,28 +340,37 @@ func (i *Invoice) AddLine(values ...string) []*InvoiceCell { return line } +// Subtotal returns the invoice subtotal description and value cells. +// The returned values can be used to customize the styles of the cells. func (i *Invoice) Subtotal() (*InvoiceCell, *InvoiceCell) { return i.subtotal[0], i.subtotal[1] } +// SetSubtotal sets the subtotal of the invoice. func (i *Invoice) SetSubtotal(value string) { i.subtotal[1].Value = value } +// Total returns the invoice total description and value cells. +// The returned values can be used to customize the styles of the cells. func (i *Invoice) Total() (*InvoiceCell, *InvoiceCell) { return i.total[0], i.total[1] } +// SetTotal sets the total of the invoice. func (i *Invoice) SetTotal(value string) { i.total[1].Value = value } +// TotalLines returns all the rows in the invoice totals table as +// description-value cell pairs. func (i *Invoice) TotalLines() [][2]*InvoiceCell { totals := [][2]*InvoiceCell{i.subtotal} totals = append(totals, i.totals...) return append(totals, i.total) } +// AddTotalLine adds a new line in the invoice totals table. func (i *Invoice) AddTotalLine(desc, value string) (*InvoiceCell, *InvoiceCell) { descCell := &InvoiceCell{ i.totalProps, @@ -330,10 +385,12 @@ func (i *Invoice) AddTotalLine(desc, value string) (*InvoiceCell, *InvoiceCell) return descCell, valueCell } +// Notes returns the notes section of the invoice as a title-content pair. func (i *Invoice) Notes() (string, string) { return i.notes[0], i.notes[1] } +// SetNotes sets the notes section of the invoice. func (i *Invoice) SetNotes(title, content string) { i.notes = [2]string{ title, @@ -341,10 +398,13 @@ func (i *Invoice) SetNotes(title, content string) { } } +// Terms returns the terms and conditions section of the invoice as a +// title-content pair. func (i *Invoice) Terms() (string, string) { return i.terms[0], i.terms[1] } +// SetTerms sets the terms and conditions section of the invoice. func (i *Invoice) SetTerms(title, content string) { i.terms = [2]string{ title, @@ -352,10 +412,13 @@ func (i *Invoice) SetTerms(title, content string) { } } +// Sections returns the custom content sections of the invoice as +// title-content pairs. func (i *Invoice) Sections() [][2]string { return i.sections } +// AddSection adds a new content section at the end of the invoice. func (i *Invoice) AddSection(title, content string) { i.sections = append(i.sections, [2]string{ title, @@ -363,6 +426,65 @@ func (i *Invoice) AddSection(title, content string) { }) } +// TitleStyle returns the style properties used to render the invoice title. +func (i *Invoice) TitleStyle() TextStyle { + return i.titleStyle +} + +// SetTitleStyle sets the style properties of the invoice title. +func (i *Invoice) SetTitleStyle(style TextStyle) { + i.titleStyle = style +} + +// AddressStyle returns the style properties used to render the content of +// the invoice address sections. +func (i *Invoice) AddressStyle() TextStyle { + return i.addressStyle +} + +// SetAddressStyle sets the style properties used to render the content of +// the invoice address sections. +func (i *Invoice) SetAddressStyle(style TextStyle) { + i.addressStyle = style +} + +// AddressHeadingStyle returns the style properties used to render the +// heading of the invoice address sections. +func (i *Invoice) AddressHeadingStyle() TextStyle { + return i.headingStyle +} + +// SetAddressHeadingStyle sets the style properties used to render the +// heading of the invoice address sections. +func (i *Invoice) SetAddressHeadingStyle(style TextStyle) { + i.addressHeadingStyle = style +} + +// NotesStyle returns the style properties used to render the content of the +// invoice note sections. +func (i *Invoice) NotesStyle() TextStyle { + return i.noteStyle +} + +// SetNotesStyle sets the style properties used to render the content of the +// invoice note sections. +func (i *Invoice) SetNotesStyle(style TextStyle) { + i.noteStyle = style +} + +// NotesHeadingStyle returns the style properties used to render the heading of +// the invoice note sections. +func (i *Invoice) NotesHeadingStyle() TextStyle { + return i.noteHeadingStyle +} + +// SetNotesHeadingStyle sets the style properties used to render the heading +// of the invoice note sections. +func (i *Invoice) SetNotesHeadingStyle(style TextStyle) { + i.noteHeadingStyle = style +} + +// NewCellProps returns the default properties of an invoice cell. func (i *Invoice) NewCellProps() InvoiceCellProps { white := ColorRGBFrom8bit(255, 255, 255) @@ -376,6 +498,7 @@ func (i *Invoice) NewCellProps() InvoiceCellProps { } } +// NewCell returns a new invoice table cell. func (i *Invoice) NewCell(value string) *InvoiceCell { return i.newCell(value, i.NewCellProps()) } @@ -387,6 +510,7 @@ func (i *Invoice) newCell(value string, props InvoiceCellProps) *InvoiceCell { } } +// NewColumn returns a new column for the line items invoice table. func (i *Invoice) NewColumn(description string) *InvoiceCell { return i.newColumn(description, CellHorizontalAlignmentLeft) } @@ -398,10 +522,6 @@ func (i *Invoice) newColumn(description string, alignment CellHorizontalAlignmen return col } -func (i *Invoice) setTitleStyle(style TextStyle) { - i.titleStyle = style -} - func (i *Invoice) setCellBorder(cell *TableCell, invoiceCell *InvoiceCell) { for _, side := range invoiceCell.BorderSides { cell.SetBorder(side, CellBorderStyleSingle, invoiceCell.BorderWidth) @@ -415,7 +535,7 @@ func (i *Invoice) drawAddress(title, name string, addr *InvoiceAddress) []*Style // Address title. if title != "" { - titleParagraph := newStyledParagraph(i.headingStyle) + titleParagraph := newStyledParagraph(i.addressHeadingStyle) titleParagraph.SetMargins(0, 0, 0, 7) titleParagraph.Append(title) @@ -423,7 +543,7 @@ func (i *Invoice) drawAddress(title, name string, addr *InvoiceAddress) []*Style } // Address information. - addressParagraph := newStyledParagraph(i.defaultStyle) + addressParagraph := newStyledParagraph(i.addressStyle) addressParagraph.SetLineHeight(1.2) city := addr.City @@ -449,7 +569,7 @@ func (i *Invoice) drawAddress(title, name string, addr *InvoiceAddress) []*Style } // Contact information. - contactParagraph := newStyledParagraph(i.defaultStyle) + contactParagraph := newStyledParagraph(i.addressStyle) contactParagraph.SetLineHeight(1.2) contactParagraph.SetMargins(0, 0, 7, 0) @@ -469,7 +589,7 @@ func (i *Invoice) drawSection(title, content string) []*StyledParagraph { // Title paragraph. if title != "" { - titleParagraph := newStyledParagraph(i.headingStyle) + titleParagraph := newStyledParagraph(i.noteHeadingStyle) titleParagraph.SetMargins(0, 0, 0, 5) titleParagraph.Append(title) @@ -478,7 +598,7 @@ func (i *Invoice) drawSection(title, content string) []*StyledParagraph { // Content paragraph. if content != "" { - contentParagraph := newStyledParagraph(i.defaultStyle) + contentParagraph := newStyledParagraph(i.noteStyle) contentParagraph.Append(content) paragraphs = append(paragraphs, contentParagraph) @@ -678,11 +798,10 @@ func (i *Invoice) generateTotalBlocks(ctx DrawContext) ([]*Block, DrawContext, e func (i *Invoice) generateNoteBlocks(ctx DrawContext) ([]*Block, DrawContext, error) { division := newDivision() - sections := [][2]string{ + sections := append([][2]string{ i.notes, i.terms, - } - sections = append(sections, i.sections...) + }, i.sections...) for _, section := range sections { if section[1] != "" { From 73f73fa1595438a5d328e3797ae3241ccd98fb83 Mon Sep 17 00:00:00 2001 From: Adrian-George Bostan Date: Thu, 1 Nov 2018 20:58:37 +0200 Subject: [PATCH 13/13] Improve invoice component test cases --- pdf/creator/invoice.go | 16 +++++------ pdf/creator/invoice_test.go | 56 +++++++++++++++++++++++++++++++++++-- 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/pdf/creator/invoice.go b/pdf/creator/invoice.go index 4a3d78ff..27d016d8 100644 --- a/pdf/creator/invoice.go +++ b/pdf/creator/invoice.go @@ -460,27 +460,27 @@ func (i *Invoice) SetAddressHeadingStyle(style TextStyle) { i.addressHeadingStyle = style } -// NotesStyle returns the style properties used to render the content of the +// NoteStyle returns the style properties used to render the content of the // invoice note sections. -func (i *Invoice) NotesStyle() TextStyle { +func (i *Invoice) NoteStyle() TextStyle { return i.noteStyle } -// SetNotesStyle sets the style properties used to render the content of the +// SetNoteStyle sets the style properties used to render the content of the // invoice note sections. -func (i *Invoice) SetNotesStyle(style TextStyle) { +func (i *Invoice) SetNoteStyle(style TextStyle) { i.noteStyle = style } -// NotesHeadingStyle returns the style properties used to render the heading of +// NoteHeadingStyle returns the style properties used to render the heading of // the invoice note sections. -func (i *Invoice) NotesHeadingStyle() TextStyle { +func (i *Invoice) NoteHeadingStyle() TextStyle { return i.noteHeadingStyle } -// SetNotesHeadingStyle sets the style properties used to render the heading +// SetNoteHeadingStyle sets the style properties used to render the heading // of the invoice note sections. -func (i *Invoice) SetNotesHeadingStyle(style TextStyle) { +func (i *Invoice) SetNoteHeadingStyle(style TextStyle) { i.noteHeadingStyle = style } diff --git a/pdf/creator/invoice_test.go b/pdf/creator/invoice_test.go index 9e34c07e..29699d21 100644 --- a/pdf/creator/invoice_test.go +++ b/pdf/creator/invoice_test.go @@ -18,13 +18,17 @@ func TestInvoiceSimple(t *testing.T) { invoice := c.NewInvoice() + // Set invoice logo. invoice.SetLogo(logo) + + // Set invoice information. invoice.SetNumber("0001") invoice.SetDate("28/07/2016") invoice.SetDueDate("28/07/2016") invoice.AddInfo("Payment terms", "Due on receipt") invoice.AddInfo("Paid", "No") + // Set invoice addresses. invoice.SetSellerAddress(&InvoiceAddress{ Name: "John Doe", Street: "8 Elm Street", @@ -45,6 +49,7 @@ func TestInvoiceSimple(t *testing.T) { Email: "janedoe@email.com", }) + // Add invoice line items. for i := 0; i < 10; i++ { invoice.AddLine( fmt.Sprintf("Test product #%d", i+1), @@ -54,10 +59,13 @@ func TestInvoiceSimple(t *testing.T) { ) } + // Set invoice totals. invoice.SetSubtotal("$100.00") invoice.AddTotalLine("Tax (10%)", "$10.00") invoice.AddTotalLine("Shipping", "$5.00") invoice.SetTotal("$115.00") + + // Set invoice content sections. invoice.SetNotes("Notes", "Thank you for your business.") invoice.SetTerms("Terms and conditions", "Full refund for 60 days after purchase.") @@ -85,24 +93,39 @@ func TestInvoiceAdvanced(t *testing.T) { white := ColorRGBFrom8bit(255, 255, 255) lightBlue := ColorRGBFrom8bit(217, 240, 250) + blue := ColorRGBFrom8bit(2, 136, 209) invoice := c.NewInvoice() + // Set invoice title. + invoice.SetTitle("Unidoc Invoice") + + // Customize invoice title style. + titleStyle := invoice.TitleStyle() + titleStyle.Color = blue + titleStyle.Font = fontHelvetica + titleStyle.FontSize = 30 + + invoice.SetTitleStyle(titleStyle) + + // Set invoice logo. invoice.SetLogo(logo) + // Set invoice information. invoice.SetNumber("0001") invoice.SetDate("28/07/2016") invoice.SetDueDate("28/07/2016") invoice.AddInfo("Payment terms", "Due on receipt") invoice.AddInfo("Paid", "No") - // Customize invoice info + // Customize invoice information styles. for _, info := range invoice.InfoLines() { descCell, contentCell := info[0], info[1] descCell.BackgroundColor = lightBlue contentCell.TextStyle.Font = fontHelvetica } + // Set invoice addresses. invoice.SetSellerAddress(&InvoiceAddress{ Name: "John Doe", Street: "8 Elm Street", @@ -123,7 +146,20 @@ func TestInvoiceAdvanced(t *testing.T) { Email: "janedoe@email.com", }) - // Insert new column + // Customize address styles. + addressStyle := invoice.AddressStyle() + addressStyle.Font = fontHelvetica + addressStyle.FontSize = 9 + + addressHeadingStyle := invoice.AddressHeadingStyle() + addressHeadingStyle.Color = blue + addressHeadingStyle.Font = fontHelvetica + addressHeadingStyle.FontSize = 16 + + invoice.SetAddressStyle(addressStyle) + invoice.SetAddressHeadingStyle(addressHeadingStyle) + + // Insert new column. col := invoice.InsertColumn(2, "Discount") col.Alignment = CellHorizontalAlignmentRight @@ -149,7 +185,7 @@ func TestInvoiceAdvanced(t *testing.T) { } } - // Customize total line style. + // Customize total line styles. titleCell, contentCell := invoice.Total() titleCell.BackgroundColor = lightBlue titleCell.BorderColor = lightBlue @@ -161,10 +197,24 @@ func TestInvoiceAdvanced(t *testing.T) { invoice.AddTotalLine("Shipping", "$5.00") invoice.SetTotal("$85.00") + // Set invoice content sections. invoice.SetNotes("Notes", "Thank you for your business.") invoice.SetTerms("Terms and conditions", "Full refund for 60 days after purchase.") invoice.AddSection("Custom section", "This is a custom section.") + // Customize note styles. + noteStyle := invoice.AddressStyle() + noteStyle.Font = fontHelvetica + noteStyle.FontSize = 12 + + noteHeadingStyle := invoice.AddressHeadingStyle() + noteHeadingStyle.Color = blue + noteHeadingStyle.Font = fontHelvetica + noteHeadingStyle.FontSize = 14 + + invoice.SetNoteStyle(noteStyle) + invoice.SetNoteHeadingStyle(noteHeadingStyle) + if err = c.Draw(invoice); err != nil { t.Fatalf("Error drawing: %v", err) }