1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2024-12-04 15:24:23 +01:00
fab-manager/app/models/invoice.rb
2023-03-28 12:44:00 +02:00

226 lines
6.6 KiB
Ruby

# frozen_string_literal: true
# Invoice correspond to a single purchase made by an user. This purchase is linked to one or many invoice_items
class Invoice < PaymentDocument
include NotificationAttachedObject
require 'fileutils'
scope :only_invoice, -> { where(type: nil) }
has_many :invoice_items, dependent: :destroy
accepts_nested_attributes_for :invoice_items
belongs_to :invoicing_profile
belongs_to :statistic_profile
belongs_to :wallet_transaction
belongs_to :coupon
has_one :chained_element, as: :element, dependent: :restrict_with_exception
has_one :avoir, class_name: 'Avoir', dependent: :destroy, inverse_of: :invoice
has_one :payment_schedule_item, dependent: :restrict_with_error
has_one :payment_gateway_object, as: :item, dependent: :destroy
has_one :order, dependent: :restrict_with_error
belongs_to :operator_profile, class_name: 'InvoicingProfile'
has_many :accounting_lines, dependent: :destroy
delegate :user, to: :invoicing_profile
delegate :footprint, to: :chained_element
before_create :add_environment
after_create :generate_order_number, :update_reference, :chain_record
after_update :log_changes
after_commit :generate_and_send_invoice, on: [:create], if: :persisted?
validates_with ClosedPeriodValidator
def file
dir = "invoices/#{invoicing_profile.id}"
dir = "test/fixtures/files/invoices/#{invoicing_profile.id}" if Rails.env.test?
# create directories if they doesn't exists (invoice & invoicing_profile_id)
FileUtils.mkdir_p dir
"#{dir}/#{filename}"
end
def filename
prefix = Setting.find_by(name: 'invoice_prefix').value_at(created_at)
prefix ||= if created_at < Setting.find_by(name: 'invoice_prefix').first_update
Setting.find_by(name: 'invoice_prefix').first_value
else
Setting.get('invoice_prefix')
end
"#{prefix}-#{id}_#{created_at.strftime('%d%m%Y')}.pdf"
end
def generate_order_number
self.order_number = order.reference and return unless order.nil? || order.reference.nil?
if !payment_schedule_item.nil? && !payment_schedule_item.first?
self.order_number = payment_schedule_item.payment_schedule.order_number
return
end
super
end
# for debug & used by rake task "fablab:maintenance:regenerate_invoices"
def regenerate_invoice_pdf
pdf = ::Pdf::Invoice.new(self).render
File.binwrite(file, pdf)
end
def build_avoir(attrs = {})
raise CannotRefundError if refunded? == true || prevent_refund?
avoir = Avoir.new(dup.attributes)
avoir.type = 'Avoir'
avoir.attributes = attrs
avoir.reference = nil
avoir.invoice_id = id
avoir.avoir_date = Time.current
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.zero?
next unless attrs[:invoice_items_ids].include? ii.id # list of items to refund (partial refunds)
raise StandardError 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.invoice_item_id = ii.id
avoir.total += avoir_ii.amount
end
# handle coupon
avoir.total = CouponService.apply_on_refund(avoir.total, avoir.coupon, paid_items, refund_items)
avoir
end
def subscription_invoice?
invoice_items.each do |ii|
return true if ii.object_type == 'Subscription'
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 refunded?
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.
# Moreover, an invoice cannot be refunded if the users' account was deleted
# @return {Boolean}
##
def prevent_refund?
return true if user.nil?
if main_item.nil?
Rails.logger.error "Invoice (id: #{id}) does not have a main_item and is probably in error"
return true
end
if main_item.object_type == 'Reservation' && main_item.object&.reservable_type == 'Training'
user.trainings.include?(main_item.object.reservable_id)
else
false
end
end
def main_item
main = invoice_items.where(main: true).first
if main.nil?
main = invoice_items.order(id: :asc).first
main&.update(main: true)
end
main
end
def other_items
invoice_items.where(main: [nil, false])
end
# get amount total paid
def amount_paid
total - (wallet_amount || 0)
end
# return a summary of the payment means used
def payment_means
res = []
res.push(means: :wallet, amount: wallet_amount) if paid_by_wallet?
if paid_by_card?
res.push(means: :card, amount: amount_paid)
else
res.push(means: :other, amount: amount_paid)
end
res
end
def payment_details(mean)
case mean
when :card
if paid_by_card?
{
payment_mean: mean,
gateway_object_id: payment_gateway_object.gateway_object_id,
gateway_object_type: payment_gateway_object.gateway_object_type
}
end
when :wallet
{ payment_mean: mean, wallet_transaction_id: wallet_transaction_id } if paid_by_wallet?
else
{ payment_mean: mean }
end
end
def footprint_children
invoice_items
end
def paid_by_card?
!payment_gateway_object.nil? && payment_method == 'card'
end
def paid_by_wallet?
wallet_transaction && wallet_amount.positive?
end
def render_resource
{ partial: 'api/invoices/invoice', locals: { invoice: self } }
end
private
def generate_and_send_invoice
return unless Setting.get('invoicing_module')
unless Rails.env.test?
Rails.logger.info "Creating an InvoiceWorker job to generate the following invoice: id(#{id}), " \
"main_item.object_id(#{main_item.object_id}), " \
"main_item.object_type(#{main_item.object_type}), user_id(#{invoicing_profile.user_id})"
end
InvoiceWorker.perform_async(id)
end
def log_changes
return if Rails.env.test?
return unless changed?
Rails.logger.warn "Invoice update triggered [ id: #{id}, reference: #{reference} ]\n" \
"---------- changes ----------#{changes}\n---------------------------------"
end
end