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

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

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