Skip to content

Commit

Permalink
first functional version of the gem
Browse files Browse the repository at this point in the history
  • Loading branch information
Rob committed Sep 6, 2011
1 parent 882d3b1 commit 6addaee
Show file tree
Hide file tree
Showing 12 changed files with 283 additions and 0 deletions.
37 changes: 37 additions & 0 deletions README
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
== Invoice number generating for ActiveRecord

The problem this gem tries to solve is that for bookkeeping, an uninterrupted sequence of
invoice numbers is needed. There are, however, situations in which the database's auto
incrementing id field won't do. For example:

* Orders for a webshop should only get an invoice number if the order is actually paid.
* The order in which record are made is different from the order in which invoices are
created, for example in case of a reservations system.

This gem makes this stupid simple:

# as soon as order is paid, invoice_nr will be set
class Order < ActiveRecord::Base
has_invoice_number :invoice_nr, assign_if: ->(order) { order.paid? }
end

# manually assign an invoice number when the user finishes the reservation
class Reservation < ActiveRecord::Base
has_invoice_number :invoice_nr

def finish
assign_invoice_number
save
end
end

# both funky order and boring order will share the same sequence of invoice numbers
class FunkyOrder < ActiveRecord::Base
has_invoice_number :invoice_nr, assign_if: ->(order) { order.paid? }, invoice_number_sequence: :order
end
class BoringOrder < ActiveRecord::Base
has_invoice_number :invoice_nr, assign_if: ->(order) { order.paid? }, invoice_number_sequence: :order
end

That's it. This gem could use some more flexibility and support for NoSQL databases, but
hopefully it is usefull to some people as is.
8 changes: 8 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
require 'rubygems'
require 'rake/testtask'
require File.dirname( __FILE__ ) + '/test/database_configuration'
require File.dirname( __FILE__ ) + '/test/database_schema'

Rake::TestTask.new do |t|
t.test_files = FileList['test/test*.rb']
end
19 changes: 19 additions & 0 deletions invoice_numbers.gemspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
require 'rake'

Gem::Specification.new do |s|
s.name = 'invoice_numbers'
s.version = '0.0.1'

s.authors = [ 'Rob Scheepmaker' ]
s.email = '[email protected]'
s.date = '2011-09-06'
s.description = 'Create sequences of uninterrupted invoice numbers'
s.summary = 'Create sequences of uninterrupted invoice numbers'
s.extra_rdoc_files = [ 'README' ]
s.files = FileList['lib/**/*.rb', 'test/**/*'].to_a
s.rdoc_options = ['--main', 'README']
s.require_paths = ['lib']
s.test_files = FileList['test/*.rb'].to_a
s.add_dependency 'activerecord', ['>= 0']
s.add_development_dependency 'database_cleaner', ['>= 0']
end
24 changes: 24 additions & 0 deletions lib/generators/invoice_numbers_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
require 'rails/generators'
require 'rails/generators/migration'

class InvoiceNumbersGenerator < Rails::Generator::Base
include Rails::Generators::Migration

def self.source_root
@source_root ||= File.join(File.dirname(__FILE__), 'templates')
end

# Implement the required interface for Rails::Generators::Migration.
def self.next_migration_number(dirname) #:nodoc:
next_migration_number = current_migration_number(dirname) + 1
if ActiveRecord::Base.timestamped_migrations
[Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % next_migration_number].max
else
"%.3d" % next_migration_number
end
end

def create_migration_file
migration_template 'migration.rb', 'db/migrate/create_invoice_number_sequences.rb'
end
end
10 changes: 10 additions & 0 deletions lib/generators/templates/migration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class CreateInvoiceNumberSequences < ActiveRecord::Migration
def change
create_table :invoice_number_sequences do |t|
t.string :name
t.integer :next_number, :default => 1
end

add_index :invoice_number_sequences, [:name], :unique => true
end
end
6 changes: 6 additions & 0 deletions lib/invoice_numbers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
require 'active_record/railtie'

require File.dirname( __FILE__ ) + '/invoice_numbers/generator'
require File.dirname( __FILE__ ) + '/invoice_numbers/active_record_extension'

ActiveRecord::Base.send :include, InvoiceNumbers::ActiveRecordExtension
30 changes: 30 additions & 0 deletions lib/invoice_numbers/active_record_extension.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
module InvoiceNumbers
module ActiveRecordExtension
def self.included( base )
base.send( :extend, ClassMethods )
end

module ClassMethods
def has_invoice_number( field_name, options = {} )
invoice_number_field = field_name
invoice_number_sequence = options[:invoice_number_sequence]
invoice_number_sequence ||= self.name.to_s.underscore
invoice_number_assign_if = options[:assign_if]

