Skip to main content

Invoicing

This module adds the ability to have invoicing in your application.

This module comes with a lot of configurations (config/invoicing.php). Make sure you edit these if needed.

Installation

To install this module:

This module uses the CkEditor plugin, make sure to install it See documentation.

composer require qore/invoicing
php artisan vendor:publish --tag=qore.invoicing.config
php artisan vendor:publish --tag=qore.invoicing.db
php artisan vendor:publish --tag=qore.invoicing.frontend
php artisan vendor:publish --tag=qore.invoicing.views

Usage

Getting started

The invoicing module is quite large, but almost everything can be configured in the config/invoicing.php.

This module comes with multiple resources (which can all be overwritten):

  • InvoiceResource (The invoices)
  • GeneralLedgerResource (General ledgers used (later) for bookkeeping)
  • InvoiceTemplateResource (When generating PDF's, the content can be managed here)
  • PaymentMethodResource (Payment methods that can be used for payments)
  • VatPercentageResource (Vat percentages that can be used for invoice lines)

The settings page for this module has a lot of options, make sure to visit /module-setting/invoicing-module as well

Available commands

The following commands are available, but need to be scheduled need to scheduled in the application:

Commandsignature
CancelOverdueInvoicesqore:invoicing:cancel-overdue-invoices

Adding to menu

You can add the invoice resource to your menu (e.g. in your GlobalsController):

->tab('home', function (MenuTab $tab) {
// ...

if (module_is_active('qore/invoicing')) {
run('invoicing.fill_menu', ['tab' => $tab]);
}
}

Creating an invoice

To create an invoice, you may use the InvoiceBuilder. Note that a lot of fields are not required. You will get an exception if something is missing:

$builder = (new InvoiceBuilder())
->setStatus(tenant_variable('invoice_statuses')->values->find($request->get('status')))
->setReceiverToEmailAddresses($request->get('receiver_mail_to')['addresses'] ?? null)
->setReceiverCcEmailAddresses($request->get('receiver_mail_cc')['addresses'] ?? null)
->setReceiverBccEmailAddresses($request->get('receiver_mail_bcc')['addresses'] ?? null)
->setAddressReference($request->get('address_reference'))
->setAddressCountry($country)
->setAddressZipcode($request->get('address')['zipcode'])
->setAddressNumber($request->get('address')['number'])
->setAddressAddition($request->get('address')['addition'])
->setAddressStreet($request->get('address')['street'])
->setAddressResidence($request->get('address')['residence'])
->setAddressProvince($request->get('address')['province'])
->setAddressMunicipality($request->get('address')['municipality'])
->setAddressLine1($request->get('address')['address_line_1'])
->setAddressLine2($request->get('address')['address_line_2'])
->setAddressLatitude($request->get('address')['lat'] ?? null)
->setAddressLongitude($request->get('address')['lng'] ?? null)
->setInvoiceDate($invoiceDate)
->setPaymentDueDays($request->get('payment_due_days'))
->setPaymentDueAt($paymentDueAt)
->setTextAbove($request->get('text_above'))
->setTextBelow($request->get('text_below'))
->setVatNumber($request->get('vat_number'))
->setSendMethod(InvoiceSendMethod::from($request->get('send_method')))
->setCurrencyCode($request->get('currency_code'))
->setInvoiceTemplate(InvoiceTemplate::find($request->get('invoiceTemplate')))
->setInvoiceLines([
(new InvoiceLineBuilder($invoice, $this->findExistingInvoiceLine($invoice, $line['id'])))
->setQuantity($line['quantity'])
->setAmount($line['amount'])
->setDescription($line['description'])
->setVatPercentage(VatPercentage::find($line['vat']))
->setDiscountType(InvoiceDiscountType::from($line['discount_type']))
->setDiscountAmount($line['discount_amount'])
->setGeneralLedger(GeneralLedger::find($line['general_ledger']))
->setDate(!is_null($line['date']) ? Carbon::parse($line['date']) : null)
->setEntity($productField->getModelType(), $line['product'])
->getInvoiceLine(),

// .. more invoice lines
])

$model = $builder->save();
info

A lof of database columns will be filled automatically based on the given data. For example, the amount_total_vat_included will be based on the invoice lines.

Setting a receiver (attaching a relation)

You can attach an invoice to a model via the InvoiceBuilder. The setReceiver expects a model class and an ID:

$builder->setReceiver(\Qore\Crm\Models\Tenant\Organization::class, $myId);

// or:
$builder->setReceiver(null);

Updating an invoice

You can update an invoice via the InvoiceBuilder by passing your invoice in the constructor:

(new InvoiceBuilder($this->model))
->setStatus(InvoiceStatus::SENT)
->save();

Changing the invoice line fields

You can manage the fields that are used for invoice lines in your invoicing.php config file:

    'invoice_line_fields' => fn() => [
InvoiceLineDate::make(__('invoicing::invoicing.Date'), 'date')
->rules('nullable', 'date'),

InvoiceLineQuantity::make(__('invoicing::invoicing.Quantity'), 'quantity')
->rules('required', 'integer'),

InvoiceLineProduct::make(__('invoicing::invoicing.Product'), 'product')
->type(Country::class)
->options(Country::all()->map(fn($country) => [
'value' => $country->id,
'label' => $country->name,
'description' => $country->full_name
])->toArray())
->onUpdate(function (ManagesForm $form, $countryId, $lineIndex) {
$lines = $form->getState('invoiceLines');
$faker = \Faker\Factory::create();
$lines[$lineIndex]->description = $faker->text(20);
$lines[$lineIndex]->amount = rand(0, 20);
$lines[$lineIndex]->quantity = rand(1, 5);
$form->setState('invoiceLines', $lines);

})
->rules('nullable', 'exists:countries,id'),

InvoiceLineGeneralLedger::make(__('invoicing::invoicing.General ledger'), 'general_ledger')
->rules('nullable', 'exists:general_ledgers,id'),

InvoiceLineDescription::make(__('invoicing::invoicing.Description'), 'description')
->rules('required', 'string'),

InvoiceLinePeriod::make(__('invoicing::invoicing.Period'), 'period')
->rules('nullable'),

InvoiceLineAmount::make(__('invoicing::invoicing.Price excl. vat'), 'amount')
->rules('required', 'numeric'),

InvoiceLineVatPercentage::make(__('invoicing::invoicing.Vat'), 'vat')
->rules('required', 'exists:vat_percentages,id')
],

You can also create your own InvoiceLineField, which is very similar to creating a custom Field. The only requirement is that you extend InvoiceLineField and supply a component, for example:

class InvoiceLineQuantity extends InvoiceLineField
{
public ?int $width = 100;

public function component(): string
{
return 'InvoiceLineQuantity';
}
}

Creating an invoice event

On the detail page for an invoice, on the right side, you can see progress activities (events). You can manually create these:

(new InvoiceEventBuilder($invoice)
->setIcon('add')
->setTitle('Invoice created')
->setDescription(__('invoicing::invoicing.Invoice was created by :creator', [
'creator' => auth()->check() ? auth()->user()->name : __('System')
]))
->save();

Creating an invoice payment

You can add payments manually via the InvoicePaymentBuilder:

$builder = (new InvoicePaymentBuilder($invoice))
->setAmount($state['amount'])
->setNote($state['note'] ?? null)
->setPaymentMethod(PaymentMethod::find($state['payment_method']))
->setImplementation(InvoicePaymentImplementation::from($state['state']['implementation']));

if ($referencedInvoice) {
$builder->setReferencedInvoice($referencedInvoice);
}

$builder->save();

Creating a credit invoice

A credit invoice is just another invoice. The only thing that differs a credit invoice from a normal invoice is that is_credited is set to true, and the invoice_id is not empty.

Typically, the amount of each invoice line is reversed, but you can have your own control over that. You can create a credit invoice using the InvoiceBuilder and add manually:

$builder = (new InvoiceBuilder())->(...);
$builder->getInvoice()->setAttribute('is_credited', true);
$builder->getInvoice()->setAttribute('invoice_id', $creditInvoice->id);
$builder->save();

Sending an invoice

In most cases, an invoice could be mailed to the receiver. Sometimes your invoice send_method is postage, meaning it doesn't need to be mailed, but the invoice status should still be updated, and an event should still be fired.

You can send an invoice using the following:

// This will automatically check if it should be mailed or not
(new InvoiceSender($model))->send();

If you need to mail with specific data, you could dispatch the following job:

    SendInvoiceJob::dispatch(
model: $model,
template: MailTemplate::find(...),
to: ['koen@qlic.nl'],
cc: ['cc@qlic.nl'],
bcc: ['bcc@qlic.nl'],
subject: 'My subject',
content: 'My content',
sendAsConcept: false // The concept status and send_at will not be changed if true
additionalData: []
);

Or alternatively:

(new InvoiceMailer($this->model, $this->template))
->to($this->to)
->cc($this->cc)
->bcc($this->bcc)
->subject($this->subject)
->content($this->content)
->send();

The invoicing module will create a mail template by default, but you could create one yourself when going to: /resources/mail_templates.

Modifying the mail message before sending

Before queueing the SendMailMessagesJob with the invoice mail message, the PreparedInvoiceMailMessageAction action is ran with the following arguments:

run(
(new PreparedInvoiceMailMessageAction())->identifier(),
[
'invoice' => $this->model, // The Invoice Model
'template' => $this->template, // The MailTemplate Model
'message' => $message, // The MailMessage Model
'additionalData' => $this->additionalData // array<string, mixed>
]
);

A developer could hook into this action:

actions()->after(
new PreparedInvoiceMailMessageAction,
AlterPreparedInvoiceMailMessage::class
);


class AlterPreparedInvoiceMailMessage extends ActionHook
{

public function identifier(): string
{
return 'invoicing.alter-prepared-invoice-mail-message';
}

public function run()
{
/** @var MailMessage $message */
$message = $this->args['message'];

$message->cc = 'administratie@qlic.nl';
}
}

Generated Invoice PDF

When sending or downloading an Invoice, a PDF will be created based on a blade template.

You can publish and override this template:

php artisan vendor:publish --tag=qore.invoicing.views

In order to generate a PDF manually, you can use:

$render = (new InvoiceRenderer($invoice, new TwigTemplateRenderer, 'pdf'))
->render();

return $render->stream();

Parts of the content of the generated invoice can be managed from the interface itself when going to: /resources/invoice_templates

Exchange Conversion rates

The Invoicing module makes use of an ExchangeRateClient to get exchange rates and convert values from one currency to another.

This client can be used outside of anything related to the Invoicing just by calling it.

getRate

ExchangeRateClient::getRate(Symbol::EUR, Symbol::RON); // returns 1 euro into rons
danger

The getRate function caches the value until the end of the day. It is to be noted that the European Central Bank updates its rates around 3-4 PM, meaning that you can get an outdated rate.

You should handle anything that has to do with this by caching yourself if you want to change this behaviour.

getConvertedValue

ExchangeRateClient::getConvertedValue(Symbol::EUR, Symbol::RON, 10); // returns 10 euros into rons

getHistoricRate

ExchangeRateClient::getHistoricRate(Symbol::RON, Symbol::RON, Carbon::now()->subMonth()) // returns the rate for this date

getHistoricRates

ExchangeRateClient::getHistoricRates(
Symbol::RON,
\Carbon\Carbon::create(2020, 10, 13),
1,
true,
Symbol::RON, Symbol::EUR, Symbol::CAD, Symbol::USD
);
// returns an associative array with values for each symbol
// for example for the above call:
[
'RON' => 1,
'EUR' => 0.4,
'CAD' => 0.3,
'USD' => 0.2
]

Accounting & Invoice booking

Invoices could potentially be booked to accounting software like Exact Online, Twinfield, Quickbooks etc. Typically, a Qore plugin should take care of this.

Inside the invoicing module there are adapters which convert Invoice models to generic objects to make sure all data is normalized before it gets processed by said plugins. You are free to override any adapter.

The configuration can also be found in the invoicing.php config file:

'accounting' => [
/**
* Leave the driver `null` if booking is disabled
* Drivers are defined by plugins, e.g. for Quickbooks: `quickbooks`
*/
'driver' => 'quickbooks',

/**
* Adapters to create generic accounting objects which are used by plugins
*/
'adapters' => [
'customer' => AccountingCustomerAdapter::class,
'address' => AccountingAddressAdapter::class,
'invoice' => AccountingInvoiceAdapter::class,
'invoiceLine' => AccountingInvoiceLineAdapter::class,
],

/**
* Event listeners for when accounting resources have been created/updated
* Add your own listeners here
*/
'event_listeners' => [
InvoiceBooked::class => [
SetInvoiceExternalId::class
],
CustomerBooked::class => [
SetCustomerExternalId::class
]
],
]

You can book an invoice manually:

// Create a new booking
BookInvoiceJob::dispatch($invoice, false);

// Or update an existing booking:
BookInvoiceJob::dispatch($invoice, true);

See also: (Quickbooks).

Invoice reminders

Invoice reminders are part of the invoicing module. They add reminder flows, manual reminders and automatic reminder dispatching for overdue invoices.

Available resources

When reminders are enabled, the invoicing module also exposes these resources:

  • InvoiceReminderResource for individual reminder messages
  • ReminderFlowResource for configuring reminder flows

Reminder flows are protected separately. Besides the normal resource permissions, a user also needs the ManageReminderFlows permission scope to manage flows.

Module settings

Reminder settings are available in the invoicing module settings under the reminders tab.

The most important settings are:

  • enabled to enable or disable reminder functionality
  • automatic_mode to choose between disabled, draft creation or direct sending
  • automatic_days to decide on which weekdays automation may run
  • automatic_time to decide at what time automation may run
  • default_flow_id to define the default reminder flow for invoices
  • minimum_outstanding_amount to skip invoices below a minimum open amount
  • excluded_conditions to exclude invoices in states like dispute, pending, concept or credit

How reminder flows work

A reminder flow contains ordered rules. Each rule defines when the next reminder becomes due and how it should be sent.

Each rule can contain:

  • a title and description
  • days_after_previous for the delay after the previous step
  • term_days for the new payment term
  • a mail template
  • a reminder document mode
  • optional PDF templates
  • optional custom attachments
  • optional open items overview attachment

For the reminder document itself there are two modes:

  • invoice_attachment to send the original invoice as attachment
  • pdf_template to generate a separate reminder PDF from a Twig template

Reminder flows can no longer be modified once they are already used by existing reminders.

Assigning a flow to invoices

The active reminder flow for an invoice is resolved in this order:

  1. The invoice-specific reminder settings
  2. The related receiver settings when available
  3. The module default flow

If the effective automation mode is disabled, no automatic reminder flow will run for that invoice.

Manual reminders

Manual reminders are stored as InvoiceReminder records. Creating or editing a reminder through the resource will also switch the related invoice to manual reminder mode.

The reminder resource stores:

  • the related invoice
  • the selected reminder flow and flow rule
  • the mail template
  • to, cc and bcc
  • subject and content
  • the outstanding amount at the time of creation
  • the reminder status

Depending on the status and permissions, reminders can be adjusted, sent or resent from the resource actions.

Automatic reminder dispatching

Automatic reminders are dispatched through the following command:

php artisan qore:invoicing:dispatch-invoice-reminders

The command supports:

php artisan qore:invoicing:dispatch-invoice-reminders --force

The --force option ignores the configured weekday and time window.

By default the invoicing service provider schedules this command every minute:

$schedule->command(ReminderAutomationRunner::class)->everyMinute();

The runner itself only continues when:

  • reminders are enabled
  • the automatic mode is not disabled
  • the current weekday is allowed
  • the current time matches the configured automatic_time

During execution, the runner:

  1. Skips invoices that are not eligible for reminders
  2. Resolves the effective flow and next due rule
  3. Creates or reuses an open reminder
  4. Creates a draft or queued reminder depending on the configured automatic mode
  5. Sends the reminder immediately when direct send is enabled

The automation uses a tenant-specific cache lock to prevent concurrent runs.

Sending reminders manually in code

If you already have an InvoiceReminder model, you can send it manually with the sender:

(new InvoiceReminderSender($reminder))->send();

This sender prepares the mail message, attaches the right invoice or reminder PDF, adds optional custom attachments and updates the reminder after the mail is sent.

When the invoicing module fills the menu, reminders are added to the invoicing group automatically.

If you use the default menu hook, both invoices and reminders are included:

if (module_is_active('qore/invoicing')) {
run('invoicing.fill_menu', ['tab' => $tab]);
}

Upgrade Guide

To upgrade this module:

composer update qore/invoicing

If you need to upgrade migrations or Vue components:

php artisan vendor:publish --tag=qore.invoicing.db --force
php artisan vendor:publish --tag=qore.invoicing.frontend --force
php artisan vendor:publish --tag=qore.invoicing.views --force