lib/osm/invoice.rb
module Osm
class Invoice < Osm::Model
SORT_BY = [:section_id, :name, :date]
# @!attribute [rw] id
# @return [Fixnum] The OSM ID for the invoice
# @!attribute [rw] section_id
# @return [Fixnum] The OSM ID for the section the invoice belongs to
# @!attribute [rw] name
# @return [String] The name given to the invoice
# @!attribute [rw] extra_details
# @return [String] Any extra details added to the invoice
# @!attribute [rw] date
# @return [Date] When the invoice was created
# @!attribute [rw] archived
# @return [Boolean] Whether the invoice has been archived
# @!attribute [rw] finalised
# @return [Boolean] Whether the invoice has been finalised
attribute :id, :type => Integer
attribute :section_id, :type => Integer
attribute :name, :type => String
attribute :extra_details, :type => String, :default => ''
attribute :date, :type => Date, :default => Date.today
attribute :archived, :type => Boolean, :default => false
attribute :finalised, :type => Boolean, :default => false
if ActiveModel::VERSION::MAJOR < 4
attr_accessible :id, :section_id, :name, :extra_details, :date, :archived, :finalised
end
validates_numericality_of :id, :only_integer=>true, :greater_than=>0, :unless => Proc.new { |r| r.id.nil? }
validates_numericality_of :section_id, :only_integer=>true, :greater_than=>0
validates_presence_of :name
validates_presence_of :date
validates_inclusion_of :archived, :in => [true, false]
validates_inclusion_of :finalised, :in => [true, false]
# @!method initialize
# Initialize a new Budget
# @param [Hash] attributes The hash of attributes (see attributes for descriptions, use Symbol of attribute name as the key)
# Get invoices for a section
# @param [Osm::Api] api The api to use to make the request
# @param [Osm::Section, Fixnum, #to_i] section The section (or its ID) to get the invoices for
# @!macro options_get
# @option options [Boolean] :include_archived (optional) if true then archived invoices will also be returned
# @return [Array<Osm::Invoice>]
def self.get_for_section(api, section, options={})
require_ability_to(api, :read, :finance, section, options)
section_id = section.to_i
cache_key = ['invoice_ids', section_id]
invoices = nil
if !options[:no_cache] && cache_exist?(api, cache_key)
ids = cache_read(api, cache_key)
invoices = get_from_ids(api, ids, 'invoice', section, options, :get_for_section)
end
if invoices.nil?
data = api.perform_query("finances.php?action=getInvoices§ionid=#{section_id}&showArchived=true")
invoices = Array.new
ids = Array.new
unless data['items'].nil?
data['items'].map { |i| i['invoiceid'].to_i }.each do |invoice_id|
invoice_data = api.perform_query("finances.php?action=getInvoice§ionid=#{section_id}&invoiceid=#{invoice_id}")
invoice = self.new_invoice_from_data(invoice_data)
invoices.push invoice
ids.push invoice.id
cache_write(api, ['invoice', invoice.id], invoice)
end
end
cache_write(api, cache_key, ids)
end
return invoices if options[:include_archived]
return invoices.reject do |invoice|
invoice.archived?
end
end
# Get an invoice
# @param [Osm::Api] api The api to use to make the request
# @param [Osm::Section, Fixnum, #to_i] section The section (or its ID) to get the events for
# @param [Fixnum, #to_i] invoice_id The id of the invoice to get
# @!macro options_get
# @return [Osm::Invoice, nil] the invoice (or nil if it couldn't be found
def self.get(api, section, invoice_id, options={})
require_ability_to(api, :read, :events, section, options)
section_id = section.to_i
invoice_id = invoice_id.to_i
cache_key = ['invoice', invoice_id]
if !options[:no_cache] && cache_exist?(api, cache_key)
return cache_read(api, cache_key)
end
invoice_data = api.perform_query("finances.php?action=getInvoice§ionid=#{section_id}&invoiceid=#{invoice_id}")
return self.new_invoice_from_data(invoice_data)
end
# Create the invoice in OSM
# @return [Boolean] Whether the invoice was created in OSM
# @raise [Osm::ObjectIsInvalid] If the Invoice is invalid
# @raise [Osm::Error] If the invoice already exists in OSM
def create(api)
raise Osm::Error, 'the invoice already exists in OSM' unless id.nil?
raise Osm::ObjectIsInvalid, 'invoice is invalid' unless valid?
Osm::Model.require_ability_to(api, :write, :finance, section_id)
data = api.perform_query("finances.php?action=addInvoice§ionid=#{section_id}", {
'name' => name,
'extra' => extra_details,
'date' => date.strftime(Osm::OSM_DATE_FORMAT),
})
if data.is_a?(Hash) && !data['id'].nil?
# The cached invoices for the section will be out of date - remove them
cache_delete(api, ['invoice_ids', section_id])
self.id = data['id'].to_i
return true
end
return false
end
# Update the invoice in OSM
# @param [Osm::Api] api The api to use to make the request
# @return [Boolan] whether the invoice was successfully updated or not
# @raise [Osm::ObjectIsInvalid] If the Invoice is invalid
def update(api)
raise Osm::ObjectIsInvalid, 'invoice is invalid' unless valid?
require_ability_to(api, :write, :finance, section_id)
data = api.perform_query("finances.php?action=addInvoice§ionid=#{section_id}", {
'invoiceid' => id,
'name' => name,
'extra' => extra_details,
'date' => date.strftime(Osm::OSM_DATE_FORMAT),
})
if data.is_a?(Hash) && data['ok'].eql?(true)
reset_changed_attributes
# The cached invoice will be out of date - remove it
cache_delete(api, ['invoice', self.id])
return true
end
return false
end
# Delete the invoice from OSM
# @return [Boolean] Whether the invoice was deleted from OSM
def delete(api)
Osm::Model.require_ability_to(api, :write, :finance, section_id)
return false if finalised?
data = api.perform_query("finances.php?action=deleteInvoice§ionid=#{section_id}", {
'invoiceid' => id,
})
if (data.is_a?(Hash) && data['ok'].eql?(true))
# The cached invoices for the section will be out of date - remove them
cache_delete(api, ['invoice_ids', section_id])
cache_delete(api, ['invoice', self.id])
return true
end
return false
end
# Archive the invoice in OSM, updating the archived attribute if successful.
# If the archived attribute is true then nothing happens and false is returned.
# @return [Boolean] Whether the invoice was archived in OSM
# @raise [Osm::Error] If the invoice does not already exist in OSM
def archive(api)
Osm::Model.require_ability_to(api, :write, :finance, section_id)
raise Osm::Error, 'the invoice does not already exist in OSM' if id.nil?
return false if archived?
data = api.perform_query("finances.php?action=deleteInvoice§ionid=#{section_id}", {
'invoiceid' => id,
'archived' => 1,
})
if (data.is_a?(Hash) && data['ok'].eql?(true))
self.archived = true
# The cached invoice for the section will be out of date - remove it
cache_delete(api, ['invoice', self.id])
return true
end
return false
end
# Finalise the invoice in OSM, updating the finalised attribute if successful.
# If the finalised attribute is true then nothing happens and false is returned.
# @return [Boolean] Whether the invoice was finalised in OSM
# @raise [Osm::Error] If the invoice does not already exist in OSM
def finalise(api)
Osm::Model.require_ability_to(api, :write, :finance, section_id)
raise Osm::Error, 'the invoice does not already exist in OSM' if id.nil?
return false if finalised?
data = api.perform_query("finances.php?action=finaliseInvoice§ionid=#{section_id}&invoiceid=#{id}")
if (data.is_a?(Hash) && data['ok'].eql?(true))
self.finalised = true
# The cached invoice for the section will be out of date - remove it
cache_delete(api, ['invoice', self.id])
return true
end
return false
end
# Get items for the invoice
# @param [Osm::Api] api The api to use to make the request
# @!macro options_get
# @return [Array<Osm::Invoice::Item>]
def get_items(api, options={})
require_ability_to(api, :read, :finance, section_id, options)
cache_key = ['invoice_items', id]
if !options[:no_cache] && cache_exist?(api, cache_key)
return cache_read(api, cache_key)
end
items = Array.new
data = api.perform_query("finances.php?action=getInvoiceRecords&invoiceid=#{id}§ionid=#{section_id}&dateFormat=generic")
data['items'].each do |item|
items.push Osm::Invoice::Item.new(
:id => Osm::to_i_or_nil(item['id']),
:invoice => self,
:record_id => Osm::to_i_or_nil(item['recordid']),
:date => Osm::parse_date(item['entrydate']),
:amount => item['amount'],
:type => item['type'].to_s.downcase.to_sym,
:payto => item['payto_userid'].to_s.strip,
:description => item['comments'],
:budget_name => item['categoryid'],
)
end
cache_write(api, cache_key, items)
return items
end
private
def self.new_invoice_from_data(invoice_data)
invoice_data = invoice_data['invoice']
return nil unless invoice_data.is_a?(Hash)
Osm::Invoice.new(
:id => Osm::to_i_or_nil(invoice_data['invoiceid']),
:section_id => Osm::to_i_or_nil(invoice_data['sectionid']),
:name => invoice_data['name'],
:extra_details => invoice_data['extra'],
:date => Osm::parse_date(invoice_data['entrydate']),
:archived => invoice_data['archived'].eql?('1'),
:finalised => invoice_data['finalised'].eql?('1'),
)
end
class Item < Osm::Model
SORT_BY = [:invoice, :date]
# @!attribute [rw] id
# @return [Fixnum] The OSM ID for the invoice item
# @!attribute [rw] invoice
# @return [Osm::Invoice] The Osm::Invoice the item belongs to
# @!attribute [rw] record_id
# @return [Fixnum] The id of the item within the invoice
# @!attribute [rw] date
# @return [Fixnum] The date the item was paid/received
# @!attribute [rw] amount
# @return [Fixnum] The amount of the transaction
# @!attribute [rw] type
# @return [Fixnum] The type of transaction (:expense or :income)
# @!attribute [rw] payto
# @return [Fixnum] Who paid/reimbursed
# @!attribute [rw] description
# @return [Fixnum] A description for the transaction
# @!attribute [rw] budget_name
# @return [Fixnum] The name of the budget this item is assigned to
attribute :id, :type => Integer
attribute :invoice, :type => Object
attribute :record_id, :type => Integer
attribute :date, :type => Date, :default => lambda { Date.today }
attribute :amount, :type => String, :default => '0.00'
attribute :type, :type => Object
attribute :payto, :type => String
attribute :description, :type => String
attribute :budget_name, :type => String, :default => 'Default'
if ActiveModel::VERSION::MAJOR < 4
attr_accessible :id, :invoice, :record_id, :date, :amount, :type, :payto, :description, :budget_name
end
validates_numericality_of :id, :only_integer=>true, :greater_than=>0, :unless => Proc.new { |r| r.id.nil? }
validates_numericality_of :record_id, :only_integer=>true, :greater_than=>0, :unless => Proc.new { |r| r.record_id.nil? }
validates_presence_of :invoice
validates_presence_of :date
validates_presence_of :payto
validates_presence_of :description
validates_presence_of :budget_name
validates_inclusion_of :type, :in => [:expense, :income]
validates_format_of :amount, :with => /\A\d+\.\d{2}\Z/
# @!method initialize
# Initialize a new Budget
# @param [Hash] attributes The hash of attributes (see attributes for descriptions, use Symbol of attribute name as the key)
# Create the item in OSM
# @return [Boolean] Whether the item was created in OSM
# @raise [Osm::ObjectIsInvalid] If the Item is invalid
# @raise [Osm::Error] If the invoice item already exists in OSM
def create(api)
raise Osm::Error, 'the invoice item already exists in OSM' unless id.nil?
raise Osm::ObjectIsInvalid, 'invoice item is invalid' unless valid?
Osm::Model.require_ability_to(api, :write, :finance, invoice.section_id)
last_item = invoice.get_items(api, {:no_cache=>true}).sort{ |a,b| a.record_id <=> b.record_id }[-1]
data = api.perform_query("finances.php?action=addRecord&invoiceid=#{invoice.id}§ionid=#{invoice.section_id}")
if data.is_a?(Hash) && data['ok'].eql?(true)
new_item = invoice.get_items(api, {:no_cache => true}).sort{ |a,b| a.record_id <=> b.record_id }[-1]
if !new_item.nil? && (last_item.try(:id) != new_item.try(:id))
# The cached invoice items for the section will be out of date - remove them
cache_delete(api, ['invoice_items', invoice.id])
self.id = new_item.id
self.record_id = new_item.record_id
# Update attributes in OSM
[['amount', amount], ['comments', description], ['type', type.to_s.titleize], ['payto_userid', payto], ['categoryid', budget_name], ['entrydate', date.strftime(Osm::OSM_DATE_FORMAT)]].each do |osm_name, value|
api.perform_query("finances.php?action=updateRecord§ionid=#{invoice.section_id}&dateFormat=generic", {
'section_id' => invoice.section_id,
'invoiceid' => invoice.id,
'recordid' => record_id,
'row' => 0,
'column' => osm_name,
'value' => value,
})
end
return true
end
end
return false
end
# Update invoice item in OSM
# @param [Osm::Api] api The api to use to make the request
# @return [Boolean] whether the update succedded
# @raise [Osm::ObjectIsInvalid] If the Invoice is invalid
def update(api)
require_ability_to(api, :write, :finance, invoice.section_id)
raise Osm::ObjectIsInvalid, 'invoice item is invalid' unless valid?
updated = true
to_update = Array.new
to_update.push ['amount', amount] if changed_attributes.include?('amount')
to_update.push ['comments', description] if changed_attributes.include?('description')
to_update.push ['type', type.to_s.titleize] if changed_attributes.include?('type')
to_update.push ['payto_userid', payto] if changed_attributes.include?('payto')
to_update.push ['categoryid', budget_name] if changed_attributes.include?('budget_name')
to_update.push ['entrydate', date.strftime(Osm::OSM_DATE_FORMAT)] if changed_attributes.include?('date')
to_update.each do |osm_name, value|
data = api.perform_query("finances.php?action=updateRecord§ionid=#{invoice.section_id}&dateFormat=generic", {
'section_id' => invoice.section_id,
'invoiceid' => invoice.id,
'recordid' => record_id,
'row' => 0,
'column' => osm_name,
'value' => value,
})
updated &&= (data.is_a?(Hash) && data[osm_name].to_s.eql?(value.to_s))
end
if updated
reset_changed_attributes
# The cached items for the invoice will be out of date - remove them
cache_delete(api, ['invoice_items', invoice.id])
return true
else
return false
end
end
# Delete invoice item from OSM
# @param [Osm::Api] api The api to use to make the request
# @return [Boolean] whether the delete succedded
def delete(api)
require_ability_to(api, :write, :finance, invoice.section_id)
data = api.perform_query("finances.php?action=deleteEntry§ionid=#{invoice.section_id}", {
'id' => id,
})
if data.is_a?(Hash) && data['ok']
# The cached invoice items for the section will be out of date - remove them
cache_delete(api, ['invoice_items', invoice.id])
return true
end
return false
end
# Get value of this item for easy totaling
# @return [Float]
def value
return amount.to_f if type.eql?(:income)
return -amount.to_f if type.eql?(:expense)
return 0.0
end
end # class Invoice::Item
end # class Invoice
end # Module