Remove status parameter from edit_expense and forms

For the same reasons as with expenses[0], users are no longer expected
to manually set invoice status, and is now linked to their collections.

In this case, however, we had to remove the ‘sent’ and ‘unpaid’ status
options, because these _should_ only be set manually, as there is no
way for the application to know when to set them.  Thus, there could
be inconsistencies, like invoices set to ‘unpaid’ when they actually
have collections, or invoices that were ‘sent’, then transitioned to
‘partial’/‘paid’ due to a collection, but then reset to ‘created’ if the
collection was deleted.

[0]: ac0143b2b0
This commit is contained in:
jordi fita mas 2024-08-26 10:42:38 +02:00
parent 7b1220c9f6
commit b815a18967
17 changed files with 294 additions and 109 deletions

View File

@ -153,9 +153,6 @@ select add_invoice(123, (current_date - '4 days'::interval)::date, 124, '', 123,
select add_collection(123, 157, (current_date - '2 days'::interval)::date, 123, 'Primer cobrament de FRA157', '1000.00', '{}'); select add_collection(123, 157, (current_date - '2 days'::interval)::date, 123, 'Primer cobrament de FRA157', '1000.00', '{}');
select add_invoice(123, (current_date - '1 days'::interval)::date, 123, '', 123, '{producte,mag}','{"(123,Or,\"Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a laigua règia.\",57.82,18,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.43,1,0.0,{124})"}'); select add_invoice(123, (current_date - '1 days'::interval)::date, 123, '', 123, '{producte,mag}','{"(123,Or,\"Metall de transició tou, brillant, groc, pesant, mal·leable, dúctil i que no reacciona amb la majoria de productes químics, però és sensible al clor i a laigua règia.\",57.82,18,0.05,{124})","(127,Cavall Fort,\"Revista quinzenal en llengua catalana i de còmic en català, destinada a infants i joves.\",3.43,1,0.0,{124})"}');
update invoice set invoice_status = 'unpaid' where invoice_id = 155;
update invoice set invoice_status = 'sent' where invoice_id = 156;
alter sequence expense_expense_id_seq restart with 123; alter sequence expense_expense_id_seq restart with 123;
select add_expense(123, (date_trunc('month', current_date) - '11 months + 14 day'::interval)::date, 130, 'ABC123', '256.12', '{124}', '{}'); select add_expense(123, (date_trunc('month', current_date) - '11 months + 14 day'::interval)::date, 130, 'ABC123', '256.12', '{124}', '{}');
select add_payment(123, 123, (date_trunc('month', current_date) - '11 months + 04 day'::interval)::date, 123, 'Pagament dABC123', '256.12', '{}'); select add_payment(123, 123, (date_trunc('month', current_date) - '11 months + 04 day'::interval)::date, 123, 'Pagament dABC123', '256.12', '{}');

View File

@ -30,4 +30,8 @@ values ('created', 'ca', 'Creada')
on conflict (invoice_status, lang_tag) do nothing on conflict (invoice_status, lang_tag) do nothing
; ;
update invoice set invoice_status = 'created' where invoice_status in ('sent', 'unpaid');
delete from invoice_status_i18n where invoice_status in ('sent', 'unpaid');
delete from invoice_status where invoice_status in ('sent', 'unpaid');
commit; commit;

View File

@ -14,7 +14,9 @@ begin;
set search_path to numerus, public; set search_path to numerus, public;
create or replace function edit_invoice(invoice_slug uuid, invoice_status text, contact_id integer, notes text, payment_method_id integer, tags tag_name[], products edited_invoice_product[]) returns uuid as drop function if exists edit_invoice(uuid, text, integer, text, integer, tag_name[], edited_invoice_product[]);
create or replace function edit_invoice(invoice_slug uuid, contact_id integer, notes text, payment_method_id integer, tags tag_name[], products edited_invoice_product[]) returns uuid as
$$ $$
declare declare
iid integer; iid integer;
@ -27,7 +29,6 @@ declare
begin begin
update invoice update invoice
set contact_id = edit_invoice.contact_id set contact_id = edit_invoice.contact_id
, invoice_status = edit_invoice.invoice_status
, notes = edit_invoice.notes , notes = edit_invoice.notes
, payment_method_id = edit_invoice.payment_method_id , payment_method_id = edit_invoice.payment_method_id
, tags = edit_invoice.tags , tags = edit_invoice.tags
@ -103,9 +104,9 @@ end;
$$ $$
language plpgsql; language plpgsql;
revoke execute on function edit_invoice(uuid, text, integer, text, integer, tag_name[], edited_invoice_product[]) from public; revoke execute on function edit_invoice(uuid, integer, text, integer, tag_name[], edited_invoice_product[]) from public;
grant execute on function edit_invoice(uuid, text, integer, text, integer, tag_name[], edited_invoice_product[]) to invoicer; grant execute on function edit_invoice(uuid, integer, text, integer, tag_name[], edited_invoice_product[]) to invoicer;
grant execute on function edit_invoice(uuid, text, integer, text, integer, tag_name[], edited_invoice_product[]) to admin; grant execute on function edit_invoice(uuid, integer, text, integer, tag_name[], edited_invoice_product[]) to admin;
commit; commit;

111
deploy/edit_invoice@v2.sql Normal file
View File

@ -0,0 +1,111 @@
-- Deploy numerus:edit_invoice to pg
-- requires: schema_numerus
-- requires: invoice
-- requires: currency
-- requires: parse_price
-- requires: edited_invoice_product
-- requires: tax
-- requires: invoice_product
-- requires: invoice_product_product
-- requires: invoice_product_tax
-- requires: tag_name
begin;
set search_path to numerus, public;
create or replace function edit_invoice(invoice_slug uuid, invoice_status text, contact_id integer, notes text, payment_method_id integer, tags tag_name[], products edited_invoice_product[]) returns uuid as
$$
declare
iid integer;
products_to_keep integer[];
products_to_delete integer[];
company integer;
ccode text;
product edited_invoice_product;
ipid integer;
begin
update invoice
set contact_id = edit_invoice.contact_id
, invoice_status = edit_invoice.invoice_status
, notes = edit_invoice.notes
, payment_method_id = edit_invoice.payment_method_id
, tags = edit_invoice.tags
where slug = invoice_slug
returning invoice_id, company_id, currency_code
into iid, company, ccode
;
if iid is null then
return null;
end if;
foreach product in array products
loop
if product.invoice_product_id is null then
insert into invoice_product (invoice_id, name, description, price, quantity, discount_rate)
select iid
, product.name
, coalesce(product.description, '')
, parse_price(product.price, currency.decimal_digits)
, product.quantity
, product.discount_rate
from currency
where currency_code = ccode
returning invoice_product_id
into ipid;
else
ipid := product.invoice_product_id;
update invoice_product
set name = product.name
, description = coalesce(product.description, '')
, price = parse_price(product.price, currency.decimal_digits)
, quantity = product.quantity
, discount_rate = product.discount_rate
from currency
where invoice_product_id = ipid
and currency_code = ccode;
end if;
products_to_keep := array_append(products_to_keep, ipid);
if product.product_id is null then
delete from invoice_product_product where invoice_product_id = ipid;
else
insert into invoice_product_product (invoice_product_id, product_id)
values (ipid, product.product_id)
on conflict (invoice_product_id) do update
set product_id = product.product_id;
end if;
delete from invoice_product_tax where invoice_product_id = ipid;
insert into invoice_product_tax (invoice_product_id, tax_id, tax_rate)
select ipid, tax_id, tax.rate
from tax
join unnest(product.tax) as ptax(tax_id) using (tax_id);
end loop;
select array_agg(invoice_product_id)
into products_to_delete
from invoice_product
where invoice_id = iid
and not (invoice_product_id = any(products_to_keep));
if array_length(products_to_delete, 1) > 0 then
delete from invoice_product_tax where invoice_product_id = any(products_to_delete);
delete from invoice_product_product where invoice_product_id = any(products_to_delete);
delete from invoice_product where invoice_product_id = any(products_to_delete);
end if;
return invoice_slug;
end;
$$
language plpgsql;
revoke execute on function edit_invoice(uuid, text, integer, text, integer, tag_name[], edited_invoice_product[]) from public;
grant execute on function edit_invoice(uuid, text, integer, text, integer, tag_name[], edited_invoice_product[]) to invoicer;
grant execute on function edit_invoice(uuid, text, integer, text, integer, tag_name[], edited_invoice_product[]) to admin;
commit;

View File

@ -262,7 +262,6 @@ func ServeInvoice(w http.ResponseWriter, r *http.Request, params httprouter.Para
form := newInvoiceForm(r.Context(), conn, locale, company) form := newInvoiceForm(r.Context(), conn, locale, company)
if invoiceToDuplicate := r.URL.Query().Get("duplicate"); ValidUuid(invoiceToDuplicate) { if invoiceToDuplicate := r.URL.Query().Get("duplicate"); ValidUuid(invoiceToDuplicate) {
form.MustFillFromDatabase(r.Context(), conn, invoiceToDuplicate) form.MustFillFromDatabase(r.Context(), conn, invoiceToDuplicate)
form.InvoiceStatus.Selected = []string{"created"}
} else if quoteToInvoice := r.URL.Query().Get("quote"); ValidUuid(quoteToInvoice) { } else if quoteToInvoice := r.URL.Query().Get("quote"); ValidUuid(quoteToInvoice) {
form.MustFillFromQuote(r.Context(), conn, quoteToInvoice) form.MustFillFromQuote(r.Context(), conn, quoteToInvoice)
} }
@ -833,7 +832,6 @@ type invoiceForm struct {
locale *Locale locale *Locale
company *Company company *Company
Number string Number string
InvoiceStatus *SelectField
Customer *SelectField Customer *SelectField
Date *InputField Date *InputField
Notes *InputField Notes *InputField
@ -848,13 +846,6 @@ func newInvoiceForm(ctx context.Context, conn *Conn, locale *Locale, company *Co
return &invoiceForm{ return &invoiceForm{
locale: locale, locale: locale,
company: company, company: company,
InvoiceStatus: &SelectField{
Name: "invoice_status",
Required: true,
Label: pgettext("input", "Invoice Status", locale),
Selected: []string{"created"},
Options: mustGetInvoiceStatusOptions(ctx, conn, locale),
},
Customer: &SelectField{ Customer: &SelectField{
Name: "customer", Name: "customer",
Label: pgettext("input", "Customer", locale), Label: pgettext("input", "Customer", locale),
@ -905,7 +896,6 @@ func (form *invoiceForm) Parse(r *http.Request) error {
if err := r.ParseMultipartForm(form.File.MaxSize); err != nil { if err := r.ParseMultipartForm(form.File.MaxSize); err != nil {
return err return err
} }
form.InvoiceStatus.FillValue(r)
form.Customer.FillValue(r) form.Customer.FillValue(r)
form.Date.FillValue(r) form.Date.FillValue(r)
form.Notes.FillValue(r) form.Notes.FillValue(r)
@ -933,7 +923,6 @@ func (form *invoiceForm) Parse(r *http.Request) error {
func (form *invoiceForm) Validate() bool { func (form *invoiceForm) Validate() bool {
validator := newFormValidator() validator := newFormValidator()
validator.CheckValidSelectOption(form.InvoiceStatus, gettext("Selected invoice status is not valid.", form.locale))
validator.CheckValidSelectOption(form.Customer, gettext("Selected customer is not valid.", form.locale)) validator.CheckValidSelectOption(form.Customer, gettext("Selected customer is not valid.", form.locale))
if validator.CheckRequiredInput(form.Date, gettext("Invoice date can not be empty.", form.locale)) { if validator.CheckRequiredInput(form.Date, gettext("Invoice date can not be empty.", form.locale)) {
validator.CheckValidDate(form.Date, gettext("Invoice date must be a valid date.", form.locale)) validator.CheckValidDate(form.Date, gettext("Invoice date must be a valid date.", form.locale))
@ -1043,13 +1032,10 @@ func (form *invoiceForm) InsertProduct(product *invoiceProductForm) {
func (form *invoiceForm) MustFillFromDatabase(ctx context.Context, conn *Conn, slug string) bool { func (form *invoiceForm) MustFillFromDatabase(ctx context.Context, conn *Conn, slug string) bool {
var invoiceId int var invoiceId int
selectedInvoiceStatus := form.InvoiceStatus.Selected
form.InvoiceStatus.Clear()
selectedPaymentMethod := form.PaymentMethod.Selected selectedPaymentMethod := form.PaymentMethod.Selected
form.PaymentMethod.Clear() form.PaymentMethod.Clear()
if notFoundErrorOrPanic(conn.QueryRow(ctx, ` if notFoundErrorOrPanic(conn.QueryRow(ctx, `
select invoice_id select invoice_id
, invoice_status
, contact_id , contact_id
, invoice_number , invoice_number
, invoice_date , invoice_date
@ -1058,9 +1044,8 @@ func (form *invoiceForm) MustFillFromDatabase(ctx context.Context, conn *Conn, s
, tags , tags
from invoice from invoice
where slug = $1 where slug = $1
`, slug).Scan(&invoiceId, form.InvoiceStatus, form.Customer, &form.Number, form.Date, form.Notes, form.PaymentMethod, form.Tags)) { `, slug).Scan(&invoiceId, form.Customer, &form.Number, form.Date, form.Notes, form.PaymentMethod, form.Tags)) {
form.PaymentMethod.Selected = selectedPaymentMethod form.PaymentMethod.Selected = selectedPaymentMethod
form.InvoiceStatus.Selected = selectedInvoiceStatus
return false return false
} }
form.Products = []*invoiceProductForm{} form.Products = []*invoiceProductForm{}
@ -1301,14 +1286,6 @@ func HandleUpdateInvoice(w http.ResponseWriter, r *http.Request, params httprout
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
if r.FormValue("quick") == "status" {
slug = conn.MustGetText(r.Context(), "", "update invoice set invoice_status = $1 where slug = $2 returning slug", form.InvoiceStatus, slug)
if slug == "" {
http.NotFound(w, r)
return
}
htmxRedirect(w, r, companyURI(mustGetCompany(r), "/invoices"))
} else {
if !form.Validate() { if !form.Validate() {
if !IsHTMxRequest(r) { if !IsHTMxRequest(r) {
w.WriteHeader(http.StatusUnprocessableEntity) w.WriteHeader(http.StatusUnprocessableEntity)
@ -1316,7 +1293,7 @@ func HandleUpdateInvoice(w http.ResponseWriter, r *http.Request, params httprout
mustRenderEditInvoiceForm(w, r, slug, form) mustRenderEditInvoiceForm(w, r, slug, form)
return return
} }
slug = conn.MustGetText(r.Context(), "", "select edit_invoice($1, $2, $3, $4, $5, $6, $7)", slug, form.InvoiceStatus, form.Customer, form.Notes, form.PaymentMethod, form.Tags, EditedInvoiceProductArray(form.Products)) slug = conn.MustGetText(r.Context(), "", "select edit_invoice($1, $2, $3, $4, $5, $6)", slug, form.Customer, form.Notes, form.PaymentMethod, form.Tags, EditedInvoiceProductArray(form.Products))
if slug == "" { if slug == "" {
http.NotFound(w, r) http.NotFound(w, r)
return return
@ -1326,7 +1303,6 @@ func HandleUpdateInvoice(w http.ResponseWriter, r *http.Request, params httprout
} }
htmxRedirect(w, r, companyURI(company, "/invoices/"+slug)) htmxRedirect(w, r, companyURI(company, "/invoices/"+slug))
} }
}
func htmxRedirect(w http.ResponseWriter, r *http.Request, uri string) { func htmxRedirect(w http.ResponseWriter, r *http.Request, uri string) {
if IsHTMxRequest(r) { if IsHTMxRequest(r) {

View File

@ -11,4 +11,18 @@ update invoice set invoice_status = 'created' where invoice_status = 'partial';
delete from invoice_status_i18n where invoice_status = 'partial'; delete from invoice_status_i18n where invoice_status = 'partial';
delete from invoice_status where invoice_status = 'partial'; delete from invoice_status where invoice_status = 'partial';
insert into invoice_status (invoice_status, name)
values ('sent', 'Sent')
, ('unpaid', 'Unpaid')
on conflict (invoice_status) do nothing
;
insert into invoice_status_i18n (invoice_status, lang_tag, name)
values ('sent', 'ca', 'Enviada')
, ('unpaid', 'ca', 'No cobrada')
, ('sent', 'es', 'Enviada')
, ('unpaid', 'es', 'No cobrada')
on conflict (invoice_status, lang_tag) do nothing
;
commit; commit;

View File

@ -1,7 +1,113 @@
-- Revert numerus:edit_invoice from pg -- Deploy numerus:edit_invoice to pg
-- requires: schema_numerus
-- requires: invoice
-- requires: currency
-- requires: parse_price
-- requires: edited_invoice_product
-- requires: tax
-- requires: invoice_product
-- requires: invoice_product_product
-- requires: invoice_product_tax
-- requires: tag_name
begin; begin;
drop function if exists numerus.edit_invoice(uuid, text, integer, text, integer, numerus.tag_name[], numerus.edited_invoice_product[]); set search_path to numerus, public;
drop function if exists edit_invoice(uuid, integer, text, integer, tag_name[], edited_invoice_product[]);
create or replace function edit_invoice(invoice_slug uuid, invoice_status text, contact_id integer, notes text, payment_method_id integer, tags tag_name[], products edited_invoice_product[]) returns uuid as
$$
declare
iid integer;
products_to_keep integer[];
products_to_delete integer[];
company integer;
ccode text;
product edited_invoice_product;
ipid integer;
begin
update invoice
set contact_id = edit_invoice.contact_id
, invoice_status = edit_invoice.invoice_status
, notes = edit_invoice.notes
, payment_method_id = edit_invoice.payment_method_id
, tags = edit_invoice.tags
where slug = invoice_slug
returning invoice_id, company_id, currency_code
into iid, company, ccode
;
if iid is null then
return null;
end if;
foreach product in array products
loop
if product.invoice_product_id is null then
insert into invoice_product (invoice_id, name, description, price, quantity, discount_rate)
select iid
, product.name
, coalesce(product.description, '')
, parse_price(product.price, currency.decimal_digits)
, product.quantity
, product.discount_rate
from currency
where currency_code = ccode
returning invoice_product_id
into ipid;
else
ipid := product.invoice_product_id;
update invoice_product
set name = product.name
, description = coalesce(product.description, '')
, price = parse_price(product.price, currency.decimal_digits)
, quantity = product.quantity
, discount_rate = product.discount_rate
from currency
where invoice_product_id = ipid
and currency_code = ccode;
end if;
products_to_keep := array_append(products_to_keep, ipid);
if product.product_id is null then
delete from invoice_product_product where invoice_product_id = ipid;
else
insert into invoice_product_product (invoice_product_id, product_id)
values (ipid, product.product_id)
on conflict (invoice_product_id) do update
set product_id = product.product_id;
end if;
delete from invoice_product_tax where invoice_product_id = ipid;
insert into invoice_product_tax (invoice_product_id, tax_id, tax_rate)
select ipid, tax_id, tax.rate
from tax
join unnest(product.tax) as ptax(tax_id) using (tax_id);
end loop;
select array_agg(invoice_product_id)
into products_to_delete
from invoice_product
where invoice_id = iid
and not (invoice_product_id = any(products_to_keep));
if array_length(products_to_delete, 1) > 0 then
delete from invoice_product_tax where invoice_product_id = any(products_to_delete);
delete from invoice_product_product where invoice_product_id = any(products_to_delete);
delete from invoice_product where invoice_product_id = any(products_to_delete);
end if;
return invoice_slug;
end;
$$
language plpgsql;
revoke execute on function edit_invoice(uuid, text, integer, text, integer, tag_name[], edited_invoice_product[]) from public;
grant execute on function edit_invoice(uuid, text, integer, text, integer, tag_name[], edited_invoice_product[]) to invoicer;
grant execute on function edit_invoice(uuid, text, integer, text, integer, tag_name[], edited_invoice_product[]) to admin;
commit; commit;

View File

@ -0,0 +1,7 @@
-- Revert numerus:edit_invoice from pg
begin;
drop function if exists numerus.edit_invoice(uuid, text, integer, text, integer, numerus.tag_name[], numerus.edited_invoice_product[]);
commit;

View File

@ -159,10 +159,11 @@ add_expense [add_expense@v2] 2024-08-12T23:48:07Z jordi fita mas <jordi@tandem.b
edit_expense [edit_expense@v2] 2024-08-12T23:53:48Z jordi fita mas <jordi@tandem.blog> # Remove the expense_status from edit_expense edit_expense [edit_expense@v2] 2024-08-12T23:53:48Z jordi fita mas <jordi@tandem.blog> # Remove the expense_status from edit_expense
collection [roles schema_numerus company payment_account currency tag_name payment_status extension_pgcrypto] 2024-08-18T02:54:28Z jordi fita mas <jordi@tandem.blog> # Add relation of cash (payment) collection collection [roles schema_numerus company payment_account currency tag_name payment_status extension_pgcrypto] 2024-08-18T02:54:28Z jordi fita mas <jordi@tandem.blog> # Add relation of cash (payment) collection
invoice_collection [roles schema_numerus invoice collection] 2024-08-18T03:22:09Z jordi fita mas <jordi@tandem.blog> # Add relation of invoice collections invoice_collection [roles schema_numerus invoice collection] 2024-08-18T03:22:09Z jordi fita mas <jordi@tandem.blog> # Add relation of invoice collections
available_invoice_status [available_invoice_status@v2] 2024-08-18T03:34:13Z jordi fita mas <jordi@tandem.blog> # Add “partial” invoice status available_invoice_status [available_invoice_status@v2] 2024-08-18T03:34:13Z jordi fita mas <jordi@tandem.blog> # Add “partial” invoice status, and remove “sent” and “unpaid”
update_invoice_collection_status [roles schema_numerus invoice collection invoice_collection invoice_amount available_invoice_status available_payment_status] 2024-08-19T01:20:56Z jordi fita mas <jordi@tandem.blog> # Add function to update invoice and collection status update_invoice_collection_status [roles schema_numerus invoice collection invoice_collection invoice_amount available_invoice_status available_payment_status] 2024-08-19T01:20:56Z jordi fita mas <jordi@tandem.blog> # Add function to update invoice and collection status
add_collection [roles schema_numerus collection invoice_collection company currency parse_price tag_name update_invoice_collection_status] 2024-08-19T01:26:01Z jordi fita mas <jordi@tandem.blog> # Add function to insert new collections add_collection [roles schema_numerus collection invoice_collection company currency parse_price tag_name update_invoice_collection_status] 2024-08-19T01:26:01Z jordi fita mas <jordi@tandem.blog> # Add function to insert new collections
edit_collection [roles schema_numerus collection invoice_collection currency parse_price tag_name update_invoice_collection_status] 2024-08-19T23:58:36Z jordi fita mas <jordi@tandem.blog> # Add function to update collections edit_collection [roles schema_numerus collection invoice_collection currency parse_price tag_name update_invoice_collection_status] 2024-08-19T23:58:36Z jordi fita mas <jordi@tandem.blog> # Add function to update collections
collection_attachment [roles schema_numerus collection] 2024-08-20T00:34:09Z jordi fita mas <jordi@tandem.blog> # Add relation of collection attachments collection_attachment [roles schema_numerus collection] 2024-08-20T00:34:09Z jordi fita mas <jordi@tandem.blog> # Add relation of collection attachments
attach_to_collection [roles schema_numerus collection collection_attachment] 2024-08-20T00:41:53Z jordi fita mas <jordi@tandem.blog> # Add function to attach files to collections attach_to_collection [roles schema_numerus collection collection_attachment] 2024-08-20T00:41:53Z jordi fita mas <jordi@tandem.blog> # Add function to attach files to collections
remove_collection [roles schema_numerus invoice_collection collection collection_attachment update_invoice_collection_status] 2024-08-20T00:47:27Z jordi fita mas <jordi@tandem.blog> # Add function to remove collections remove_collection [roles schema_numerus invoice_collection collection collection_attachment update_invoice_collection_status] 2024-08-20T00:47:27Z jordi fita mas <jordi@tandem.blog> # Add function to remove collections
edit_invoice [edit_invoice@v2] 2024-08-26T08:18:08Z jordi fita mas <jordi@tandem.blog> # Remove the invoice_status parameter from edit_invoice

View File

@ -9,15 +9,15 @@ select plan(15);
set search_path to auth, numerus, public; set search_path to auth, numerus, public;
select has_function('numerus', 'edit_invoice', array ['uuid', 'text', 'integer', 'text', 'integer', 'tag_name[]', 'edited_invoice_product[]']); select has_function('numerus', 'edit_invoice', array ['uuid', 'integer', 'text', 'integer', 'tag_name[]', 'edited_invoice_product[]']);
select function_lang_is('numerus', 'edit_invoice', array ['uuid', 'text', 'integer', 'text', 'integer', 'tag_name[]', 'edited_invoice_product[]'], 'plpgsql'); select function_lang_is('numerus', 'edit_invoice', array ['uuid', 'integer', 'text', 'integer', 'tag_name[]', 'edited_invoice_product[]'], 'plpgsql');
select function_returns('numerus', 'edit_invoice', array ['uuid', 'text', 'integer', 'text', 'integer', 'tag_name[]', 'edited_invoice_product[]'], 'uuid'); select function_returns('numerus', 'edit_invoice', array ['uuid', 'integer', 'text', 'integer', 'tag_name[]', 'edited_invoice_product[]'], 'uuid');
select isnt_definer('numerus', 'edit_invoice', array ['uuid', 'text', 'integer', 'text', 'integer', 'tag_name[]', 'edited_invoice_product[]']); select isnt_definer('numerus', 'edit_invoice', array ['uuid', 'integer', 'text', 'integer', 'tag_name[]', 'edited_invoice_product[]']);
select volatility_is('numerus', 'edit_invoice', array ['uuid', 'text', 'integer', 'text', 'integer', 'tag_name[]', 'edited_invoice_product[]'], 'volatile'); select volatility_is('numerus', 'edit_invoice', array ['uuid', 'integer', 'text', 'integer', 'tag_name[]', 'edited_invoice_product[]'], 'volatile');
select function_privs_are('numerus', 'edit_invoice', array ['uuid', 'text', 'integer', 'text', 'integer', 'tag_name[]', 'edited_invoice_product[]'], 'guest', array []::text[]); select function_privs_are('numerus', 'edit_invoice', array ['uuid', 'integer', 'text', 'integer', 'tag_name[]', 'edited_invoice_product[]'], 'guest', array []::text[]);
select function_privs_are('numerus', 'edit_invoice', array ['uuid', 'text', 'integer', 'text', 'integer', 'tag_name[]', 'edited_invoice_product[]'], 'invoicer', array ['EXECUTE']); select function_privs_are('numerus', 'edit_invoice', array ['uuid', 'integer', 'text', 'integer', 'tag_name[]', 'edited_invoice_product[]'], 'invoicer', array ['EXECUTE']);
select function_privs_are('numerus', 'edit_invoice', array ['uuid', 'text', 'integer', 'text', 'integer', 'tag_name[]', 'edited_invoice_product[]'], 'admin', array ['EXECUTE']); select function_privs_are('numerus', 'edit_invoice', array ['uuid', 'integer', 'text', 'integer', 'tag_name[]', 'edited_invoice_product[]'], 'admin', array ['EXECUTE']);
select function_privs_are('numerus', 'edit_invoice', array ['uuid', 'text', 'integer', 'text', 'integer', 'tag_name[]', 'edited_invoice_product[]'], 'authenticator', array []::text[]); select function_privs_are('numerus', 'edit_invoice', array ['uuid', 'integer', 'text', 'integer', 'tag_name[]', 'edited_invoice_product[]'], 'authenticator', array []::text[]);
set client_min_messages to warning; set client_min_messages to warning;
@ -100,19 +100,19 @@ values (19, 4, 0.21)
; ;
select lives_ok( select lives_ok(
$$ select edit_invoice('7ac3ae0e-b0c1-4206-a19b-0be20835edd4', 'paid', 13, 'Notes 1', 112, array['tag1'], '{"(20,,p1.0,D1,11.01,2,0.50,{4})","(,,p1.3,D3,33.33,3,0.05,{3})"}') $$, $$ select edit_invoice('7ac3ae0e-b0c1-4206-a19b-0be20835edd4', 13, 'Notes 1', 112, array['tag1'], '{"(20,,p1.0,D1,11.01,2,0.50,{4})","(,,p1.3,D3,33.33,3,0.05,{3})"}') $$,
'Should be able to edit the first invoice' 'Should be able to edit the first invoice'
); );
select lives_ok( select lives_ok(
$$ select edit_invoice('b57b980b-247b-4be4-a0b7-03a7819c53ae', 'sent', 12, 'Notes 2', 111, array['tag1', 'tag3'], '{"(21,7,P1.1,,11.11,1,0.0,{3})","(22,8,p2.1,D2,24.00,3,0.75,\"{3,4}\")","(,9,p3.3,,36.36,2,0.05,{4})"}') $$, $$ select edit_invoice('b57b980b-247b-4be4-a0b7-03a7819c53ae', 12, 'Notes 2', 111, array['tag1', 'tag3'], '{"(21,7,P1.1,,11.11,1,0.0,{3})","(22,8,p2.1,D2,24.00,3,0.75,\"{3,4}\")","(,9,p3.3,,36.36,2,0.05,{4})"}') $$,
'Should be able to edit the second invoice' 'Should be able to edit the second invoice'
); );
select bag_eq( select bag_eq(
$$ select invoice_number, invoice_date, contact_id, invoice_status, notes, tags, payment_method_id from invoice $$, $$ select invoice_number, invoice_date, contact_id, invoice_status, notes, tags, payment_method_id from invoice $$,
$$ values ('INV1', '2023-03-10'::date, 13, 'paid', 'Notes 1', '{tag1}'::tag_name[], 112) $$ values ('INV1', '2023-03-10'::date, 13, 'created', 'Notes 1', '{tag1}'::tag_name[], 112)
, ('INV2', '2023-03-09'::date, 12, 'sent', 'Notes 2', '{tag1,tag3}'::tag_name[], 111) , ('INV2', '2023-03-09'::date, 12, 'created', 'Notes 2', '{tag1,tag3}'::tag_name[], 111)
$$, $$,
'Should have updated all invoices' 'Should have updated all invoices'
); );

View File

@ -5,20 +5,14 @@ begin;
set search_path to numerus; set search_path to numerus;
select 1 / count(*) from invoice_status where invoice_status = 'created' and name ='Created'; select 1 / count(*) from invoice_status where invoice_status = 'created' and name ='Created';
select 1 / count(*) from invoice_status where invoice_status = 'sent' and name ='Sent';
select 1 / count(*) from invoice_status where invoice_status = 'partial' and name ='Partial'; select 1 / count(*) from invoice_status where invoice_status = 'partial' and name ='Partial';
select 1 / count(*) from invoice_status where invoice_status = 'paid' and name ='Paid'; select 1 / count(*) from invoice_status where invoice_status = 'paid' and name ='Paid';
select 1 / count(*) from invoice_status where invoice_status = 'unpaid' and name ='Unpaid';
select 1 / count(*) from invoice_status_i18n where invoice_status = 'created' and name ='Creada' and lang_tag = 'ca'; select 1 / count(*) from invoice_status_i18n where invoice_status = 'created' and name ='Creada' and lang_tag = 'ca';
select 1 / count(*) from invoice_status_i18n where invoice_status = 'created' and name ='Creada' and lang_tag = 'es'; select 1 / count(*) from invoice_status_i18n where invoice_status = 'created' and name ='Creada' and lang_tag = 'es';
select 1 / count(*) from invoice_status_i18n where invoice_status = 'sent' and name ='Enviada' and lang_tag= 'ca';
select 1 / count(*) from invoice_status_i18n where invoice_status = 'sent' and name ='Enviada' and lang_tag= 'es';
select 1 / count(*) from invoice_status_i18n where invoice_status = 'partial' and name ='Parcial' and lang_tag = 'ca'; select 1 / count(*) from invoice_status_i18n where invoice_status = 'partial' and name ='Parcial' and lang_tag = 'ca';
select 1 / count(*) from invoice_status_i18n where invoice_status = 'partial' and name ='Parcial' and lang_tag = 'es'; select 1 / count(*) from invoice_status_i18n where invoice_status = 'partial' and name ='Parcial' and lang_tag = 'es';
select 1 / count(*) from invoice_status_i18n where invoice_status = 'paid' and name ='Cobrada' and lang_tag= 'ca'; select 1 / count(*) from invoice_status_i18n where invoice_status = 'paid' and name ='Cobrada' and lang_tag= 'ca';
select 1 / count(*) from invoice_status_i18n where invoice_status = 'paid' and name ='Cobrada' and lang_tag= 'es'; select 1 / count(*) from invoice_status_i18n where invoice_status = 'paid' and name ='Cobrada' and lang_tag= 'es';
select 1 / count(*) from invoice_status_i18n where invoice_status = 'unpaid' and name ='No cobrada' and lang_tag= 'ca';
select 1 / count(*) from invoice_status_i18n where invoice_status = 'unpaid' and name ='No cobrada' and lang_tag= 'es';
rollback; rollback;

View File

@ -2,6 +2,6 @@
begin; begin;
select has_function_privilege('numerus.edit_invoice(uuid, text, integer, text, integer, numerus.tag_name[], numerus.edited_invoice_product[])', 'execute'); select has_function_privilege('numerus.edit_invoice(uuid, integer, text, integer, numerus.tag_name[], numerus.edited_invoice_product[])', 'execute');
rollback; rollback;

View File

@ -0,0 +1,7 @@
-- Verify numerus:edit_invoice on pg
begin;
select has_function_privilege('numerus.edit_invoice(uuid, text, integer, text, integer, numerus.tag_name[], numerus.edited_invoice_product[])', 'execute');
rollback;

View File

@ -648,21 +648,15 @@ main > nav {
text-decoration: none; text-decoration: none;
} }
.quote-status, .quote-status {
.expense-status,
.invoice-status {
position: relative; position: relative;
} }
.quote-status summary, .quote-status summary {
.expense-status summary,
.invoice-status summary {
height: 3rem; height: 3rem;
} }
.quote-status ul, .quote-status ul {
.expense-status ul,
.invoice-status ul {
position: absolute; position: absolute;
top: 0; top: 0;
left: 100%; left: 100%;
@ -672,9 +666,7 @@ main > nav {
gap: 1rem; gap: 1rem;
} }
.quote-status button, .quote-status button {
.expense-status button,
.invoice-status button {
border: 0; border: 0;
min-width: 15rem; min-width: 15rem;
} }
@ -687,8 +679,7 @@ main > nav {
text-transform: uppercase; text-transform: uppercase;
} }
[class^='quote-status-'], [class^='quote-status-'] {
[class^='invoice-status-'] {
cursor: pointer; cursor: pointer;
} }

View File

@ -49,7 +49,6 @@
{{ template "hidden-field" .Date }} {{ template "hidden-field" .Date }}
{{ template "tags-field" .Tags }} {{ template "tags-field" .Tags }}
{{ template "select-field" .PaymentMethod }} {{ template "select-field" .PaymentMethod }}
{{ template "select-field" .InvoiceStatus }}
{{ template "file-field" .File }} {{ template "file-field" .File }}
{{ template "input-field" .Notes }} {{ template "input-field" .Notes }}
</div> </div>

View File

@ -87,29 +87,7 @@
<td><a href="{{ companyURI "/invoices/"}}{{ .Slug }}" data-hx-target="main" <td><a href="{{ companyURI "/invoices/"}}{{ .Slug }}" data-hx-target="main"
data-hx-boost="true">{{ .Number }}</a></td> data-hx-boost="true">{{ .Number }}</a></td>
<td>{{ .CustomerName }}</td> <td>{{ .CustomerName }}</td>
<td> <td class="invoice-status-{{ .Status }}">{{ .StatusLabel }}</td>
<details class="invoice-status menu">
<summary class="invoice-status-{{ .Status }}">{{ .StatusLabel }}</summary>
<form action="{{companyURI "/invoices/"}}{{ .Slug }}" method="POST"
enctype="multipart/form-data" data-hx-boost="true">
{{ csrfToken }}
{{ putMethod }}
<input type="hidden" name="quick" value="status">
<ul role="menu">
{{- range $status, $name := $.InvoiceStatuses }}
{{- if ne $status $invoice.Status }}
<li role="presentation">
<button role="menuitem" type="submit"
name="invoice_status" value="{{ $status }}"
class="invoice-status-{{ $status }}"
>{{ $name }}</button>
</li>
{{- end }}
{{- end }}
</ul>
</form>
</details>
</td>
<td data-hx-get="{{companyURI "/invoices/"}}{{ .Slug }}/tags/edit" <td data-hx-get="{{companyURI "/invoices/"}}{{ .Slug }}/tags/edit"
data-hx-target="this" data-hx-target="this"
data-hx-swap="outerHTML" data-hx-swap="outerHTML"

View File

@ -45,7 +45,6 @@
{{- end }} {{- end }}
<div class="invoice-data"> <div class="invoice-data">
{{ template "hidden-select-field" .InvoiceStatus }}
{{ template "select-field" .Customer }} {{ template "select-field" .Customer }}
{{ template "input-field" .Date }} {{ template "input-field" .Date }}
{{ template "tags-field" .Tags }} {{ template "tags-field" .Tags }}