1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2024-11-28 09:24:24 +01:00

(feat) generate invoices for missing references

This commit is contained in:
Sylvain 2023-03-27 17:18:44 +02:00
parent 227c6d1844
commit 3cff0d4c28
12 changed files with 153 additions and 75 deletions

View File

@ -13,7 +13,7 @@ class PaymentDocument < Footprintable
end
def update_reference
generate_reference
generate_reference if reference.blank?
save
end

View File

@ -33,15 +33,10 @@ class Pdf::Invoice < Prawn::Document
Rails.logger.error "Unable to decode invoice logo from base64: #{e}"
end
move_down 20
# the following line is a special comment to workaround RubyMine inspection problem
# noinspection RubyScope
font('Open-Sans', size: 10) do
# general information
if invoice.is_a?(Avoir)
text I18n.t('invoices.refund_invoice_reference', **{ REF: invoice.reference }), leading: 3
else
text I18n.t('invoices.invoice_reference', **{ REF: invoice.reference }), leading: 3
end
text I18n.t(invoice.is_a?(Avoir) ? 'invoices.refund_invoice_reference' : 'invoices.invoice_reference',
**{ REF: invoice.reference }), leading: 3
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
text I18n.t('invoices.order_number', **{ NUMBER: invoice.order_number }), leading: 3

View File

@ -21,7 +21,7 @@ class Invoices::LabelService
when 'OfferDay'
offer_day_label(invoice.main_item.object, username)
when 'Error'
I18n.t('invoices.error_invoice')
invoice.main_item&.object_id&.zero? ? I18n.t('invoices.error_invoice') : invoice.main_item&.description
when 'StatisticProfilePrepaidPack'
I18n.t('invoices.prepaid_pack')
when 'OrderItem'

View File

@ -22,20 +22,32 @@ class Invoices::NumberService
saved_number[indices[0]..indices[1]]&.to_i
end
# Replace the number of the reference of the given document and return the new reference
# @param document [PaymentDocument,NilClass]
# Search for any document matching the provided period and number
# @param number [Integer] the number to search
# @param date [Time] the date to search around, when using periodicity != 'global'
# @param setting [String] 'invoice_reference' | 'invoice_order-nb'
# @return [String,NilClass]
def change_number(document, new_number, setting = 'invoice_reference')
# @param klass [Class] Invoice | Order | PaymentSchedule
# @return [PaymentDocument,NilClass]
def find_by_number(number, date: Time.current, setting: 'invoice_reference', klass: Invoice)
raise TypeError, "invalid setting #{setting}" unless %w[invoice_order-nb invoice_reference].include?(setting)
return nil if document.nil?
return nil if number.nil?
saved_number = setting == 'invoice_reference' ? document.reference : document.order_number
return nil if saved_number.nil?
pattern = pattern(date, setting)
pattern = pattern.gsub(/([SXR]\[[^\]]+\])+/, '%')
if pattern.match?(/n+/)
pattern = pattern.gsub(/n+(?![^\[]*\])/) do |match|
pad_and_truncate(number, match.to_s.length)
end
else
start_idx = pattern.index(/y+|m+|d+/)
end_idx = pattern.rindex(/y+|m+|d+/)
pattern[start_idx..end_idx] = pad_and_truncate(number, end_idx - start_idx + 1)
end
pattern = PaymentDocumentService.send(:replace_date_pattern, pattern, date)
indices = number_indices(document, setting)
saved_number[indices[0]..indices[1]] = pad_and_truncate(new_number, indices[1] - indices[0])
saved_number
field = setting == 'invoice_reference' ? 'reference' : 'order_number'
field = 'reference' if klass == Order
klass.where("#{field} LIKE '#{pattern}'").first
end
# @param document [PaymentDocument,NilClass]
@ -45,7 +57,7 @@ class Invoices::NumberService
raise TypeError, "invalid setting #{setting}" unless %w[invoice_order-nb invoice_reference].include?(setting)
return nil if document.nil?
pattern = pattern(document, setting)
pattern = pattern(document.created_at, setting)
pattern = PaymentDocumentService.send(:replace_document_type_pattern, document, pattern)
return 'global' if pattern.match?(/n+/)
@ -56,15 +68,15 @@ class Invoices::NumberService
nil
end
# Get the pattern applicable to generate the number of the given invoice.
# @param document [PaymentDocument]
# Get the pattern applicable to generate the given number at the given date.
# @param date [Time]
# @param setting [String] 'invoice_reference' | 'invoice_order-nb'
# @return [String]
def pattern(document, setting = 'invoice_reference')
def pattern(date, setting = 'invoice_reference')
raise TypeError, "invalid setting #{setting}" unless %w[invoice_order-nb invoice_reference].include?(setting)
value = Setting.find_by(name: setting).value_at(document.created_at)
value || if document.created_at < Setting.find_by(name: setting).first_update
value = Setting.find_by(name: setting).value_at(date)
value || if date < Setting.find_by(name: setting).first_update
Setting.find_by(name: setting).first_value
else
Setting.get(setting)
@ -89,7 +101,7 @@ class Invoices::NumberService
raise TypeError, "invalid setting #{setting}" unless %w[invoice_order-nb invoice_reference].include?(setting)
return nil if document.nil?
pattern = pattern(document, setting)
pattern = pattern(document.created_at, setting)
pattern = PaymentDocumentService.send(:replace_document_type_pattern, document, pattern)
start_idx = pattern.index(/n+|y+|m+|d+/)
end_idx = pattern.rindex(/n+|y+|m+|d+/)

