1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-20 14:54:15 +01:00

(feat) recover order numbers

This commit is contained in:
Sylvain 2023-03-24 17:21:44 +01:00
parent a92df2150e
commit f9123fe20f
24 changed files with 185 additions and 56 deletions

View File

@ -12,7 +12,10 @@
- Updated bootsnap to 1.16
- Updated pg to 1.4
- Updated nodejs to 18.15
- Updated oj to 3.14
- Updated multi_json to 1.15
- Fix a bug: broken display after a plan category was deleted
- [TODO DEPLOY] `rails fablab:restore_order_number` THEN `rails fablab:fix_references`
## v5.9.1 2023 March 22

View File

@ -43,6 +43,7 @@ group :development do
# Preview mail in the browser
gem 'listen', '~> 3.0.5'
gem 'overcommit'
gem 'pry'
gem 'rb-readline'
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
gem 'railroady'
@ -56,7 +57,6 @@ group :test do
gem 'database_cleaner'
gem 'faker'
gem 'minitest-reporters'
gem 'pdf-reader'
gem 'rubyXL'
gem 'vcr', '~> 6.1.0'
gem 'webmock'
@ -101,6 +101,7 @@ gem 'stripe', '5.29.0'
gem 'recurrence'
# PDF
gem 'pdf-reader'
gem 'prawn'
gem 'prawn-table'

View File

@ -1,7 +1,7 @@
GEM
remote: https://rubygems.org/
specs:
Ascii85 (1.0.3)
Ascii85 (1.1.0)
aasm (5.0.8)
concurrent-ruby (~> 1.0)
actioncable (6.1.7.2)
@ -113,6 +113,7 @@ GEM
childprocess (4.1.0)
chroma (0.2.0)
cldr-plurals-runtime-rb (1.0.1)
coderay (1.1.3)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
concurrent-ruby (1.2.2)
@ -245,7 +246,7 @@ GEM
minitest (>= 5.0)
ruby-progressbar
msgpack (1.6.1)
multi_json (1.14.1)
multi_json (1.15.0)
multi_xml (0.6.0)
multipart-post (2.1.1)
net-imap (0.3.4)
@ -266,7 +267,7 @@ GEM
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 3)
oj (3.10.5)
oj (3.14.2)
omniauth (1.9.2)
hashie (>= 3.4.6)
rack (>= 1.6.2, < 3)
@ -301,8 +302,8 @@ GEM
parser (3.1.2.0)
ast (~> 2.4.1)
pdf-core (0.9.0)
pdf-reader (2.4.0)
Ascii85 (~> 1.0.0)
pdf-reader (2.11.0)
Ascii85 (~> 1.0)
afm (~> 0.2.1)
hashery (~> 2.0)
ruby-rc4
@ -316,6 +317,9 @@ GEM
ttfunk (~> 1.7)
prawn-table (0.2.2)
prawn (>= 1.3.0, < 3.0.0)
pry (0.14.2)
coderay (~> 1.1)
method_source (~> 1.0)
public_suffix (4.0.6)
puma (6.1.0)
nio4r (~> 2.0)
@ -568,6 +572,7 @@ DEPENDENCIES
pg_search
prawn
prawn-table
pry
puma (= 6.1.0)
pundit
railroady

View File

@ -15,10 +15,19 @@ class ChainedElement < ApplicationRecord
# @return [Boolean]
def corrupted?
comparable(FootprintService.chained_data(element, previous&.footprint)) != comparable(content) ||
comparable(FootprintService.chained_data(element, previous&.footprint, columns)) != comparable(content) ||
footprint != Integrity::Checksum.text(comparable(content).to_json)
end
# return ths list of columns in the saved JSON. This is used to do the comparison with the
# saved item, as it may have changed between (some columns may have been added)
# @return [Array<String>]
def columns
%w[id].concat(content.keys.delete_if do |column|
%w[id previous].concat(element.class.columns_out_of_footprint).include?(column)
end.sort)
end
private
def set_content

View File

@ -19,6 +19,7 @@ class Footprintable < ApplicationRecord
def check_footprint
return false unless persisted?
reload
footprint_children.map(&:check_footprint).all? && !chained_element.corrupted?
end

View File

@ -6,6 +6,7 @@ class HistoryValue < Footprintable
belongs_to :invoicing_profile
has_one :chained_element, as: :element, dependent: :restrict_with_exception
delegate :footprint, to: :chained_element
delegate :user, to: :invoicing_profile
after_create :chain_record

