Commit Graph

101 Commits

Author SHA1 Message Date
jordi fita mas 4d2379555e Convert invoices to PDF with WeasyPrint
Although it is possible to just print the invoice from the browser, many
people will not even try an assume that they can not create a PDF for
the invoice.

I thought of using Groff or TeX to create the PDF, but it would mean
maintaining two templates in two different systems (HTML and whatever i
would use), and would probably look very different, because i do not
know Groff or TeX that well.

I wish there was a way to tell the browser to print to PDF, and it can
be done, but only with the Chrome Protocol to a server-side running
Chrome instance.   This works, but i would need a Chrome running as a
daemon.

I also wrote a Qt application that uses QWebEngine to print the PDF,
much like wkhtmltopdf, but with support for more recent HTML and CSS
standards.  Unfortunately, Qt 6.4’s embedded Chromium does not follow
break-page-inside as well as WeasyPrint does.

To use WeasyPrint, at first i wanted to reach the same URL as the user,
passing the cookie to WeasyPrint so that i can access the same invoice
as the user, something that can be done with wkhtmltopdf, but WeasyPrint
does not have such option.  I did it with a custom Python script, but
then i need to package and install that script, that is not that much
work, but using the Debian-provided script is even less work, and less
likely to drift when WeasyPrint changes API.

Also, it is unnecessary to do a network round-trip from Go to Python
back to Go, because i can already write the invoice HTML as is to
WeasyPrint’s stdin.
2023-02-26 17:26:09 +01:00
jordi fita mas 843f7746cf Adapt the invoice design to Oriol’s 2023-02-25 13:48:57 +01:00
jordi fita mas 419ac3ed46 Adjust invoice.css to work with WeasyPrint too
I am planning to use WeasyPrint to “generate PDF” from the same HTML
that the user view, but it seems that it does not support flex’s gap
and some other properties that i had to change to work in both user
agents.

I also moved the invoice’s “footer” inside the last product’s body
because i do not want the footer to be a “widow”.
2023-02-25 03:16:20 +01:00
jordi fita mas 18fba2964f Add invoice view, with print CSS
Had to group name and description rows in tbody because i do not want
to break them on pagination.

I also could not use tfoot for subtotal, taxes, and total because then
they appear on every page.

The disclaimer should appear only at the very bottom of the last page,
but i do not know how to do that; using position fixed shows it on
every page.
2023-02-24 12:22:15 +01:00
jordi fita mas 985f843e8e Show the invoice subtotal, taxes, and total when creating it 2023-02-23 15:31:57 +01:00
jordi fita mas 8dbf8ef2d0 Add currency_pattern to language relation
The design calls for rendering all amounts with their currency symbol,
but golang.org/x/text’s currency package always render the symbol in
front, which is wrong in Catalan and Spanish, and a lot of other
languages.

Consulting the Internet, the most popular package for that is
accounting[0], which is almost as useless because they confuse locale
with the currency’s country of origin’s “usual locale” (e.g., en-US for
USD), which is also wrong: in Catalan i need to write USD prices as
"1.234,56 $" regardless of what Americans do.

With accounting i have the recourse of initializing the struct that
holds all the “locale” information, which is also wrong because i have
to define the decimal and thousands separators, something that depends
only on the locale, next to the currency’s precision, that is
locale-independent.  But, since all CLDR data from golang.org/x/text
is inside an internal package, i can not access it and would need to
define all that information myself, which defeats the purpose of using
an external package.

Since for now i only need the format pattern for currency, i just saved
it into the database of available languages, that i do not expect to
grow too much.

[0]: https://github.com/leekchan/accounting
2023-02-23 12:12:33 +01:00
jordi fita mas 97ef02b0f9 Add views to compute taxes and total amount of invoices
They are not functions because i need to join them with the main
invoice relation, and although possible is a bit more awkward with
functions.