View File

@ -16,27 +16,17 @@ class Invoices::PaymentDetailsService
build_avoir_details(invoice, total)
else
# subtract the wallet amount for this invoice from the total
if invoice.wallet_amount
wallet_amount = invoice.wallet_amount / 100.00
total -= wallet_amount
else
wallet_amount = nil
end
wallet_amount = wallet_amount(invoice)
total -= wallet_amount unless wallet_amount.nil?
return '' if wallet_amount.nil? && total.zero?
# payment method
payment_verbose = if invoice.paid_by_card?
I18n.t('invoices.settlement_by_debit_card')
else
I18n.t('invoices.settlement_done_at_the_reception')
end
# if the invoice was 100% payed with the wallet ...
payment_verbose = I18n.t('invoices.settlement_by_wallet') if total.zero? && wallet_amount
payment_verbose = payment_mean(invoice, total, wallet_amount)
payment_verbose += " #{I18n.t('invoices.on_DATE_at_TIME',
**{ DATE: I18n.l(invoice.created_at.to_date),
TIME: I18n.l(invoice.created_at, format: :hour_minute) })}"
if total.positive? || !invoice.wallet_amount
if total.positive? || wallet_amount.nil?
payment_verbose += " #{I18n.t('invoices.for_an_amount_of_AMOUNT', **{ AMOUNT: number_to_currency(total) })}"
end
if invoice.wallet_amount
@ -53,6 +43,30 @@ class Invoices::PaymentDetailsService
private
# @param invoice [Invoice]
# @return [Float,NilClass]
def wallet_amount(invoice)
return invoice.wallet_amount / 100.00 if invoice.wallet_amount
nil
end
# @param invoice [Invoice]
# @param total [Float]
# @param wallet_amount [Float,NilClass]
# @return [String]
def payment_mean(invoice, total, wallet_amount)
# if the invoice was 100% payed with the wallet ...
return I18n.t('invoices.settlement_by_wallet') if total.zero? && !wallet_amount.nil?
# else
if invoice.paid_by_card?
I18n.t('invoices.settlement_by_debit_card')
else
I18n.t('invoices.settlement_done_at_the_reception')
end
end
# @param invoice [Invoice]
# @param total [Float]
# @return [String]

View File

