Commit Graph

71 Commits

Author SHA1 Message Date
Darko Gjorgjijoski
dbc6ca7ad6 fix(recurring-invoices): scale scheduled generation 2026-08-02 23:01:22 +02:00
Darko Gjorgjijoski
1e72d9d449 fix(invoices): require settlement before completion (#739) 2026-08-02 17:02:29 +02:00
Darko Gjorgjijoski
885042f13a feat(expenses): add tax tracking and reporting (#737) 2026-08-02 13:01:01 +02:00
lnx1-1
3455ceb594 feat(invoices): native credit notes (#689)
* feat(invoices): add credit note (Stornorechnung) backend

Implement native credit notes as invoices with type=CREDIT_NOTE that
reference the original invoice they reverse. All monetary fields are
negated as integer cents (no float arithmetic).

- Migration: add invoices.type + related_invoice_id (unsignedInteger,
  matching invoices.id which is INT UNSIGNED via increments() rather than
  bigIncrements() -- required for the self-referencing foreign key to
  form correctly on MySQL/MariaDB) + widen invoice_items.price/base_price
  to signed (idempotent guards)
- Invoice model: TYPE_* constants, relatedInvoice/creditNotes relations,
  isCreditNote() helper
- InvoiceService::createCreditNote() copies + negates the source invoice,
  sets creator_id from the authenticated user
- POST /invoices/{invoice}/credit-note endpoint (201) with CreditNotePolicy
  authorization; 422 when the source is already a credit note, 403 across
  companies
- CreditNoteResource exposing type + related_invoice reference
- Dedicated credit-note PDF template (Stornorechnung header + reference line)
- SendCreditNoteRequest + SendCreditNoteMail + send endpoint/email template
- en/de translations for the credit-note PDF labels
- Feature tests: create, item negation, relation, 422, 403, PDF, email

Fresh implementation addressing all review feedback from PR #536.
Refs #317

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat(invoices): add credit note UI (button, badge, back-link)

- 'Create Credit Note' action in the invoice dropdown (hidden on rows that
  are already credit notes)
- Credit-note badge in the invoice list
- Credit-note banner with a link back to the reversed invoice on the detail
  view; show() returns CreditNoteResource for credit notes so the original
  invoice reference is available
- Expose type + related_invoice_id on InvoiceResource
- invoiceService.createCreditNote + store action
- Invoice domain type: InvoiceType, RelatedInvoice, type/related fields
- en/de translations for the credit-note UI strings

Refs #317

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Settle balances on credit note creation and surface cancellation in UI

Creating a credit note (full reversal) now settles both documents, the
mechanism @gdarko praised in PR #536 ("Invoice due amount adjustment
logic for create/update/delete is well thought out"), adapted to the
invoice-row-with-type architecture:

- The original invoice's due_amount/base_due_amount drop to 0 and its
  status/paid_status are recalculated through the existing
  changeInvoiceStatus() path (COMPLETED/PAID), so it falls out of every
  awaiting-payment view. The UI shows a distinct "Cancelled" badge and a
  "Cancelled via credit note: ST-XXXX" banner instead of the generic
  Paid badge, avoiding Xero's documented paid-vs-credited ambiguity.
- The credit note itself is created settled (due 0, PAID): nothing is
  ever owed on it, so it never surfaces as an open negative balance.
- Deleting a credit note restores the original invoice's balance,
  recomputed from recorded payments (integer cents), covering unpaid
  and partially-paid invoices; skipped when both documents are deleted
  in one batch.
- One credit note per invoice: a second full reversal would
  double-negate the books (422 + hidden dropdown action).
- InvoiceResource exposes minimal credit_notes refs, mirroring the
  existing related_invoice back-link, for the banner/badge in
  InvoiceDetailView and InvoiceIndexView.

8 new feature tests cover settlement, resource exposure, the
one-per-invoice guard, and delete-side restoration.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Render credit notes through the invoice's own template, add cancellation banner to PDFs

Fixes two bugs found via screenshot: the actual generated PDF (not just the
Vue preview) is what customers download/print/email, and it was broken in
both directions.

1. Credit notes always rendered through one hardcoded standalone layout
   (app/pdf/credit-note/credit-note.blade.php) regardless of which of the
   3 invoice templates the company actually uses. A company on invoice2/3
   branding got a credit note PDF that looked nothing like their real
   invoices. Fixed by making invoice1/2/3.blade.php credit-note-aware
   (swap number/date labels, add the red "CREDIT NOTE" banner + reference
   line, scaled to each template's own visual language) and removing the
   isCreditNote() special case in InvoiceService::getPdfData() so credit
   notes now go through the same PdfTemplateUtils resolution as regular
   invoices. The standalone template is deleted.

2. The original (now-cancelled) invoice's PDF showed zero indication it
   had been reversed by a credit note — only the Vue UI banner existed.
   Added an equivalent amber "Cancelled" banner + reference line to all
   three templates, conditional on $invoice->creditNotes->isNotEmpty().
   getPdfData() now eager-loads both relatedInvoice and creditNotes
   unconditionally (cheap either way) so every template has what it needs.

Covers all 3 templates x both document types (6 combinations), verified
by generating real PDFs (not just Vue previews) for each. 4 new feature
tests hit the actual /invoices/pdf/{hash}?preview=1 endpoint under
non-default templates (invoice2, invoice3) and assert on
template-specific structural markers plus the new banner text, so this
class of bug (works on the default template, breaks on the others)
cannot regress silently.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Create credit notes as COMPLETED and hide Record Payment on settled documents

The credit note is born fully settled (due_amount 0, paid_status PAID), but
its own status was hardcoded to SENT at creation and never advanced. It
therefore showed a stale "Sent" badge forever and the detail view kept
offering "Record Payment" on a document with nothing owed.

- InvoiceService::createCreditNote() now creates the credit note with
  STATUS_COMPLETED, matching the end-state the original invoice reaches
  through the settlement path.
- The "Record Payment" button (detail view) and dropdown item now also
  require due_amount > 0, so no already-settled document ever offers to
  record a payment, regardless of its status string.
- CreditNoteTest: assert the credit note is created with STATUS_COMPLETED.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Refresh invoice sidebar list after creating a credit note from detail view

Creating a credit note from the split-pane invoice detail view
(/admin/invoices/{id}/view) left the left-hand invoice list stale until a
manual page reload: the new credit note never appeared, and the original
invoice's row kept showing its pre-cancellation status.

InvoiceDropdown's createCreditNote() now calls the loadData/table refresh
callbacks after success, matching the pattern already used by
removeInvoice(). InvoiceDetailView wires loadData to a new
refreshInvoiceList() that resets invoiceList before refetching (loadInvoices()
is append-only, built for infinite scroll, so calling it without a reset
would duplicate every already-loaded row) — mirroring the existing
onSearched() reset-and-refetch pattern.

Also add the "Cancelled" badge to the sidebar's list-item template; it
previously only existed in InvoiceIndexView.vue, so the split-pane sidebar
never reflected an invoice's cancelled-by-credit-note state even after a
full reload.

* fix(migrations): drop the credit-note foreign key constraint

related_invoice_id kept a DB-level foreign key with nullOnDelete(). That is
against the codebase-wide convention established after the FK breakage in
PRs #618 / #683: reference columns are plain unsignedInteger plus an index,
with the relation declared on the model and cascades handled in app code.
Replace the constraint with an index, and swap the dropForeign() in down()
for a dropIndex() that runs before the column drop (MySQL and PostgreSQL
would drop the index with the column, SQLite leaves it behind and then fails
with "no such column").

Documents why the type default is load-bearing while here: it backfills
existing rows with INVOICE, and the type-scoped serial-number queries would
otherwise restart numbering on an existing install.

* feat(invoices): give credit notes their own number sequence

Credit notes were numbered out of the invoice sequence, so cancelling an
invoice consumed an invoice number and the two document series interleaved
(INV-000001, INV-000002 for a credit note, INV-000003 for the next invoice).
Credit notes are Invoice rows with type = CREDIT_NOTE, and SerialNumberService
derived both the format setting and the sequence lookup from the model class
alone, which cannot distinguish them.

SerialNumberService gains two optional, fluent hooks that leave the default
behaviour untouched:

- setSettingKey() overrides the company setting the format is read from, so a
  document type sharing a table can have its own format.
- setSequenceScope() takes column => value constraints applied on top of the
  company (and customer) filters in both setNextSequenceNumber() and
  setNextCustomerSequenceNumber(), so each type counts on its own.

createCreditNote() uses both: credit_note_number_format (default
{{SERIES:CN}}{{DELIMITER:-}}{{SEQUENCE:6}}) and a CREDIT_NOTE scope. Every
call site that numbers a real invoice is scoped to TYPE_INVOICE so the credit
notes now sharing the table do not inflate the invoice sequence: create,
update, clone, the recurring-invoice generator, estimate conversion, and the
factory. The update() scope is not redundant: a customer change there triggers
a customer-sequence recompute through setModelObject(), which would otherwise
count credit notes. Estimate and payment numbering is unaffected.

The format is configurable in Settings alongside invoice numbering (a second
NumberCustomizer on the Invoices tab, no component changes needed) and is
seeded for new companies by CompanyService. A data migration backfills it for
existing companies: without a row, CompanySetting::getSetting() returns null
and the generated serial is silently empty.

The next-number endpoint answers key=credit_note with the CN format, so the
settings preview and any future create form read the right sequence.

* feat(pdf): show credit-note and cancellation banners on invoice PDFs

The generated PDF is what the customer downloads, prints and receives by
mail, and it was silent in both directions: a credit note rendered as an
ordinary invoice, and the invoice it reversed carried no sign that it had
been cancelled. Only the Vue UI said anything.

The banner lives in one partial, resources/views/app/pdf/partials/
credit-note-banner.blade.php, rather than being copied into the three stock
templates. It reads the shared $invoice, renders a red CREDIT NOTE box with
a reference to the original when the document is a credit note, an amber
CANCELLED box naming the credit note when the document has been reversed,
and nothing at all otherwise -- pdf:compare reports byte-identical ink
boxes for all seven stock templates, so regular documents are untouched.

Every rule is inline on the elements. A partial included in the body cannot
add anything to <head>, and inline styles are the one thing dompdf and
Chromium honour identically; line-height is stated explicitly for the same
reason. The box is a plain block with clear: both, so it cannot disturb the
float layouts the templates are built on.

Custom templates published into the pdf_templates namespace keep rendering
exactly as they do today and opt in by including the partial.

Alongside it, the cheap label swaps the same document type needs: the
<title>, invoice1's and invoice3's number/date labels, invoice2's header
heading, and the PDF's own Title/Subject metadata now say Credit Note, and
the due-date row is dropped from a credit note, which has none.

* feat(invoices): give credit notes the normal lifecycle and guard the unsafe paths

A credit note had a parallel universe of its own: a dedicated send endpoint,
its own request class, its own two gates and policy methods, all of which
duplicated the invoice send path verbatim except for which mailable was
constructed. It was also born COMPLETED, which is the one status that hides
every Send affordance in the UI, so the endpoint that existed for it could
not be reached from the app at all.

Both halves are now the invoice's. InvoiceService::send() picks the mailable
by document type and sendPreview() picks the matching template, so a credit
note goes out through POST /invoices/{invoice}/send under the existing
'send invoice' authorization. The credit-note send route, controller action,
service method, SendCreditNoteRequest, and the 'send credit note' /
'view credit note' gates are gone; CreditNotePolicy keeps only create(),
which is the one ability that is genuinely different (it is gated on the
*source* invoice's company). SendCreditNoteMail and its Blade template stay
exactly as they were.

The credit note is now created DRAFT with due_date null, still settled
(paid_status PAID, both due amounts 0). DRAFT gives it the ordinary
create-review-send lifecycle: the Send button appears on the detail page and
send() promotes it to SENT. Nothing is owed on it in any status, so Record
Payment stays hidden either way.

The guards are the other half. A credit note is now immutable (InvoicePolicy
::update returns false: saving it back through the invoice form would
recompute its totals positive), cannot be cloned or converted to an estimate
(both copy the amounts unnegated), and can never be minted through the create
endpoint -- getInvoicePayload() hard-sets type and related_invoice_id, which
were previously forwarded straight from the request into Invoice::create.
Reversing an invoice that already took a payment, or one that was never
issued at all, is refused with a 422 alongside the two existing domain rules.
CheckInvoiceStatus only considers real invoices, so a credit note can never
be flagged overdue, and the invoice next-number preview is scoped by type
like every invoice create path already is, so it can never count credit
notes.

Test changes follow the behaviour changes: invoices being credited are
created SENT rather than relying on the factory's DRAFT default, the
"created settled" test now expects DRAFT plus a null due date, the send test
goes through the invoice endpoint and asserts the promotion to SENT, and the
partially-paid delete-restore test mints its credit note through the service
because the API now refuses that invoice. Nine tests cover the new guards.

* fix(invoices): stop credit notes distorting counts, queries and deletes

Three follow-ups to the credit-note feature, all of them cases where a
reversal row was treated as if it were another invoice.

Counts. A credit note is not an issued document, so "invoices: 12" must not
become 13 the moment one is cancelled. The company dashboard, the customer
portal dashboard and its invoice list, and both AI stat tools now count
type = INVOICE only. Every sum is deliberately left alone: the negated total
is exactly what nets a reversed sale back out of the figure, which is the
whole point of storing it that way. The admin index meta count is also left
alone, because that list shows credit notes and the count has to match it.

Queries. InvoiceResource probed creditNotes twice per row -- an exists()
then a fetch -- on a resource that is serialized for every row of a
paginated list, and CreditNoteResource did the same for relatedInvoice.
Both now read the loaded relation and emit nothing when it was not loaded,
and the two places whose UI consumes the field (the index list badge, which
also feeds the detail-page sidebar, and the detail page banner) eager-load
it with the two columns they need.

Deletes. related_invoice_id has no DB foreign key by convention, so both
sides of the cascade have to be written out. Deleting an invoice whose
credit note is not in the same batch is now a validation failure --
RelationNotExist cannot express "unless it is also going", so the rule takes
the whole ids list. And InvoiceService::delete() nulls out any surviving
pointer into the deleted batch, so even a caller that bypasses the request
layer cannot leave a credit note referencing a row that is gone.

Also: creating a credit note from the dropdown swallowed server errors, so a
refused reversal (paid invoice, draft, already credited) looked like it had
worked. It now surfaces the message the way the sibling actions in the app
do.

* fix(i18n): translate credit-note guard errors and complete locale coverage

The create/clone/convert guards throw ValidationException with bare
snake_case keys, and none of them were in ERROR_TRANSLATION_MAP, so the
toast rendered the raw key instead of a sentence.

- map the six guard keys to new errors.* translations
- give clone and convert-to-estimate a catch so their guards surface at
  all, and factor the toast into showApiErrorNotification()
- add every credit-note string to fr, it and mk (avoir / nota di credito
  / книжно одобрение) so the branch ships in all five maintained locales
- proofread the German: "durch" instead of "via", reword
  errors.credit_note_attached

* refactor(db): consolidate credit-note migrations into one

Nothing in PR #689 has shipped, so the branch's two separate migrations are
squashed into a single 2026_08_02_120000_add_credit_note_support. Installs
upgrading to 3.0.0 apply one migration instead of three, and the columns the
next phases need land in the same file rather than as follow-ups.

Carried over unchanged from the deleted files: the INVOICE default on
invoices.type (it backfills existing rows so they stay inside the type-scoped
serial-number queries), the no-FK unsignedInteger convention for
related_invoice_id per PRs #618 / #683, the SQLite requirement to dropIndex
before dropColumn in down(), and the company_settings backfill of
credit_note_number_format for installs predating independent credit-note
numbering.

New in this migration, for partial credit notes:

- invoices.credit_reason (nullable text)
- invoice_items.source_invoice_item_id (nullable unsignedInteger + index),
  recording which line of the original invoice a credit-note line credits
- taxes.base_amount widened to a signed bigInteger

The base_amount change fixes a pre-existing bug, not just a credit-note need.
taxes.amount was made signed in 2024_02_08_181804_taxes_amount_as_signed but
base_amount, added as unsignedBigInteger in 2021_07_16_075100, was left behind,
so any negative tax row on a company with an exchange rate is rejected outright
under strict-mode MySQL.

CreditNoteSchemaTest covers the new columns and their nullability, the INVOICE
default, a Tax row with a negative base_amount, a negative-price invoice item
linked to its source line, and the seeded credit_note_number_format setting.
Round-tripped up and down on both SQLite and MySQL 8.0.

* feat(credit-notes): add the pure telescoping credit-note calculator

CreditNoteAmounts computes partial and full credit notes from a plain-array
snapshot of the original invoice, with no Eloquent involved. Every credit is
the difference between two cumulative credits derived from the original's
stored integers, so crediting a line in chunks sums back to the invoice to the
cent and the order of the chunks does not matter.

Quantities are carried as integer hundredths, money stays in integer minor
units, and rounding is the same half-away-from-zero rule DocumentTotals uses.
Fixed-amount tax rows are pro-rated like percentage ones (otherwise a flat tax
is credited once per chunk and exceeds what was invoiced), and percentage rows
are pro-rated from the stored amount rather than re-derived, so a full credit
reproduces the invoice instead of re-making a rounding decision.

* feat(credit-notes): credit an invoice line by line, not all at once

A credit note is no longer a whole-invoice reversal. CreditNoteService takes
the lines and quantities to credit (an empty list still means everything that
is left) and drives CreditNoteAmounts, so an invoice can be credited in as
many pieces as needed.

The model is telescoping, not incremental: each credit note is the difference
between the cumulative credit at the quantities credited after it and the one
before it, both derived from the original invoice's stored integers and never
from earlier credit notes. Chunks therefore sum back to the invoice to the
cent in every field, in any order, and a fully credited invoice nets to zero.
Because those integers are already exact, the line items are written with
recompute disabled: re-deriving a total from price times quantity, or a base_*
column through the exchange rate, would round a second time and break the
identity by a cent.

How much of each line is already credited is read off the surviving credit
notes through source_invoice_item_id, so deleting one gives its quantities
back with no separate bookkeeping. The invariants live in the service, under a
row lock, because each is a read-then-write on the invoice: nothing may be
credited twice, no line beyond what was invoiced, and no more in total than
the invoice's unpaid balance. The one-credit-note-per-invoice and no-payments
guards in the controller are gone, replaced by those.

Paid status and balance are kept apart. A credit is not a payment: an invoice
credited in part is still UNPAID, just for less, and only the payments decide
whether it reads PARTIALLY_PAID. recalculateBalance() owns that rule and runs
only when a credit note is created or deleted, which is why it does not live
in getInvoiceStatusByAmount(): PaymentService::create() adjusts the invoice
before the Payment row exists, so a payments-derived rule there would read a
stale sum. PaymentService::delete() now derives the status from the payments
that remain rather than from due == total, which is the same answer on an
uncredited invoice and the correct one on a credited one.

A credited invoice is frozen (its item ids anchor the credit-note lines), the
reason for a credit is stored and cannot be written through the invoice form,
and the invoice resource reports how much has been credited, whether that is
partial or full, and how much of each line.

* feat(credit-notes): choose the lines and quantities to credit in the UI

Creating a credit note was a yes/no dialog that reversed the whole
invoice, which is all the API could do. The API now credits any subset
of the lines, so the front end has to ask which ones.

The dropdown action opens a form instead of a confirmation: one row per
invoice line showing what was invoiced, what is already credited and
what is left, with the remaining quantity pre-filled and editable.
Quantities are compared in integer hundredths so the last unit of a
line is never blocked by float drift, and selecting every line at its
full remaining quantity on an untouched invoice is sent as the
no-items full reversal the server already implements.

Only the line subtotals are shown: discounts and taxes are apportioned
by the server's calculator, so re-deriving them here would disagree
with the document that gets written. The note under the table says so.

The "Cancelled" badge and banner now key on credited_status rather than
on the presence of any credit note, so a partly credited invoice reads
as partly credited and keeps its real paid status alongside.

* feat(credit-notes): show partial credits on the document and in every locale

The PDF said one of two things about a credited invoice: nothing, or
"Cancelled". Partial credits made both wrong. An invoice credited by half now
carries a Partially Credited banner naming the amount and the credit notes it
came from, a fully credited one lists every credit note rather than the first,
and the credit note itself prints the reason it was issued.

The totals block was the more dangerous half. It was gated on paid_status, so
an invoice settled by a credit note announced "Amount Paid" for money nobody
sent. It now splits the two: Amount Credited comes off the credit notes, Amount
Paid is whatever the balance dropped by beyond them. For a document with no
credit notes the arithmetic collapses to the old expression and the rendering
is unchanged word for word and position for position, checked against dompdf
output and against pdf:compare across all seven stock templates.

The reason is operator-written free text, so it is echoed escaped rather than
through @lang, which does not escape.

Locale-side, the phase-4 English-only strings are translated into the four
maintained catalogues, and the two guards partial crediting retired (one credit
note per invoice, and no crediting an invoice with payments) are deleted
everywhere so no translated install can still show them. A test walks all five
catalogues and fails on either a missing key or a surviving one.

The dashboard's recent-due list eager-loads the credit notes so its rows can
tell a credited invoice from a paid one.

* fix(payments): stop an overpayment from silently vanishing

A payment carried no maximum, so any amount larger than the invoice's
outstanding balance was accepted. PaymentService then handed it to
Invoice::subtractInvoicePayment(), which drove the balance negative, and
Invoice::getInvoiceStatusByAmount() returns an empty array for a negative
amount, so the status change was never applied. The money was recorded
against an invoice whose balance and paid status no longer described it.

The bug predates credit notes, but partial credit notes shrink an
invoice's balance without shrinking its total, which makes it easy to
walk into.

PaymentRequest now caps the amount at the invoice's due amount. On an
edit of a payment that already belongs to that same invoice its own
amount returns to the pool, matching what PaymentService::update() does;
a payment with no invoice stays uncapped. The message string is the
translation key, as elsewhere in the app, and is mapped and localized in
all five shipped locales.

The 'update payment' fixture stacked two randomly sized payments on a
randomly sized invoice, which the cap turns into an intermittent failure,
so its amounts are pinned.

* fix(payments): show the rejected-save message on the payment form

The create-payment view swallowed every API failure in a bare catch, so a
validation rejection, including the new remaining-balance cap, looked like
a save that silently stalled. The catch now routes the error through the
shared toast path the other forms use.

* fix(dashboard): stop the recent-due list crashing on partial credits

The recent-due invoices are serialized as raw models, so every loaded
relation runs the full $appends set. The column-limited creditNotes eager
load left its children without company_id, the company date-format lookup
returned null, and formattedCreatedAt took the endpoint down with a 500.

The relation is not needed there at all: credited_status is a resource
field the raw payload never carried, and a fully credited invoice has no
due amount so it never appears in this list. Drop the eager load and pin
the scenario (a partially credited invoice among the recent due) with a
test that fails 500 on the old code.

---------

Co-authored-by: Linus <lkurz@posteo.de>
Co-authored-by: Darko Gjorgjijoski <dg@darkog.com>
2026-08-01 22:15:00 +02:00
Darko Gjorgjijoski
773670c18f feat(pdf): archival PDF/A output and document properties (#732)
Generated files carried no document properties at all, so an archive of them
showed a column of blank titles and no author. Title, Subject, Author and
Creator are now written from the document number and company, on both drivers:
dompdf via addInfo(), Gotenberg via metadata().

dompdf needed more than the API call. It reads Title from the <title> element
during render(), which happens after addInfo(), so metadata set through the API
alone was silently overwritten by whatever the template put there and the two
drivers disagreed about what the file was called. The title is written into the
markup as well, escaped.

Also adds an archival format setting for Gotenberg: off, PDF/A-1b, -2b or -3b.
PDF/A-3 is what the EU e-invoicing formats expect. Verified against a stock
gotenberg:8 -- LibreOffice inside the image does the conversion and the output
carries the right pdfaid:part in its XMP -- so no extra components are needed.

A fixed list rather than free text, because the SDK forwards whatever it is
given and an unsupported value would surface only as an HTTP error from the
service at render time. Empty is a real choice meaning an ordinary PDF, so it
overrides an env default rather than falling through it.

Gotenberg only: dompdf cannot produce PDF/A.

Claude-Session: https://claude.ai/code/session_01QmECndmNZwzN65Zz9P87dF
2026-08-01 14:37:22 +02:00
Darko Gjorgjijoski
8ab860a1ae fix(pdf): make custom templates behave the way the docs describe (#730)
Custom templates are a real feature with no test coverage at all, and several
rough edges that only show up once someone actually uses one.

make:template validated nothing. --type was checked only by the interactive
prompt, so `--type=payment` skipped the prompt and died on an uncaught
FileNotFoundException looking for payment1.blade.php: a stack trace instead of a
message. The name was not checked either, so `../escaped` wrote outside the
templates directory. Both are refused now.

Every custom template of a type shared one partials/table.blade.php. It was
written on first use and reused thereafter, so editing the items table for one
custom template silently changed it for all of them -- a file that looks
per-template and behaves globally. Each template now gets its own copy under
partials/{name}/, and its include is rewritten to match. Existing templates keep
including the old shared path, which still resolves.

A custom template sharing a built-in's name appeared twice in the picker with
the same label, and findFormattedTemplate() array_reverses and takes the first
match, so the custom one silently won. The listing is keyed by name now, so it
appears once, as the entry that will actually be used.

A custom template with no same-named .png rendered <img src=""> in the picker: a
blank tile, no error, no hint anything was missing. It falls back to the preview
of the template it was cloned from.

template_name was validated as `required` and nothing else, so any string was
accepted and stored. findFormattedTemplate() returns null for an unknown name,
the null reads as "not custom", and rendering falls through to
app.pdf.{type}.{name} -- a raw "view not found" 500 at PDF time, long after the
save that caused it. New PdfTemplateExists rule, scoped per document type.

make:template also copies a _header/_footer companion when the source template
has one, so a scaffolded template keeps the repeating page furniture.

Both getEstimateTemplateName/getInvoiceTemplateName asked for the template list
with the default image format, base64-encoding a preview of every template just
to read the names back.

Claude-Session: https://claude.ai/code/session_01QmECndmNZwzN65Zz9P87dF
2026-08-01 12:56:39 +02:00
Darko Gjorgjijoski
713e0bc2e8 feat(pdf): repeating page headers and footers, and page numbers (#729)
Takes over #690 by csoscd. The companion-view idea is theirs; this reworks it
onto the shared page setup and fills in the gaps that stopped it landing.

A `{template}_header` or `{template}_footer` view next to a template is rendered
alongside it and repeated by Chromium on every page. The suffix resolves through
the pdf_templates:: namespace too, so custom templates get it with no extra
wiring.

Two things had to change for that to be useful.

Companion views are now hidden from the template picker. getFormattedTemplates()
lists every .blade.php it finds, so an invoice1_footer would otherwise appear as
a separately selectable template with no preview image -- the feature would have
introduced that the moment anyone used it.

And it does something out of the box. #690 shipped no companion views, so both
of its margin settings were visible no-ops until someone hand-wrote a Blade file.
Instead there is a pdf_page_numbers setting, off by default, that supplies a
footer when a template has none. A template's own companion still wins, so
turning page numbers on cannot overwrite a designed footer.

The setting sits under Gotenberg because only Chromium can repeat a footer;
dompdf has no equivalent. Its value is still carried by the dompdf form so
saving from there cannot clear the choice -- the field is absent from that
payload, and the controller only writes it when present.

Margins come from the page setup rather than #690's separate header_margin and
footer_margin. Chromium draws header and footer inside the page margin, so the
existing margins are the space they occupy; two more settings for the same
distance would have been a second way to say the same thing.

Verified against a live gotenberg:8 on a two-page document: off produces no
footer, on produces "1/2" and "2/2" on the respective pages, and a companion
footer replaces both.

#690's own test is not carried over. It asserted nothing: a bare
View::shouldReceive('exists') is an allowance rather than an expectation,
andReturn(false) never entered the companion branch, and the call sat inside
try { } catch (Throwable) { }, so it passed with the feature deleted.

Claude-Session: https://claude.ai/code/session_01QmECndmNZwzN65Zz9P87dF
2026-08-01 12:52:47 +02:00
Darko Gjorgjijoski
a54a5ee007 feat(pdf): one page setup, honoured by both drivers (#728)
Paper size was a Gotenberg-only setting stored as a single "210mm 297mm"
string. dompdf had no page settings at all: size was pinned to config/dompdf.php's
fixed 'a4', its top-level `orientation` key was read by nothing (the installed
barryvdh v3 builds options only from `defines`), and margins were whatever
dompdf's own stylesheet said. So the two drivers disagreed about margins by
default -- dompdf 1.2cm, Gotenberg hardcoded to zero -- and selecting dompdf
silently discarded the paper size.

Replaces gotenberg_papersize with pdf_paper_width / pdf_paper_height /
pdf_orientation / pdf_margin_{top,right,bottom,left}, saved and applied for
either driver. Width and height are separate CSS lengths because that is the
only lossless shared notation: Gotenberg has no named sizes, and dompdf's named
table cannot express everything Gotenberg accepts. Named presets (A3/A4/A5/
Letter/Legal) are a convenience in the UI that resolve to a pair of lengths.

PdfPageSetup resolves it once and translates: a points array plus an orientation
argument for dompdf, CSS lengths plus landscape() for Gotenberg. Both are handed
the portrait pair, since each swaps the axes itself. Gotenberg's margins() takes
top, bottom, left, right, which is not the CSS order.

dompdf exposes no margin API, so DompdfDriver injects an @page rule -- at the
top of <head>, so a template declaring its own still wins. Doing it in the driver
rather than a Blade partial means custom templates get it without including
anything.

Margins default to 1.2cm, dompdf's existing default, so Gotenberg starts
matching it rather than rendering edge-to-edge. Verified against a live
gotenberg:8: A4 portrait, A4 landscape and Letter at zero margins all come out
with the same page box and the same ink offsets on both drivers.

A malformed length now throws rather than being ignored. Blank still falls back,
but a value that is set and wrong is an operator mistake, and the drivers would
otherwise fail differently: dompdf throws converting to points, Gotenberg would
forward the string and render at some other size.

Also here:
- Migration splits an existing gotenberg_papersize into the new pair. It earns
  its place because that key ships in 2.x, not just a 3.x alpha, so a stable
  install that chose Letter would otherwise come back up on A4. Drops
  gotenberg_margins, which 2.x also stores and neither driver ever read.
- Removes EnvironmentManager::savePDFVariables/getPDFConfiguration, which had no
  caller anywhere, and the unused EnvironmentManager injection in the controller.
- config/dompdf.php: drops the dead `orientation` key and defaults enable_remote
  to false, matching .env.example, which sets it explicitly and explains why.
  Installs predating that line were falling back to true.
- Retires the settings.pdf.footer_text and pdf_layout strings, which no component
  referenced.

Claude-Session: https://claude.ai/code/session_01QmECndmNZwzN65Zz9P87dF
2026-08-01 12:48:51 +02:00
csoscd
f8cfb6cd33 fix: allow Gotenberg to reach private/Docker-internal hosts (#691)
* fix: allow Gotenberg to reach private/Docker-internal hosts (Issue #688)

The SSRF guard introduced in #664/#671 correctly blocks arbitrary
private URLs, but also prevents legitimate use-cases where Gotenberg
runs alongside InvoiceShelf in a Docker Compose network (e.g. the
default http://pdf:3000 service name resolves to a private IP).

Add a `gotenberg_allow_private_host` setting (env:
GOTENBERG_ALLOW_PRIVATE_HOST, default false) that:
- skips PrivateNetworkGuard in GotenbergPdfDriver
- skips PublicHttpUrl validation in PDFConfigurationRequest
- exposes a clearly-warned toggle in the admin PDF settings UI
- is persisted to the settings table and loaded via AppConfigProvider

A disabled guard is safe for controlled private networks (Docker
Compose, LAN); it must never be enabled for untrusted hosts. The UI
surfaces a prominent warning to communicate this constraint.

Closes #688

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(gotenberg): scope the private-host exemption to a declared host

Reshapes the escape hatch from a boolean admin setting into an
environment-declared host allowlist.

The driver streams the upstream response body back as the PDF, so a
mis-set Gotenberg host is full-response SSRF — pointed at a link-local
metadata endpoint it returns cloud credentials. A blanket "allow
private" switch left that reachable: gotenberg_host stays editable from
the admin UI, so any install that enabled the switch to run a sidecar
could have the host repointed at an internal service. The population the
flag existed to serve was exactly the population it failed to protect.

GOTENBERG_ALLOWED_PRIVATE_HOST now names the single host that may skip
the guard. Only that exact value is exempt; every other private target
stays blocked. GotenbergHostPolicy owns the comparison so the save-time
rule and the runtime driver guard cannot drift, and normalises case,
trailing slash and surrounding whitespace on both sides.

Being env-only also drops the settings-table key, the AppConfigProvider
branch and the whole admin UI surface — the toggle there could not be
switched on in any case, since BaseSwitchSection has no slot and was
passed no v-model, so the child BaseSwitch was discarded and the value
never changed from false.

Restores the gotenberg_margins validation rule, which the previous
revision replaced rather than added alongside.

Tests cover both directions, including that declaring one private host
does not exempt another; sabotaging the policy to always exempt fails 16
of the 22.

Co-authored-by: csoscd <csoscd@users.noreply.github.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Darko Gjorgjijoski <dg@darkog.com>
Co-authored-by: csoscd <csoscd@users.noreply.github.com>
2026-07-29 10:57:52 +02:00
Darko Gjorgjijoski
d3202b8b2a fix(members): scope member view & update to the acting company
Member view/update bound the target user by global id and authorized only that the requester owns their active company, not that the target belonged to it. Bind the route model under the members param and require shared company membership in UserPolicy so an owner of one company can no longer read or modify users of another.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 23:42:11 +02:00
Darko Gjorgjijoski
e41ee8083d fix(documents): preserve edited line-item description (#658)
The per-line item description typed into the row's textarea was never
saved. BaseItemSelect's description field pushes edits out only via an
`update:description` emit, but DocumentItemRow (the single shared row used
by invoices, estimates, and recurring invoices) wired only `@search` and
`@select` — so the emit was dropped and `form.items[index].description`
never updated. On submit the field was lost; a catalog item's description
also reverted on the next re-render.

Capture the event and route it through the existing `updateItemAttribute`
store updater, mirroring how name/quantity/price/discount already sync.
One fix covers all three document types since they share the row.

Also add the missing `items.*.description` nullable rule to
RecurringInvoiceRequest for parity with the invoice/estimate requests.

The backend already persisted and returned description correctly; this was
purely a dropped frontend event.
2026-06-12 11:27:56 +02:00
Darko Gjorgjijoski
84524ce247 fix(security): recompute document totals server-side (GHSA-8c69) (#672)
v3 port. Invoice/estimate/recurring creation and update accepted total,
sub_total, tax and due_amount straight from the request with no recalculation,
letting a client persist financial totals that don't match the line items
(and corrupt the invoice update due-amount/paid-amount logic which keyed off
the client total).

- Adds App\Support\DocumentTotals (mirrors the front-end calc) trusting only
  price/quantity/discounts/tax-line amounts.
- getInvoicePayload/getEstimatePayload/getRecurringInvoicePayload override the
  client totals; the shared DocumentItemService::createItems recomputes each
  item total; InvoiceService::update keys its due-amount logic off the
  recomputed total.

Adds DocumentTotals unit tests + a feature test proving a tampered invoice
total is ignored; existing create/update tests no longer assert the now
server-authoritative derived totals.
2026-06-12 11:02:23 +02:00
Darko Gjorgjijoski
99ea898e88 fix(security): block SSRF via the Gotenberg host setting (GHSA-mfxg) (#671)
v3 port. The Gotenberg PDF driver was missed when the SSRF guards were added
to the AI, exchange-rate and file-disk drivers: gotenberg_host was validated
only with 'url', and the driver POSTs the rendered HTML to it.

Reuses the existing infrastructure (consistency with the other drivers):
- Wires App\Rules\PublicHttpUrl into the gotenberg_host validation rule.
- Adds PrivateNetworkGuard::assertAllowed() in GotenbergPdfDriver before the
  outbound call (covers env/seed/stale config + DNS rebinding).

Adds a unit test asserting the gotenberg_host rule rejects private/loopback/
link-local addresses and allows a public one.
2026-06-12 11:02:19 +02:00
Darko Gjorgjijoski
ac2a8ca939 fix(security): gate AI tools by user ability and block admin-URL SSRF
The AI chat assistant scoped tool queries by company but ignored the
per-user Bouncer abilities the rest of the app enforces, so any `use ai`
holder could read customers, invoices, payments, and company financials
their role couldn't otherwise see. Each AiTool now declares a required
ability (entity-aligned); the registry hides unauthorized tools from the
model and refuses to execute them as a backstop.

Separately, admin/owner-supplied URLs were fetched server-side with no
guard against private/reserved targets (SSRF): the AI base URL, the
CurrencyConverter "DEDICATED" exchange-rate URL, and S3/Spaces file-disk
endpoints. A shared PrivateNetworkGuard now backs a PublicHttpUrl
validation rule (save-time) and runtime guards in each driver.

- AiTool::requiredAbility() + mapping across all 12 tools
- AiToolRegistry filters schemas() by ability and re-checks in execute()
- PrivateNetworkGuard / BlockedUrlException / PublicHttpUrl rule (new)
- Rule wired into AI config (service + 3 controllers), exchange-rate,
  and file-disk endpoints; runtime guards in OpenRouterDriver,
  CurrencyConverterDriver, and FileDiskService
- Tests for ability filtering, the guard, the rule, and 422 rejections
2026-06-05 00:01:09 +02:00
Darko Gjorgjijoski
6d1816bd1b refactor: reorganize app/Services and app/Support by domain
The app/Services/ directory had grown into 22 flat files at the root plus 7 uneven subdirectories — finding anything required scrolling through an alphabetical mix of small CRUD services, infrastructure drivers, and install-time utilities. This commit groups services by domain, folds Backup into a new Storage namespace, and moves framework-infrastructure and install-time helpers out of Services and into Support where they belong.

New Services layout: Documents/ (Invoice, Estimate, RecurringInvoice, Payment, Expense, Transaction, DocumentItem, SerialNumber, Currency — matches the 'Documents' navigation group); Company/ (Company, Member, Invitation); Mail/ (MailConfiguration, CompanyMailConfig); Storage/ (FileDisk, plus Backup folded in). ExchangeRateProviderService moves next to its drivers in ExchangeRate/; FontService moves into Pdf/ where it belongs. CustomerService, ItemService, CustomFieldService stay at the Services root as standalone single-file domains.

Moves to Support/: Hashids/ (library wrapper — not business logic); Setup/ (one-shot install-time utilities — stateless helpers); Pdf/ (ImageUtils, PdfTemplateUtils, plus the existing PdfHtmlSanitizer consolidated into the same subdir). These are all framework infrastructure and stateless utilities — the 'service' label never really fit them.

Namespace declarations in 29 moved files updated to match new paths. 62 consumer files (controllers, other services, tests, database factories, seeders, routes, bootstrap/providers.php) have their use statements rewritten via a literal-string replacement script — no regex meant no risk of half-matching. Three Documents services needed an explicit 'use App\Services\Mail\CompanyMailConfigService' added because the same-namespace short reference they relied on no longer resolves after the split.

Verified: composer dump-autoload, 350 tests pass (850 assertions), vendor/bin/pint clean, npm run build succeeds.
2026-04-11 10:00:00 +02:00
Darko Gjorgjijoski
23d1476870 refactor(modules): marketplace install flow with checksum validation
Rewires module installation to use slug + version + checksum_sha256 instead of the opaque module identifier. ModuleInstaller splits marketplace token handling out of install() into helpers, adopts structured error responses, and validates the downloaded archive's SHA-256 against the marketplace manifest before unpacking.

ModuleResource is simplified to accept an already-loaded installed-module instance rather than fetching it from state, exposes access_tier and checksum fields, and drops the auto-disable-on-unpurchased side effect that was bleeding write logic into a read resource. UnzipUpdateRequest accepts a nullable module with a conditional module_name field so the same endpoint serves both app and module updates.

ModulesPolicy::manageModules now short-circuits for super-admins so administration flows (token validation, store state) are not blocked on a company-scoped ability. Two new feature tests cover both the authorization bypass and ModuleResource serialization.
2026-04-10 17:30:00 +02:00
Darko Gjorgjijoski
9174254165 Refactor install wizard and mail configuration 2026-04-09 10:06:27 +02:00
Darko Gjorgjijoski
cb37de6da4 Auto-generate company slug server-side in CompanyRequest
CompanyRequest::getCompanyPayload() accepted 'slug' from the client but never generated it, so the installation wizard (which PUTs /api/v1/company) left the slug empty when setting up the first company. Match the sibling CompaniesRequest (which already does Str::slug($this->name)) and generate the slug from the name server-side; drop the now-unused 'slug' validation rule.

Fixes the same bug that master's ed7af3fc tried to fix client-side with a lodash deburr + regex workaround in Step7CompanyInfo.vue. v3.0's installation wizard is a rewrite under resources/scripts/features/installation/CompanyView.vue and doesn't carry that workaround, so the cleaner fix is to make the backend authoritative like CompaniesRequest already is.
2026-04-07 17:30:34 +02:00
Darko Gjorgjijoski
39c9179888 Support internationalized domain names (IDN) in email validation
Add IdnEmail validation rule that converts IDN domains to Punycode
via idn_to_ascii() before validating with FILTER_VALIDATE_EMAIL.
Applied to all email fields: customers, members, profiles, admin
users, customer portal profiles, and mail configuration.

Includes unit tests for standard emails, IDN emails, and invalid
inputs.

Fixes #388
2026-04-06 23:55:29 +02:00
Darko Gjorgjijoski
8a6c085288 Rename company-scoped Users to Members throughout
Complete rename across backend and frontend:
- Controller: Company/Users/UsersController -> Company/Members/MembersController
- Service: UserService -> MemberService
- Requests: UserRequest -> MemberRequest, DeleteUserRequest -> DeleteMemberRequest
- API routes: /api/v1/users -> /api/v1/members (company-scoped only)
- Sidebar menu: "Users" -> "Members"
- Frontend: views/users -> views/members, stores/users -> stores/members
- Router: users.index -> members.index, /admin/users -> /admin/members
- i18n: new "members" section with invitation-related keys
- Tests: UserTest -> MemberTest

Admin/super-admin Users (system-wide user management) remains unchanged.
2026-04-03 23:12:30 +02:00
Darko Gjorgjijoski
de06c335ac Remove dead code from app/Http: unused middleware, requests, and resources
Delete 23 unused files:
- AdminMiddleware (never registered, SuperAdminMiddleware used instead)
- 4 form requests with no controller references
- AbilityResource/Collection and ExchangeRateLogResource/Collection (zero usage)
- Customer/RecurringInvoiceResource and Collection (no controller or nested reference)
- 10 Customer Collection classes whose Resources are only used via new, never ::collection()
2026-04-03 16:41:33 +02:00
Darko Gjorgjijoski
9432da467e Add super-admin Administration section and restructure global vs company settings
- Add Administration sidebar section (super-admin only) with Companies, Users, and Global Settings pages
- Add super-admin middleware, controllers, and API routes under /api/v1/super-admin/
- Allow super-admins to manage all companies and users across tenants
- Add user impersonation with short-lived tokens, audit logging, and UI banner
- Move system-level settings (Mail, PDF, Backup, Update, File Disk) from per-company to Administration > Global Settings
- Convert save_pdf_to_disk from CompanySetting to global Setting
- Add per-company mail configuration overrides (optional, falls back to global)
- Add CompanyMailConfigService to apply company mail config before sending emails
2026-04-03 10:35:40 +02:00
Abdulrazzaq Alhendi
65d1fdd3f0 feat(mail): add CC and BCC fields to email requests and forms (#466)
* feat(mail): add CC and BCC fields to email requests and forms

* chore: fmt
2026-02-06 01:59:38 +01:00
Darko Gjorgjijoski
18d63a3375 Configurations cleanup & database configurations for mail and pdfs (#479)
* Move Mail, PDF configuration to Database, standardize configurations

* Set default currency to USD on install

* Pint code
2025-09-19 15:42:53 +02:00
Christos Yiakoumettis
3e96297699 Add expense number at Expenses (#406)
* add expense number at expenses

* Re-order expense fields

* Rename expense_number migration

* Add expense_number to tests

---------

Co-authored-by: Darko Gjorgjijoski <dg@darkog.com>
2025-09-02 03:20:27 +02:00
Darko Gjorgjijoski
bae8dbe083 Upgrade mail configuration (#455)
* Upgrade the mail configuration

* Update mail configuration to match Laravel 12

* Update mail configuration to properly set none or null

* Pint code

* Upgrade Symfony Mailers
2025-08-31 03:04:31 +02:00
mchev
bf5b544ca3 Adding Flat Tax support with fixed amount (#253)
* Possibility to set a fixed amount on tax types settings

* Pint and manage flat taxes on items

* Fix display errors and handle global taxes

* Tests

* Pint with PHP 8.2 cause with PHP 8.3 version it cause workflow error

* Merging percent and fixed amount into one column

* Now display the currency on SelectTaxPopup on fixed taxes
2025-05-04 02:24:56 +02:00
Tim van Osch
bf40f792c2 Feat(Gotenberg): Opt-in alternative pdf generation for modern CSS (#184)
* WIP(gotenberg): add pdf generation abstraction and UI

* feat(pdf): settings validate(clien+server) & save

* fix(gotenberg): Use correct default papersize
chore(gotengberg): Remove unused GOTENBERG_MARGINS env from .env

* style(gotenberg): fix linter/styling issues

* fix(pdf): use pdf config policy

* fix: revert accidental capitalization in mail config vue

* Update composer, remove whitespace typo

* Fix small typos

* fix cookie/env issue

* Add gotenberg to .dev, move admin menu item up
2025-05-04 02:10:15 +02:00
Yannic Inselmann
b32c334a71 feat: default notes (#263)
* feat: default notes

* feat: include default invoice note in recurring invoice

* feat: use default export in tw config

* fix: test and naming

* fix: consistent ui for switch in note modal

* feat: little text improvements
2025-04-05 12:01:06 +02:00
Darko Gjorgjijoski
f52b73f517 Invoice time support (#269)
* Changed invoice date to datetime

* Fixed code style errors

* Update TimeFormatsController.php

* Update TimeFormatter.php

* Update TimeFormatsController namespace

* Fix missing comma in language file

* Fix formatting

---------

Co-authored-by: troky <troky2001@yahoo.com>
2025-01-12 13:32:47 +01:00
mchev
ef9adbd6c9 Adding sent and viewed field to payload (#247) 2025-01-12 11:27:29 +01:00
mchev
967c225df9 Merge pull request #198 from mchev/invoice_cancellation
Support for Zero and Negative Item Quantities on Invoices
2024-11-02 12:20:55 +01:00
Martin Chevignard
03b9defeb1 Customers tax id field 2024-10-04 12:07:29 +02:00
mchev
14c599ed4f Allow decimals on items quantities (#94)
* Allow decimals on items quantities #80

* Updating all requests validation

* Revert discount_val
2024-06-05 15:10:36 +02:00
mchev
bf5a8cdb28 Increasing base total validation limit for IDR currency (#98) 2024-06-05 12:16:09 +02:00
mchev
3259173066 Laravel 11 (#84)
* Convert string references to `::class`

PHP 5.5.9 adds the new static `class` property which provides the fully qualified class name. This is preferred over using strings for class names since the `class` property references are checked by PHP.

* Use Faker methods

Accessing Faker properties was deprecated in Faker 1.14.

* Convert route options to fluent methods

Laravel 8 adopts the tuple syntax for controller actions. Since the old options array is incompatible with this syntax, Shift converted them to use modern, fluent methods.

* Adopt class based routes

* Remove default `app` files

* Shift core files

* Streamline config files

* Set new `ENV` variables

* Default new `bootstrap/app.php`

* Re-register HTTP middleware

* Consolidate service providers

* Re-register service providers

* Re-register routes

* Re-register scheduled commands

* Bump Composer dependencies

* Use `<env>` tags for configuration

`<env>` tags have a lower precedence than system environment variables making it easier to overwrite PHPUnit configuration values in additional environments, such a CI.

Review this blog post for more details on configuration precedence when testing Laravel: https://jasonmccreary.me/articles/laravel-testing-configuration-precedence/

* Adopt anonymous migrations

* Rename `password_resets` table

* Convert `$casts` property to method

* Adopt Laravel type hints

* Mark base controller as `abstract`

* Remove `CreatesApplication` testing trait

* Shift cleanup

* Fix shift first issues

* Updating Rules for laravel 11, sanctum config and pint

* Fix Carbon issue on dashboard

* Temporary fix for tests while migration is issue fixed on laravel side

* Carbon needs numerical values, not strings

* Minimum php version

* Fix domain installation step not fetching the correct company_id

* Fix Role Policy wasn't properly registered

---------
2024-06-05 11:33:52 +02:00
Timo
8c83df558c Add Company VAT-ID and Tax-ID (#54)
* add company vat_id & tax_id field

* add tax & vat id field in company settings

* fix vat & tax id validation

* add german vat & tax id translation

* add translations for pdf

* add vat_id and tax_id field before timestamps

* make fields nullable and fix code style
2024-04-20 23:08:32 +02:00
Darko Gjorgjijoski
8788f3d504 Tax calculation issue (#38)
* fix initial tax per item issue

* remove commit in estimate storage

* add changes in tax per item calculation

* add validation on requests

* fix minimum total issue

* fix table pagination filter issue

* minor fix

* remove compound interest and remove unused code

---------

Co-authored-by: yashkanakiya <yashkanakiya281297@gmail.com>
Co-authored-by: dhruvbhattt <dhruvbhatt7790@gmail.com>
Co-authored-by: gdarko <dg@darkog.com>
2024-02-18 10:54:12 +01:00
gdarko
4ab92473e9 Setup pint & run code style fix 2024-01-29 04:46:01 -06:00
Darko Gjorgjijoski
6b80b5f48d Change namespace 2024-01-27 23:53:20 +01:00
theWorstComrade
7cde971f8b Module upload validation (#857)
https://huntr.dev/bounties/cb9a0393-be34-4021-a06c-00c7791c7622/
2022-03-29 12:55:35 +05:30
theWorstComrade
88035ea490 Expense attachment validation fix (#855)
https://huntr.dev/bounties/4d7d4fc9-e0cf-42d3-b89c-6ea57a769045/
2022-03-22 16:58:55 +05:30
Mohit Panjwani
b781f11175 Merge branch 'master' of https://github.com/bytefury/crater 2022-02-10 11:50:13 +05:30
avinashrathod
c82dc94252 fix receipt not uploading in expense 2022-02-03 11:39:18 +05:30
theWorstComrade
ff3cd0f7b9 Customer avatar validation (#732)
* Customer avatar validation

https://huntr.dev/bounties/19f3e5f7-b419-44b1-9c37-7e4404cbec94/

* Customer avatar validation test

https://huntr.dev/bounties/19f3e5f7-b419-44b1-9c37-7e4404cbec94/
2022-01-19 17:08:34 +05:30
Mohit Panjwani
bdea879273 v6 update 2022-01-10 16:06:17 +05:30
Mohit Panjwani
8d94f4efbe fix code style 2021-12-01 16:18:27 +05:30
harshjagad20
88bfb38b56 solve unit tests 2021-12-01 13:25:24 +05:30
Mohit Panjwani
082d5cacf2 v5.0.0 update 2021-11-30 18:58:19 +05:30
gohil jayvirsinh
d1dd704cdf Add File based templates 2021-06-19 12:11:21 +00:00