During this lockdown, I thought I would learn accounting. I love accounting, or anything with numbers, but never fully get a chance to act on that thought.
Xero Central is my goto for anything accounting. You could pretty much read their entire documentation and API to get an understanding of how things work or accounting in general. This is what I did. I've decided to focus on the invoicing section. I also like their invoice interface that I made mine the similar (kids, do not copy π):
This post is about how I've done the invoice numbering. I know there should be a better way but I could only come up with this method. Using Rails 6 API, I'll show you how I've pulled it off.
The Invoice Settings
This model is responsible for validating your chosen invoice sequence ie INV-001
. INV-
is used as the prefix and 001
is used as a numeric sequence. Sequence can only be numeric. Should you try to enter an alpha-numeric value, it will fail. Note this has nothing to do with the actual invoice number. We'll get to that in a bit.
class InvoiceSetting < ApplicationRecord
belongs_to :organisation
validates :invoice_number_sequence, :quote_number_sequence, :purchase_order_number_sequence, should_be_sequencialised: true
validate :check_for_duplicate_numbers
private
# TODO: Check for credit note for invoice number
def check_for_duplicate_numbers
exists = Invoice.exists?(['lower(invoice_number) = ? AND organisation_id = ?', "#{(invoice_prefix + invoice_number_sequence).downcase}", organisation_id])
if exists
errors.add(:invoice_number_sequence, "invoice number already exists.")
end
end
end
So what is should_be_sequencialised
?
We use a Rails concerns to implement this logic. This logic ensures the sequence value is numeric:
# /models/concerns/should_be_sequencialised_validator.rb
class ShouldBeSequencialisedValidator < ActiveModel::EachValidator
# The Client should also ensure users only enter numeric values ie 001, 1 etc.
def validate_each(record, attribute, value)
begin
Integer(value)
rescue StandardError => e
record.errors.add(attribute, "Invalid number. Please use numeric numbers only")
end
end
end
validate_each
accepts three arguments: model, model's column name and the incoming value to be checked. EachValidator
will loop this method, in our example, 3 times. Later I'll refactor to only validate on invoice_number_sequence
.
Integer
will throw an exception so it should be wrapped in catch block. When there's an error, we add the error message back to our model for the front-end to display. Your data will be rolled back. A neat feature with Rails and other frameworks.
check_for_duplicate_numbers
Should the first validator succeeds, check_for_duplicate_numbers
checks for existing invoice numbers already generated. Pretty easy to understand.
That takes care of the validation but how does an invoice number increments?
JΓΆrg W Mittag on Stack overflow says:
Then it's not a number but rather an "invoice identifier object", and you model it as an InvoiceIdentifier
I've "Googled" what's an InvoiceIdentifier but nothing came up. I should have asked for him to explain but I never wanted to extend the post when I've already selected an answer, so I left it as is. If invoice_number
is not a string then what is it? I've created a model called invoice
with a string attribute as invoice_number
. I've used string because I cannot find information regarding this InvoiceIdentifier
.
Let's validate our invoice model to ensure invoice_number
is unique. This is only unique for each of the user's organisation:
class Invoice < ApplicationRecord
validates :invoice_number, uniqueness: { case_sensitive: :false, scope: :organisation_id }
[...] # Other validations and relationships etc
end
We'll now focus our attention over to invoice_controller.rb
:
Invoice Controller
# POST /organisations/:organisation_id/invoices
def create
invoice = @organisation.invoices.build(invoice_params)
if invoice.save
return render json: invoice
end
render json: {error: invoice.errors.messages}
end
I won't explain what's going on here as this is not a beginners guide. At this point we should be able to create an invoice. If it fails then our invoice_number
is not unique.
How and when does the invoice number auto increments?
The Wisper gem is a great tool for listening to events. We'd listen for two events: create and update (this example for focuses on create). This is when we would increment our invoice number or leave it as is. Let's look at this IncrementInvoiceNumber
class:
# /lib/invoices/increment_invoice_number.rb
module Invoices
class IncrementInvoiceNumber
include Wisper::Publisher
def create(invoice, organisation)
if invoice.save
puts "-----INVOICE CREATED------"
increment_invoice_number(invoice.invoice_number, organisation)
broadcast(:invoice_created, invoice)
else
puts "-----INVOICE FAILED------"
broadcast(:invoice_failed, invoice)
end
end
private
def increment_invoice_number(inv_num, organisation)
settings = organisation.invoice_setting
invoice_setting_prefix = settings.invoice_prefix
arr = inv_num.split(invoice_setting_prefix)
if arr.count == 2
# We have an untouched invoice number
incremented_sequence = arr[1]
# We need to test if it's a numeric value
begin
Integer(incremented_sequence)
# We can safely increment and save
incremented_sequence = incremented_sequence.next
organisation.invoice_setting.update(invoice_number_sequence: incremented_sequence)
rescue StandardError => e
# We do nothing
end
end
# At this point, we have not incremented the invoice number that came in as they've entered a custom invoice number.
end
end
end
Our create method is self-explanatory so let's look at increment_invoice_number
.
We increment the invoice number only if the prefix is the same as when you're creating a new invoice. See main image, but we still need to check as a user could simply enter INV-foo
. If so, we save the invoice as is and only increment if the sequence is of a numeric value. At this point, validation has passed and invoice number is unique.
foo.next
will also increment to fop
but our validation checks if that section is numeric with Integer(string)
.
Once you've install Wisper, our create
method now becomes:
# /controllers/invoice_controller.rb
# POST /organisations/:organisation_id/invoices
def create
service = Invoices::IncrementInvoiceNumber.new
service.on(:invoice_created) { |invoice| render json: invoice }
service.on(:invoice_failed) { | invoice |render json: {error: invoice.errors.messages} }
service.create(@organisation.invoices.build(invoice_params), @organisation)
end
Incrementing an invoice number only runs after an invoice is created. Is there a better way? I would love to know. If you're a Xero dev, I'd love to hear your thoughts and it does not matter what programming language you guys are using.
Top comments (0)