@ -7,7 +7,7 @@ class PaymentDocumentService
# @param document [PaymentDocument]
# @param date [Time]
def generate_reference(document, date: Time.current)
pattern = Invoices::NumberService.pattern(document, 'invoice_reference')
pattern = Invoices::NumberService.pattern(document.created_at, 'invoice_reference')
reference = replace_document_number_pattern(pattern, document)
reference = replace_date_pattern(reference, date)
@ -16,7 +16,7 @@ class PaymentDocumentService
# @param document [PaymentDocument]
def generate_order_number(document)
pattern = Invoices::NumberService.pattern(document, 'invoice_order-nb')
pattern = Invoices::NumberService.pattern(document.created_at, 'invoice_order-nb')
# global document number (nn..nn)
reference = pattern.gsub(/n+(?![^\[]*\])/) do |match|
@ -27,6 +27,19 @@ class PaymentDocumentService
replace_date_pattern(reference, document.created_at)
end
# Generate a reference for the given document using the given document number
# @param number [Integer]
# @param document [PaymentDocument]
def generate_numbered_reference(number, document)
pattern = Invoices::NumberService.pattern(document.created_at, 'invoice_reference')
reference = pattern.gsub(/n+|y+|m+|d+(?![^\[]*\])/) do |match|
pad_and_truncate(number, match.to_s.length)
end
reference = replace_date_pattern(reference, document.created_at)
replace_document_type_pattern(document, reference)
end
private
# Output the given integer with leading zeros. If the given value is longer than the given

View File

@ -102,6 +102,7 @@ en:
training_reservation_DESCRIPTION: "Training reservation - %{DESCRIPTION}"
event_reservation_DESCRIPTION: "Event reservation - %{DESCRIPTION}"
from_payment_schedule: "Due %{NUMBER} out of %{TOTAL}, from %{DATE}. Repayment schedule %{SCHEDULE}"
null_invoice: "Invoice at nil, billing jump following a malfunction of the Fab Manager software"
full_price_ticket:
one: "One full price ticket"
other: "%{count} full price tickets"

View File

@ -102,6 +102,7 @@ fr:
training_reservation_DESCRIPTION: "Réservation Formation - %{DESCRIPTION}"
event_reservation_DESCRIPTION: "Réservation Événement - %{DESCRIPTION}"
from_payment_schedule: "Échéance %{NUMBER} sur %{TOTAL}, du %{DATE}. Échéancier de paiement %{SCHEDULE}"
null_invoice: 'Facture à néant, saut de facturation suite à un dysfonctionnement du logiciel Fab Manager'
full_price_ticket:
one: "Une place plein tarif"
other: "%{count} places plein tarif"

View File