The taxes have their own relation because i will need them grouped by
their name in the PDF, so it will probably be a select for that
relation.
2023-02-22 14:39:38 +01:00
jordi fita mas c1e443e3bc Move the new product description before the taxes
Because in the “default view” that position has a lot of space that is
best used by a large text area than a simple number input.
2023-02-21 13:55:59 +01:00
jordi fita mas 045bf7ff6a Add the formnovalidate attribute to update and add products buttons
They are to complete the invoice, so it can be in an invalid date, but
we do not want to force people to finish all required inputs before they
can add products or update quantities, do we?

Now had to add the empty option label for customer in all cases, because
it could be empty, although that should be done regardless in case
someone has a browser that does not validate fields.
2023-02-14 12:55:19 +01:00
jordi fita mas 4903c8a3b9 Add the form to add products to an invoice and create invoices too
Still missing: the invoice number, that requires more tables and
possibly a PL/pgSQL function to do it properly.
2023-02-12 21:06:48 +01:00
jordi fita mas 5c15b9de20 Add the bare-bones form for invoices 2023-02-11 22:16:48 +01:00
jordi fita mas 4be2597a86 Allow multiple taxes, and even not tax, for products
It seems that we do not agree en whether the IRPF tax should be
something of the product or the contact, so we decided to make the
product have multiple taxes, just in case, and if only one is needed,
then users can just select one; no need to limit to one.
2023-02-08 13:47:36 +01:00
jordi fita mas 73ca559209 Add template for InputField of type textarea 2023-02-07 15:28:22 +01:00
jordi fita mas ae1949024b Allow optional select with empty label
This is not yet necessary, but the empty label is because i do not want
to select a default tax for products—at least, not without a setting for
it.

Since i need to add the required attribute now to select, because
otherwise the browser would allow sending that empty value, i did not
want to do it unconditionally, just in case.
2023-02-05 14:06:33 +01:00
jordi fita mas 60f9792e58 Convert from cents to “price” and back
I do not want to use floats in the Go lang application, because it is
not supposed to do anything with these values other than to print and
retrieve them from the user; all computations will be performed by
PostgreSQL in cents.

That means i have to “convert” from the price format that users expect
to see (e.g., 1.234,56) to cents (e.g., 123456) and back when passing
data between Go and PostgreSQL, and that conversion depends on the
currency’s decimal places.

At first i did everything in Go, but saw that i would need to do it in
a loop when retrieving the list of products, and immediately knew it was
a mistake—i needed a PL/pgSQL function for that.

I still need to convert from string to float, however, when printing the
value to the user.  Because the string representation is in C, but i
need to format it according to the locale with golang/x/text.  That
package has the information of how to correctly format numbers, but it
is in an internal package that i can not use, and numbers.Digit only
accepts numeric types, not a string.
2023-02-05 13:55:12 +01:00
jordi fita mas e9cc331ee0 Add products section
There is still some issues with the price field, because for now it is
in cents, but do not have time now to fix that.
2023-02-04 11:32:39 +01:00
jordi fita mas f611162b0e Move contacts templates in their own directory
This is what directories are for: namespacing; no need for cumbersome
file name prefixes.
2023-02-04 10:48:03 +01:00
jordi fita mas a0a3a5561d Add breadcrumbs 2023-02-03 13:58:10 +01:00
jordi fita mas 7d17620f48 Add the edit contact page 2023-02-03 13:57:43 +01:00
jordi fita mas 917db31227 Add cross-request forgery detection
I use the ten first digits of the cookie’s hash, that i believe it is
not a problem, has the advantage of not expiring until the user logs
out, and using a per user session token is explicitly allowed by
OWASP[0].

[0]: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#synchronizer-token-pattern
2023-02-02 11:39:34 +01:00
jordi fita mas 7a439a40cc Use a proper struct for the contact’s form
Our company is a kind-of contact, although it does not appear in the
contact section, thus i could embed the contact form inside the tax
details form to reuse all the common fields.
2023-02-01 14:34:40 +01:00
jordi fita mas 2883438157 Handle tax details and new tax forms with structs and validation
I implemented the Valuer and Scanner interfaces to InputField and
SelectField for better passing values between the database and Go.  I
had a conflict with the Value name and renamed the struct member to Val.

