1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-30 19:52:20 +01:00

(feat) advanced accounting parameters

for: machines, spaces, trainings, events
This commit is contained in:
Sylvain 2022-11-10 16:14:49 +01:00
parent 3d796549f2
commit 7868f31a58
40 changed files with 451 additions and 272 deletions

View File

@ -10,9 +10,9 @@ class API::EventsController < API::ApiController
@scope = params[:scope]
# filters
@events = @events.joins(:category).where('categories.id = :category', category: params[:category_id]) if params[:category_id]
@events = @events.joins(:event_themes).where('event_themes.id = :theme', theme: params[:theme_id]) if params[:theme_id]
@events = @events.where('age_range_id = :age_range', age_range: params[:age_range_id]) if params[:age_range_id]
@events = @events.joins(:category).where(categories: { id: params[:category_id] }) if params[:category_id]
@events = @events.joins(:event_themes).where(event_themes: { id: params[:theme_id] }) if params[:theme_id]
@events = @events.where(age_range_id: params[:age_range_id]) if params[:age_range_id]
if current_user&.admin? || current_user&.manager?
@events = case params[:scope]
@ -65,7 +65,7 @@ class API::EventsController < API::ApiController
def update
authorize Event
res = EventService.update(@event, event_params.permit!, params[:edit_mode])
res = Event::UpdateEventService.update(@event, event_params.permit!, params[:edit_mode])
render json: { action: 'update', total: res[:events].length, updated: res[:events].select { |r| r[:status] }.length, details: res },
status: :ok,
location: @event
@ -97,7 +97,8 @@ class API::EventsController < API::ApiController
event_theme_ids: [],
event_image_attributes: [:attachment],
event_files_attributes: %i[id attachment _destroy],
event_price_categories_attributes: %i[id price_category_id amount _destroy])
event_price_categories_attributes: %i[id price_category_id amount _destroy],
advanced_accounting_attributes: %i[code analytical_section])
EventService.process_params(event_preparams)
end
end

View File

@ -51,6 +51,7 @@ class API::MachinesController < API::ApiController
def machine_params
params.require(:machine).permit(:name, :description, :spec, :disabled, :plan_ids,
plan_ids: [], machine_image_attributes: [:attachment],
machine_files_attributes: %i[id attachment _destroy])
machine_files_attributes: %i[id attachment _destroy],
advanced_accounting_attributes: %i[code analytical_section])
end
end

View File

@ -51,6 +51,7 @@ class API::SpacesController < API::ApiController
def space_params
params.require(:space).permit(:name, :description, :characteristics, :default_places, :disabled,
space_image_attributes: [:attachment],
space_files_attributes: %i[id attachment _destroy])
space_files_attributes: %i[id attachment _destroy],
advanced_accounting_attributes: %i[code analytical_section])
end
end

View File

@ -76,6 +76,7 @@ class API::TrainingsController < API::ApiController
def training_params
params.require(:training)
.permit(:id, :name, :description, :machine_ids, :plan_ids, :nb_total_places, :public_page, :disabled,
training_image_attributes: [:attachment], machine_ids: [], plan_ids: [])
training_image_attributes: [:attachment], machine_ids: [], plan_ids: [],
advanced_accounting_attributes: %i[code analytical_section])
end
end

View File

@ -0,0 +1,39 @@
import React, { useEffect, useState } from 'react';
import SettingAPI from '../../api/setting';
import { UseFormRegister } from 'react-hook-form';
import { FieldValues } from 'react-hook-form/dist/types/fields';
import { FormInput } from '../form/form-input';
import { useTranslation } from 'react-i18next';
interface AdvancedAccountingFormProps<TFieldValues> {
register: UseFormRegister<TFieldValues>,
onError: (message: string) => void
}
/**
* This component is a partial form, to be included in a resource form managed by react-hook-form.
* It will add advanced accounting attributes to the parent form, if they are enabled
*/
export const AdvancedAccountingForm = <TFieldValues extends FieldValues>({ register, onError }: AdvancedAccountingFormProps<TFieldValues>) => {
const [isEnabled, setIsEnabled] = useState<boolean>(false);
const { t } = useTranslation('admin');
useEffect(() => {
SettingAPI.get('advanced_accounting').then(res => setIsEnabled(res.value === 'true')).catch(onError);
}, []);
return (
<div className="advanced-accounting-form">
{isEnabled && <div>
<h4>{t('app.admin.advanced_accounting_form.title')}</h4>
<FormInput register={register}
id="advanced_accounting_attributes.code"
label={t('app.admin.advanced_accounting_form.code')} />
<FormInput register={register}
id="advanced_accounting_attributes.analytical_section"
label={t('app.admin.advanced_accounting_form.analytical_section')} />
</div>}
</div>
);
};

View File

