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:
| Command | signature |
|---|---|
| CancelOverdueInvoices | qore: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();
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
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:
InvoiceReminderResourcefor individual reminder messagesReminderFlowResourcefor 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:
enabledto enable or disable reminder functionalityautomatic_modeto choose between disabled, draft creation or direct sendingautomatic_daysto decide on which weekdays automation may runautomatic_timeto decide at what time automation may rundefault_flow_idto define the default reminder flow for invoicesminimum_outstanding_amountto skip invoices below a minimum open amountexcluded_conditionsto 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_previousfor the delay after the previous stepterm_daysfor 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_attachmentto send the original invoice as attachmentpdf_templateto 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:
- The invoice-specific reminder settings
- The related receiver settings when available
- 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,ccandbcc- 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:
- Skips invoices that are not eligible for reminders
- Resolves the effective flow and next due rule
- Creates or reuses an open reminder
- Creates a draft or queued reminder depending on the configured automatic mode
- 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.
Menu integration
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