I also had to change the attributes array to be of type
template.HTMLAttr or html/template would replace `form="newtax"`
attribute to `zgotmplz="newtax"` because it deems it “unsafe”.  I do
not like having to use template.HTMLAttr when assigning values, but
i do not know what else i can do now.
2023-02-01 14:15:02 +01:00
jordi fita mas ff5b76b4f5 Use a “proper” struct for the login form
Similar to the profile form, the login form now parses and validates
itself, with the InputField structs that the templates expect.

I realized that i was doing more work than necessary when parsing fields
fro the profile form because i was repeating the operation and the field
name, so now it is a function of InputField.

This time i needed extra attributes for the login form.  I am not sure
that the Go source code needs to know about HTML attributes, but it was
the easiest way to pass them to the template.
2023-02-01 11:02:32 +01:00
jordi fita mas 75fd12bf1c Rename Customer to Contact
That section is intended for both customers and suppliers, collectively
called “contacts”.
2023-02-01 10:14:26 +01:00
jordi fita mas e0abf98bb1 Add custom function to get the current locale from templates
This is just to set the correct `lang` attribute on the HTML, so that
text readers can do its job and the `(optional)` suffix of labels gets
the correct ”translation”.
2023-01-31 15:45:51 +01:00
jordi fita mas 9f17f55547 Validate profile form and use templates for fields
Let’s start first with a non-fancy validation method with just if
conditionals instead of bringing yet another complicated library.  I
hope i do not regret it.

I wanted to move all the input field to a template because all that
gobbledygook with the .input div and repeating the label in the
placeholder was starting to annoy me.  Now with error messages was even
more concerning.

I did not know whether the label should be a part of the input fields
or something that the template should do.  At the end i decided that
it makes more sense to be part of the input field because in the error
messages i use that same label, thus the template does not have a say
in that, and, besides, it was just easier to write the template.

The same with the error messages: i’ve seen frameworks that have a map
with the field’s id/name to the error slice, but then it would be
a bit harder to write the template.

I added AddError functions instead of just using append inside the
validator function, and have a local variable for whether it all went
OK, because i was worried that i would leave out the `ok = false`
in some conditions.

I had started writing “constructors” functions for InputField and
SelectField, but then had to add other methods to change the required
field and who knows what else, and in the end it was easier to just
construct the field inline.
2023-01-31 15:40:12 +01:00
jordi fita mas 89256d5b4c Add nav link to dashboard 2023-01-31 13:29:56 +01:00
jordi fita mas 3117c9a268 Rename #profilemenu to #profile-menu, for consistency 2023-01-31 13:25:57 +01:00
jordi fita mas 4d452c5522 Fix a duplicate attribute in the _method hidden field 2023-01-31 13:07:55 +01:00
jordi fita mas 9aee33511a Move page titles to their respective templates
I have been thinking about that, and it does not make that much sense to
have the titles in the Go source anymore: most of them are static text
that i have to remember to set in the controller each time, and when
the time come i have to face a dynamic title i am sure i will manage
with only the template capabilities—worst comes worst, i can always
define a function.

On the other hand, there is no way i can define a template without its
title and i know that everytime that template is used, no matter what
controller rendered it, it will always have that title.
2023-01-31 13:07:17 +01:00
jordi fita mas 586db8d553 Fix the end tag of login’s email field 2023-01-30 16:52:13 +01:00
jordi fita mas 9be4bf538c Remove non-allowed form attribute from a label 2023-01-30 16:51:08 +01:00
jordi fita mas 1a7b9f6bdd Rename extension of templates to .gohtml
Apparently, there are tools that only know how to use that extensions
when referring to Go templates.
2023-01-30 16:48:21 +01:00
jordi fita mas 5a199a3d8e Add the contact relation and a rough first form 2023-01-29 15:14:31 +01:00
jordi fita mas 1712a81dfc Move the /profile under the company router
This is not necessary per se, but it makes my life easier because that
way i know which company the user was when she went to its profile and
can “return” back in the menu and future nav items.
2023-01-29 15:13:47 +01:00
jordi fita mas 666935b54c Add the tax relation with very rough form and handler 2023-01-28 14:18:58 +01:00
jordi fita mas 3b7d4e0d3e Add fieldset for currency in tax details page 2023-01-28 12:25:11 +01:00
jordi fita mas 7513030334 Fix the width of the country’s select 2023-01-28 00:11:56 +01:00
jordi fita mas 0a58e2699e Use a select for company’s country field
At first we thought that a regular text field would do, because we were
afraid that a dropdown would be worse from the point of view of user
experience, but then we realized that we need the country code for VAT
and phone validation, and we can not expect users to input that, of
course.