View File

@ -23,9 +23,10 @@ class Invoice < PaymentDocument
has_many :accounting_lines, dependent: :destroy
delegate :user, to: :invoicing_profile
delegate :footprint, to: :chained_element
before_create :add_environment
after_create :update_reference, :chain_record
after_create :generate_order_number, :update_reference, :chain_record
after_update :log_changes
after_commit :generate_and_send_invoice, on: [:create], if: :persisted?
@ -50,12 +51,15 @@ class Invoice < PaymentDocument
"#{prefix}-#{id}_#{created_at.strftime('%d%m%Y')}.pdf"
end
def order_number
return order.reference unless order.nil? || order.reference.nil?
def generate_order_number
self.order_number = order.reference and return unless order.nil? || order.reference.nil?
return payment_schedule_item.payment_schedule.order_number if !payment_schedule_item.nil? && !payment_schedule_item.first?
if !payment_schedule_item.nil? && !payment_schedule_item.first?
self.order_number = payment_schedule_item.payment_schedule.order_number
return
end
PaymentDocumentService.generate_order_number(self)
super
end
# for debug & used by rake task "fablab:maintenance:regenerate_invoices"

View File

@ -13,6 +13,8 @@ class InvoiceItem < Footprintable
after_create :chain_record
after_update :log_changes
delegate :footprint, to: :chained_element
def amount_after_coupon
# deduct coupon discount
coupon_service = CouponService.new

View File

@ -8,6 +8,10 @@ class PaymentDocument < Footprintable
self.reference = PaymentDocumentService.generate_reference(self, date: date)
end
def generate_order_number
self.order_number = PaymentDocumentService.generate_order_number(self)
end
def update_reference
generate_reference
save

View File

@ -17,10 +17,12 @@ class PaymentSchedule < PaymentDocument
has_many :payment_schedule_objects, dependent: :destroy
before_create :add_environment
after_create :update_reference, :chain_record
after_create :generate_order_number, :update_reference, :chain_record
after_commit :generate_and_send_document, on: [:create], if: :persisted?
after_commit :generate_initial_invoice, on: [:create], if: :persisted?
delegate :footprint, to: :chained_element
def file
dir = "payment_schedules/#{invoicing_profile.id}"
@ -39,10 +41,6 @@ class PaymentSchedule < PaymentDocument
"#{prefix}-#{id}_#{created_at.strftime('%d%m%Y')}.pdf"
end
def order_number
ordered_items.first&.invoice&.order_number || PaymentDocumentService.generate_order_number(self)
end
##
# This is useful to check the first item because its amount may be different from the others
##

View File

@ -9,6 +9,8 @@ class PaymentScheduleItem < Footprintable
after_create :chain_record
delegate :footprint, to: :chained_element
def first?
payment_schedule.ordered_items.first == self
end

View File

@ -10,4 +10,6 @@ class PaymentScheduleObject < Footprintable
has_one :chained_element, as: :element, dependent: :restrict_with_exception
after_create :chain_record
delegate :footprint, to: :chained_element
end

View File

@ -44,8 +44,7 @@ class Pdf::Invoice < Prawn::Document
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
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
text I18n.t('invoices.order_number', **{ NUMBER: invoice.order_number }), leading: 3
end
if invoice.is_a?(Avoir)
text I18n.t('invoices.refund_invoice_issued_on_DATE', **{ DATE: I18n.l(invoice.avoir_date.to_date) })

View File

@ -8,9 +8,10 @@ class FootprintService
class << self
# @param item [Footprintable]
# @param previous_footprint [String,NilClass]
# @return {Hash<Symbol->String,Integer,Hash>}
def chained_data(item, previous_footprint = nil)
columns = footprint_columns(item.class)
# @param columns [Array<String>]
# @return [Hash<Symbol->String,Integer,Hash>]
def chained_data(item, previous_footprint = nil, columns = nil)
columns ||= footprint_columns(item.class)
res = {}
columns.each do |column|
next if column.blank? || item[column].blank?
@ -35,7 +36,7 @@ class FootprintService
# @param klass [Class] a class inheriting from Footprintable
# @param item [Footprintable] an instance of the provided class
def debug_footprint(klass, item)
current = chained_data(item, item.chained_element.previous&.footprint)
current = chained_data(item, item.chained_element.previous&.footprint, item.chained_element.columns)
saved = item.chained_element&.content&.sort&.to_h&.transform_values { |val| val.is_a?(Hash) ? val.sort.to_h : val }
if saved.nil?

