From 185589407f6f6410da75a6f858e83b280b914a37 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Fri, 9 Sep 2022 16:35:49 +0200 Subject: [PATCH] (feat) order invoice --- app/models/coupon.rb | 2 +- app/models/invoice_item.rb | 4 +-- app/models/order.rb | 1 + app/pdfs/pdf/invoice.rb | 17 +++++------ app/services/accounting_export_service.rb | 6 ++-- app/services/invoices_service.rb | 29 ++++++++++++++----- app/services/payments/payment_concern.rb | 16 +++++++++- config/locales/en.yml | 1 + config/locales/fr.yml | 1 + .../20220909131300_add_invoice_id_to_order.rb | 5 ++++ db/schema.rb | 5 +++- 11 files changed, 63 insertions(+), 24 deletions(-) create mode 100644 db/migrate/20220909131300_add_invoice_id_to_order.rb diff --git a/app/models/coupon.rb b/app/models/coupon.rb index 623c8623e..a1439221e 100644 --- a/app/models/coupon.rb +++ b/app/models/coupon.rb @@ -83,7 +83,7 @@ class Coupon < ApplicationRecord end def users - invoices.map(&:user).concat(orders.map(&:user)).uniq(&:id) + invoices.map(&:user).uniq(&:id) end def users_ids diff --git a/app/models/invoice_item.rb b/app/models/invoice_item.rb index 584aa2329..193dbdec1 100644 --- a/app/models/invoice_item.rb +++ b/app/models/invoice_item.rb @@ -31,7 +31,7 @@ class InvoiceItem < Footprintable amount_after_coupon - net_amount end - # return invoice item type (Machine/Training/Space/Event/Subscription/Order) used to determine the VAT rate + # return invoice item type (Machine/Training/Space/Event/Subscription/OrderItem) used to determine the VAT rate def invoice_item_type if object_type == Reservation.name object.try(:reservable_type) || '' @@ -39,7 +39,7 @@ class InvoiceItem < Footprintable Subscription.name elsif object_type == StatisticProfilePrepaidPack.name object.prepaid_pack.priceable_type - elsif object_type == Order.name + elsif object_type == OrderItem.name Product.name else '' diff --git a/app/models/order.rb b/app/models/order.rb index 5ea02af79..be171a6b6 100644 --- a/app/models/order.rb +++ b/app/models/order.rb @@ -5,6 +5,7 @@ class Order < PaymentDocument belongs_to :statistic_profile belongs_to :operator_profile, class_name: 'InvoicingProfile' belongs_to :coupon + belongs_to :invoice has_many :order_items, dependent: :destroy has_one :payment_gateway_object, as: :item diff --git a/app/pdfs/pdf/invoice.rb b/app/pdfs/pdf/invoice.rb index e2962d57e..4b2824f43 100644 --- a/app/pdfs/pdf/invoice.rb +++ b/app/pdfs/pdf/invoice.rb @@ -41,14 +41,13 @@ class PDF::Invoice < Prawn::Document else text I18n.t('invoices.invoice_reference', REF: invoice.reference), leading: 3 end - if Setting.get('invoice_code-active') - text I18n.t('invoices.code', CODE: Setting.get('invoice_code-value')), leading: 3 - end + text I18n.t('invoices.code', CODE: Setting.get('invoice_code-value')), leading: 3 if Setting.get('invoice_code-active') if invoice.main_item.object_type != WalletTransaction.name if invoice.is_a?(Avoir) text I18n.t('invoices.order_number', NUMBER: invoice.invoice.order_number), leading: 3 else - text I18n.t('invoices.order_number', NUMBER: invoice.order_number), leading: 3 + order_number = invoice.main_item.object_type == OrderItem.name ? invoice.main_item.object.order.reference : invoice.order_number + text I18n.t('invoices.order_number', NUMBER: order_number), leading: 3 end end if invoice.is_a?(Avoir) @@ -119,6 +118,8 @@ class PDF::Invoice < Prawn::Document object = I18n.t('invoices.error_invoice') when 'StatisticProfilePrepaidPack' object = I18n.t('invoices.prepaid_pack') + when 'OrderItem' + object = I18n.t('invoices.order') else Rails.logger.error "specified main_item.object_type type (#{invoice.main_item.object_type}) is unknown" end @@ -136,7 +137,6 @@ class PDF::Invoice < Prawn::Document total_vat = 0 # going through invoice_items invoice.invoice_items.each do |item| - price = item.amount.to_i / 100.00 details = invoice.is_a?(Avoir) ? I18n.t('invoices.cancellation') + ' - ' : '' @@ -162,7 +162,6 @@ class PDF::Invoice < Prawn::Document END: I18n.l(subscription_end_at.to_date)) end - elsif item.object_type == Reservation.name case invoice.main_item.object.try(:reservable_type) ### Machine reservation @@ -243,7 +242,8 @@ class PDF::Invoice < Prawn::Document else data += [[I18n.t('invoices.total_including_all_taxes'), number_to_currency(total)]] vat_rate_group.each do |_type, rate| - data += [[I18n.t('invoices.including_VAT_RATE', RATE: rate[:vat_rate], AMOUNT: number_to_currency(rate[:amount] / 100.00)), number_to_currency(rate[:total_vat] / 100.00)]] + data += [[I18n.t('invoices.including_VAT_RATE', RATE: rate[:vat_rate], AMOUNT: number_to_currency(rate[:amount] / 100.00)), + number_to_currency(rate[:total_vat] / 100.00)]] end data += [[I18n.t('invoices.including_total_excluding_taxes'), number_to_currency(total_ht / 100.00)]] data += [[I18n.t('invoices.including_amount_payed_on_ordering'), number_to_currency(total)]] @@ -290,7 +290,7 @@ class PDF::Invoice < Prawn::Document # payment details move_down 20 if invoice.is_a?(Avoir) - payment_verbose = I18n.t('invoices.refund_on_DATE', DATE:I18n.l(invoice.avoir_date.to_date)) + ' ' + payment_verbose = I18n.t('invoices.refund_on_DATE', DATE: I18n.l(invoice.avoir_date.to_date)) + ' ' case invoice.payment_method when 'stripe' payment_verbose += I18n.t('invoices.by_card_online_payment') @@ -351,7 +351,6 @@ class PDF::Invoice < Prawn::Document text line, style: :bold, inline_format: true end - # address and legals information move_down 40 txt = parse_html(Setting.get('invoice_legals')) diff --git a/app/services/accounting_export_service.rb b/app/services/accounting_export_service.rb index 4b06b6386..54d103c38 100644 --- a/app/services/accounting_export_service.rb +++ b/app/services/accounting_export_service.rb @@ -72,7 +72,7 @@ class AccountingExportService rows << "#{wallet_row(invoice)}\n" when 'StatisticProfilePrepaidPack' rows << "#{pack_row(invoice)}\n" - when 'Order' + when 'OrderItem' rows << "#{product_row(invoice)}\n" when 'Error' items = invoice.invoice_items.reject { |ii| ii.object_type == 'Subscription' } @@ -248,8 +248,8 @@ class AccountingExportService Rails.logger.debug { "WARN: Invoice #{invoice.id} has no prepaid-pack" } end when :product - if invoice.main_item.object_type == 'Order' - Setting.get("accounting_VAT_#{type}") + if invoice.main_item.object_type == 'OrderItem' + Setting.get("accounting_Product_#{type}") else Rails.logger.debug { "WARN: Invoice #{invoice.id} has no prepaid-pack" } end diff --git a/app/services/invoices_service.rb b/app/services/invoices_service.rb index b70688bfc..e85a5fe79 100644 --- a/app/services/invoices_service.rb +++ b/app/services/invoices_service.rb @@ -15,7 +15,6 @@ class InvoicesService .page(page) .per(size) - if filters[:number].size.positive? invoices = invoices.where( 'invoices.reference LIKE :search', @@ -96,7 +95,7 @@ class InvoicesService # Generate an array of {InvoiceItem} with the provided elements, price included. # @param invoice {Invoice} the parent invoice # @param payment_details {Hash} as generated by ShoppingCart.total - # @param objects {Array} + # @param objects {Array} ## def self.generate_invoice_items(invoice, payment_details, objects) objects.each_with_index do |object, index| @@ -108,6 +107,8 @@ class InvoicesService InvoicesService.generate_reservation_item(invoice, object, payment_details, index.zero?) elsif object.is_a?(StatisticProfilePrepaidPack) InvoicesService.generate_prepaid_pack_item(invoice, object, payment_details, index.zero?) + elsif object.is_a?(OrderItem) + InvoicesService.generate_order_item(invoice, object, payment_details, index.zero?) else InvoicesService.generate_generic_item(invoice, object, payment_details, index.zero?) end @@ -123,16 +124,16 @@ class InvoicesService reservation.slots_reservations.map(&:slot).each do |slot| description = "#{reservation.reservable.name}\n" - description += if slot.start_at.to_date != slot.end_at.to_date + description += if slot.start_at.to_date == slot.end_at.to_date + "#{I18n.l slot.start_at.to_date, format: :long} #{I18n.l slot.start_at, format: :hour_minute}" \ + " - #{I18n.l slot.end_at, format: :hour_minute}" + else I18n.t('events.from_STARTDATE_to_ENDDATE', STARTDATE: I18n.l(slot.start_at.to_date, format: :long), ENDDATE: I18n.l(slot.end_at.to_date, format: :long)) + ' ' + I18n.t('events.from_STARTTIME_to_ENDTIME', STARTTIME: I18n.l(slot.start_at, format: :hour_minute), ENDTIME: I18n.l(slot.end_at, format: :hour_minute)) - else - "#{I18n.l slot.start_at.to_date, format: :long} #{I18n.l slot.start_at, format: :hour_minute}" \ - " - #{I18n.l slot.end_at, format: :hour_minute}" end price_slot = payment_details[:elements][:slots].detect { |p_slot| p_slot[:start_at].to_time.in_time_zone == slot[:start_at] } @@ -196,6 +197,21 @@ class InvoicesService ) end + ## + # Generate an InvoiceItem for given OrderItem and sva it in invoice.invoice_items + # This method must be called whith an order + ## + def self.generate_order_item(invoice, item, _payment_details, main = false) + raise TypeError unless item + + invoice.invoice_items.push InvoiceItem.new( + amount: item.is_offered ? 0 : item.amount * item.quantity, + description: "#{item.orderable.name} x #{item.quantity}", + object: item, + main: main + ) + end + def self.generate_generic_item(invoice, item, payment_details, main = false) invoice.invoice_items.push InvoiceItem.new( amount: payment_details[:elements][item.class.name.to_sym], @@ -205,7 +221,6 @@ class InvoicesService ) end - ## # Set the total price to the reservation's invoice, summing its whole items. # Additionally a coupon may be applied to this invoice to make a discount on the total price diff --git a/app/services/payments/payment_concern.rb b/app/services/payments/payment_concern.rb index 90ea97334..64e9d1840 100644 --- a/app/services/payments/payment_concern.rb +++ b/app/services/payments/payment_concern.rb @@ -35,7 +35,21 @@ module Payments::PaymentConcern order.order_items.each do |item| ProductService.update_stock(item.orderable, 'external', 'sold', -item.quantity, item.id) end - order.save + if order.save + invoice = InvoicesService.create( + { total: order.total, coupon: coupon }, + order.operator_profile_id, + order.order_items, + order.statistic_profile.user, + payment_id: payment_id, + payment_type: payment_type, + payment_method: order.payment_method + ) + invoice.wallet_amount = order.wallet_amount + invoice.wallet_transaction_id = order.wallet_transaction_id + invoice.save + order.update_attribute(:invoice_id, invoice.id) + end order.reload end end diff --git a/config/locales/en.yml b/config/locales/en.yml index d5d937dce..f8c3d32d5 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -121,6 +121,7 @@ en: error_invoice: "Erroneous invoice. The items below ware not booked. Please contact the FabLab for a refund." prepaid_pack: "Prepaid pack of hours" pack_item: "Pack of %{COUNT} hours for the %{ITEM}" + order: "Order of products" #PDF payment schedule generation payment_schedules: schedule_reference: "Payment schedule reference: %{REF}" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index e96d35021..ec7f5a0f6 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -121,6 +121,7 @@ fr: error_invoice: "Facture en erreur. Les éléments ci-dessous n'ont pas été réservés. Veuillez contacter le Fablab pour un remboursement." prepaid_pack: "Paquet d'heures prépayé" pack_item: "Pack de %{COUNT} heures pour la %{ITEM}" + order: "La commande des produits" #PDF payment schedule generation payment_schedules: schedule_reference: "Référence de l'échéancier : %{REF}" diff --git a/db/migrate/20220909131300_add_invoice_id_to_order.rb b/db/migrate/20220909131300_add_invoice_id_to_order.rb new file mode 100644 index 000000000..2d9fa804f --- /dev/null +++ b/db/migrate/20220909131300_add_invoice_id_to_order.rb @@ -0,0 +1,5 @@ +class AddInvoiceIdToOrder < ActiveRecord::Migration[5.2] + def change + add_reference :orders, :invoice, index: true, foreign_key: true + end +end diff --git a/db/schema.rb b/db/schema.rb index e9373c7c0..72a4f8e53 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_08_26_175129) do +ActiveRecord::Schema.define(version: 2022_09_09_131300) do # These are extensions that must be enabled in order to support this database enable_extension "fuzzystrmatch" @@ -475,7 +475,9 @@ ActiveRecord::Schema.define(version: 2022_08_26_175129) do t.string "environment" t.bigint "coupon_id" t.integer "paid_total" + t.bigint "invoice_id" t.index ["coupon_id"], name: "index_orders_on_coupon_id" + t.index ["invoice_id"], name: "index_orders_on_invoice_id" t.index ["operator_profile_id"], name: "index_orders_on_operator_profile_id" t.index ["statistic_profile_id"], name: "index_orders_on_statistic_profile_id" end @@ -1171,6 +1173,7 @@ ActiveRecord::Schema.define(version: 2022_08_26_175129) do add_foreign_key "invoicing_profiles", "users" add_foreign_key "order_items", "orders" add_foreign_key "orders", "coupons" + add_foreign_key "orders", "invoices" add_foreign_key "orders", "invoicing_profiles", column: "operator_profile_id" add_foreign_key "orders", "statistic_profiles" add_foreign_key "organizations", "invoicing_profiles"