2016-03-23 18:39:41 +01:00
class Invoice < ActiveRecord :: Base
include NotifyWith :: NotificationAttachedObject
require 'fileutils'
scope :only_invoice , - > { where ( type : nil ) }
belongs_to :invoiced , polymorphic : true
has_many :invoice_items , dependent : :destroy
accepts_nested_attributes_for :invoice_items
belongs_to :user
2016-07-20 15:07:43 +02:00
belongs_to :wallet_transaction
2016-08-03 17:25:00 +02:00
belongs_to :coupon
2016-03-23 18:39:41 +01:00
has_one :avoir , class_name : 'Invoice' , foreign_key : :invoice_id , dependent : :destroy
after_create :update_reference
2017-01-04 14:48:32 +01:00
after_commit :generate_and_send_invoice , on : [ :create ] , :if = > :persisted?
2016-03-23 18:39:41 +01:00
def file
dir = " invoices/ #{ user . id } "
# create directories if they doesn't exists (invoice & user_id)
FileUtils :: mkdir_p dir
2016-03-29 18:02:40 +02:00
" #{ dir } / #{ self . filename } "
end
def filename
" #{ ENV [ 'INVOICE_PREFIX' ] } - #{ self . id } _ #{ self . created_at . strftime ( '%d%m%Y' ) } .pdf "
2016-03-23 18:39:41 +01:00
end
def generate_reference
pattern = Setting . find_by ( { name : 'invoice_reference' } ) . value
# invoice number per day (dd..dd)
reference = pattern . gsub ( / d+(?![^ \ []* \ ]) / ) do | match |
pad_and_truncate ( number_of_invoices ( 'day' ) , match . to_s . length )
end
# invoice number per month (mm..mm)
reference . gsub! ( / m+(?![^ \ []* \ ]) / ) do | match |
pad_and_truncate ( number_of_invoices ( 'month' ) , match . to_s . length )
end
# invoice number per year (yy..yy)
reference . gsub! ( / y+(?![^ \ []* \ ]) / ) do | match |
pad_and_truncate ( number_of_invoices ( 'year' ) , match . to_s . length )
end
# full year (YYYY)
reference . gsub! ( / YYYY(?![^ \ []* \ ]) / , Time . now . strftime ( '%Y' ) )
# year without century (YY)
reference . gsub! ( / YY(?![^ \ []* \ ]) / , Time . now . strftime ( '%y' ) )
# abreviated month name (MMM)
reference . gsub! ( / MMM(?![^ \ []* \ ]) / , Time . now . strftime ( '%^b' ) )
# month of the year, zero-padded (MM)
reference . gsub! ( / MM(?![^ \ []* \ ]) / , Time . now . strftime ( '%m' ) )
# month of the year, non zero-padded (M)
reference . gsub! ( / M(?![^ \ []* \ ]) / , Time . now . strftime ( '%-m' ) )
# day of the month, zero-padded (DD)
reference . gsub! ( / DD(?![^ \ []* \ ]) / , Time . now . strftime ( '%d' ) )
# day of the month, non zero-padded (DD)
reference . gsub! ( / DD(?![^ \ []* \ ]) / , Time . now . strftime ( '%-d' ) )
# information about online selling (X[text])
if self . stp_invoice_id
reference . gsub! ( / X \ [([^ \ ]]+) \ ] / , '\1' )
else
reference . gsub! ( / X \ [([^ \ ]]+) \ ] / , '' . to_s )
end
2016-07-12 12:29:51 +02:00
# information about wallet (W[text])
2016-09-23 12:52:43 +02:00
#reference.gsub!(/W\[([^\]]+)\]/, ''.to_s)
2016-07-12 12:29:51 +02:00
2016-03-23 18:39:41 +01:00
# remove information about refunds (R[text])
reference . gsub! ( / R \ [([^ \ ]]+) \ ] / , '' . to_s )
self . reference = reference
end
def update_reference
generate_reference
save
end
def order_number
pattern = Setting . find_by ( { name : 'invoice_order-nb' } ) . value
# global invoice number (nn..nn)
reference = pattern . gsub ( / n+(?![^ \ []* \ ]) / ) do | match |
pad_and_truncate ( number_of_invoices ( 'global' ) , match . to_s . length )
end
# invoice number per year (yy..yy)
reference . gsub! ( / y+(?![^ \ []* \ ]) / ) do | match |
pad_and_truncate ( number_of_invoices ( 'year' ) , match . to_s . length )
end
# invoice number per month (mm..mm)
reference . gsub! ( / m+(?![^ \ []* \ ]) / ) do | match |
pad_and_truncate ( number_of_invoices ( 'month' ) , match . to_s . length )
end
# invoice number per day (dd..dd)
reference . gsub! ( / d+(?![^ \ []* \ ]) / ) do | match |
pad_and_truncate ( number_of_invoices ( 'day' ) , match . to_s . length )
end
# full year (YYYY)
reference . gsub! ( / YYYY(?![^ \ []* \ ]) / , self . created_at . strftime ( '%Y' ) )
# year without century (YY)
reference . gsub! ( / YY(?![^ \ []* \ ]) / , self . created_at . strftime ( '%y' ) )
# abreviated month name (MMM)
reference . gsub! ( / MMM(?![^ \ []* \ ]) / , self . created_at . strftime ( '%^b' ) )
# month of the year, zero-padded (MM)
reference . gsub! ( / MM(?![^ \ []* \ ]) / , self . created_at . strftime ( '%m' ) )
# month of the year, non zero-padded (M)
reference . gsub! ( / M(?![^ \ []* \ ]) / , self . created_at . strftime ( '%-m' ) )
# day of the month, zero-padded (DD)
reference . gsub! ( / DD(?![^ \ []* \ ]) / , self . created_at . strftime ( '%d' ) )
# day of the month, non zero-padded (DD)
reference . gsub! ( / DD(?![^ \ []* \ ]) / , self . created_at . strftime ( '%-d' ) )
reference
end
2016-04-06 17:46:24 +02:00
# for debug & used by rake task "fablab:regenerate_invoices"
2016-03-23 18:39:41 +01:00
def regenerate_invoice_pdf
pdf = :: PDF :: Invoice . new ( self ) . render
File . binwrite ( file , pdf )
end
def build_avoir ( attrs = { } )
raise Exception if has_avoir === true or prevent_refund?
avoir = Avoir . new ( self . dup . attributes )
avoir . type = 'Avoir'
avoir . attributes = attrs
avoir . reference = nil
avoir . invoice_id = id
# override created_at to compute CA in stats
avoir . created_at = avoir . avoir_date
avoir . total = 0
2016-11-28 16:34:39 +01:00
# refunds of invoices with cash coupons: we need to ventilate coupons on paid items
paid_items = 0
refund_items = 0
2016-03-23 18:39:41 +01:00
invoice_items . each do | ii |
2016-11-28 16:34:39 +01:00
paid_items += 1 unless ii . amount == 0
if attrs [ :invoice_items_ids ] . include? ii . id # list of items to refund (partial refunds)
raise Exception if ii . invoice_item # cannot refund an item that was already refunded
refund_items += 1 unless ii . amount == 0
2016-03-23 18:39:41 +01:00
avoir_ii = avoir . invoice_items . build ( ii . dup . attributes )
avoir_ii . created_at = avoir . avoir_date
avoir_ii . invoice_item_id = ii . id
avoir . total += avoir_ii . amount
end
end
2016-08-11 13:44:42 +02:00
# handle coupon
unless avoir . coupon_id . nil?
2016-11-23 17:17:34 +01:00
discount = avoir . total
if avoir . coupon . type == 'percent_off'
discount = avoir . total * avoir . coupon . percent_off / 100 . 0
elsif avoir . coupon . type == 'amount_off'
2016-11-28 16:34:39 +01:00
discount = ( avoir . coupon . amount_off / paid_items ) * refund_items
2016-11-24 17:57:48 +01:00
else
raise InvalidCouponError
2016-11-23 17:17:34 +01:00
end
2016-08-11 13:44:42 +02:00
avoir . total -= discount
end
2016-03-23 18:39:41 +01:00
avoir
end
def is_subscription_invoice?
invoice_items . each do | ii |
return true if ii . subscription and ! ii . subscription . is_expired?
end
false
end
##
# Test if the current invoice has been refund, totally or partially.
# @return {Boolean|'partial'}, true means fully refund, false means not refunded
##
def has_avoir
if avoir
invoice_items . each do | item |
return 'partial' unless item . invoice_item
end
true
else
false
end
end
##
# Check if the current invoice is about a training that was previously validated for the concerned user.
2016-07-27 11:28:54 +02:00
# In that case refunding the invoice shouldn't be allowed.
2016-03-23 18:39:41 +01:00
# @return {Boolean}
##
def prevent_refund?
if invoiced_type == 'Reservation' and invoiced . reservable_type == 'Training'
user . trainings . include? ( invoiced . reservable_id )
else
false
end
end
private
def generate_and_send_invoice
2017-02-13 14:38:28 +01:00
unless Rails . env . test?
puts " Creating an InvoiceWorker job to generate the following invoice: id( #{ id } ), invoiced_id( #{ invoiced_id } ), invoiced_type( #{ invoiced_type } ), user_id( #{ user_id } ) "
end
2016-03-23 18:39:41 +01:00
InvoiceWorker . perform_async ( id )
end
##
# Output the given integer with leading zeros. If the given value is longer than the given
# length, it will be truncated.
# @param value {Integer} the integer to pad
# @param length {Integer} the length of the resulting string.
##
def pad_and_truncate ( value , length )
value . to_s . rjust ( length , '0' ) . gsub ( / ^.*(.{ #{ length } ,}?)$ /m , '\1' )
end
##
# Returns the number of current invoices in the given range around the current date.
# If range is invalid or not specified, the total number of invoices is returned.
# @param range {String} 'day', 'month', 'year'
# @return {Integer}
##
def number_of_invoices ( range )
case range . to_s
when 'day'
start = DateTime . current . beginning_of_day
ending = DateTime . current . end_of_day
when 'month'
start = DateTime . current . beginning_of_month
ending = DateTime . current . end_of_month
when 'year'
start = DateTime . current . beginning_of_year
ending = DateTime . current . end_of_year
else
return self . id
end
if defined? start and defined? ending
Invoice . where ( 'created_at >= :start_date AND created_at < :end_date' , { start_date : start , end_date : ending } ) . length
end
end
end