Compare commits

...

2 Commits

Author SHA1 Message Date
jordi fita mas f77f933e4a Add the payment method to invoices 2023-03-05 18:50:57 +01:00
jordi fita mas 93e92cf62d Move call to mustGetTaxOptions of AddProducts
I was trying to query the database while the connection was still busy
quering the products.
2023-03-05 18:43:22 +01:00
15 changed files with 87 additions and 71 deletions

View File

@ -14,7 +14,7 @@ begin;
set search_path to numerus, public;
create or replace function add_invoice(company_id integer, invoice_number text, invoice_date date, contact_id integer, notes text, products new_invoice_product[]) returns uuid as
create or replace function add_invoice(company_id integer, invoice_number text, invoice_date date, contact_id integer, notes text, payment_method_id integer, products new_invoice_product[]) returns uuid as
$$
declare
iid integer;
@ -27,13 +27,14 @@ begin
invoice_number = next_invoice_number(company_id, invoice_date);
end if;
insert into invoice (company_id, invoice_number, invoice_date, contact_id, notes, currency_code)
insert into invoice (company_id, invoice_number, invoice_date, contact_id, notes, currency_code, payment_method_id)
select company.company_id
, invoice_number
, invoice_date
, contact_id
, notes
, currency_code
, add_invoice.payment_method_id
from company
where company.company_id = add_invoice.company_id
returning invoice_id, slug, currency_code
@ -65,8 +66,8 @@ end;
$$
language plpgsql;
revoke execute on function add_invoice(integer, text, date, integer, text, new_invoice_product[]) from public;
grant execute on function add_invoice(integer, text, date, integer, text, new_invoice_product[]) to invoicer;
grant execute on function add_invoice(integer, text, date, integer, text, new_invoice_product[]) to admin;
revoke execute on function add_invoice(integer, text, date, integer, text, integer, new_invoice_product[]) from public;
grant execute on function add_invoice(integer, text, date, integer, text, integer, new_invoice_product[]) to invoicer;
grant execute on function add_invoice(integer, text, date, integer, text, integer, new_invoice_product[]) to admin;
commit;

View File

@ -18,6 +18,7 @@ create table invoice (
contact_id integer not null references contact,
invoice_status text not null default 'created' references invoice_status,
notes text not null default '',
payment_method_id integer not null references payment_method,
currency_code text not null references currency,
created_at timestamptz not null default current_timestamp
);

View File

