mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-20 14:54:15 +01:00
(feat) rebuild accounting lines on updates
This commit is contained in:
parent
b3072ec444
commit
749f848034
@ -12,10 +12,10 @@ class API::SettingsController < API::ApiController
|
||||
authorize Setting
|
||||
@setting = Setting.find_or_initialize_by(name: params[:name])
|
||||
render status: :not_modified and return if setting_params[:value] == @setting.value
|
||||
render status: :locked, json: { error: I18n.t('settings.locked_setting') } and return unless SettingService.before_update(@setting)
|
||||
render status: :locked, json: { error: I18n.t('settings.locked_setting') } and return unless SettingService.update_allowed?(@setting)
|
||||
|
||||
if @setting.save && @setting.history_values.create(value: setting_params[:value], invoicing_profile: current_user.invoicing_profile)
|
||||
SettingService.after_update(@setting)
|
||||
SettingService.run_after_update([@setting])
|
||||
render status: :ok
|
||||
else
|
||||
render json: @setting.errors.full_messages, status: :unprocessable_entity
|
||||
@ -31,17 +31,17 @@ class API::SettingsController < API::ApiController
|
||||
next if !setting[:name] || !setting[:value]
|
||||
|
||||
db_setting = Setting.find_or_initialize_by(name: setting[:name])
|
||||
if !SettingService.before_update(db_setting)
|
||||
if !SettingService.update_allowed?(db_setting)
|
||||
db_setting.errors.add(:-, "#{I18n.t("settings.#{setting[:name]}")}: #{I18n.t('settings.locked_setting')}")
|
||||
elsif db_setting.save
|
||||
db_setting.history_values.create(value: setting[:value], invoicing_profile: current_user.invoicing_profile)
|
||||
SettingService.after_update(db_setting)
|
||||
end
|
||||
|
||||
@settings.push db_setting
|
||||
may_rollback(params[:transactional]) if db_setting.errors.keys.count.positive?
|
||||
end
|
||||
end
|
||||
SettingService.run_after_update(@settings)
|
||||
end
|
||||
|
||||
def show
|
||||
@ -61,7 +61,7 @@ class API::SettingsController < API::ApiController
|
||||
authorize Setting
|
||||
|
||||
setting = Setting.find_or_create_by(name: params[:name])
|
||||
render status: :locked, json: { error: 'locked setting' } and return unless SettingService.before_update(setting)
|
||||
render status: :locked, json: { error: 'locked setting' } and return unless SettingService.update_allowed?(setting)
|
||||
|
||||
first_val = setting.history_values.order(created_at: :asc).limit(1).first
|
||||
new_val = HistoryValue.create!(
|
||||
@ -69,7 +69,7 @@ class API::SettingsController < API::ApiController
|
||||
value: first_val&.value,
|
||||
invoicing_profile_id: current_user.invoicing_profile.id
|
||||
)
|
||||
SettingService.after_update(setting)
|
||||
SettingService.run_after_update([setting])
|
||||
render json: new_val, status: :ok
|
||||
end
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { ReactNode, useEffect, useState } from 'react';
|
||||
import * as React from 'react';
|
||||
import _ from 'lodash';
|
||||
import { usePrevious } from '../../lib/use-previous';
|
||||
|
||||
type tabId = string|number;
|
||||
|
||||
@ -21,9 +23,12 @@ interface FabTabsProps {
|
||||
*/
|
||||
export const FabTabs: React.FC<FabTabsProps> = ({ tabs, defaultTab, className }) => {
|
||||
const [active, setActive] = useState<Tab>(tabs.filter(Boolean).find(t => t.id === defaultTab) || tabs.filter(Boolean)[0]);
|
||||
const previousTabs = usePrevious<Tab[]>(tabs);
|
||||
|
||||
useEffect(() => {
|
||||
setActive(tabs.filter(Boolean).find(t => t.id === defaultTab) || tabs.filter(Boolean)[0]);
|
||||
if (!_.isEqual(previousTabs?.filter(Boolean).map(t => t.id), tabs?.filter(Boolean).map(t => t?.id))) {
|
||||
setActive(tabs.filter(Boolean).find(t => t.id === defaultTab) || tabs.filter(Boolean)[0]);
|
||||
}
|
||||
}, [tabs]);
|
||||
|
||||
/**
|
||||
|
11
app/frontend/src/javascript/lib/use-previous.ts
Normal file
11
app/frontend/src/javascript/lib/use-previous.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
// provides the previous value of a Prop, in a useEffect hook
|
||||
// Credits to: https://stackoverflow.com/a/57706747/1039377
|
||||
export const usePrevious = <T>(value: T): T | undefined => {
|
||||
const ref = useRef<T>();
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
});
|
||||
return ref.current;
|
||||
};
|
@ -8,4 +8,25 @@ class AdvancedAccounting < ApplicationRecord
|
||||
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
|
||||
belongs_to :product, foreign_type: 'Product', foreign_key: 'accountable_id', inverse_of: :advanced_accounting
|
||||
belongs_to :plan, foreign_type: 'Plan', foreign_key: 'accountable_id', inverse_of: :advanced_accounting
|
||||
|
||||
after_save :rebuild_accounting_lines
|
||||
|
||||
private
|
||||
|
||||
def rebuild_accounting_lines
|
||||
invoices = case accountable_type
|
||||
when 'Machine', 'Training', 'Space', 'Event'
|
||||
accountable.reservations.map(&:invoice_items).flatten.map(&:invoice).uniq
|
||||
when 'Product'
|
||||
accountable.order_items.map(&:order).flatten.map(&:invoice).uniq
|
||||
when 'Plan'
|
||||
accountable.subscriptions.map(&:invoice_items).flatten.map(&:invoice).uniq
|
||||
else
|
||||
raise TypeError "Unknown accountable_type #{accountable_type}"
|
||||
end
|
||||
ids = invoices.map(&:id)
|
||||
AccountingLine.where(invoice_id: ids).destroy_all
|
||||
AccountingWorker.perform_async(:invoices, ids)
|
||||
end
|
||||
end
|
||||
|
44
app/models/concerns/openlab_sync.rb
Normal file
44
app/models/concerns/openlab_sync.rb
Normal file
@ -0,0 +1,44 @@
|
||||
# frozen_string_literal: false
|
||||
|
||||
# module definition
|
||||
module OpenlabSync
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
after_create :openlab_create, if: :openlab_sync_active?
|
||||
run_after_update :openlab_update, if: :openlab_sync_active?
|
||||
after_destroy :openlab_destroy, if: :openlab_sync_active?
|
||||
|
||||
def openlab_create
|
||||
OpenlabWorker.perform_in(2.seconds, :create, id) if published?
|
||||
end
|
||||
|
||||
def openlab_update
|
||||
return unless published?
|
||||
|
||||
if state_was == 'draft'
|
||||
OpenlabWorker.perform_async(:create, id)
|
||||
else
|
||||
OpenlabWorker.perform_async(:update, id)
|
||||
end
|
||||
end
|
||||
|
||||
def openlab_destroy
|
||||
OpenlabWorker.perform_async(:destroy, id)
|
||||
end
|
||||
|
||||
def openlab_attributes
|
||||
OpenLabService.to_hash(self)
|
||||
end
|
||||
|
||||
def openlab_sync_active?
|
||||
self.class.openlab_sync_active?
|
||||
end
|
||||
end
|
||||
|
||||
class_methods do
|
||||
def openlab_sync_active?
|
||||
Setting.get('openlab_app_secret').present?
|
||||
end
|
||||
end
|
||||
end
|
@ -4,6 +4,7 @@
|
||||
class OrderItem < ApplicationRecord
|
||||
belongs_to :order
|
||||
belongs_to :orderable, polymorphic: true
|
||||
belongs_to :product, foreign_type: 'Product', foreign_key: 'orderable_id', inverse_of: :order_items
|
||||
|
||||
validates :orderable, :order_id, :amount, presence: true
|
||||
end
|
||||
|
@ -20,6 +20,8 @@ class Product < ApplicationRecord
|
||||
has_many :product_stock_movements, dependent: :destroy
|
||||
accepts_nested_attributes_for :product_stock_movements, allow_destroy: true, reject_if: :all_blank
|
||||
|
||||
has_many :order_items, as: :orderable, dependent: :nullify
|
||||
|
||||
has_one :advanced_accounting, as: :accountable, dependent: :destroy
|
||||
accepts_nested_attributes_for :advanced_accounting, allow_destroy: true
|
||||
|
||||
|
@ -1,65 +0,0 @@
|
||||
module Project::OpenlabSync
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
include ActionView::Helpers::SanitizeHelper
|
||||
|
||||
after_create :openlab_create, if: :openlab_sync_active?
|
||||
after_update :openlab_update, if: :openlab_sync_active?
|
||||
after_destroy :openlab_destroy, if: :openlab_sync_active?
|
||||
|
||||
def openlab_create
|
||||
OpenlabWorker.perform_in(2.seconds, :create, self.id) if self.published?
|
||||
end
|
||||
|
||||
def openlab_update
|
||||
if self.published?
|
||||
if self.state_was == 'draft'
|
||||
OpenlabWorker.perform_async(:create, self.id)
|
||||
else
|
||||
OpenlabWorker.perform_async(:update, self.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def openlab_destroy
|
||||
OpenlabWorker.perform_async(:destroy, self.id)
|
||||
end
|
||||
|
||||
def openlab_attributes
|
||||
{
|
||||
id: id, slug: slug, name: name, description: description, tags: tags,
|
||||
machines: machines.map(&:name),
|
||||
components: components.map(&:name),
|
||||
themes: themes.map(&:name),
|
||||
author: author&.user&.profile&.full_name,
|
||||
collaborators: users.map { |u| u&.profile&.full_name },
|
||||
steps_body: steps_body,
|
||||
image_path: project_image&.attachment&.medium&.url,
|
||||
project_path: "/#!/projects/#{slug}",
|
||||
updated_at: updated_at.to_s(:iso8601),
|
||||
created_at: created_at.to_s(:iso8601),
|
||||
published_at: published_at.to_s(:iso8601)
|
||||
}
|
||||
end
|
||||
|
||||
def steps_body
|
||||
concatenated_steps = project_steps.map { |s| "#{s.title} #{s.description}" }
|
||||
.join(' ').gsub('</p>', ' </p>')
|
||||
.gsub("\r\n", ' ').gsub("\n\r", ' ')
|
||||
.gsub("\n", ' ').gsub("\r", ' ').gsub("\t", ' ')
|
||||
|
||||
strip_tags(concatenated_steps).strip
|
||||
end
|
||||
|
||||
def openlab_sync_active?
|
||||
self.class.openlab_sync_active?
|
||||
end
|
||||
end
|
||||
|
||||
class_methods do
|
||||
def openlab_sync_active?
|
||||
Setting.get('openlab_app_secret').present?
|
||||
end
|
||||
end
|
||||
end
|
@ -43,7 +43,7 @@ class Accounting::AccountingCodeService
|
||||
raise ArgumentError('invalid section') unless %i[code analytical_section].include?(section)
|
||||
|
||||
if type == :code
|
||||
item_code = Setting.get('advanced_accounting') ? invoice_item.object.reservable.advanced_accounting.send(section) : nil
|
||||
item_code = Setting.get('advanced_accounting') ? invoice_item.object.reservable.advanced_accounting&.send(section) : nil
|
||||
return Setting.get("accounting_#{invoice_item.object.reservable_type}_code") if item_code.nil? && section == :code
|
||||
|
||||
item_code
|
||||
|
@ -12,11 +12,16 @@ class Accounting::AccountingService
|
||||
@journal_code = Setting.get('accounting_journal_code') || ''
|
||||
end
|
||||
|
||||
# build accounting lines for invoices between the provided dates
|
||||
def build(start_date, end_date)
|
||||
# build accounting lines
|
||||
lines = []
|
||||
invoices = Invoice.where('created_at >= ? AND created_at <= ?', start_date, end_date).order('created_at ASC')
|
||||
invoices.each do |i|
|
||||
build_from_invoices(invoices)
|
||||
end
|
||||
|
||||
# build accounting lines for the provided invoices
|
||||
def build_from_invoices(invoices)
|
||||
lines = []
|
||||
invoices.find_each do |i|
|
||||
Rails.logger.debug { "processing invoice #{i.id}..." } unless Rails.env.test?
|
||||
lines << generate_lines(i)
|
||||
end
|
||||
|
38
app/services/open_lab_service.rb
Normal file
38
app/services/open_lab_service.rb
Normal file
@ -0,0 +1,38 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Provides methods to sync projects on OpenLab
|
||||
class OpenLabService
|
||||
class << self
|
||||
include ActionView::Helpers::SanitizeHelper
|
||||
|
||||
def to_hash(project)
|
||||
{
|
||||
id: project.id,
|
||||
slug: project.slug,
|
||||
name: project.name,
|
||||
description: project.description,
|
||||
tags: project.tags,
|
||||
machines: project.machines.map(&:name),
|
||||
components: project.components.map(&:name),
|
||||
themes: project.themes.map(&:name),
|
||||
author: project.author&.user&.profile&.full_name,
|
||||
collaborators: project.users.map { |u| u&.profile&.full_name },
|
||||
steps_body: steps_body(project),
|
||||
image_path: project.project_image&.attachment&.medium&.url,
|
||||
project_path: "/#!/projects/#{project.slug}",
|
||||
updated_at: project.updated_at.to_s(:iso8601),
|
||||
created_at: project.created_at.to_s(:iso8601),
|
||||
published_at: project.published_at.to_s(:iso8601)
|
||||
}
|
||||
end
|
||||
|
||||
def steps_body(project)
|
||||
concatenated_steps = project.project_steps.map { |s| "#{s.title} #{s.description}" }
|
||||
.join(' ').gsub('</p>', ' </p>')
|
||||
.gsub("\r\n", ' ').gsub("\n\r", ' ')
|
||||
.gsub("\n", ' ').gsub("\r", ' ').gsub("\t", ' ')
|
||||
|
||||
strip_tags(concatenated_steps).strip
|
||||
end
|
||||
end
|
||||
end
|
@ -5,72 +5,80 @@
|
||||
# so this service provides a wrapper around these operations.
|
||||
class SettingService
|
||||
class << self
|
||||
def before_update(setting)
|
||||
def update_allowed?(setting)
|
||||
return false if Rails.application.secrets.locked_settings.include? setting.name
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def after_update(setting)
|
||||
update_theme_stylesheet(setting)
|
||||
update_home_stylesheet(setting)
|
||||
notify_privacy_update(setting)
|
||||
sync_stripe_objects(setting)
|
||||
build_stats(setting)
|
||||
export_projects_to_openlab(setting)
|
||||
validate_admins(setting)
|
||||
def run_after_update(settings)
|
||||
update_theme_stylesheet(settings)
|
||||
update_home_stylesheet(settings)
|
||||
notify_privacy_update(settings)
|
||||
sync_stripe_objects(settings)
|
||||
build_stats(settings)
|
||||
export_projects_to_openlab(settings)
|
||||
validate_admins(settings)
|
||||
update_accounting_line(settings)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# rebuild the theme stylesheet
|
||||
def update_theme_stylesheet(setting)
|
||||
return unless %w[main_color secondary_color].include? setting.name
|
||||
def update_theme_stylesheet(settings)
|
||||
return unless (%w[main_color secondary_color] & settings.map(&:name)).count.positive?
|
||||
|
||||
Stylesheet.theme&.rebuild!
|
||||
end
|
||||
|
||||
# rebuild the home page stylesheet
|
||||
def update_home_stylesheet(setting)
|
||||
return unless setting.name == 'home_css'
|
||||
def update_home_stylesheet(settings)
|
||||
return unless settings.any? { |s| s.name == 'home_css' }
|
||||
|
||||
Stylesheet.home_page&.rebuild!
|
||||
end
|
||||
|
||||
# notify about a change in privacy policy
|
||||
def notify_privacy_update(setting)
|
||||
return unless setting.name == 'privacy_body'
|
||||
def notify_privacy_update(settings)
|
||||
return unless settings.any? { |s| s.name == 'privacy_body' }
|
||||
|
||||
NotifyPrivacyUpdateWorker.perform_async(setting.id)
|
||||
end
|
||||
|
||||
# sync all objects on stripe
|
||||
def sync_stripe_objects(setting)
|
||||
return unless %w[stripe_secret_key online_payment_module].include?(setting.name)
|
||||
def sync_stripe_objects(settings)
|
||||
return unless (%w[stripe_secret_key online_payment_module] & settings.map(&:name)).count.positive?
|
||||
|
||||
SyncObjectsOnStripeWorker.perform_async(setting.history_values.last&.invoicing_profile&.user&.id)
|
||||
end
|
||||
|
||||
# generate the statistics since the last update
|
||||
def build_stats(setting)
|
||||
return unless setting.name == 'statistics_module' && setting.value == 'true'
|
||||
def build_stats(settings)
|
||||
return unless settings.any? { |s| s.name == 'statistics_module' && s.value == 'true' }
|
||||
|
||||
PeriodStatisticsWorker.perform_async(setting.previous_update)
|
||||
end
|
||||
|
||||
# export projects to openlab
|
||||
def export_projects_to_openlab(setting)
|
||||
return unless %w[openlab_app_id openlab_app_secret].include?(setting.name) &&
|
||||
def export_projects_to_openlab(settings)
|
||||
return unless (%w[openlab_app_id openlab_app_secret] & settings.map(&:name)).count.positive? &&
|
||||
Setting.get('openlab_app_id').present? && Setting.get('openlab_app_secret').present?
|
||||
|
||||
Project.all.each(&:openlab_create)
|
||||
end
|
||||
|
||||
# automatically validate the admins
|
||||
def validate_admins(setting)
|
||||
return unless setting.name == 'user_validation_required' && setting.value == 'true'
|
||||
def validate_admins(settings)
|
||||
return unless settings.any? { |s| s.name == 'user_validation_required' && s.value == 'true' }
|
||||
|
||||
User.admins.each { |admin| admin.update(validated_at: DateTime.current) if admin.validated_at.nil? }
|
||||
end
|
||||
|
||||
def update_accounting_line(settings)
|
||||
return unless settings.any? { |s| s.name.match(/^accounting_/) || s.name == 'advanced_accounting' }
|
||||
|
||||
AccountingLine.destroy_all
|
||||
AccountingWorker.perform_async(:all)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -4,8 +4,23 @@
|
||||
class AccountingWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
def perform
|
||||
def perform(action = :today, *params)
|
||||
send(action, *params)
|
||||
end
|
||||
|
||||
def today
|
||||
service = Accounting::AccountingService.new
|
||||
service.build(DateTime.current.beginning_of_day, DateTime.current.end_of_day)
|
||||
end
|
||||
|
||||
def invoices(invoices_ids)
|
||||
service = Accounting::AccountingService.new
|
||||
invoices = Invoice.where(id: invoices_ids)
|
||||
service.build_from_invoices(invoices)
|
||||
end
|
||||
|
||||
def all
|
||||
service = Accounting::AccountingService.new
|
||||
service.build_from_invoices(Invoice.all)
|
||||
end
|
||||
end
|
||||
|
Loading…
x
Reference in New Issue
Block a user