diff --git a/bill/invoice.go b/bill/invoice.go index b4fed3dc..1c9c54e0 100644 --- a/bill/invoice.go +++ b/bill/invoice.go @@ -184,10 +184,22 @@ func (inv *Invoice) ValidateWithContext(ctx context.Context) error { } // Invert effectively reverses the invoice by inverting the sign of all quantity -// or amount values. -func (inv *Invoice) Invert() { +// or amount values. Caution should be taken when using this method as +// advances will also be inverted, while payment terms will remain the same, +// which could be confusing if no further modifications are made. +// After inverting the invoice is recalculated and any differences will raise +// an error. +func (inv *Invoice) Invert() error { + payable := inv.Totals.Payable.Invert() + for _, row := range inv.Lines { row.Quantity = row.Quantity.Invert() + for _, d := range row.Discounts { + d.Amount = d.Amount.Invert() + } + for _, c := range row.Charges { + c.Amount = c.Amount.Invert() + } } for _, row := range inv.Charges { row.Amount = row.Amount.Invert() @@ -198,7 +210,24 @@ func (inv *Invoice) Invert() { for _, row := range inv.Outlays { row.Amount = row.Amount.Invert() } + if inv.Payment != nil { + for _, row := range inv.Payment.Advances { + row.Amount = row.Amount.Invert() + } + } inv.Totals = nil + + if err := inv.Calculate(); err != nil { + return err + } + + // The following check tries to ensure that any future fields do not cause + // unexpected results. + if !payable.Equals(inv.Totals.Payable) { + return fmt.Errorf("inverted invoice totals do not match %s != %s", payable.String(), inv.Totals.Payable.String()) + } + + return nil } // Empty is a convenience method that will empty all the lines and diff --git a/bill/invoice_test.go b/bill/invoice_test.go index 0d508017..14aace93 100644 --- a/bill/invoice_test.go +++ b/bill/invoice_test.go @@ -836,6 +836,77 @@ func TestCalculate(t *testing.T) { assert.Equal(t, i.Totals.Due.String(), "675.00") } +func TestCalculateInverted(t *testing.T) { + i := &bill.Invoice{ + Code: "123TEST", + Tax: &bill.Tax{ + PricesInclude: tax.CategoryVAT, + }, + Supplier: &org.Party{ + TaxID: &tax.Identity{ + Country: l10n.ES, + Code: "B98602642", + }, + }, + Customer: &org.Party{ + TaxID: &tax.Identity{ + Country: l10n.ES, + Code: "54387763P", + }, + }, + IssueDate: cal.MakeDate(2022, 6, 13), + Lines: []*bill.Line{ + { + Quantity: num.MakeAmount(10, 0), + Item: &org.Item{ + Name: "Test Item", + Price: num.MakeAmount(10000, 2), + }, + Taxes: tax.Set{ + { + Category: "VAT", + Rate: "standard", + }, + }, + Discounts: []*bill.LineDiscount{ + { + Reason: "Testing", + Amount: num.MakeAmount(10000, 2), + }, + }, + Charges: []*bill.LineCharge{ + { + Reason: "Testing Charge", + Amount: num.MakeAmount(5000, 2), + }, + }, + }, + }, + Outlays: []*bill.Outlay{ + { + Description: "Something paid in advance", + Amount: num.MakeAmount(1000, 2), + }, + }, + Payment: &bill.Payment{ + Advances: []*pay.Advance{ + { + Description: "Test Advance", + Amount: num.MakeAmount(25000, 2), + }, + }, + }, + } + + require.NoError(t, i.Calculate()) + assert.Equal(t, i.Totals.Sum.String(), "950.00") + assert.Equal(t, i.Totals.Due.String(), "710.00") + + require.NoError(t, i.Invert()) + assert.Equal(t, i.Totals.Sum.String(), "-950.00") + assert.Equal(t, i.Totals.Due.String(), "-710.00") +} + func TestValidation(t *testing.T) { inv := &bill.Invoice{ Currency: currency.EUR,