@ -23,6 +23,7 @@ import { Plus, Trash } from 'phosphor-react';
import FormatLib from '../../lib/format';
import EventPriceCategoryAPI from '../../api/event-price-category';
import { UpdateRecurrentModal } from './update-recurrent-modal';
import { AdvancedAccountingForm } from '../accounting/advanced-accounting-form';
declare const Application: IApplication;
@ -290,6 +291,7 @@ export const EventForm: React.FC<EventFormProps> = ({ action, event, onError, on
id="event_files_attributes"
className="event-files" />
</div>
<AdvancedAccountingForm register={register} onError={onError} />
<FabButton type="submit" className="is-info submit-btn">
{t('app.admin.event_form.ACTION_event', { ACTION: action })}
</FabButton>

View File

@ -13,6 +13,7 @@ import { FormRichText } from '../form/form-rich-text';
import { FormSwitch } from '../form/form-switch';
import { FormMultiFileUpload } from '../form/form-multi-file-upload';
import { FabButton } from '../base/fab-button';
import { AdvancedAccountingForm } from '../accounting/advanced-accounting-form';
declare const Application: IApplication;
@ -86,6 +87,7 @@ export const MachineForm: React.FC<MachineFormProps> = ({ action, machine, onErr
id="disabled"
label={t('app.admin.machine_form.disable_machine')}
tooltip={t('app.admin.machine_form.disabled_help')} />
<AdvancedAccountingForm register={register} onError={onError} />
<FabButton type="submit" className="is-info submit-btn">
{t('app.admin.machine_form.ACTION_machine', { ACTION: action })}
</FabButton>

View File

@ -13,6 +13,7 @@ import { FormSwitch } from '../form/form-switch';
import { FormMultiFileUpload } from '../form/form-multi-file-upload';
import { FabButton } from '../base/fab-button';
import { Space } from '../../models/space';
import { AdvancedAccountingForm } from '../accounting/advanced-accounting-form';
declare const Application: IApplication;
@ -91,6 +92,7 @@ export const SpaceForm: React.FC<SpaceFormProps> = ({ action, space, onError, on
id="disabled"
label={t('app.admin.space_form.disable_space')}
tooltip={t('app.admin.space_form.disabled_help')} />
<AdvancedAccountingForm register={register} onError={onError} />
<FabButton type="submit" className="is-info submit-btn">
{t('app.admin.space_form.ACTION_space', { ACTION: action })}
</FabButton>

View File

@ -18,6 +18,7 @@ import { Machine } from '../../models/machine';
import { SelectOption } from '../../models/select';
import SettingAPI from '../../api/setting';
import { Setting } from '../../models/setting';
import { AdvancedAccountingForm } from '../accounting/advanced-accounting-form';
declare const Application: IApplication;
@ -110,6 +111,7 @@ export const TrainingForm: React.FC<TrainingFormProps> = ({ action, training, on
id="disabled"
label={t('app.admin.training_form.disable_training')}
tooltip={t('app.admin.training_form.disabled_help')} />
<AdvancedAccountingForm register={register} onError={onError} />
<FabButton type="submit" className="is-info submit-btn">
{t('app.admin.training_form.ACTION_training', { ACTION: action })}
</FabButton>

View File

@ -17,7 +17,7 @@ export default class FormatLib {
*/
static time = (date: Date|TDateISO|`${THours}:${TMinutes}`): string => {
let tempDate: Date;
const isoTimeMatch = (date as string).match(/^(\d\d):(\d\d)$/);
const isoTimeMatch = (date as string)?.match(/^(\d\d):(\d\d)$/);
if (isoTimeMatch) {
tempDate = new Date();
tempDate.setHours(parseInt(isoTimeMatch[1], 10));

View File

@ -0,0 +1,5 @@
export interface AdvancedAccounting {
id?: number,
code?: string,
analytical_section?: string
}

View File

@ -1,5 +1,6 @@
import { TDateISO, TDateISODate, THours, TMinutes } from '../typings/date-iso';
import { FileType } from './file';
import { AdvancedAccounting } from './advanced-accounting';
export interface EventPriceCategoryAttributes {
id?: number,
@ -61,7 +62,8 @@ export interface Event {
availability_id: number
}>,
recurrence: RecurrenceOption,
recurrence_end_at: Date
recurrence_end_at: Date,
advanced_accounting_attributes?: AdvancedAccounting
}
export interface EventDecoration {

View File

@ -1,5 +1,7 @@
import { Reservation } from './reservation';
import { ApiFilter } from './api';
import { FileType } from './file';
import { AdvancedAccounting } from './advanced-accounting';
export interface MachineIndexFilter extends ApiFilter {
disabled: boolean,
@ -12,12 +14,8 @@ export interface Machine {
spec?: string,
disabled: boolean,
slug: string,
machine_image: string,
machine_files_attributes?: Array<{
id: number,
attachment: string,
attachment_url: string
}>,
machine_image_attributes: FileType,
machine_files_attributes?: Array<FileType>,
trainings?: Array<{
id: number,
name: string,
@ -31,5 +29,6 @@ export interface Machine {
id: number,
name: string,
slug: string,
}>
}>,
advanced_accounting_attributes?: AdvancedAccounting
}

View File

@ -118,7 +118,8 @@ export const accountingSettings = [
'accounting_Space_code',
'accounting_Space_label',
'accounting_Product_code',
'accounting_Product_label'
'accounting_Product_label',
'advanced_accounting'
] as const;
export const modulesSettings = [

View File

@ -1,4 +1,5 @@
import { FileType } from './file';
import { AdvancedAccounting } from './advanced-accounting';
export interface Space {
id: number,
@ -8,5 +9,6 @@ export interface Space {
default_places: number,
disabled: boolean,
space_image_attributes: FileType,
space_file_attributes?: Array<FileType>
space_file_attributes?: Array<FileType>,
advanced_accounting_attributes?: AdvancedAccounting
}

View File

@ -1,6 +1,7 @@
import { ApiFilter } from './api';
import { TDateISO } from '../typings/date-iso';
import { FileType } from './file';
import { AdvancedAccounting } from './advanced-accounting';
export interface Training {
id?: number,
@ -22,7 +23,8 @@ export interface Training {
full_name: string,
is_valid: boolean
}>
}>
}>,
advanced_accounting_attributes?: AdvancedAccounting
}
export interface TrainingIndexFilter extends ApiFilter {

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
# AdvancedAccounting enables the various objects to have detailed accounting settings
class AdvancedAccounting < ApplicationRecord
belongs_to :accountable, polymorphic: true
belongs_to :machine, foreign_type: 'Machine', foreign_key: 'accountable_id', inverse_of: :advanced_accounting
belongs_to :training, foreign_type: 'Training', foreign_key: 'accountable_id', inverse_of: :advanced_accounting
belongs_to :space, foreign_type: 'Space', foreign_key: 'accountable_id', inverse_of: :advanced_accounting
belongs_to :event, foreign_type: 'event', foreign_key: 'accountable_id', inverse_of: :advanced_accounting
end

View File

@ -7,12 +7,17 @@ class Event < ApplicationRecord
has_one :event_image, as: :viewable, dependent: :destroy
accepts_nested_attributes_for :event_image, allow_destroy: true
has_many :event_files, as: :viewable, dependent: :destroy
accepts_nested_attributes_for :event_files, allow_destroy: true, reject_if: :all_blank
belongs_to :category
validates :category, presence: true
has_many :reservations, as: :reservable, dependent: :destroy
has_and_belongs_to_many :event_themes, join_table: 'events_event_themes', dependent: :destroy
has_many :events_event_themes, dependent: :destroy
has_many :event_themes, through: :events_event_themes
has_many :event_price_categories, dependent: :destroy
has_many :price_categories, through: :event_price_categories
@ -23,12 +28,16 @@ class Event < ApplicationRecord
belongs_to :availability, dependent: :destroy
accepts_nested_attributes_for :availability
has_one :advanced_accounting, as: :accountable, dependent: :destroy
accepts_nested_attributes_for :advanced_accounting, allow_destroy: true
attr_accessor :recurrence, :recurrence_end_at
before_save :update_nb_free_places
after_create :event_recurrence
# update event updated_at for index cache
after_save -> { touch }
after_save -> { touch } # rubocop:disable Rails/SkipsModelValidations
def name
title
@ -71,10 +80,6 @@ class Event < ApplicationRecord
end
end
# def reservations
# Reservation.where(reservable: self)
# end
def update_nb_free_places
if nb_total_places.nil?
self.nb_free_places = nil
@ -97,8 +102,6 @@ class Event < ApplicationRecord
return unless recurrence.present? && recurrence != 'none'
on = case recurrence
when 'day'
nil
when 'week'
availability.start_at.wday
when 'month'
@ -108,58 +111,11 @@ class Event < ApplicationRecord
else
nil
end
r = Recurrence.new(every: recurrence, on: on, starts: availability.start_at + 1.day, until: recurrence_end_at)
service = Availabilities::CreateAvailabilitiesService.new
r.events.each do |date|
days_diff = availability.end_at.day - availability.start_at.day
start_at = DateTime.new(
date.year,
date.month,
date.day,
availability.start_at.hour,
availability.start_at.min,
availability.start_at.sec,
availability.start_at.zone
)
start_at = dst_correction(availability.start_at, start_at)
end_date = date + days_diff.days
end_at = DateTime.new(
end_date.year,
end_date.month,
end_date.day,
availability.end_at.hour,
availability.end_at.min,
availability.end_at.sec,
availability.end_at.zone
)
end_at = dst_correction(availability.start_at, end_at)
ei = EventImage.new(attachment: event_image.attachment) if event_image
efs = event_files.map do |f|
EventFile.new(attachment: f.attachment)
end
event_price_cats = []
event_price_categories.each do |epc|
event_price_cats.push(EventPriceCategory.new(price_category_id: epc.price_category_id, amount: epc.amount))
end
event = Event.new(
recurrence: 'none',
title: title,
description: description,
event_image: ei,
event_files: efs,
availability: Availability.new(start_at: start_at, end_at: end_at, available_type: 'event'),
availability_id: nil,
category_id: category_id,
age_range_id: age_range_id,
event_themes: event_themes,
amount: amount,
event_price_categories: event_price_cats,
nb_total_places: nb_total_places,
recurrence_id: id
)
event.save
service.create_slots(event.availability)
Event::CreateEventService.create_occurence(event, date)
end
update_columns(recurrence_id: id)
update(recurrence_id: id)
end
end

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
# EventsEventTheme is the relation table between an Event and an EventTheme
# => theme associated with an Event
class EventsEventTheme < ApplicationRecord
belongs_to :event
belongs_to :event_theme
end

View File

@ -34,6 +34,9 @@ class Machine < ApplicationRecord
has_many :machines_products, dependent: :destroy
has_many :products, through: :machines_products
has_one :advanced_accounting, as: :accountable, dependent: :destroy
accepts_nested_attributes_for :advanced_accounting, allow_destroy: true
after_create :create_statistic_subtype
after_create :create_machine_prices
after_create :update_gateway_product
@ -50,21 +53,19 @@ class Machine < ApplicationRecord
end
def create_statistic_subtype
index = StatisticIndex.where(es_type_key: 'machine')
StatisticSubType.create!(statistic_types: index.first.statistic_types, key: slug, label: name)
index = StatisticIndex.find_by(es_type_key: 'machine')
StatisticSubType.create!(statistic_types: index.statistic_types, key: slug, label: name)
end
def update_statistic_subtype
index = StatisticIndex.where(es_type_key: 'machine')
index = StatisticIndex.find_by(es_type_key: 'machine')
subtype = StatisticSubType.joins(statistic_type_sub_types: :statistic_type)
.where(key: slug, statistic_types: { statistic_index_id: index.first.id })
.first
subtype.label = name
subtype.save!
.find_by(key: slug, statistic_types: { statistic_index_id: index.id })
subtype.update(label: name)
end
def remove_statistic_subtype
subtype = StatisticSubType.where(key: slug).first
subtype = StatisticSubType.find_by(key: slug)
subtype.destroy!
end

View File

@ -5,17 +5,17 @@ require 'payment/item_builder'
# A link between an object in the local database and another object in the remote payment gateway database
class PaymentGatewayObject < ApplicationRecord
belongs_to :item, polymorphic: true
belongs_to :invoice, foreign_type: 'Invoice', foreign_key: 'item_id'
belongs_to :invoice_item, foreign_type: 'InvoiceItem', foreign_key: 'item_id'
belongs_to :subscription, foreign_type: 'Subscription', foreign_key: 'item_id'
belongs_to :payment_schedule, foreign_type: 'PaymentSchedule', foreign_key: 'item_id'
belongs_to :payment_schedule_item, foreign_type: 'PaymentScheduleItem', foreign_key: 'item_id'
belongs_to :user, foreign_type: 'User', foreign_key: 'item_id'
belongs_to :plan, foreign_type: 'Plan', foreign_key: 'item_id'
belongs_to :machine, foreign_type: 'Machine', foreign_key: 'item_id'
belongs_to :space, foreign_type: 'Space', foreign_key: 'item_id'
belongs_to :training, foreign_type: 'Training', foreign_key: 'item_id'
belongs_to :order, foreign_type: 'Order', foreign_key: 'item_id'
belongs_to :invoice, foreign_type: 'Invoice', foreign_key: 'item_id', inverse_of: :payment_gateway_object
belongs_to :invoice_item, foreign_type: 'InvoiceItem', foreign_key: 'item_id', inverse_of: :payment_gateway_object
belongs_to :subscription, foreign_type: 'Subscription', foreign_key: 'item_id', inverse_of: :payment_gateway_object
belongs_to :payment_schedule, foreign_type: 'PaymentSchedule', foreign_key: 'item_id', inverse_of: :payment_gateway_object
belongs_to :payment_schedule_item, foreign_type: 'PaymentScheduleItem', foreign_key: 'item_id', inverse_of: :payment_gateway_object
belongs_to :user, foreign_type: 'User', foreign_key: 'item_id', inverse_of: :payment_gateway_object
belongs_to :plan, foreign_type: 'Plan', foreign_key: 'item_id', inverse_of: :payment_gateway_object
belongs_to :machine, foreign_type: 'Machine', foreign_key: 'item_id', inverse_of: :payment_gateway_object
belongs_to :space, foreign_type: 'Space', foreign_key: 'item_id', inverse_of: :payment_gateway_object
belongs_to :training, foreign_type: 'Training', foreign_key: 'item_id', inverse_of: :payment_gateway_object
belongs_to :order, foreign_type: 'Order', foreign_key: 'item_id', inverse_of: :payment_gateway_object
belongs_to :payment_gateway_object # some objects may require a reference to another object for remote recovery

View File

@ -11,9 +11,9 @@ class PaymentSchedule < PaymentDocument
belongs_to :statistic_profile
belongs_to :operator_profile, class_name: 'InvoicingProfile'
has_many :payment_schedule_items
has_many :payment_gateway_objects, as: :item
has_many :payment_schedule_objects
has_many :payment_schedule_items, dependent: :destroy
has_many :payment_gateway_objects, as: :item, dependent: :destroy
has_many :payment_schedule_objects, dependent: :destroy
before_create :add_environment
after_create :update_reference, :chain_record

View File

@ -3,11 +3,10 @@
# Links an object bought and a payment schedule used to pay this object
class PaymentScheduleObject < Footprintable
belongs_to :object, polymorphic: true
belongs_to :reservation, foreign_type: 'Reservation', foreign_key: 'object_id'
belongs_to :subscription, foreign_type: 'Subscription', foreign_key: 'object_id'
belongs_to :wallet_transaction, foreign_type: 'WalletTransaction', foreign_key: 'object_id'
belongs_to :offer_day, foreign_type: 'OfferDay', foreign_key: 'object_id'
belongs_to :statistic_profile_prepaid_pack, foreign_type: 'StatisticProfilePrepaidPack', foreign_key: 'object_id'
belongs_to :reservation, foreign_type: 'Reservation', foreign_key: 'object_id', inverse_of: :payment_schedule_object
belongs_to :subscription, foreign_type: 'Subscription', foreign_key: 'object_id', inverse_of: :payment_schedule_object
belongs_to :statistic_profile_prepaid_pack, foreign_type: 'StatisticProfilePrepaidPack', foreign_key: 'object_id',
inverse_of: :payment_schedule_object
belongs_to :payment_schedule
after_create :chain_record

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
# ProjectsSpace is the relation table between a Project and a Space
# => spaces used in a project
class ProjectsSpace < ApplicationRecord
belongs_to :space
belongs_to :project
end

View File

@ -156,7 +156,8 @@ class Setting < ApplicationRecord
show_username_in_admin_list
store_module
store_withdrawal_instructions
store_hidden] }
store_hidden
advanced_accounting] }
# WARNING: when adding a new key, you may also want to add it in:
# - config/locales/en.yml#settings
# - app/frontend/src/javascript/models/setting.ts#SettingName

View File

@ -14,9 +14,10 @@ class Space < ApplicationRecord
has_many :space_files, as: :viewable, dependent: :destroy
accepts_nested_attributes_for :space_files, allow_destroy: true, reject_if: :all_blank
has_and_belongs_to_many :projects, join_table: :projects_spaces
has_many :projects_spaces, dependent: :destroy
has_many :projects, through: :projects_spaces
has_many :spaces_availabilities
has_many :spaces_availabilities, dependent: :destroy
has_many :availabilities, through: :spaces_availabilities, dependent: :destroy
has_many :reservations, as: :reservable, dependent: :destroy
@ -25,7 +26,10 @@ class Space < ApplicationRecord
has_many :prepaid_packs, as: :priceable, dependent: :destroy
has_many :credits, as: :creditable, dependent: :destroy
has_one :payment_gateway_object, as: :item
has_one :payment_gateway_object, as: :item, dependent: :destroy
has_one :advanced_accounting, as: :accountable, dependent: :destroy
accepts_nested_attributes_for :advanced_accounting, allow_destroy: true
after_create :create_statistic_subtype
after_create :create_space_prices
@ -34,30 +38,29 @@ class Space < ApplicationRecord
after_update :update_statistic_subtype, if: :saved_change_to_name?
after_destroy :remove_statistic_subtype
def create_statistic_subtype
index = StatisticIndex.find_by(es_type_key: 'space')
StatisticSubType.create!({statistic_types: index.statistic_types, key: self.slug, label: self.name})
StatisticSubType.create!({ statistic_types: index.statistic_types, key: slug, label: name })
end
def update_statistic_subtype
index = StatisticIndex.find_by(es_type_key: 'space')
subtype = StatisticSubType.joins(statistic_type_sub_types: :statistic_type).find_by(key: self.slug, statistic_types: { statistic_index_id: index.id })
subtype.label = self.name
subtype.save!
subtype = StatisticSubType.joins(statistic_type_sub_types: :statistic_type)
.find_by(key: slug, statistic_types: { statistic_index_id: index.id })
subtype.update(label: name)
end
def remove_statistic_subtype
subtype = StatisticSubType.find_by(key: self.slug)
subtype = StatisticSubType.find_by(key: slug)
subtype.destroy!
end
def create_space_prices
Group.all.each do |group|
Group.find_each do |group|
Price.create(priceable: self, group: group, amount: 0)
end
Plan.all.includes(:group).each do |plan|
Plan.includes(:group).find_each do |plan|
Price.create(group: plan.group, plan: plan, priceable: self, amount: 0)
end
end

View File

@ -10,9 +10,10 @@ class Training < ApplicationRecord
has_one :training_image, as: :viewable, dependent: :destroy
accepts_nested_attributes_for :training_image, allow_destroy: true
has_and_belongs_to_many :machines, join_table: 'trainings_machines'
has_many :trainings_machines, dependent: :destroy
has_many :machines, through: :trainings_machines
has_many :trainings_availabilities
has_many :trainings_availabilities, dependent: :destroy
has_many :availabilities, through: :trainings_availabilities, dependent: :destroy
has_many :reservations, as: :reservable, dependent: :destroy
@ -26,7 +27,10 @@ class Training < ApplicationRecord
has_many :credits, as: :creditable, dependent: :destroy
has_many :plans, through: :credits
has_one :payment_gateway_object, as: :item
has_one :payment_gateway_object, as: :item, dependent: :destroy
has_one :advanced_accounting, as: :accountable, dependent: :destroy
accepts_nested_attributes_for :advanced_accounting, allow_destroy: true
after_create :create_statistic_subtype
after_create :create_trainings_pricings
@ -40,20 +44,19 @@ class Training < ApplicationRecord
end
def create_statistic_subtype
index = StatisticIndex.where(es_type_key: 'training')
StatisticSubType.create!(statistic_types: index.first.statistic_types, key: slug, label: name)
index = StatisticIndex.find_by(es_type_key: 'training')
StatisticSubType.create!(statistic_types: index.statistic_types, key: slug, label: name)
end
def update_statistic_subtype
index = StatisticIndex.where(es_type_key: 'training')
subtype = StatisticSubType.joins(statistic_type_sub_types: :statistic_type)
.where(key: slug, statistic_types: { statistic_index_id: index.first.id }).first
subtype.label = name
subtype.save!
.find_by(key: slug, statistic_types: { statistic_index_id: index.id })
subtype.update(label: name)
end
def remove_statistic_subtype
subtype = StatisticSubType.where(key: slug).first
subtype = StatisticSubType.find_by(key: slug)
subtype.destroy!
end
@ -64,7 +67,7 @@ class Training < ApplicationRecord
private
def create_trainings_pricings
Group.all.each do |group|
Group.find_each do |group|
TrainingsPricing.create(training: self, group: group, amount: 0)
end
end

View File

@ -0,0 +1,64 @@
# frozen_string_literal: true
# Provides helper methods to create new Events and its recurring occurrences
class Event::CreateEventService
class << self
def create_occurence(event, date)
service = Availabilities::CreateAvailabilitiesService.new
occurrence = Event.new(
recurrence: 'none',
title: title,
description: event.description,
event_image: occurrence_image(event),
event_files: occurrence_files(event),
availability: Availability.new(start_at: occurence_start_date(event, date),
end_at: occurrence_end_date(event, date),
available_type: 'event'),
availability_id: nil,
category_id: event.category_id,
age_range_id: event.age_range_id,
event_themes: event.event_themes,
amount: event.amount,
event_price_categories: occurrence_price_categories(event),
nb_total_places: event.nb_total_places,
recurrence_id: event.id
)
occurrence.save
service.create_slots(occurrence.availability)
end
private
def occurence_start_date(event, date)
start_at = DateTime.new(date.year, date.month, date.day,
event.availability.start_at.hour, event.availability.start_at.min, event.availability.start_at.sec,
event.availability.start_at.zone)
dst_correction(event.availability.start_at, start_at)
end
def occurrence_end_date(event, date)
days_diff = event.availability.end_at.day - event.availability.start_at.day
end_date = date + days_diff.days
end_at = DateTime.new(end_date.year, end_date.month, end_date.day,
event.availability.end_at.hour, event.availability.end_at.min, event.availability.end_at.sec,
event.availability.end_at.zone)
dst_correction(event.availability.start_at, end_at)
end
def occurrence_image(event)
EventImage.new(attachment: event.event_image.attachment) if event.event_image
end
def occurrence_files(event)
event.event_files.map do |f|
EventFile.new(attachment: f.attachment)
end
end
def occurrence_price_categories(event)
event.event_price_categories.map do |epc|
EventPriceCategory.new(price_category_id: epc.price_category_id, amount: epc.amount)
end
end
end
end

View File

@ -0,0 +1,141 @@
# frozen_string_literal: true
# Provides helper methods to update existing Events and their recurring occurrences
class Event::UpdateEventService
class << self
# update one or more events (if periodic)
def update(event, event_params, mode = 'single')
events = case mode
when 'single'
[event]
when 'next'
Event.includes(:availability, :event_price_categories, :event_files)
.where(
'availabilities.start_at >= ? AND events.recurrence_id = ?',
event.availability.start_at,
event.recurrence_id
)
.references(:availabilities, :events)
when 'all'
Event.includes(:availability, :event_price_categories, :event_files)
.where(recurrence_id: event.recurrence_id)
else
[]
end
update_occurrences(event, events, event_params)
end
private
def update_occurrences(base_event, occurrences, event_params)
results = {
events: [],
slots: []
}
original_slots_ids = base_event.availability.slots.map(&:id)
occurrences.each do |occurrence|
next unless occurrence.id != base_event.id
e_params = occurrence_params(base_event, occurrence, event_params)
begin
results[:events].push status: !!occurrence.update(e_params.permit!), event: occurrence # rubocop:disable Style/DoubleNegation
rescue StandardError => e
results[:events].push status: false, event: occurrence, error: e.try(:record).try(:class).try(:name), message: e.message
end
results[:slots].concat(update_slots(occurrence.availability_id, original_slots_ids))
end
begin
event_params[:availability_attributes][:id] = base_event.availability_id
results[:events].push status: !!base_event.update(event_params), event: base_event # rubocop:disable Style/DoubleNegation
rescue StandardError => e
results[:events].push status: false, event: base_event, error: e.try(:record).try(:class).try(:name), message: e.message
end
results[:slots].concat(update_slots(base_event.availability_id, original_slots_ids))
results
end
def update_slots(availability_id, original_slots_ids)
results = []
avail = Availability.find(availability_id)
Slot.where(id: original_slots_ids).each do |slot|
results.push(
status: !!slot.update(availability_id: availability_id, start_at: avail.start_at, end_at: avail.end_at), # rubocop:disable Style/DoubleNegation
slot: slot
)
rescue StandardError => e
results.push status: false, slot: s, error: e.try(:record).try(:class).try(:name), message: e.message
end
results
end
def occurrence_params(base_event, occurrence, event_params)
start_at = event_params['availability_attributes']['start_at']
end_at = event_params['availability_attributes']['end_at']
e_params = event_params.merge(
availability_id: occurrence.availability_id,
availability_attributes: {
id: occurrence.availability_id,
start_at: occurrence.availability.start_at.change(hour: start_at.hour, min: start_at.min),
end_at: occurrence.availability.end_at.change(hour: end_at.hour, min: end_at.min),
available_type: occurrence.availability.available_type
}
)
epc_attributes = price_categories_attributes(base_event, occurrence, event_params)
unless epc_attributes.empty?
e_params = e_params.merge(
event_price_categories_attributes: epc_attributes
)
end
ef_attributes = file_attributes(base_event, occurrence, event_params)
e_params.merge(
event_files_attributes: ef_attributes
)
end
def price_categories_attributes(base_event, occurrence, event_params)
epc_attributes = []
event_params['event_price_categories_attributes']&.each do |epca|
epc = occurrence.event_price_categories.find_by(price_category_id: epca['price_category_id'])
if epc
epc_attributes.push(
id: epc.id,
price_category_id: epc.price_category_id,
amount: epca['amount'],
_destroy: epca['_destroy']
)
elsif epca['id'].present?
event_price = base_event.event_price_categories.find(epca['id'])
epc_attributes.push(
price_category_id: epca['price_category_id'],
amount: event_price.amount,
_destroy: ''
)
end
end
epc_attributes
end
def file_attributes(base_event, occurrence, event_params)
ef_attributes = []
event_params['event_files_attributes']&.each do |efa|
if efa['id'].present?
event_file = base_event.event_files.find(efa['id'])
ef = occurrence.event_files.find_by(attachment: event_file.attachment.file.filename)
if ef
ef_attributes.push(
id: ef.id,
attachment: efa['attachment'],
_destroy: efa['_destroy']
)
end
else
ef_attributes.push(efa)
end
end
ef_attributes
end
end
end

View File

@ -75,140 +75,5 @@ class EventService
end
results
end
# update one or more events (if periodic)
def update(event, event_params, mode = 'single')
events = case mode
when 'single'
[event]
when 'next'
Event.includes(:availability, :event_price_categories, :event_files)
.where(
'availabilities.start_at >= ? AND events.recurrence_id = ?',
event.availability.start_at,
event.recurrence_id
)
.references(:availabilities, :events)
when 'all'
Event.includes(:availability, :event_price_categories, :event_files)
.where(recurrence_id: event.recurrence_id)
else
[]
end
update_occurrences(event, events, event_params)
end
private
def update_occurrences(base_event, occurrences, event_params)
results = {
events: [],
slots: []
}
original_slots_ids = base_event.availability.slots.map(&:id)
occurrences.each do |occurrence|
next unless occurrence.id != base_event.id
e_params = occurrence_params(base_event, occurrence, event_params)
begin
results[:events].push status: !!occurrence.update(e_params.permit!), event: occurrence # rubocop:disable Style/DoubleNegation
rescue StandardError => e
results[:events].push status: false, event: occurrence, error: e.try(:record).try(:class).try(:name), message: e.message
end
results[:slots].concat(update_slots(occurrence.availability_id, original_slots_ids))
end
begin
event_params[:availability_attributes][:id] = base_event.availability_id
results[:events].push status: !!base_event.update(event_params), event: base_event # rubocop:disable Style/DoubleNegation
rescue StandardError => e
results[:events].push status: false, event: base_event, error: e.try(:record).try(:class).try(:name), message: e.message
end
results[:slots].concat(update_slots(base_event.availability_id, original_slots_ids))
results
end
def update_slots(availability_id, original_slots_ids)
results = []
avail = Availability.find(availability_id)
Slot.where(id: original_slots_ids).each do |slot|
results.push(
status: !!slot.update(availability_id: availability_id, start_at: avail.start_at, end_at: avail.end_at), # rubocop:disable Style/DoubleNegation
slot: slot
)
rescue StandardError => e
results.push status: false, slot: s, error: e.try(:record).try(:class).try(:name), message: e.message
end
results
end
def occurrence_params(base_event, occurrence, event_params)
start_at = event_params['availability_attributes']['start_at']
end_at = event_params['availability_attributes']['end_at']
e_params = event_params.merge(
availability_id: occurrence.availability_id,
availability_attributes: {
id: occurrence.availability_id,
start_at: occurrence.availability.start_at.change(hour: start_at.hour, min: start_at.min),
end_at: occurrence.availability.end_at.change(hour: end_at.hour, min: end_at.min),
available_type: occurrence.availability.available_type
}
)
epc_attributes = price_categories_attributes(base_event, occurrence, event_params)
unless epc_attributes.empty?
e_params = e_params.merge(
event_price_categories_attributes: epc_attributes
)
end
ef_attributes = file_attributes(base_event, occurrence, event_params)
e_params.merge(
event_files_attributes: ef_attributes
)
end
def price_categories_attributes(base_event, occurrence, event_params)
epc_attributes = []
event_params['event_price_categories_attributes']&.each do |epca|
epc = occurrence.event_price_categories.find_by(price_category_id: epca['price_category_id'])
if epc
epc_attributes.push(
id: epc.id,
price_category_id: epc.price_category_id,
amount: epca['amount'],
_destroy: epca['_destroy']
)
elsif epca['id'].present?
event_price = base_event.event_price_categories.find(epca['id'])
epc_attributes.push(
price_category_id: epca['price_category_id'],
amount: event_price.amount,
_destroy: ''
)
end
end
epc_attributes
end
def file_attributes(base_event, occurrence, event_params)
ef_attributes = []
event_params['event_files_attributes']&.each do |efa|
if efa['id'].present?
event_file = base_event.event_files.find(efa['id'])
ef = occurrence.event_files.find_by(attachment: event_file.attachment.file.filename)
if ef
ef_attributes.push(
id: ef.id,
attachment: efa['attachment'],
_destroy: efa['_destroy']
)
end
else
ef_attributes.push(efa)
end
end
ef_attributes
end
end
end

View File

@ -0,0 +1,3 @@
# frozen_string_literal: true
json.extract! advanced_accounting, :id, :code, :analytical_section

View File

@ -58,3 +58,8 @@ end
json.nb_total_places event.nb_total_places
json.nb_free_places event.nb_free_places || event.nb_total_places
if event.advanced_accounting
json.advanced_accounting_attributes do
json.partial! 'api/advanced_accounting/advanced_accounting', advanced_accounting: event.advanced_accounting
end
end

View File

@ -9,3 +9,9 @@ if machine.machine_image
json.attachment_url machine.machine_image.attachment.url
end
end
if machine.advanced_accounting
json.advanced_accounting_attributes do
json.partial! 'api/advanced_accounting/advanced_accounting', advanced_accounting: machine.advanced_accounting
end
end

View File

@ -8,3 +8,9 @@ if space.space_image
json.attachment_url space.space_image.attachment.url
end
end
if space.advanced_accounting
json.advanced_accounting_attributes do
json.partial! 'api/advanced_accounting/advanced_accounting', advanced_accounting: space.advanced_accounting
end
end

View File

@ -8,3 +8,9 @@ if training.training_image
json.attachment_url training.training_image.attachment.url
end
end
if training.advanced_accounting
json.advanced_accounting_attributes do
json.partial! 'api/advanced_accounting/advanced_accounting', advanced_accounting: training.advanced_accounting
end
end

View File

@ -87,6 +87,10 @@ en:
edit_all: "All events"
date_wont_change: "Warning: you have changed the event date. This modification won't be propagated to other occurrences of the periodic event."
confirm: "Update the {MODE, select, single{event} other{events}}"
advanced_accounting_form:
title: "Advanced accounting parameters"
code: "Accounting code"
analytical_section: "Analytical section"
#add a new machine
machines_new:
declare_a_new_machine: "Declare a new machine"

View File

@ -622,3 +622,4 @@ en:
store_module: "Store module"
store_withdrawal_instructions: "Withdrawal instructions"
store_hidden: "Store hidden to the public"
advanced_accounting: "Advanced accounting"

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
# Advanced accouting parameters are stored in a dedicated table,
# with a polymorphic relation per object
class CreateAdvancedAccountings < ActiveRecord::Migration[5.2]
def change
create_table :advanced_accountings do |t|
t.string :code
t.string :analytical_section
t.references :accountable, polymorphic: true, index: { name: 'index_advanced_accountings_on_accountable' }
t.timestamps
end
end
end

View File

@ -19,8 +19,8 @@ ActiveRecord::Schema.define(version: 2022_11_22_123605) do
enable_extension "unaccent"
create_table "abuses", id: :serial, force: :cascade do |t|
t.string "signaled_type"
t.integer "signaled_id"
t.string "signaled_type"
t.string "first_name"
t.string "last_name"
t.string "email"
@ -49,12 +49,22 @@ ActiveRecord::Schema.define(version: 2022_11_22_123605) do
t.string "locality"
t.string "country"
t.string "postal_code"
t.string "placeable_type"
t.integer "placeable_id"
t.string "placeable_type"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "advanced_accountings", force: :cascade do |t|
t.string "code"
t.string "analytical_section"
t.string "accountable_type"
t.bigint "accountable_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["accountable_type", "accountable_id"], name: "index_advanced_accountings_on_accountable"
end
create_table "age_ranges", id: :serial, force: :cascade do |t|
t.string "name"
t.datetime "created_at", null: false
@ -64,8 +74,8 @@ ActiveRecord::Schema.define(version: 2022_11_22_123605) do
end
create_table "assets", id: :serial, force: :cascade do |t|
t.string "viewable_type"
t.integer "viewable_id"
t.string "viewable_type"
t.string "attachment"
t.string "type"
t.datetime "created_at"
@ -147,8 +157,8 @@ ActiveRecord::Schema.define(version: 2022_11_22_123605) do
end
create_table "credits", id: :serial, force: :cascade do |t|
t.string "creditable_type"
t.integer "creditable_id"
t.string "creditable_type"
t.integer "plan_id"
t.integer "hours"
t.datetime "created_at"
@ -377,15 +387,15 @@ ActiveRecord::Schema.define(version: 2022_11_22_123605) do
create_table "notifications", id: :serial, force: :cascade do |t|
t.integer "receiver_id"
t.string "attached_object_type"
t.integer "attached_object_id"
t.string "attached_object_type"
t.integer "notification_type_id"
t.boolean "is_read", default: false
t.datetime "created_at"
t.datetime "updated_at"
t.string "receiver_type"
t.boolean "is_send", default: false
t.jsonb "meta_data", default: "{}"
t.jsonb "meta_data", default: {}
t.index ["notification_type_id"], name: "index_notifications_on_notification_type_id"
t.index ["receiver_id"], name: "index_notifications_on_receiver_id"
end
@ -625,8 +635,8 @@ ActiveRecord::Schema.define(version: 2022_11_22_123605) do
create_table "prices", id: :serial, force: :cascade do |t|
t.integer "group_id"
t.integer "plan_id"
t.string "priceable_type"
t.integer "priceable_id"
t.string "priceable_type"
t.integer "amount"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
@ -826,8 +836,8 @@ ActiveRecord::Schema.define(version: 2022_11_22_123605) do
t.text "message"
t.datetime "created_at"
t.datetime "updated_at"
t.string "reservable_type"
t.integer "reservable_id"
t.string "reservable_type"
t.integer "nb_reserve_places"
t.integer "statistic_profile_id"
t.index ["reservable_type", "reservable_id"], name: "index_reservations_on_reservable_type_and_reservable_id"
@ -836,8 +846,8 @@ ActiveRecord::Schema.define(version: 2022_11_22_123605) do
create_table "roles", id: :serial, force: :cascade do |t|
t.string "name"
t.string "resource_type"
t.integer "resource_id"
t.string "resource_type"
t.datetime "created_at"
t.datetime "updated_at"
t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id"
@ -1119,8 +1129,8 @@ ActiveRecord::Schema.define(version: 2022_11_22_123605) do
t.boolean "is_allow_newsletter"
t.inet "current_sign_in_ip"
t.inet "last_sign_in_ip"
t.datetime "validated_at"
t.string "mapped_from_sso"
t.datetime "validated_at"
t.index ["auth_token"], name: "index_users_on_auth_token"
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
t.index ["email"], name: "index_users_on_email", unique: true

View File

@ -993,6 +993,8 @@ Setting.set('store_module', false) unless Setting.find_by(name: 'store_module').
Setting.set('store_hidden', true) unless Setting.find_by(name: 'store_hidden').try(:value)
Setting.set('advanced_accounting', false) unless Setting.find_by(name: 'advanced_accounting').try(:value)
if StatisticCustomAggregation.count.zero?
# available reservations hours for machines
machine_hours = StatisticType.find_by(key: 'hour', statistic_index_id: 2)