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 belongs_to :wallet_transaction belongs_to :coupon has_one :avoir, class_name: 'Invoice', foreign_key: :invoice_id, dependent: :destroy after_create :update_reference after_commit :generate_and_send_invoice, on: [:create], :if => :persisted? def file dir = "invoices/#{user.id}" # create directories if they doesn't exists (invoice & user_id) FileUtils::mkdir_p dir "#{dir}/#{self.filename}" end def filename "#{ENV['INVOICE_PREFIX']}-#{self.id}_#{self.created_at.strftime('%d%m%Y')}.pdf" 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 # information about wallet (W[text]) #reference.gsub!(/W\[([^\]]+)\]/, ''.to_s) # 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 # for debug & used by rake task "fablab:regenerate_invoices" 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 # refunds of invoices with cash coupons: we need to ventilate coupons on paid items paid_items = 0 refund_items = 0 invoice_items.each do |ii| 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 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 # handle coupon unless avoir.coupon_id.nil? discount = avoir.total if avoir.coupon.type == 'percent_off' discount = avoir.total * avoir.coupon.percent_off / 100.0 elsif avoir.coupon.type == 'amount_off' discount = (avoir.coupon.amount_off / paid_items) * refund_items else raise InvalidCouponError end avoir.total -= discount end 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. # In that case refunding the invoice shouldn't be allowed. # @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 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 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