I had to add the first “i18n table” to the database with the name of all
countries in both Catalan and Spanish and Catalan; English is the
default.  For now i think i do not need a view that would select the
name based on the locale of the current request, because currently i do
not plan on adding any other such table —the currency uses the code and
the symbol, thus no need for localization.

However, now i need the language tag from the locale in order to get the
correct translation, and gotext does not give me any way to access the
inner language.  Thus the need for our Locale type.
2023-01-27 21:30:14 +01:00
jordi fita mas 57d5137913 Allow users update their tax details 2023-01-27 01:08:03 +01:00
jordi fita mas 627841d4dd Add the company relation and read-only form to edit
I do not have more time to update the update to the company today, but i
believe this is already a good amount of work for a commit.

The company is going to be used for row level security, as users will
only have access to the data from companies they are granted access, by
virtue of being in the company_user relation.

I did not know how add a row level security policy to the company_user
because i needed the to select on the same relation and this is not
allowed, because it would create an infinite loop.

Had to add the vat, pg_libphonenumber, and uri extensions in order to
validate VAT identification numbers, phone numbers, and URIs,
repectively.  These libraries are not in Debian, but i created packages
for them all in https://dev.tandem.ws/tandem.
2023-01-24 21:46:07 +01:00
jordi fita mas f9e22c0789 Complete the style of the profile dialog 2023-01-23 19:35:49 +01:00
jordi fita mas 22509dd683 Implement profile menu with <details>
It works better than with the weird hover behaviour i could do in CSS,
and it already has most of the aria roles needed.

The only tricky part is to allow closing it by clicking anywhere else,
that is done by “extending” the <summary> to the whole screen, with a
lower z-index than the menu but higher than the rest of controls, that
way we force people to click on that summary.
2023-01-23 18:52:18 +01:00
jordi fita mas c6eb1ef24e Change input field to be “Material-like”, as per design 2023-01-23 00:41:54 +01:00
jordi fita mas 1675ada70b Make the menu work as a menu 2023-01-22 22:30:15 +01:00
jordi fita mas 5505fa41c3 Use “layouts” for the common HTML between pages
Had to call xgettext on Go source files because now the title comes from
there, as i assume i will have titles like "Invoice #INVxxxx" that have
to come from the database that the template does not know.
2023-01-22 21:41:50 +01:00
jordi fita mas ea9e830a75 Add user_profile view to update the profile with form
Since users do not have access to the auth scheme, i had to add a view
that selects only the data that they can see of themselves (i.e., no
password or cookie).

I wanted to use the `request.user.id` setting that i set in
check_cookie, but this would be bad because anyone can change that
parameter and, since the view is created by the owner, could see and
*change* the values of everyone just by knowing their id.  Thus, now i
use the cookie instead, because it is way harder to figure out, and if
you already have it you can just set to your browser and the user is
fucked anyway; the database can not help here.

I **am** going to use the user id in row level security policies, but
not the value coming for the setting but instaed the one in the
`user_profile`, since it already is “derived” from the cookie, that’s
why i added that column to the view.

The profile includes the language, that i do not use it yet to switch
the locale, so i had to add a relation of the available languages, for
constraint purposes.  There is no NULL language, and instead i added the
“Undefined” language, with ‘und’ tag’, to represent “do not know/use
content negotiation”.