if invoice_number_assign_if
before_save :assign_invoice_number
end

send(:define_method, :assign_invoice_number) do
transaction do
if read_attribute( invoice_number_field ).blank?
if invoice_number_assign_if.nil? or invoice_number_assign_if.call(self)
write_attribute( invoice_number_field, Generator.next_invoice_number( invoice_number_sequence ) )
end
end
end
end
end
end
end
end
20 changes: 20 additions & 0 deletions lib/invoice_numbers/generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module InvoiceNumbers
class InvoiceNumberSequence < ActiveRecord::Base
def increment!
transaction do
lock!
number = self.next_number
self.next_number = number + 1
self.save!
number
end
end
end

class Generator
def self.next_invoice_number( sequence_name = :default )
sequence = InvoiceNumberSequence.find_or_create_by_name sequence_name
sequence.increment!
end
end
end
8 changes: 8 additions & 0 deletions test/database_configuration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
require 'active_record'
require 'sqlite3'

ActiveRecord::Base.establish_connection(
:adapter => 'sqlite3',
:database => 'db/test.sqlite3',
:pool => 5
)
6 changes: 6 additions & 0 deletions test/database_schema.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
ActiveRecord::Schema.define :version => 1 do
create_table :invoice_number_sequences, :force => true do |t|
t.string :name
t.integer :next_number, :default => 1
end
end
86 changes: 86 additions & 0 deletions test/test_invoice_active_record_class_methods.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
require 'test/unit'
require 'database_cleaner'
require 'minitest/spec'
require 'invoice_numbers'
require File.dirname( __FILE__ ) + '/database_configuration'

ActiveRecord::Schema.define :version => 1 do
create_table :orders, :force => true do |t|
t.boolean :finished, :default => false
t.integer :invoice_nr
end
create_table :reservations, :force => true do |t|
t.boolean :finished, :default => false
t.integer :invoice_nr
end
end

class Order < ActiveRecord::Base
has_invoice_number :invoice_nr, :assign_if => lambda { |order| order.finished? }
end

class Reservation < ActiveRecord::Base
has_invoice_number :invoice_nr, :invoice_number_sequence => :shared
end

describe Reservation do
before do
@reservation = Reservation.new
DatabaseCleaner.strategy = :truncation
DatabaseCleaner.clean
end

it 'does not assign an invoice number' do
@reservation.save
@reservation.invoice_nr.must_be_nil
end

it 'assigns an invoice number when forced' do
@reservation.assign_invoice_number
@reservation.save
@reservation.invoice_nr.must_equal 1
end

it 'uses the shared sequence' do
InvoiceNumbers::Generator.next_invoice_number(:shared)
InvoiceNumbers::Generator.next_invoice_number(:shared)
@reservation.assign_invoice_number
@reservation.invoice_nr.must_equal 3
end
end

describe Order do
before do
@order = Order.new
DatabaseCleaner.strategy = :truncation
DatabaseCleaner.clean
end

it 'does not assign an invoice number' do
@order.save
@order.invoice_nr.must_be_nil
end

it 'assigns an invoice number when finished' do
@order.finished = true
@order.save
@order.invoice_nr.must_equal 1
end

it 'does not update an invoice number once assigned' do
@order.finished = true
@order.save
@order.invoice_nr.must_equal 1
@order.invoice_nr_will_change! # force a new save
@order.save
@order.invoice_nr.must_equal 1
end

it 'uses the order sequence' do
InvoiceNumbers::Generator.next_invoice_number(:order)
InvoiceNumbers::Generator.next_invoice_number(:order)
@order.finished = true
@order.assign_invoice_number
@order.invoice_nr.must_equal 3
end
end
29 changes: 29 additions & 0 deletions test/test_invoice_number_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
require 'test/unit'
require 'database_cleaner'
require 'minitest/spec'
require 'invoice_numbers'
require File.dirname( __FILE__ ) + '/database_configuration'

describe InvoiceNumbers::Generator do
before do
DatabaseCleaner.strategy = :truncation
DatabaseCleaner.clean
end

describe 'next_invoice_number' do
it 'should return 1 the first time we request a certain sequence' do
InvoiceNumbers::Generator.next_invoice_number(:test).must_equal 1
end

it 'should increment with one on future request of a certain sequence' do
InvoiceNumbers::Generator.next_invoice_number(:test)
InvoiceNumbers::Generator.next_invoice_number(:test).must_equal 2
end

it 'should not increment other sequences' do
InvoiceNumbers::Generator.next_invoice_number(:test)
InvoiceNumbers::Generator.next_invoice_number(:other)
InvoiceNumbers::Generator.next_invoice_number(:test).must_equal 2
end
end
end

0 comments on commit 6addaee

Please sign in to comment.