1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2024-11-29 10:24:20 +01:00
fab-manager/app/models/invoice.rb

287 lines
8.9 KiB
Ruby
Raw Normal View History

2019-01-09 16:28:23 +01:00
# Invoice correspond to a single purchase made by an user. This purchase may
# include reservation(s) and/or a subscription
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
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
before_create :add_environment
2019-02-11 13:57:07 +01:00
after_create :update_reference, :chain_record
2019-01-09 16:28:23 +01:00
after_commit :generate_and_send_invoice, on: [:create], if: :persisted?
2016-03-23 18:39:41 +01:00
validates_with ClosedPeriodValidator
2016-03-23 18:39:41 +01:00
def file
dir = "invoices/#{user.id}"
# create directories if they doesn't exists (invoice & user_id)
2019-01-09 16:28:23 +01:00
FileUtils.mkdir_p dir
"#{dir}/#{filename}"
2016-03-29 18:02:40 +02:00
end
def filename
2019-01-09 16:28:23 +01:00
"#{ENV['INVOICE_PREFIX']}-#{id}_#{created_at.strftime('%d%m%Y')}.pdf"
2016-03-23 18:39:41 +01:00
end
def generate_reference
2018-12-17 16:02:02 +01:00
pattern = Setting.find_by(name: 'invoice_reference').value
2016-03-23 18:39:41 +01:00
# 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])
2019-01-09 16:28:23 +01:00
if stp_invoice_id
2016-03-23 18:39:41 +01:00
reference.gsub!(/X\[([^\]]+)\]/, '\1')
else
reference.gsub!(/X\[([^\]]+)\]/, ''.to_s)
end
2016-07-12 12:29:51 +02:00
# information about wallet (W[text])
2019-01-09 16:28:23 +01: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
2019-01-09 16:28:23 +01:00
pattern = Setting.find_by(name: 'invoice_order-nb').value
2016-03-23 18:39:41 +01:00
# 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)
2019-01-09 16:28:23 +01:00
reference.gsub!(/YYYY(?![^\[]*\])/, created_at.strftime('%Y'))
2016-03-23 18:39:41 +01:00
# year without century (YY)
2019-01-09 16:28:23 +01:00
reference.gsub!(/YY(?![^\[]*\])/, created_at.strftime('%y'))
2016-03-23 18:39:41 +01:00
2019-01-09 16:28:23 +01:00
# abbreviated month name (MMM)
reference.gsub!(/MMM(?![^\[]*\])/, created_at.strftime('%^b'))
2016-03-23 18:39:41 +01:00
# month of the year, zero-padded (MM)
2019-01-09 16:28:23 +01:00
reference.gsub!(/MM(?![^\[]*\])/, created_at.strftime('%m'))
2016-03-23 18:39:41 +01:00
# month of the year, non zero-padded (M)
2019-01-09 16:28:23 +01:00
reference.gsub!(/M(?![^\[]*\])/, created_at.strftime('%-m'))
2016-03-23 18:39:41 +01:00
# day of the month, zero-padded (DD)
2019-01-09 16:28:23 +01:00
reference.gsub!(/DD(?![^\[]*\])/, created_at.strftime('%d'))
2016-03-23 18:39:41 +01:00
# day of the month, non zero-padded (DD)
2019-01-09 16:28:23 +01:00
reference.gsub!(/DD(?![^\[]*\])/, created_at.strftime('%-d'))
2016-03-23 18:39:41 +01:00
reference
end
2019-02-13 12:59:28 +01:00
# for debug & used by rake task "fablab:maintenance:regenerate_invoices"
2016-03-23 18:39:41 +01:00
def regenerate_invoice_pdf
2019-01-09 16:28:23 +01:00
pdf = ::PDF::Invoice.new(self, nil).render
2016-03-23 18:39:41 +01:00
File.binwrite(file, pdf)
end
def build_avoir(attrs = {})
2019-01-09 16:28:23 +01:00
raise Exception if refunded? === true || prevent_refund?
avoir = Avoir.new(dup.attributes)
2016-03-23 18:39:41 +01:00
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
2016-03-23 18:39:41 +01:00
invoice_items.each do |ii|
2019-01-09 16:28:23 +01:00
paid_items += 1 unless ii.amount.zero?
next unless 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.zero?
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
2016-03-23 18:39:41 +01:00
end
2016-08-11 13:44:42 +02:00
# 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
2016-08-11 13:44:42 +02:00
avoir.total -= discount
end
2016-03-23 18:39:41 +01:00
avoir
end
2019-01-09 16:28:23 +01:00
def subscription_invoice?
2016-03-23 18:39:41 +01:00
invoice_items.each do |ii|
2019-01-09 16:28:23 +01:00
return true if ii.subscription && !ii.subscription.expired?
2016-03-23 18:39:41 +01:00
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
##
2019-01-09 16:28:23 +01:00
def refunded?
2016-03-23 18:39:41 +01:00
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.
2016-03-23 18:39:41 +01:00
# @return {Boolean}
##
def prevent_refund?
2019-01-09 16:28:23 +01:00
if invoiced_type == 'Reservation' && invoiced.reservable_type == 'Training'
2016-03-23 18:39:41 +01:00
user.trainings.include?(invoiced.reservable_id)
else
false
end
end
# get amount total paid
def amount_paid
2019-01-09 16:28:23 +01:00
total - (wallet_amount || 0)
end
def add_environment
self.environment = Rails.env
end
2019-02-11 13:57:07 +01:00
def chain_record
2019-02-12 16:00:36 +01:00
self.footprint = compute_footprint
2019-03-11 13:49:16 +01:00
save!
2019-02-12 16:00:36 +01:00
end
2019-02-11 13:57:07 +01:00
2019-02-12 16:00:36 +01:00
def check_footprint
invoice_items.map(&:check_footprint).all? && footprint == compute_footprint
2019-02-11 13:57:07 +01:00
end
2016-03-23 18:39:41 +01:00
private
2019-01-09 16:28:23 +01:00
2016-03-23 18:39:41 +01:00
def generate_and_send_invoice
2017-02-13 14:38:28 +01:00
unless Rails.env.test?
2019-01-09 16:28:23 +01:00
puts "Creating an InvoiceWorker job to generate the following invoice: id(#{id}), invoiced_id(#{invoiced_id}), " \
"invoiced_type(#{invoiced_type}), user_id(#{user_id})"
2017-02-13 14:38:28 +01:00
end
InvoiceWorker.perform_async(id, user&.subscription&.expired_at)
2016-03-23 18:39:41 +01:00
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
2019-01-09 16:28:23 +01:00
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 id
2016-03-23 18:39:41 +01:00
end
2019-01-09 16:28:23 +01:00
return Invoice.count unless defined? start && defined? ending
Invoice.where('created_at >= :start_date AND created_at < :end_date', start_date: start, end_date: ending).length
2016-03-23 18:39:41 +01:00
end
2019-02-12 16:00:36 +01:00
def compute_footprint
max_date = created_at || DateTime.now
previous = Invoice.where('created_at < ?', max_date)
.order('created_at DESC')
.limit(1)
columns = Invoice.columns.map(&:name)
.delete_if { |c| %w[footprint updated_at].include? c }
sha256 = Digest::SHA256.new
sha256.hexdigest "#{columns.map { |c| self[c] }.join}#{previous.first ? previous.first.footprint : ''}"
end
2016-03-23 18:39:41 +01:00
end