1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-18 07:52:23 +01:00

Merge branch 'dev' for release 5.6.1

This commit is contained in:
Sylvain 2023-01-06 13:12:27 +01:00
commit 5ab2989a61
14 changed files with 140 additions and 23 deletions

View File

@ -1,5 +1,17 @@
# Changelog Fab-manager
## v5.6.1 2023 January 6
- Fix a bug: allow decimal values for VAT rates
- Fix a bug: canceled reservations/slots not shown as it in the reservations dashboard
- Fix a bug: no main item on some invoices
- Fix a bug: unable to build accounting lines if no invoices
- Fix a bug: unable to apply rounding correction on accounting lines
- Fix a bug: empty object for some invoice item
- Fix a bug: unable to filter Show only slots with reservations in public calendar for admin
- Fix a security issue: updated json5 to 1.0.2 to fix [CVE-2022-46175](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-46175)
- [TODO DEPLOY] `rails fablab:fix_invoice_items` => run this script BEFORE running the migrations
## v5.6.0 2023 January 5
- Ability to group machines by categories

View File

@ -89,18 +89,25 @@ const ReservationsPanel: React.FC<SpaceReservationsProps> = ({ userId, onError,
);
};
/**
* Check if all slots of the given reservation are canceled
*/
const isCancelled = (reservation: Reservation): boolean => {
return reservation.slots_reservations_attributes.map(sr => sr.canceled_at).every(ca => ca != null);
};
/**
* Render the reservation in a user-friendly way
*/
const renderReservation = (reservation: Reservation, state: 'past' | 'futur'): ReactNode => {
return (
<li key={reservation.id} className="reservation">
<a className={`reservation-title ${details[reservation.id] ? 'clicked' : ''}`} onClick={toggleDetails(reservation.id)}>
<a className={`reservation-title ${details[reservation.id] ? 'clicked' : ''} ${isCancelled(reservation) ? 'canceled' : ''}`} onClick={toggleDetails(reservation.id)}>
{reservation.reservable.name} - {FormatLib.date(reservation.slots_reservations_attributes[0].slot_attributes.start_at)}
</a>
{details[reservation.id] && <FabPopover title={t('app.logged.dashboard.reservations.reservations_panel.slots_details')}>
{reservation.slots_reservations_attributes.filter(s => filterSlot(s, state)).map(
slotReservation => <span key={slotReservation.id} className="slot-details">
slotReservation => <span key={slotReservation.id} className={`slot-details ${slotReservation.canceled_at ? 'canceled' : ''}`}>
{FormatLib.date(slotReservation.slot_attributes.start_at)}, {FormatLib.time(slotReservation.slot_attributes.start_at)} - {FormatLib.time(slotReservation.slot_attributes.end_at)}
</span>
)}

View File

@ -115,6 +115,7 @@ export const VatSettingsModal: React.FC<VatSettingsModalProps> = ({ isOpen, togg
rules={{ required: true }}
tooltip={t('app.admin.vat_settings_modal.VAT_rate_help')}
type='number'
step={0.001}
label={t('app.admin.vat_settings_modal.VAT_rate')}
addOn={<ClockCounterClockwise size={24}/>}
addOnAriaLabel={t('app.admin.vat_settings_modal.show_history')}
@ -132,6 +133,7 @@ export const VatSettingsModal: React.FC<VatSettingsModalProps> = ({ isOpen, togg
<FormInput register={register}
id="invoice_VAT-rate_Product"
type='number'
step={0.001}
label={t('app.admin.vat_settings_modal.VAT_rate_product')}
addOn={<ClockCounterClockwise size={24}/>}
addOnAriaLabel={t('app.admin.vat_settings_modal.show_history')}
@ -143,6 +145,7 @@ export const VatSettingsModal: React.FC<VatSettingsModalProps> = ({ isOpen, togg
<FormInput register={register}
id="invoice_VAT-rate_Event"
type='number'
step={0.001}
label={t('app.admin.vat_settings_modal.VAT_rate_event')}
addOn={<ClockCounterClockwise size={24}/>}
addOnAriaLabel={t('app.admin.vat_settings_modal.show_history')}
@ -154,6 +157,7 @@ export const VatSettingsModal: React.FC<VatSettingsModalProps> = ({ isOpen, togg
<FormInput register={register}
id="invoice_VAT-rate_Machine"
type='number'
step={0.001}
label={t('app.admin.vat_settings_modal.VAT_rate_machine')}
addOn={<ClockCounterClockwise size={24}/>}
addOnAriaLabel={t('app.admin.vat_settings_modal.show_history')}
@ -165,6 +169,7 @@ export const VatSettingsModal: React.FC<VatSettingsModalProps> = ({ isOpen, togg
<FormInput register={register}
id="invoice_VAT-rate_Subscription"
type='number'
step={0.001}
label={t('app.admin.vat_settings_modal.VAT_rate_subscription')}
addOn={<ClockCounterClockwise size={24}/>}
addOnAriaLabel={t('app.admin.vat_settings_modal.show_history')}
@ -176,6 +181,7 @@ export const VatSettingsModal: React.FC<VatSettingsModalProps> = ({ isOpen, togg
<FormInput register={register}
id="invoice_VAT-rate_Space"
type='number'
step={0.001}
label={t('app.admin.vat_settings_modal.VAT_rate_space')}
addOn={<ClockCounterClockwise size={24}/>}
addOnAriaLabel={t('app.admin.vat_settings_modal.show_history')}
@ -187,6 +193,7 @@ export const VatSettingsModal: React.FC<VatSettingsModalProps> = ({ isOpen, togg
<FormInput register={register}
id="invoice_VAT-rate_Training"
type='number'
step={0.001}
label={t('app.admin.vat_settings_modal.VAT_rate_training')}
addOn={<ClockCounterClockwise size={24}/>}
addOnAriaLabel={t('app.admin.vat_settings_modal.show_history')}

View File

@ -59,7 +59,8 @@ Application.Controllers.controller('CalendarController', ['$scope', '$state', '$
spaces: isSelectAll('spaces', scope),
externals: isSelectAll('externals', scope),
evt: filter.evt,
dispo: filter.dispo
dispo: filter.dispo,
reserved: filter.reserved
});
scope.machinesGroupByCategory.forEach(c => c.checked = _.every(c.machines, 'checked'));
// remove all
@ -319,7 +320,7 @@ Application.Controllers.controller('CalendarController', ['$scope', '$state', '$
const t = $scope.trainings.filter(t => t.checked).map(t => t.id);
const m = $scope.machines.filter(m => m.checked).map(m => m.id);
const s = $scope.spaces.filter(s => s.checked).map(s => s.id);
return { t, m, s, evt: $scope.filter.evt, dispo: $scope.filter.dispo };
return { t, m, s, evt: $scope.filter.evt, dispo: $scope.filter.dispo, reserved: $scope.filter.reserved };
};
const availabilitySourceUrl = () => `/api/availabilities/public?${$.param(getFilter())}`;

View File

@ -8,9 +8,16 @@
&.clicked {
color: var(--secondary-dark);
}
&.canceled {
text-decoration: line-through;
}
}
.slot-details {
display: block;
&.canceled {
text-decoration: line-through;
}
}
.fab-popover {

View File

@ -132,7 +132,12 @@ class Invoice < PaymentDocument
end
def main_item
invoice_items.where(main: true).first
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

View File

@ -169,6 +169,6 @@ class Accounting::AccountingService
diff = debit_sum - credit_sum
fixable_line = lines.filter { |l| l[:line_type] == 'payment' }.last
fixable_line.credit += diff
fixable_line[:credit] += diff
end
end

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
# From this migration, ths object_type and object_id columns in InvoiceItem won't be able to be null anymore
# This will prevent issues while building the accounting data, and ensure data integrity
class AddNotNullToInvoiceItemsObject < ActiveRecord::Migration[5.2]
def change
change_column_null :invoice_items, :object_type, false
change_column_null :invoice_items, :object_id, false
end
end

View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2022_12_27_141529) do
ActiveRecord::Schema.define(version: 2023_01_06_081943) do
# These are extensions that must be enabled in order to support this database
enable_extension "fuzzystrmatch"
@ -331,8 +331,8 @@ ActiveRecord::Schema.define(version: 2022_12_27_141529) do
t.text "description"
t.integer "invoice_item_id"
t.string "footprint"
t.string "object_type"
t.bigint "object_id"
t.string "object_type", null: false
t.bigint "object_id", null: false
t.boolean "main"
t.index ["invoice_id"], name: "index_invoice_items_on_invoice_id"
t.index ["object_type", "object_id"], name: "index_invoice_items_on_object_type_and_object_id"

View File

@ -0,0 +1,64 @@
# frozen_string_literal: true
require 'integrity/archive_helper'
# This take will ensure data integrity for invoices_items.
# Due a an unknown bug, some invoice items may not contains the reference to their object.
# This task will re-associate these items with their reservation/subscription/etc
namespace :fablab do
desc 'Associate the invoice_items w/o object'
task fix_invoice_items: :environment do |_task, _args|
next unless InvoiceItem.where(object_type: nil)
.or(InvoiceItem.where(object_id: nil))
.count
.positive?
include ActionView::Helpers::NumberHelper
# check the footprints and save the archives
Integrity::ArchiveHelper.check_footprints
ActiveRecord::Base.transaction do
periods = Integrity::ArchiveHelper.backup_and_remove_periods
# fix invoice items data
InvoiceItem.where(object_type: nil)
.or(InvoiceItem.where(object_id: nil))
.find_each do |ii|
invoice = ii.invoice
other_items = invoice.invoice_items.where.not(id: ii.id)
puts "\e[4;33mFound an invalid InvoiceItem\e[0m"
puts '=============================================='
puts "Invoice #{invoice.id} (# #{invoice.reference})"
puts "Total: #{number_to_currency(invoice.total / 100.0)}"
puts "Customer: #{invoice.invoicing_profile.full_name} (#{invoice.invoicing_profile.email})"
puts "Operator: #{invoice.operator_profile&.user&.profile&.full_name} (#{invoice.operator_profile&.user&.email})"
puts "Date: #{invoice.created_at}"
puts '=============================================='
puts "Concerned item: #{ii.id}"
puts "Item subject: #{ii.description}."
other_items.find_each do |oii|
puts '=============================================='
puts "Other item: #{oii.description} (#{oii.id})"
puts "Other item object: #{oii.object_type} #{oii.object_id}"
puts "Other item slots: #{oii.object.try(:slots)&.map { |s| "#{s.start_at} - #{s.end_at}" }}"
print "\e[1;34m[ ? ]\e[0m Associate the item with #{oii.object_type} #{oii.object_id} ? (y/N) > "
confirm = $stdin.gets.chomp
ii.update(object_id: oii.object_id, object_type: oii.object_type) if confirm == 'y'
end
ii.reload
if ii.object_id.nil? || ii.object_type.nil?
puts "\n\e[0;31mERROR\e[0m: InvoiceItem(#{ii.id}) was not associated with an object. Please open a rails console " \
"to manually fix the issue using `InvoiceItem.find(#{ii.id}.update(object_id: XXX, object_type: 'XXX')`.\n"
end
end
# chain records
puts 'Chaining all record. This may take a while...'
InvoiceItem.order(:id).all.each(&:chain_record)
Invoice.order(:id).all.each(&:chain_record)
# re-create all archives from the memory dump
Integrity::ArchiveHelper.restore_periods(periods)
end
end
end

View File

@ -12,9 +12,7 @@ namespace :fablab do
desc 'add missing VAT rate to history'
task :add_vat_rate, %i[rate date] => :environment do |_task, args|
unless args.rate && args.date
raise 'Missing argument. Usage exemple: rails fablab:setup:add_vat_rate[20,2014-01-01]. Use 0 to disable'
end
raise 'Missing argument. Usage exemple: rails fablab:setup:add_vat_rate[20,2014-01-01]. Use 0 to disable' unless args.rate && args.date
if args.rate == '0'
setting = Setting.find_by(name: 'invoice_VAT-active')
@ -129,7 +127,7 @@ namespace :fablab do
desc 'generate acconting lines'
task build_accounting_lines: :environment do
start_date = Invoice.order(created_at: :asc).first&.created_at
start_date = Invoice.order(created_at: :asc).first&.created_at || DateTime.current
end_date = DateTime.current
AccountingLine.where(date: start_date..end_date).destroy_all
Accounting::AccountingService.new.build(start_date&.beginning_of_day, end_date.end_of_day)

View File

@ -1,6 +1,6 @@
{
"name": "fab-manager",
"version": "5.6.0",
"version": "5.6.1",
"description": "Fab-manager is the FabLab management solution. It provides a comprehensive, web-based, open-source tool to simplify your administrative tasks and your marker's projects.",
"keywords": [
"fablab",

View File

@ -26,6 +26,7 @@ describe('VatSettingsModal', () => {
expect(screen.getByLabelText(/app.admin.vat_settings_modal.enable_VAT/)).toBeChecked();
});
fireEvent.click(screen.getByRole('button', { name: /app.admin.vat_settings_modal.advanced/, hidden: true }));
expect(screen.getByLabelText(/app.admin.vat_settings_modal.VAT_rate/, { selector: '#invoice_VAT-rate' })).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.vat_settings_modal.VAT_rate_product/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.vat_settings_modal.VAT_rate_event/)).toBeInTheDocument();
expect(screen.getByLabelText(/app.admin.vat_settings_modal.VAT_rate_machine/)).toBeInTheDocument();
@ -44,4 +45,14 @@ describe('VatSettingsModal', () => {
expect(screen.getByRole('heading', { name: /app.admin.setting_history_modal.title/, hidden: true })).toBeInTheDocument();
});
});
test('input 3 decimals rate', async () => {
render(<VatSettingsModal isOpen={true} toggleModal={toggleModal} onError={onError} onSuccess={onSuccess} />);
await waitFor(() => {
expect(screen.getByLabelText(/app.admin.vat_settings_modal.enable_VAT/)).toBeChecked();
});
const input = screen.getByLabelText(/app.admin.vat_settings_modal.VAT_rate/, { selector: '#invoice_VAT-rate' });
fireEvent.change(input, { target: { value: 14.976 } });
expect(input).toHaveValue(14.976);
});
});

View File

@ -7696,9 +7696,9 @@ json-stable-stringify-without-jsonify@^1.0.1:
integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=
json5@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==
version "1.0.2"
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593"
integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==
dependencies:
minimist "^1.2.0"
@ -8059,16 +8059,11 @@ minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatc
dependencies:
brace-expansion "^1.1.7"
minimist@^1.2.0:
minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6:
version "1.2.7"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==
minimist@^1.2.5, minimist@^1.2.6:
version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
mkdirp@^0.5.5:
version "0.5.5"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"