View File

@ -55,6 +55,7 @@ class PaymentDocumentService
# @param document [PaymentDocument]
# @param periodicity [String] 'day' | 'month' | 'year' | 'global'
# @return [Hash<Symbol->Footprintable,Number>]
def previous_order(document, periodicity)
start = periodicity == 'global' ? nil : document.created_at.send("beginning_of_#{periodicity}")
ending = document.created_at
@ -67,11 +68,30 @@ class PaymentDocumentService
.where('created_at < :end_date', end_date: db_time(ending))
invoices = invoices.where('created_at >= :start_date', start_date: db_time(start)) unless start.nil?
[
orders.order(created_at: :desc).limit(1).first,
schedules.order(created_at: :desc).limit(1).first,
invoices.order(created_at: :desc).limit(1).first
last_with_number = [
orders.where.not(reference: nil).order(created_at: :desc).limit(1).first,
schedules.where.not(order_number: nil).order(created_at: :desc).limit(1).first,
invoices.where.not(order_number: nil).order(created_at: :desc).limit(1).first
].filter(&:present?).max_by { |item| item&.created_at }
{
last_order: last_with_number,
unnumbered: orders_without_number(orders, schedules, invoices, last_with_number)
}
end
def orders_without_number(orders, schedules, invoices, last_item_with_number = nil)
items_after(orders.where(reference: nil), last_item_with_number).count +
items_after(schedules.where(order_number: nil), last_item_with_number).count +
items_after(invoices.where(order_number: nil), last_item_with_number).count
end
# @param items [ActiveRecord::Relation]
# @param previous_item [Footprintable,NilClass]
# @return [ActiveRecord::Relation]
def items_after(items, previous_item = nil)
return items if previous_item.nil?
items.where('created_at > :date', date: previous_item&.created_at)
end
# @param document [PaymentDocument] invoice to exclude
@ -145,12 +165,12 @@ class PaymentDocumentService
# @return [Integer]
def order_number(document, periodicity)
previous = previous_order(document, periodicity)
if Invoices::NumberService.number_periodicity(previous, 'invoice_order-nb') == periodicity
number = Invoices::NumberService.number(previous, 'invoice_order-nb')
if Invoices::NumberService.number_periodicity(previous[:last_order], 'invoice_order-nb') == periodicity
number = Invoices::NumberService.number(previous[:last_order], 'invoice_order-nb')
end
number ||= 0
number + 1
number + previous[:unnumbered] + 1
end
# Replace the document number elements in the provided pattern with counts from the database

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
# We now store the order number in DB, instead of generating it on every access
class AddOrderNumber < ActiveRecord::Migration[6.1]
def change
add_column :invoices, :order_number, :string
add_column :payment_schedules, :order_number, :string
end
end

View File

@ -1490,7 +1490,8 @@ CREATE TABLE public.invoices (
environment character varying,
invoicing_profile_id integer,
operator_profile_id integer,
statistic_profile_id integer
statistic_profile_id integer,
order_number character varying
);
@ -2217,7 +2218,8 @@ CREATE TABLE public.payment_schedules (
operator_profile_id bigint,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL,
start_at timestamp without time zone
start_at timestamp without time zone,
order_number character varying
);
@ -7383,14 +7385,6 @@ 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: 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: -
--
@ -7400,6 +7394,15 @@ CREATE RULE accounting_periods_upd_protect AS
WHERE ((new.start_at <> old.start_at) OR (new.end_at <> old.end_at) OR (new.closed_at <> old.closed_at) OR (new.period_total <> old.period_total) OR (new.perpetual_total <> old.perpetual_total)) DO INSTEAD NOTHING;
--
-- Name: chained_elements chained_elements_upd_protect; Type: RULE; Schema: public; Owner: -
--
CREATE RULE chained_elements_upd_protect AS
ON UPDATE TO public.chained_elements
WHERE ((new.content <> old.content) OR ((new.footprint)::text <> (old.footprint)::text) OR (new.previous_id <> old.previous_id) OR (new.element_id <> old.element_id) OR ((new.element_type)::text <> (old.element_type)::text)) DO INSTEAD NOTHING;
--
-- Name: projects projects_search_content_trigger; Type: TRIGGER; Schema: public; Owner: -
--
@ -8675,6 +8678,7 @@ INSERT INTO "schema_migrations" (version) VALUES
('20230323085947'),
('20230323104259'),
('20230323104727'),
('20230324090312');
('20230324090312'),
('20230324095639');

View File

@ -1,16 +1,12 @@
# frozen_string_literal: true
require 'integrity/archive_helper'
namespace :fablab do
desc 'Fill the holes in the logical sequence of invoices references and regenerate invoices w/ duplicate reference'
desc 'Fill the holes in the logical sequence of invoices references'
task fix_references: :environment do |_task, _args|
include ActionView::Helpers::NumberHelper
user = User.adminsys || User.admins.first
# check the footprints
Integrity::ArchiveHelper.check_footprints
ActiveRecord::Base.transaction do
missing_references = {}
@ -48,11 +44,6 @@ namespace :fablab do
)
end
end
# chain records
puts 'Chaining all record. This may take a while...'
not_closed(InvoiceItem).order(:id).find_each(&:chain_record)
not_closed(Invoice).order(:id).find_each(&:chain_record)
end
end

