diff --git a/deploy/invoice_number_counter.sql b/deploy/invoice_number_counter.sql index 255b8f3..2713e6b 100644 --- a/deploy/invoice_number_counter.sql +++ b/deploy/invoice_number_counter.sql @@ -9,7 +9,7 @@ set search_path to numerus, public; create table invoice_number_counter ( company_id integer not null, 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) ); diff --git a/pkg/company.go b/pkg/company.go index 8a5a823..8dcc4e0 100644 --- a/pkg/company.go +++ b/pkg/company.go @@ -5,6 +5,7 @@ import ( "errors" "github.com/julienschmidt/httprouter" "html/template" + "math" "net/http" "net/url" "strconv" @@ -87,6 +88,7 @@ type taxDetailsForm struct { *contactForm Currency *SelectField InvoiceNumberFormat *InputField + NextInvoiceNumber *InputField LegalDisclaimer *InputField } @@ -106,6 +108,15 @@ func newTaxDetailsForm(ctx context.Context, conn *Conn, locale *Locale) *taxDeta Type: "text", 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{ Name: "legal_disclaimer", Label: pgettext("input", "Legal disclaimer", locale), @@ -120,6 +131,7 @@ func (form *taxDetailsForm) Parse(r *http.Request) error { } form.Currency.FillValue(r) form.InvoiceNumberFormat.FillValue(r) + form.NextInvoiceNumber.FillValue(r) form.LegalDisclaimer.FillValue(r) return nil } @@ -128,11 +140,51 @@ func (form *taxDetailsForm) Validate(ctx context.Context, conn *Conn) bool { validator := newFormValidator() 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.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() } 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 { panic(err) } @@ -183,6 +235,7 @@ func HandleCompanyTaxDetailsForm(w http.ResponseWriter, r *http.Request, _ httpr } 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 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) { w.Header().Set(HxTrigger, "closeModal") w.WriteHeader(http.StatusNoContent) diff --git a/test/invoice_number_counter.sql b/test/invoice_number_counter.sql index c83c9e6..a2dd262 100644 --- a/test/invoice_number_counter.sql +++ b/test/invoice_number_counter.sql @@ -117,12 +117,11 @@ select throws_ok( reset role; -select throws_ok( $$ +select lives_ok( $$ insert into invoice_number_counter (company_id, year, currval) values (2, 2008, 0) $$, - '23514', 'new row for relation "invoice_number_counter" violates check constraint "counter_always_positive"', - 'Should not allow starting a counter from zero' + 'Should allow starting a counter from zero' ); select throws_ok( $$ diff --git a/web/static/numerus.css b/web/static/numerus.css index 573fb83..5b1f134 100644 --- a/web/static/numerus.css +++ b/web/static/numerus.css @@ -560,6 +560,21 @@ main > nav { 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 */ .new-invoice-product input { diff --git a/web/template/tax-details.gohtml b/web/template/tax-details.gohtml index 1825ab9..4a56f2d 100644 --- a/web/template/tax-details.gohtml +++ b/web/template/tax-details.gohtml @@ -37,10 +37,11 @@ {{ template "select-field" .Currency }} -
+
{{( pgettext "Invoicing" "title" )}} {{ template "input-field" .InvoiceNumberFormat }} + {{ template "input-field" .NextInvoiceNumber }} {{ template "input-field" .LegalDisclaimer }}