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
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',
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).
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