View File

@ -0,0 +1,50 @@
# frozen_string_literal: true
namespace :fablab do
desc 'Scans PDF files of invoices to find order numbers'
task restore_order_number: :environment do |_task, _args|
order_text = [
I18n.t('invoices.order_number', **{ NUMBER: 'REPLACE' }).gsub('REPLACE', ''),
I18n.t('invoices.order_number', locale: 'en', **{ NUMBER: 'REPLACE' }).gsub('REPLACE', ''),
I18n.t('invoices.order_number', locale: 'fr', **{ NUMBER: 'REPLACE' }).gsub('REPLACE', '')
]
Invoice.order(id: :asc).find_each do |invoice|
next unless File.exist?(invoice.file)
found = false
reader = PDF::Reader.new(invoice.file)
page = reader.pages.first
page.text.scan(/^.+/).each do |line|
next unless order_text.any? { |label| line.include? label }
break if found
order_text.each do |label|
next unless line.include? label
number = line.gsub(label, '').strip
invoice.update(order_number: number)
found = true
end
end
end
Order.where(reference: nil).order(id: :asc).find_each do |order|
order.update(reference: PaymentDocumentService.generate_order_number(order))
end
Invoice.where(order_number: nil).order(id: :asc).find_each do |invoice|
next unless invoice.payment_schedule_item.nil?
unless invoice.order.nil?
invoice.update(order_number: invoice.order.reference)
next
end
invoice.update(order_number: PaymentDocumentService.generate_order_number(invoice))
end
PaymentSchedule.order(id: :asc).find_each do |schedule|
schedule.update(order_number: PaymentDocumentService.generate_order_number(schedule))
schedule.ordered_items.each do |item|
item.invoice&.update(order_number: schedule.order_number)
end
end
end
end

View File

