# 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 :avoir, class_name: 'Invoice', dependent: :destroy, inverse_of: :avoir 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 before_create :add_environment after_create :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 order_number return order.reference unless order.nil? if !payment_schedule_item.nil? && !payment_schedule_item.first? return payment_schedule_item.payment_schedule.ordered_items.first.invoice.order_number end PaymentDocumentService.generate_order_number(self) 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