@ -127,6 +127,7 @@ type invoice struct {
Invoicer taxDetails
Invoicee taxDetails
Notes string
PaymentInstructions string
Products []*invoiceProduct
Subtotal string
Taxes [][]string
@ -164,7 +165,7 @@ func mustGetInvoice(ctx context.Context, conn *Conn, company *Company, slug stri
}
var invoiceId int
var decimalDigits int
if notFoundErrorOrPanic(conn.QueryRow(ctx, "select invoice_id, decimal_digits, invoice_number, invoice_date, notes, business_name, vatin, phone, email, address, city, province, postal_code, to_price(subtotal, decimal_digits), to_price(total, decimal_digits) from invoice join contact using (contact_id) join invoice_amount using (invoice_id) join currency using (currency_code) where invoice.slug = $1", slug).Scan(&invoiceId, &decimalDigits, &inv.Number, &inv.Date, &inv.Notes, &inv.Invoicee.Name, &inv.Invoicee.VATIN, &inv.Invoicee.Phone, &inv.Invoicee.Email, &inv.Invoicee.Address, &inv.Invoicee.City, &inv.Invoicee.Province, &inv.Invoicee.PostalCode, &inv.Subtotal, &inv.Total)) {
if notFoundErrorOrPanic(conn.QueryRow(ctx, "select invoice_id, decimal_digits, invoice_number, invoice_date, notes, instructions, business_name, vatin, phone, email, address, city, province, postal_code, to_price(subtotal, decimal_digits), to_price(total, decimal_digits) from invoice join payment_method using (payment_method_id) join contact using (contact_id) join invoice_amount using (invoice_id) join currency using (currency_code) where invoice.slug = $1", slug).Scan(&invoiceId, &decimalDigits, &inv.Number, &inv.Date, &inv.Notes, &inv.PaymentInstructions, &inv.Invoicee.Name, &inv.Invoicee.VATIN, &inv.Invoicee.Phone, &inv.Invoicee.Email, &inv.Invoicee.Address, &inv.Invoicee.City, &inv.Invoicee.Province, &inv.Invoicee.PostalCode, &inv.Subtotal, &inv.Total)) {
return nil
}
if err := conn.QueryRow(ctx, "select business_name, vatin, phone, email, address, city, province, postal_code, legal_disclaimer from company where company_id = $1", company.Id).Scan(&inv.Invoicer.Name, &inv.Invoicer.VATIN, &inv.Invoicer.Phone, &inv.Invoicer.Email, &inv.Invoicer.Address, &inv.Invoicer.City, &inv.Invoicer.Province, &inv.Invoicer.PostalCode, &inv.LegalDisclaimer); err != nil {
@ -290,7 +291,7 @@ func HandleAddInvoice(w http.ResponseWriter, r *http.Request, _ httprouter.Param
mustRenderNewInvoiceForm(w, r, form)
return
}
slug := conn.MustGetText(r.Context(), "", "select add_invoice($1, $2, $3, $4, $5, $6)", company.Id, form.Number, form.Date, form.Customer, form.Notes, NewInvoiceProductArray(form.Products))
slug := conn.MustGetText(r.Context(), "", "select add_invoice($1, $2, $3, $4, $5, $6, $7)", company.Id, form.Number, form.Date, form.Customer, form.Notes, form.PaymentMethod, NewInvoiceProductArray(form.Products))
http.Redirect(w, r, companyURI(company, "/invoices/"+slug), http.StatusSeeOther)
}
@ -427,9 +428,9 @@ func (form *invoiceForm) Update() {
func (form *invoiceForm) AddProducts(ctx context.Context, conn *Conn, productsId []string) {
index := len(form.Products)
taxOptions := mustGetTaxOptions(ctx, conn, form.company)
rows := conn.MustQuery(ctx, "select product_id, name, description, to_price(price, decimal_digits), 1 as quantity, 0 as discount, array_remove(array_agg(tax_id), null) from product join company using (company_id) join currency using (currency_code) left join product_tax using (product_id) where product_id = any ($1) group by product_id, name, description, price, decimal_digits", productsId)
defer rows.Close()
taxOptions := mustGetTaxOptions(ctx, conn, form.company)
for rows.Next() {
product := newInvoiceProductForm(index, form.company, form.locale, taxOptions)
if err := rows.Scan(product.ProductId, product.Name, product.Description, product.Price, product.Quantity, product.Discount, product.Tax); err != nil {

View File

@ -2,6 +2,6 @@
begin;
drop function if exists numerus.add_invoice(integer, text, date, integer, text, numerus.new_invoice_product[]);
drop function if exists numerus.add_invoice(integer, text, date, integer, text, integer, numerus.new_invoice_product[]);
commit;

View File

@ -9,15 +9,15 @@ select plan(17);
set search_path to auth, numerus, public;
select has_function('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'new_invoice_product[]']);
select function_lang_is('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'new_invoice_product[]'], 'plpgsql');
select function_returns('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'new_invoice_product[]'], 'uuid');
select isnt_definer('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'new_invoice_product[]']);
select volatility_is('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'new_invoice_product[]'], 'volatile');
select function_privs_are('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'new_invoice_product[]'], 'guest', array []::text[]);
select function_privs_are('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'new_invoice_product[]'], 'invoicer', array ['EXECUTE']);
select function_privs_are('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'new_invoice_product[]'], 'admin', array ['EXECUTE']);
select function_privs_are('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'new_invoice_product[]'], 'authenticator', array []::text[]);
select has_function('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'integer', 'new_invoice_product[]']);
select function_lang_is('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'integer', 'new_invoice_product[]'], 'plpgsql');
select function_returns('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'integer', 'new_invoice_product[]'], 'uuid');
select isnt_definer('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'integer', 'new_invoice_product[]']);
select volatility_is('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'integer', 'new_invoice_product[]'], 'volatile');
select function_privs_are('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'integer', 'new_invoice_product[]'], 'guest', array []::text[]);
select function_privs_are('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'integer', 'new_invoice_product[]'], 'invoicer', array ['EXECUTE']);
select function_privs_are('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'integer', 'new_invoice_product[]'], 'admin', array ['EXECUTE']);
select function_privs_are('numerus', 'add_invoice', array ['integer', 'text', 'date', 'integer', 'text', 'integer', 'new_invoice_product[]'], 'authenticator', array []::text[]);
set client_min_messages to warning;
@ -81,37 +81,37 @@ values (12, 1, 'Contact 2.1', 'XX555', '', '777-777-777', 'c@c', '', '', '', '',
select lives_ok(
$$ select add_invoice(1, 'INV001', '2023-02-15', 12, 'Notes 1', '{"(7,Product 1,Description 1,12.24,2,0.0,{4})"}') $$,
$$ select add_invoice(1, 'INV001', '2023-02-15', 12, 'Notes 1', 111, '{"(7,Product 1,Description 1,12.24,2,0.0,{4})"}') $$,
'Should be able to insert an invoice for the first company with a product'
);
select lives_ok(
$$ select add_invoice(1, 'INV002', '2023-02-16', 13, 'Notes 2', '{"(7,Product 1 bis,Description 1 bis,33.33,1,0.50,\"{4,3}\")","(8,Product 2,Description 2,24.00,3,0.75,{})"}') $$,
$$ select add_invoice(1, 'INV002', '2023-02-16', 13, 'Notes 2', 111, '{"(7,Product 1 bis,Description 1 bis,33.33,1,0.50,\"{4,3}\")","(8,Product 2,Description 2,24.00,3,0.75,{})"}') $$,
'Should be able to insert a second invoice for the first company with two product'
);
select lives_ok(
$$ select add_invoice(2, 'INV101', '2023-02-14', 15, 'Notes 3', '{"(11,Product 4.3,,11.11,1,0.0,{6})"}') $$,
$$ select add_invoice(2, 'INV101', '2023-02-14', 15, 'Notes 3', 222, '{"(11,Product 4.3,,11.11,1,0.0,{6})"}') $$,
'Should be able to insert an invoice for the second company with a product'
);
select lives_ok(
$$ select add_invoice(1, NULL, '2023-03-15', 13, '', '{"(7,PA1,DA1,44.33,1,0.50,{})"}') $$,
$$ select add_invoice(1, NULL, '2023-03-15', 13, '', 111, '{"(7,PA1,DA1,44.33,1,0.50,{})"}') $$,
'Should be able to insert an invoice with an autogenerated number'
);
select lives_ok(
$$ select add_invoice(2, ' ', '2023-04-16', 14, '', '{"(11,PA2,DA2,55.33,10,0.75,{})"}') $$,
$$ select add_invoice(2, ' ', '2023-04-16', 14, '', 222, '{"(11,PA2,DA2,55.33,10,0.75,{})"}') $$,
'Should consider non-null, but otherwise empty numbers the same as null and autogenerate it'
);
select bag_eq(
$$ select company_id, invoice_number, invoice_date, contact_id, invoice_status, notes, currency_code, created_at from invoice $$,
$$ values (1, 'INV001', '2023-02-15'::date, 12, 'created', 'Notes 1', 'EUR', current_timestamp)
, (1, 'INV002', '2023-02-16'::date, 13, 'created', 'Notes 2', 'EUR', current_timestamp)
, (2, 'INV101', '2023-02-14'::date, 15, 'created', 'Notes 3', 'USD', current_timestamp)
, (1, 'F20230006', '2023-03-15'::date, 13, 'created', '', 'EUR', current_timestamp)
, (2, 'INV056-23', '2023-04-16'::date, 14, 'created', '', 'USD', current_timestamp)
$$ select company_id, invoice_number, invoice_date, contact_id, invoice_status, notes, payment_method_id, currency_code, created_at from invoice $$,
$$ values (1, 'INV001', '2023-02-15'::date, 12, 'created', 'Notes 1', 111, 'EUR', current_timestamp)
, (1, 'INV002', '2023-02-16'::date, 13, 'created', 'Notes 2', 111, 'EUR', current_timestamp)
, (2, 'INV101', '2023-02-14'::date, 15, 'created', 'Notes 3', 222, 'USD', current_timestamp)
, (1, 'F20230006', '2023-03-15'::date, 13, 'created', '', 111, 'EUR', current_timestamp)
, (2, 'INV056-23', '2023-04-16'::date, 14, 'created', '', 222, 'USD', current_timestamp)
$$,
'Should have created all invoices'
);

View File

@ -5,7 +5,7 @@ reset client_min_messages;
begin;
select plan(72);
select plan(78);
set search_path to numerus, auth, public;
@ -75,6 +75,13 @@ select col_not_null('invoice', 'notes');
select col_has_default('invoice', 'notes');
select col_default_is('invoice', 'notes', '');
select has_column('invoice', 'payment_method_id');
select col_is_fk('invoice', 'payment_method_id');
select fk_ok('invoice', 'payment_method_id', 'payment_method', 'payment_method_id');
select col_type_is('invoice', 'payment_method_id', 'integer');
select col_not_null('invoice', 'payment_method_id');
select col_hasnt_default('invoice', 'payment_method_id');
select has_column('invoice', 'currency_code');
select col_is_fk('invoice', 'currency_code');
select fk_ok('invoice', 'currency_code', 'currency', 'currency_code');
@ -127,9 +134,9 @@ values (6, 2, 'Contact 1', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', ''
, (8, 4, 'Contact 2', 'XX666', '', '888-888-888', 'd@d', '', '', '', '', '', 'ES')
;
insert into invoice (company_id, invoice_number, contact_id, currency_code)
values (2, 'INV020001', 6, 'EUR')
, (4, 'INV040001', 8, 'EUR')
insert into invoice (company_id, invoice_number, contact_id, currency_code, payment_method_id)
values (2, 'INV020001', 6, 'EUR', 222)
, (4, 'INV040001', 8, 'EUR', 444)
;
@ -170,8 +177,8 @@ reset role;
select throws_ok( $$
insert into invoice (company_id, invoice_number, contact_id, currency_code)
values (2, ' ', 6, 'EUR')
insert into invoice (company_id, invoice_number, contact_id, currency_code, payment_method_id)
values (2, ' ', 6, 'EUR', 222)
$$,
'23514', 'new row for relation "invoice" violates check constraint "invoice_number_not_empty"',
'Should not allow invoice with blank number'

View File

@ -68,11 +68,11 @@ insert into contact (contact_id, company_id, business_name, vatin, trade_name, p
values (7, 1, 'Contact', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', '', 'ES')
;
insert into invoice (invoice_id, company_id, invoice_number, invoice_date, contact_id, currency_code)
values ( 8, 1, 'I1', current_date, 7, 'EUR')
, ( 9, 1, 'I2', current_date, 7, 'EUR')
, (10, 1, 'I3', current_date, 7, 'EUR')
, (11, 1, 'I4', current_date, 7, 'EUR')
insert into invoice (invoice_id, company_id, invoice_number, invoice_date, contact_id, currency_code, payment_method_id)
values ( 8, 1, 'I1', current_date, 7, 'EUR', '111')
, ( 9, 1, 'I2', current_date, 7, 'EUR', '111')
, (10, 1, 'I3', current_date, 7, 'EUR', '111')
, (11, 1, 'I4', current_date, 7, 'EUR', '111')
;
insert into invoice_product (invoice_product_id, invoice_id, product_id, name, price, quantity, discount_rate)

View File

@ -112,9 +112,9 @@ values (6, 2, 'Contact 1', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', ''
, (8, 4, 'Contact 2', 'XX666', '', '888-888-888', 'd@d', '', '', '', '', '', 'ES')
;
insert into invoice (invoice_id, company_id, invoice_number, contact_id, currency_code)
values (10, 2, 'INV020001', 6, 'EUR')
, (12, 4, 'INV040001', 8, 'EUR')
insert into invoice (invoice_id, company_id, invoice_number, contact_id, currency_code, payment_method_id)
values (10, 2, 'INV020001', 6, 'EUR', 222)
, (12, 4, 'INV040001', 8, 'EUR', 444)
;
insert into product (product_id, company_id, name, description, price)

View File

@ -68,11 +68,11 @@ insert into contact (contact_id, company_id, business_name, vatin, trade_name, p
values (7, 1, 'Contact', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', '', 'ES')
;
insert into invoice (invoice_id, company_id, invoice_number, invoice_date, contact_id, currency_code)
values ( 8, 1, 'I1', current_date, 7, 'EUR')
, ( 9, 1, 'I2', current_date, 7, 'EUR')
, (10, 1, 'I3', current_date, 7, 'EUR')
, (11, 1, 'I4', current_date, 7, 'EUR')
insert into invoice (invoice_id, company_id, invoice_number, invoice_date, contact_id, currency_code, payment_method_id)
values ( 8, 1, 'I1', current_date, 7, 'EUR', 111)
, ( 9, 1, 'I2', current_date, 7, 'EUR', 111)
, (10, 1, 'I3', current_date, 7, 'EUR', 111)
, (11, 1, 'I4', current_date, 7, 'EUR', 111)
;
insert into invoice_product (invoice_product_id, invoice_id, product_id, name, price, quantity, discount_rate)

View File

@ -95,9 +95,9 @@ values ( 9, 2, 'Customer 1', 'XX555', '', '777-777-777', 'c1@e', '', '', '', '',
, (10, 4, 'Customer 2', 'XX666', '', '888-888-888', 'c2@e', '', '', '', '', '', 'ES')
;
insert into invoice (invoice_id, company_id, invoice_number, contact_id, currency_code)
values (11, 2, 'INV001', 9, 'EUR')
, (12, 4, 'INV002', 10, 'EUR')
insert into invoice (invoice_id, company_id, invoice_number, contact_id, currency_code, payment_method_id)
values (11, 2, 'INV001', 9, 'EUR', 222)
, (12, 4, 'INV002', 10, 'EUR', 444)
;
insert into invoice_product (invoice_product_id, invoice_id, product_id, name, price)

View File

@ -68,11 +68,11 @@ insert into contact (contact_id, company_id, business_name, vatin, trade_name, p
values (7, 1, 'Contact', 'XX555', '', '777-777-777', 'c@c', '', '', '', '', '', 'ES')
;
insert into invoice (invoice_id, company_id, invoice_number, invoice_date, contact_id, currency_code)
values ( 8, 1, 'I1', current_date, 7, 'EUR')
, ( 9, 1, 'I2', current_date, 7, 'EUR')
, (10, 1, 'I3', current_date, 7, 'EUR')
, (11, 1, 'I4', current_date, 7, 'EUR')
insert into invoice (invoice_id, company_id, invoice_number, invoice_date, contact_id, currency_code, payment_method_id)
values ( 8, 1, 'I1', current_date, 7, 'EUR', 111)
, ( 9, 1, 'I2', current_date, 7, 'EUR', 111)
, (10, 1, 'I3', current_date, 7, 'EUR', 111)
, (11, 1, 'I4', current_date, 7, 'EUR', 111)
;
insert into invoice_product (invoice_product_id, invoice_id, product_id, name, price, quantity, discount_rate)

View File

@ -2,6 +2,6 @@
begin;
select has_function_privilege('numerus.add_invoice(integer, text, date, integer, text, numerus.new_invoice_product[])', 'execute');
select has_function_privilege('numerus.add_invoice(integer, text, date, integer, text, integer, numerus.new_invoice_product[])', 'execute');
rollback;

View File

@ -10,6 +10,7 @@ select invoice_id
, contact_id
, invoice_status
, notes
, payment_method_id
, currency_code
, created_at
from numerus.invoice

View File

@ -55,11 +55,15 @@
text-align: right;
}
.invoice .notes {
.invoice .notes, .invoice .payment-instructions {
white-space: pre;
text-align: right;
}
.invoice .notes + .payment-instructions {
margin-top: 5rem;
}
.invoice td {
vertical-align: top;
}

View File

@ -110,6 +110,7 @@
{{ if .Notes -}}
<p class="notes">{{ .Notes }}</p>
{{- end }}
<p class="payment-instructions">{{ .PaymentInstructions }}</p>
</div>
</article>