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:
parent
7b1220c9f6
commit
b815a18967
|
@ -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 l’aigua 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 l’aigua 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 d’ABC123', '256.12', '{}');
|
select add_payment(123, 123, (date_trunc('month', current_date) - '11 months + 04 day'::interval)::date, 123, 'Pagament d’ABC123', '256.12', '{}');
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
|
@ -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
|
||||||
|
@ -1325,7 +1302,6 @@ func HandleUpdateInvoice(w http.ResponseWriter, r *http.Request, params httprout
|
||||||
conn.MustQuery(r.Context(), "select attach_to_invoice($1, $2, $3, $4)", slug, form.File.OriginalFileName, form.File.ContentType, form.File.Content)
|
conn.MustQuery(r.Context(), "select attach_to_invoice($1, $2, $3, $4)", slug, form.File.OriginalFileName, form.File.ContentType, form.File.Content)
|
||||||
}
|
}
|
||||||
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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
|
@ -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
|
||||||
|
|
|
@ -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'
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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 }}
|
||||||
|
|
Loading…
Reference in New Issue