@ -4,7 +4,7 @@ invoice_1:
total: 10000
created_at: '2012-03-12 11:03:31.651441'
updated_at: '2021-05-27 09:26:26.481266'
reference: 1604001/VL
reference: 1203001/VL
payment_method: card
avoir_date:
invoice_id:
@ -18,13 +18,14 @@ invoice_1:
invoicing_profile_id: 3
operator_profile_id: 3
statistic_profile_id: 3
order_number: 000001-03-12
invoice_2:
id: 2
total: 2000
created_at: '2012-03-12 13:40:22.342717'
updated_at: '2021-05-27 09:26:26.485839'
reference: '1604002'
reference: '1203002'
payment_method:
avoir_date:
invoice_id:
@ -38,6 +39,7 @@ invoice_2:
invoicing_profile_id: 4
operator_profile_id: 1
statistic_profile_id: 4
order_number: 000002-03-12
invoice_3:
id: 3
@ -58,13 +60,14 @@ invoice_3:
invoicing_profile_id: 7
operator_profile_id: 1
statistic_profile_id: 7
order_number: 000003-06-15
invoice_4:
id: 4
total: 0
created_at: '2016-04-05 08:35:52.931187'
updated_at: '2021-05-27 09:26:26.494005'
reference: '1203002'
reference: '1604002'
payment_method:
avoir_date:
invoice_id:
@ -78,13 +81,14 @@ invoice_4:
invoicing_profile_id: 7
operator_profile_id: 1
statistic_profile_id: 7
order_number: 000004-04-16
invoice_5:
id: 5
total: 1500
created_at: '2016-04-05 08:36:46.853368'
updated_at: '2021-05-27 09:26:26.498207'
reference: '1506031'
reference: '1604031'
payment_method:
avoir_date:
invoice_id:
@ -98,6 +102,7 @@ invoice_5:
invoicing_profile_id: 3
operator_profile_id: 1
statistic_profile_id: 3
order_number: 000005-04-16
invoice_6:
id: 6
@ -118,6 +123,7 @@ invoice_6:
invoicing_profile_id: 8
operator_profile_id: 1
statistic_profile_id: 8
order_number: 000006-01-21
invoice_5811:
id: 5811
total: 4500
@ -137,6 +143,7 @@ invoice_5811:
invoicing_profile_id: 3
operator_profile_id: 1
statistic_profile_id: 3
order_number: 000009-08-22
invoice_5812:
id: 5812
total: 6000
@ -156,6 +163,7 @@ invoice_5812:
invoicing_profile_id: 7
operator_profile_id: 1
statistic_profile_id: 7
order_number: 005877-09-22
invoice_5816:
id: 5816
total: 319
@ -175,6 +183,7 @@ invoice_5816:
invoicing_profile_id: 4
operator_profile_id: 4
statistic_profile_id: 4
order_number: 005888-10-22
invoice_5817:
id: 5817
total: 1295
@ -194,6 +203,7 @@ invoice_5817:
invoicing_profile_id: 4
operator_profile_id: 4
statistic_profile_id: 4
order_number: 005890-10-22
invoice_5818:
id: 5818
total: 1000
@ -213,6 +223,7 @@ invoice_5818:
invoicing_profile_id: 4
operator_profile_id: 4
statistic_profile_id: 4
order_number: 005892-10-22
invoice_5819:
id: 5819
total: 4002
@ -232,6 +243,7 @@ invoice_5819:
invoicing_profile_id: 4
operator_profile_id: 4
statistic_profile_id: 4
order_number: 005894-10-22
invoice_5820:
id: 5820
total: 12000
@ -251,6 +263,7 @@ invoice_5820:
invoicing_profile_id: 3
operator_profile_id: 1
statistic_profile_id: 3
order_number: 005882-09-22
invoice_5821:
id: 5821
total: 12000
@ -270,6 +283,7 @@ invoice_5821:
invoicing_profile_id: 2
operator_profile_id: 1
statistic_profile_id: 2
order_number: 005898-10-22
invoice_5822:
id: 5822
total: 3000
@ -289,3 +303,4 @@ invoice_5822:
invoicing_profile_id: 2
operator_profile_id: 1
statistic_profile_id: 2
order_number: 005900-10-22

View File

@ -3,7 +3,7 @@ order_1:
statistic_profile_id: 3
operator_profile_id: 1
token: 3R9wtsPjyYMHKqy-I2V5Cg1661868752184
reference:
reference: 000009-08-22
state: paid
total: 4500
created_at: '2022-08-30 14:12:32.213832'

View File

@ -12,6 +12,7 @@ payment_schedule_12:
operator_profile_id: 1
created_at: '2021-06-14 12:24:45.843714'
updated_at: '2021-06-14 12:24:45.908386'
order_number: 000007-06-21
payment_schedule_13:
id: 13
@ -27,3 +28,4 @@ payment_schedule_13:
operator_profile_id: 10
created_at: <%= 9.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %>
updated_at: <%= 9.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %>
order_number: <%= 9.months.ago.utc.strftime('000008-%m-%y') %>

View File

@ -55,5 +55,8 @@ class OpenIdConnectTest < ActionDispatch::IntegrationTest
# end
# User.devise :omniauthable, omniauth_providers: [db_provider&.strategy_name&.to_sym]
# Rails.application.reload_routes!
#
# === OR === (need to try)
# Rails.application.reloader.reload!
end
end

View File

@ -36,6 +36,7 @@ class ChainedElementTest < ActiveSupport::TestCase
source1 = sample_reservation_invoice(users(:user2), users(:user1))
element1 = source1.chained_element
assert element1.persisted?
assert source1.check_footprint
source2 = sample_reservation_invoice(users(:user3), users(:user1))
element2 = source2.chained_element
@ -46,6 +47,7 @@ class ChainedElementTest < ActiveSupport::TestCase
assert_not element1.corrupted?
assert_not element2.corrupted?
assert source2.check_footprint
end
test 'chain element with children embedded json' do
@ -78,5 +80,6 @@ class ChainedElementTest < ActiveSupport::TestCase
previous = element
end
assert source.check_footprint
end
end