@ -85,11 +85,11 @@ CREATE FUNCTION public.fill_search_vector_for_project() RETURNS trigger
select string_agg(description, ' ') as content into step_description from project_steps where project_id = new.id;
new.search_vector :=
setweight(to_tsvector('pg_catalog.norwegian', unaccent(coalesce(new.name, ''))), 'A') ||
setweight(to_tsvector('pg_catalog.norwegian', unaccent(coalesce(new.tags, ''))), 'B') ||
setweight(to_tsvector('pg_catalog.norwegian', unaccent(coalesce(new.description, ''))), 'D') ||
setweight(to_tsvector('pg_catalog.norwegian', unaccent(coalesce(step_title.title, ''))), 'C') ||
setweight(to_tsvector('pg_catalog.norwegian', unaccent(coalesce(step_description.content, ''))), 'D');
setweight(to_tsvector('pg_catalog.french', unaccent(coalesce(new.name, ''))), 'A') ||
setweight(to_tsvector('pg_catalog.french', unaccent(coalesce(new.tags, ''))), 'B') ||
setweight(to_tsvector('pg_catalog.french', unaccent(coalesce(new.description, ''))), 'D') ||
setweight(to_tsvector('pg_catalog.french', unaccent(coalesce(step_title.title, ''))), 'C') ||
setweight(to_tsvector('pg_catalog.french', unaccent(coalesce(step_description.content, ''))), 'D');
return new;
end
@ -115,8 +115,8 @@ SET default_tablespace = '';
CREATE TABLE public.abuses (
id integer NOT NULL,
signaled_type character varying,
signaled_id integer,
signaled_type character varying,
first_name character varying,
last_name character varying,
email character varying,
@ -236,8 +236,8 @@ CREATE TABLE public.addresses (
locality character varying,
country character varying,
postal_code character varying,
placeable_type character varying,
placeable_id integer,
placeable_type character varying,
created_at timestamp without time zone,
updated_at timestamp without time zone
);
@ -346,8 +346,8 @@ CREATE TABLE public.ar_internal_metadata (
CREATE TABLE public.assets (
id integer NOT NULL,
viewable_type character varying,
viewable_id integer,
viewable_type character varying,
attachment character varying,
type character varying,
created_at timestamp without time zone,
@ -965,8 +965,8 @@ ALTER SEQUENCE public.coupons_id_seq OWNED BY public.coupons.id;
CREATE TABLE public.credits (
id integer NOT NULL,
creditable_type character varying,
creditable_id integer,
creditable_type character varying,
plan_id integer,
hours integer,
created_at timestamp without time zone,
@ -1762,15 +1762,15 @@ ALTER SEQUENCE public.notification_types_id_seq OWNED BY public.notification_typ
CREATE TABLE public.notifications (
id integer NOT NULL,
receiver_id integer,
attached_object_type character varying,
attached_object_id integer,
attached_object_type character varying,
notification_type_id integer,
is_read boolean DEFAULT false,
created_at timestamp without time zone,
updated_at timestamp without time zone,
receiver_type character varying,
is_send boolean DEFAULT false,
meta_data jsonb DEFAULT '"{}"'::jsonb
meta_data jsonb DEFAULT '{}'::jsonb
);
@ -2498,8 +2498,8 @@ CREATE TABLE public.prices (
id integer NOT NULL,
group_id integer,
plan_id integer,
priceable_type character varying,
priceable_id integer,
priceable_type character varying,
amount integer,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL,
@ -2962,8 +2962,8 @@ CREATE TABLE public.reservations (
message text,
created_at timestamp without time zone,
updated_at timestamp without time zone,
reservable_type character varying,
reservable_id integer,
reservable_type character varying,
nb_reserve_places integer,
statistic_profile_id integer
);
@ -2995,8 +2995,8 @@ ALTER SEQUENCE public.reservations_id_seq OWNED BY public.reservations.id;
CREATE TABLE public.roles (
id integer NOT NULL,
name character varying,
resource_type character varying,
resource_id integer,
resource_type character varying,
created_at timestamp without time zone,
updated_at timestamp without time zone
);
@ -5718,14 +5718,6 @@ ALTER TABLE ONLY public.roles
ADD CONSTRAINT roles_pkey PRIMARY KEY (id);
--
-- Name: schema_migrations schema_migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.schema_migrations
ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version);
--
-- Name: settings settings_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@ -7385,6 +7377,21 @@ CREATE INDEX proof_of_identity_type_id_and_proof_of_identity_refusal_id ON publi
CREATE UNIQUE INDEX unique_not_null_external_id ON public.invoicing_profiles USING btree (external_id) WHERE (external_id IS NOT NULL);
--
-- Name: unique_schema_migrations; Type: INDEX; Schema: public; Owner: -
--
CREATE UNIQUE INDEX unique_schema_migrations ON public.schema_migrations USING btree (version);
--
-- Name: accounting_periods accounting_periods_del_protect; Type: RULE; Schema: public; Owner: -
--
CREATE RULE accounting_periods_del_protect AS
ON DELETE TO public.accounting_periods DO INSTEAD NOTHING;
--
-- Name: accounting_periods accounting_periods_upd_protect; Type: RULE; Schema: public; Owner: -
--
@ -8361,6 +8368,7 @@ INSERT INTO "schema_migrations" (version) VALUES
('20140605125131'),
('20140605142133'),
('20140605151442'),
('20140606133116'),
('20140609092700'),
('20140609092827'),
('20140610153123'),
@ -8429,12 +8437,14 @@ INSERT INTO "schema_migrations" (version) VALUES
('20150507075620'),
('20150512123546'),
('20150520132030'),
('20150520133409'),
('20150526130729'),
('20150527153312'),
('20150529113555'),
('20150601125944'),
('20150603104502'),
('20150603104658'),
('20150603133050'),
('20150604081757'),
('20150604131525'),
('20150608142234'),
@ -8516,6 +8526,7 @@ INSERT INTO "schema_migrations" (version) VALUES
('20160905142700'),
('20160906094739'),
('20160906094847'),
('20160906145713'),
('20160915105234'),
('20161123104604'),
('20170109085345'),

View File

@ -4,6 +4,7 @@ namespace :fablab do
desc 'Fill the holes in the logical sequence of invoices references'
task fix_references: :environment do |_task, _args|
include ActionView::Helpers::NumberHelper
include DbHelper
user = User.adminsys || User.admins.first
@ -11,37 +12,53 @@ namespace :fablab do
missing_references = {}
# browse invoices to list missing reference
puts 'Computing missing references...'
not_closed(Invoice).order(created_at: :desc).each do |invoice|
number = Invoices::NumberService.number(invoice)
next if number == 1
previous = Invoices::NumberService.change_number(invoice, number - 1)
next unless Invoice.find_by(reference: previous).nil?
previous = Invoice.where('created_at < :date', date: db_time(invoice.created_at))
.order(created_at: :desc)
.limit(1)
.first
previous_number = Invoices::NumberService.number(previous)
next if previous_number.nil? || previous_number == number - 1
missing_references[invoice.created_at] ||= []
missing_references[invoice.created_at].push(previous)
# ignore numbers of already existing invoices
(previous_number + 1...number).to_a.each do |num|
next unless Invoices::NumberService.find_by_number(num, date: invoice.created_at).nil?
missing_references[invoice.created_at].push(num)
end
end
# create placeholder invoices for found missing references
missing_references.each_pair do |date, references|
references.reverse_each.with_index do |reference, index|
Invoice.create!(
puts 'Creating missing invoices...'
total = missing_references.values.filter(&:present?).flatten.count
counter = 1
missing_references.each_pair do |date, numbers|
numbers.reverse_each.with_index do |number, index|
puts "#{counter} / #{total}"
invoice = Invoice.new(
total: 0,
invoicing_profile: user.invoicing_profile,
statistic_profile: user.statistic_profile,
operator_profile: user.invoicing_profile,
payment_method: '',
reference: reference,
created_at: date - (index + 1).seconds,
description: 'Facture à néant, saut de facturation suite à un dysfonctionnement du logiciel Fab Manager',
invoice_items_attributes: [{
amount: 0,
description: 'facture à zéro',
description: I18n.t('invoices.null_invoice'),
object_type: 'Error',
object_id: 1,
main: true
}]
)
invoice.reference = PaymentDocumentService.generate_numbered_reference(number, invoice)
invoice.save!
counter += 1
end
end
end

View File

@ -8,7 +8,9 @@ namespace :fablab do
I18n.t('invoices.order_number', locale: 'en', **{ NUMBER: 'REPLACE' }).gsub('REPLACE', ''),
I18n.t('invoices.order_number', locale: 'fr', **{ NUMBER: 'REPLACE' }).gsub('REPLACE', '')
]
max_id = ActiveRecord::Base.connection.execute('SELECT max(id) as max_id FROM invoices').first['max_id']
Invoice.order(id: :asc).find_each do |invoice|
puts "Processing: #{invoice.id} / #{max_id}"
next unless File.exist?(invoice.file)
found = false
@ -21,7 +23,7 @@ namespace :fablab do
order_text.each do |label|
next unless line.include? label
number = line.gsub(label, '').strip
number = line.gsub(label, '').gsub(/\s{5,}.+$/, '').strip
invoice.update(order_number: number)
found = true
end
@ -46,5 +48,12 @@ namespace :fablab do
item.invoice&.update(order_number: schedule.order_number)
end
end
Avoir.where(order_number: nil).order(id: :asc).find_each do |refund|
next if refund.invoice.nil?
# refunds are validated against their avoir_date for inclusion in closed periods, so we must bypass the validation
# (Invoices are validated on Time.current, so this was not necesseary above)
refund.update_attribute('order_number', refund.invoice.order_number) # rubocop:disable Rails/SkipsModelValidations
end
end
end

View File

@ -140,4 +140,9 @@ class Invoices::NumberServiceTest < ActiveSupport::TestCase
periodicity = Invoices::NumberService.number_periodicity(invoice, 'invoice_order-nb')
assert_equal 'month', periodicity
end
test 'find document by number' do
invoice = Invoices::NumberService.find_by_number(1, date: Time.zone.parse('2012-03-01'))
assert_equal Invoice.first, invoice
end
end