Allow to change the current year’s invoice number counter

This is for new users that do not start using the application from the
beginning of the current fiscal year and, therefore, need to create
invoices starting from a specific number.

I had to change the constraint on the currval to allow zero, otherwise
it would not be possible to set 1 as the next number, because users
can also not delete the row.
This commit is contained in:
jordi fita mas 2023-05-31 20:01:00 +02:00
parent 1855122d16
commit 083d14e324
5 changed files with 74 additions and 6 deletions

View File

@ -9,7 +9,7 @@ set search_path to numerus, public;
create table invoice_number_counter ( create table invoice_number_counter (
company_id integer not null, company_id integer not null,
year integer not null constraint year_always_positive check(year > 0), year integer not null constraint year_always_positive check(year > 0),
currval integer not null constraint counter_always_positive check(currval > 0), currval integer not null constraint counter_zero_or_positive check(currval >= 0),
primary key (company_id, year) primary key (company_id, year)
); );

View File

@ -5,6 +5,7 @@ import (
"errors" "errors"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"html/template" "html/template"
"math"
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
@ -87,6 +88,7 @@ type taxDetailsForm struct {
*contactForm *contactForm
Currency *SelectField Currency *SelectField
InvoiceNumberFormat *InputField InvoiceNumberFormat *InputField
NextInvoiceNumber *InputField
LegalDisclaimer *InputField LegalDisclaimer *InputField
} }
@ -106,6 +108,15 @@ func newTaxDetailsForm(ctx context.Context, conn *Conn, locale *Locale) *taxDeta
Type: "text", Type: "text",
Required: true, Required: true,
}, },
NextInvoiceNumber: &InputField{
Name: "next_invoice_number",
Label: pgettext("input", "Next invoice number", locale),
Type: "number",
Required: true,
Attributes: []template.HTMLAttr{
"min=1",
},
},
LegalDisclaimer: &InputField{ LegalDisclaimer: &InputField{
Name: "legal_disclaimer", Name: "legal_disclaimer",
Label: pgettext("input", "Legal disclaimer", locale), Label: pgettext("input", "Legal disclaimer", locale),
@ -120,6 +131,7 @@ func (form *taxDetailsForm) Parse(r *http.Request) error {
} }
form.Currency.FillValue(r) form.Currency.FillValue(r)
form.InvoiceNumberFormat.FillValue(r) form.InvoiceNumberFormat.FillValue(r)
form.NextInvoiceNumber.FillValue(r)
form.LegalDisclaimer.FillValue(r) form.LegalDisclaimer.FillValue(r)
return nil return nil
} }
@ -128,11 +140,51 @@ func (form *taxDetailsForm) Validate(ctx context.Context, conn *Conn) bool {
validator := newFormValidator() validator := newFormValidator()
validator.CheckValidSelectOption(form.Currency, gettext("Selected currency is not valid.", form.locale)) validator.CheckValidSelectOption(form.Currency, gettext("Selected currency is not valid.", form.locale))
validator.CheckRequiredInput(form.InvoiceNumberFormat, gettext("Invoice number format can not be empty.", form.locale)) validator.CheckRequiredInput(form.InvoiceNumberFormat, gettext("Invoice number format can not be empty.", form.locale))
validator.CheckValidInteger(form.NextInvoiceNumber, 1, math.MaxInt32, gettext("Next invoice number must be a number greater than zero.", form.locale))
return form.contactForm.Validate(ctx, conn) && validator.AllOK() return form.contactForm.Validate(ctx, conn) && validator.AllOK()
} }
func (form *taxDetailsForm) mustFillFromDatabase(ctx context.Context, conn *Conn, company *Company) *taxDetailsForm { func (form *taxDetailsForm) mustFillFromDatabase(ctx context.Context, conn *Conn, company *Company) *taxDetailsForm {
err := conn.QueryRow(ctx, "select business_name, substr(vatin::text, 3), trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code, invoice_number_format, legal_disclaimer from company where company_id = $1", company.Id).Scan(form.BusinessName, form.VATIN, form.TradeName, form.Phone, form.Email, form.Web, form.Address, form.City, form.Province, form.PostalCode, form.Country, form.Currency, form.InvoiceNumberFormat, form.LegalDisclaimer) err := conn.QueryRow(ctx, `
select business_name
, substr(vatin::text, 3)
, trade_name
, phone
, email
, web
, address
, city
, province
, postal_code
, country_code
, currency_code
, invoice_number_format
, legal_disclaimer
, coalesce(invoice_number_counter.currval, 0) + 1
from company
left join invoice_number_counter
on invoice_number_counter.company_id = company.company_id
and year = date_part('year', current_date)
where company.company_id = $1`, company.Id).Scan(
form.BusinessName,
form.VATIN,
form.TradeName,
form.Phone,
form.Email,
form.Web,
form.Address,
form.City,
form.Province,
form.PostalCode,
form.Country,
form.Currency,
form.InvoiceNumberFormat,
form.LegalDisclaimer,
form.NextInvoiceNumber,
)
if err != nil {
panic(err)
}
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -183,6 +235,7 @@ func HandleCompanyTaxDetailsForm(w http.ResponseWriter, r *http.Request, _ httpr
} }
company := mustGetCompany(r) company := mustGetCompany(r)
conn.MustExec(r.Context(), "update company set business_name = $1, vatin = ($11 || $2)::vatin, trade_name = $3, phone = parse_packed_phone_number($4, $11), email = $5, web = $6, address = $7, city = $8, province = $9, postal_code = $10, country_code = $11, currency_code = $12, invoice_number_format = $13, legal_disclaimer = $14 where company_id = $15", form.BusinessName, form.VATIN, form.TradeName, form.Phone, form.Email, form.Web, form.Address, form.City, form.Province, form.PostalCode, form.Country, form.Currency, form.InvoiceNumberFormat, form.LegalDisclaimer, company.Id) conn.MustExec(r.Context(), "update company set business_name = $1, vatin = ($11 || $2)::vatin, trade_name = $3, phone = parse_packed_phone_number($4, $11), email = $5, web = $6, address = $7, city = $8, province = $9, postal_code = $10, country_code = $11, currency_code = $12, invoice_number_format = $13, legal_disclaimer = $14 where company_id = $15", form.BusinessName, form.VATIN, form.TradeName, form.Phone, form.Email, form.Web, form.Address, form.City, form.Province, form.PostalCode, form.Country, form.Currency, form.InvoiceNumberFormat, form.LegalDisclaimer, company.Id)
conn.MustExec(r.Context(), "update invoice_number_counter set currval = $1 where company_id = $2 and year = date_part('year', current_date)", form.NextInvoiceNumber.Integer()-1, company.Id)
if IsHTMxRequest(r) { if IsHTMxRequest(r) {
w.Header().Set(HxTrigger, "closeModal") w.Header().Set(HxTrigger, "closeModal")
w.WriteHeader(http.StatusNoContent) w.WriteHeader(http.StatusNoContent)

View File

@ -117,12 +117,11 @@ select throws_ok(
reset role; reset role;
select throws_ok( $$ select lives_ok( $$
insert into invoice_number_counter (company_id, year, currval) insert into invoice_number_counter (company_id, year, currval)
values (2, 2008, 0) values (2, 2008, 0)
$$, $$,
'23514', 'new row for relation "invoice_number_counter" violates check constraint "counter_always_positive"', 'Should allow starting a counter from zero'
'Should not allow starting a counter from zero'
); );
select throws_ok( $$ select throws_ok( $$

View File

@ -560,6 +560,21 @@ main > nav {
margin-bottom: 4rem; margin-bottom: 4rem;
} }
/* Tax Details */
#invoicing {
display: grid;
grid-template-columns: repeat(2, 1fr);
}
#invoicing .input:last-child {
grid-column-start: 1;
grid-column-end: 3;
}
#invoicing .input:last-child textarea {
min-height: 20ex;
}
/* Invoice */ /* Invoice */
.new-invoice-product input { .new-invoice-product input {

View File

@ -37,10 +37,11 @@
{{ template "select-field" .Currency }} {{ template "select-field" .Currency }}
</fieldset> </fieldset>
<fieldset> <fieldset id="invoicing">
<legend>{{( pgettext "Invoicing" "title" )}}</legend> <legend>{{( pgettext "Invoicing" "title" )}}</legend>
{{ template "input-field" .InvoiceNumberFormat }} {{ template "input-field" .InvoiceNumberFormat }}
{{ template "input-field" .NextInvoiceNumber }}
{{ template "input-field" .LegalDisclaimer }} {{ template "input-field" .LegalDisclaimer }}
</fieldset> </fieldset>
</form> </form>