2019-03-13 16:49:03 +01:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2021-05-27 15:58:55 +02:00
|
|
|
# Invoice correspond to a single purchase made by an user. This purchase is linked to one or many invoice_items
|
2020-12-22 14:43:08 +01:00
|
|
|
class Invoice < PaymentDocument
|
2023-01-27 13:53:23 +01:00
|
|
|
include NotificationAttachedObject
|
2016-03-23 18:39:41 +01:00
|
|
|
require 'fileutils'
|
|
|
|
scope :only_invoice, -> { where(type: nil) }
|
|
|
|
|
|
|
|
has_many :invoice_items, dependent: :destroy
|
|
|
|
accepts_nested_attributes_for :invoice_items
|
2019-05-22 17:49:22 +02:00
|
|
|
belongs_to :invoicing_profile
|
2019-06-11 10:02:48 +02:00
|
|
|
belongs_to :statistic_profile
|
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
|
|
|
|
2023-03-23 17:39:06 +01:00
|
|
|
has_one :chained_element, as: :element, dependent: :restrict_with_exception
|
2023-03-28 12:44:00 +02:00
|
|
|
has_one :avoir, class_name: 'Avoir', dependent: :destroy, inverse_of: :invoice
|
2023-03-16 17:17:00 +01:00
|
|
|
has_one :payment_schedule_item, dependent: :restrict_with_error
|
2022-10-28 10:24:41 +02:00
|
|
|
has_one :payment_gateway_object, as: :item, dependent: :destroy
|
2023-03-16 17:17:00 +01:00
|
|
|
has_one :order, dependent: :restrict_with_error
|
2022-10-28 10:24:41 +02:00
|
|
|
belongs_to :operator_profile, class_name: 'InvoicingProfile'
|
|
|
|
|
2022-11-18 16:42:11 +01:00
|
|
|
has_many :accounting_lines, dependent: :destroy
|
|
|
|
|
2022-10-28 10:24:41 +02:00
|
|
|
delegate :user, to: :invoicing_profile
|
2023-03-24 17:21:44 +01:00
|
|
|
delegate :footprint, to: :chained_element
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2019-02-27 17:44:52 +01:00
|
|
|
before_create :add_environment
|
2023-03-24 17:21:44 +01:00
|
|
|
after_create :generate_order_number, :update_reference, :chain_record
|
2019-09-11 12:22:14 +02:00
|
|
|
after_update :log_changes
|
2022-10-28 10:24:41 +02:00
|
|
|
after_commit :generate_and_send_invoice, on: [:create], if: :persisted?
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2019-01-09 16:54:09 +01:00
|
|
|
validates_with ClosedPeriodValidator
|
|
|
|
|
2016-03-23 18:39:41 +01:00
|
|
|
def file
|
2019-06-11 16:56:11 +02:00
|
|
|
dir = "invoices/#{invoicing_profile.id}"
|
2021-06-28 09:50:37 +02:00
|
|
|
dir = "test/fixtures/files/invoices/#{invoicing_profile.id}" if Rails.env.test?
|
2016-03-23 18:39:41 +01:00
|
|
|
|
2019-06-11 16:56:11 +02:00
|
|
|
# create directories if they doesn't exists (invoice & invoicing_profile_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
|
2020-12-22 14:43:08 +01:00
|
|
|
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
|
2020-06-15 10:58:15 +02:00
|
|
|
else
|
2020-12-22 14:43:08 +01:00
|
|
|
Setting.get('invoice_prefix')
|
2020-06-15 10:58:15 +02:00
|
|
|
end
|
2020-12-22 14:43:08 +01:00
|
|
|
"#{prefix}-#{id}_#{created_at.strftime('%d%m%Y')}.pdf"
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
|
|
|
|
2023-03-24 17:21:44 +01:00
|
|
|
def generate_order_number
|
|
|
|
self.order_number = order.reference and return unless order.nil? || order.reference.nil?
|
2023-03-16 17:17:00 +01:00
|
|
|
|
2023-03-24 17:21:44 +01:00
|
|
|
if !payment_schedule_item.nil? && !payment_schedule_item.first?
|
|
|
|
self.order_number = payment_schedule_item.payment_schedule.order_number
|
|
|
|
return
|
|
|
|
end
|
2023-03-16 17:17:00 +01:00
|
|
|
|
2023-03-24 17:21:44 +01:00
|
|
|
super
|
2016-03-23 18:39:41 +01:00
|
|
|
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
|
2023-02-24 17:26:55 +01:00
|
|
|
pdf = ::Pdf::Invoice.new(self).render
|
2016-03-23 18:39:41 +01:00
|
|
|
File.binwrite(file, pdf)
|
|
|
|
end
|
|
|
|
|
|
|
|
def build_avoir(attrs = {})
|
2020-12-22 14:43:08 +01:00
|
|
|
raise CannotRefundError if refunded? == true || prevent_refund?
|
2019-01-09 16:28:23 +01:00
|
|
|
|
|
|
|
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
|
2023-03-16 17:17:00 +01:00
|
|
|
avoir.avoir_date = Time.current
|
2016-03-23 18:39:41 +01:00
|
|
|
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|
|
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)
|
2022-10-28 10:24:41 +02:00
|
|
|
raise StandardError if ii.invoice_item # cannot refund an item that was already refunded
|
2019-01-09 16:28:23 +01:00
|
|
|
|
|
|
|
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
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
2016-08-11 13:44:42 +02:00
|
|
|
# handle coupon
|
2022-10-28 10:24:41 +02:00
|
|
|
avoir.total = CouponService.apply_on_refund(avoir.total, avoir.coupon, paid_items, refund_items)
|
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|
|
2021-05-27 15:58:55 +02:00
|
|
|
return true if ii.object_type == 'Subscription'
|
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.
|
2016-07-27 11:28:54 +02:00
|
|
|
# In that case refunding the invoice shouldn't be allowed.
|
2019-06-11 16:56:11 +02:00
|
|
|
# Moreover, an invoice cannot be refunded if the users' account was deleted
|
2016-03-23 18:39:41 +01:00
|
|
|
# @return {Boolean}
|
|
|
|
##
|
|
|
|
def prevent_refund?
|
2019-06-11 16:56:11 +02:00
|
|
|
return true if user.nil?
|
|
|
|
|
2022-07-26 17:38:33 +02:00
|
|
|
if main_item.nil?
|
|
|
|
Rails.logger.error "Invoice (id: #{id}) does not have a main_item and is probably in error"
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
|
2021-05-27 15:58:55 +02:00
|
|
|
if main_item.object_type == 'Reservation' && main_item.object&.reservable_type == 'Training'
|
|
|
|
user.trainings.include?(main_item.object.reservable_id)
|
2016-03-23 18:39:41 +01:00
|
|
|
else
|
|
|
|
false
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-05-27 15:58:55 +02:00
|
|
|
def main_item
|
2023-01-06 10:31:20 +01:00
|
|
|
main = invoice_items.where(main: true).first
|
|
|
|
if main.nil?
|
|
|
|
main = invoice_items.order(id: :asc).first
|
|
|
|
main&.update(main: true)
|
|
|
|
end
|
|
|
|
main
|
2021-05-27 15:58:55 +02:00
|
|
|
end
|
|
|
|
|
2022-10-28 10:24:41 +02:00
|
|
|
def other_items
|
|
|
|
invoice_items.where(main: [nil, false])
|
|
|
|
end
|
|
|
|
|
add task Id: 3713, reference: 1706002/VL, stripe id: in_1ASRQy2sOmf47Nz9Xpxtw46A, invoice total: 30.0, stripe invoice total: 80.0, date: 2017-06-08 16:16:26 +0200
Id: 3716, reference: 1706005/VL, stripe id: in_1ASRye2sOmf47Nz9utkjPDve, invoice total: 30.0, stripe invoice total: 40.0, date: 2017-06-08 16:51:15 +0200
Id: 3717, reference: 1706006/VL, stripe id: in_1ASS1X2sOmf47Nz93Xn2UxVh, invoice total: 30.0, stripe invoice total: 40.0, date: 2017-06-08 16:54:14 +0200
Id: 3718, reference: 1706007/VL, stripe id: in_1ASSBI2sOmf47Nz9Ol0gEEfC, invoice total: 30.0, stripe invoice total: 40.0, date: 2017-06-08 17:04:19 +0200 allow find the invoices incoherent
2017-06-09 11:08:08 +02:00
|
|
|
# get amount total paid
|
|
|
|
def amount_paid
|
2019-01-09 16:28:23 +01:00
|
|
|
total - (wallet_amount || 0)
|
add task Id: 3713, reference: 1706002/VL, stripe id: in_1ASRQy2sOmf47Nz9Xpxtw46A, invoice total: 30.0, stripe invoice total: 80.0, date: 2017-06-08 16:16:26 +0200
Id: 3716, reference: 1706005/VL, stripe id: in_1ASRye2sOmf47Nz9utkjPDve, invoice total: 30.0, stripe invoice total: 40.0, date: 2017-06-08 16:51:15 +0200
Id: 3717, reference: 1706006/VL, stripe id: in_1ASS1X2sOmf47Nz93Xn2UxVh, invoice total: 30.0, stripe invoice total: 40.0, date: 2017-06-08 16:54:14 +0200
Id: 3718, reference: 1706007/VL, stripe id: in_1ASSBI2sOmf47Nz9Ol0gEEfC, invoice total: 30.0, stripe invoice total: 40.0, date: 2017-06-08 17:04:19 +0200 allow find the invoices incoherent
2017-06-09 11:08:08 +02:00
|
|
|
end
|
|
|
|
|
2019-09-19 13:57:33 +02:00
|
|
|
# return a summary of the payment means used
|
|
|
|
def payment_means
|
|
|
|
res = []
|
2022-12-09 12:28:13 +01:00
|
|
|
res.push(means: :wallet, amount: wallet_amount) if paid_by_wallet?
|
2021-04-20 17:22:53 +02:00
|
|
|
if paid_by_card?
|
2019-09-19 13:57:33 +02:00
|
|
|
res.push(means: :card, amount: amount_paid)
|
|
|
|
else
|
|
|
|
res.push(means: :other, amount: amount_paid)
|
|
|
|
end
|
|
|
|
res
|
|
|
|
end
|
|
|
|
|
2022-12-09 12:28:13 +01:00
|
|
|
def payment_details(mean)
|
|
|
|
case mean
|
|
|
|
when :card
|
|
|
|
if paid_by_card?
|
|
|
|
{
|
2022-12-09 16:50:01 +01:00
|
|
|
payment_mean: mean,
|
2022-12-09 12:28:13 +01:00
|
|
|
gateway_object_id: payment_gateway_object.gateway_object_id,
|
|
|
|
gateway_object_type: payment_gateway_object.gateway_object_type
|
|
|
|
}
|
|
|
|
end
|
|
|
|
when :wallet
|
2022-12-09 16:50:01 +01:00
|
|
|
{ payment_mean: mean, wallet_transaction_id: wallet_transaction_id } if paid_by_wallet?
|
2022-12-09 12:28:13 +01:00
|
|
|
else
|
2022-12-09 16:50:01 +01:00
|
|
|
{ payment_mean: mean }
|
2022-12-09 12:28:13 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-04-16 16:03:10 +02:00
|
|
|
def footprint_children
|
|
|
|
invoice_items
|
2019-02-11 13:57:07 +01:00
|
|
|
end
|
|
|
|
|
2021-04-20 17:22:53 +02:00
|
|
|
def paid_by_card?
|
2021-04-30 16:07:19 +02:00
|
|
|
!payment_gateway_object.nil? && payment_method == 'card'
|
2019-09-10 16:45:45 +02:00
|
|
|
end
|
|
|
|
|
2022-12-09 12:28:13 +01:00
|
|
|
def paid_by_wallet?
|
|
|
|
wallet_transaction && wallet_amount.positive?
|
|
|
|
end
|
|
|
|
|
2021-05-28 17:34:20 +02:00
|
|
|
def render_resource
|
|
|
|
{ partial: 'api/invoices/invoice', locals: { invoice: self } }
|
|
|
|
end
|
|
|
|
|
2019-03-20 11:01:53 +01:00
|
|
|
private
|
2019-01-09 16:28:23 +01:00
|
|
|
|
2016-03-23 18:39:41 +01:00
|
|
|
def generate_and_send_invoice
|
2020-05-26 18:07:07 +02:00
|
|
|
return unless Setting.get('invoicing_module')
|
2019-09-30 15:29:10 +02:00
|
|
|
|
2017-02-13 14:38:28 +01:00
|
|
|
unless Rails.env.test?
|
2022-07-26 17:27:33 +02:00
|
|
|
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})"
|
2017-02-13 14:38:28 +01:00
|
|
|
end
|
2023-01-05 12:09:16 +01:00
|
|
|
InvoiceWorker.perform_async(id)
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|
|
|
|
|
2019-09-11 12:22:14 +02:00
|
|
|
def log_changes
|
2019-09-11 14:29:35 +02:00
|
|
|
return if Rails.env.test?
|
2019-09-11 12:22:14 +02:00
|
|
|
return unless changed?
|
|
|
|
|
2022-07-26 17:27:33 +02:00
|
|
|
Rails.logger.warn "Invoice update triggered [ id: #{id}, reference: #{reference} ]\n" \
|
|
|
|
"---------- changes ----------#{changes}\n---------------------------------"
|
2019-09-11 12:22:14 +02:00
|
|
|
end
|
2016-03-23 18:39:41 +01:00
|
|
|
end
|