The languages in that relation are the same i used to have inside
locale.go, because there is no point on having options for languages i
do not have the translation for, so i now configure the list of
available languages user in content negotiation from that relation.

Finally, i have added all font from RemixIcon because that’s what we
used in the design and i am going to use quite a lot of them.

There is duplication in the views; i will address that in a different
commit.
2023-01-22 02:23:09 +01:00
jordi fita mas e38420697b Add Catalan and Spanish translation with gotext[3]
I had to choose between [1], [2], and [3].

As far as i could find, [1] is not easy to work with templates[4] and at
the moment is not maintained[5].

Both [2] and [3] use the same approach to be used from within templates:
you have to define a FuncMap with template functions that call the
message catalog.  Also, both libraries seems to be reasonably
maintained, and have packages in Debian’s repository.

However, [2] repeats the same mistakes that POSIX did with its
catalogs—using identifiers that are not the strings in the source
language—, however this time the catalogs are written in JSON or YAML!
This, somehow, makes things worse….

[3], the one i settled with, is fine and decently maintained.  There are
some surprising things, such as to be able to use directly the PO file,
and that it has higher priority over the corresponding MO, or that the
order of parameters is reversed in respect to gettext.  However, it uses
a saner format, and is a lot easier to work with than [3].

The problem, of course, is that xgettext does not know how to find
translatable strings inside the template.  [3] includes a CLI tool
similar to xgettext, but is not a drop-in replacement[6] and does not
process templates.

The proper way to handle this would be to add a parser to xgettext, but
for now i found out that if i surround the call to the translation
functions from within the template with parentheses, i can trick
xgettext into believing it is parsing Scheme code, and extracts the
strings successfully—at least, for what i have tried.  Had to add the
keyword for pgettext, because Schemed does not have it, but at least i
can do that with command line parameters.

For now i left only Spanish and Catalan as the two available languages,
even though the source text is written in English, because that way i
can make sure i do not leave strings untranslated.

[1]: https://golang.org/x/text
[2]: https://github.com/nicksnyder/go-i18n
[3]: https://github.com/leonelquinteros/gotext
[4]: https://github.com/golang/go/issues/39954
[5]: https://github.com/golang/go/issues/12750
[6]: https://github.com/leonelquinteros/gotext/issues/38
2023-01-18 20:26:30 +01:00
jordi fita mas d434d040af Add the very basic styles 2023-01-17 22:28:47 +01:00
jordi fita mas f1bf1f896d Implement login cookie, its verification, and logout
At first i thought that i would need to implement sessions, the ones
that keep small files onto the disk, to know which user is talking to
the server, but then i realized that, for now at least, i only need a
very large number, plus the email address, to be used as a lookup, and
that can be stored in the user table, in a separate schema.

Had to change login to avoid raising exceptions when login failed
because i now keep a record of login attemps, and functions are always
run in a single transaction, thus the exception would prevent me to
insert into login_attempt.  Even if i use a separate procedure, i could
not keep the records.

I did not want to add a parameter to the logout function because i was
afraid that it could be called from separate users.  I do not know
whether it is possible with the current approach, since the settings
variable is also set by the same applications; time will tell.
2023-01-17 20:58:13 +01:00
jordi fita mas 9d202e82ca Add the simplest possible web server to test login
This is a very rough test to actually check the login function outside
pgTAP; it is very ugly, in both design and code, and (i hope) does not
reflect future quality.

I was about to use Echo[0] as a “web framework”, but something feels
wrong when using a framework with Go—i do not know what.  I actually
tried it and was even more put off by the JSON-formatted logger that can
not be disabled; i was already losing control of the application!

I created the folder following the apparently de facto guidelines for Go
projects, and i see no problem with mixing Go’s folders with Sqitch’s:
both are part of the same application and there are not conflicts.

[0]: https://echo.labstack.com/
[1]: https://github.com/golang-standards/project-layout
2023-01-13 20:53:43 +01:00