mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-20 14:54:15 +01:00
Merge branch 'dev' into wip-demo-update
This commit is contained in:
commit
f15907c405
59
CHANGELOG.md
59
CHANGELOG.md
@ -1,5 +1,30 @@
|
||||
# Changelog Fab-manager
|
||||
|
||||
- Ability to cancel a payement schedule from the interface
|
||||
- Ability to create slots in the past
|
||||
- Ability to disable public account creation
|
||||
- Ability to select "bank transfer" as the payment mean for a payment schedule
|
||||
- When a payment schedule was canceled by the payment gateway, inform the user in the interface
|
||||
- Updated caniuse db
|
||||
- Optimized the load time of the payment schedules list
|
||||
- Fix a bug: do not load Stripe if no keys were defined
|
||||
- [TODO DEPLOY] `rails db:seed`
|
||||
|
||||
# v5.3.0 2021 December 29
|
||||
|
||||
- Ability to configure multiple VAT rates, per kind of invoiced item
|
||||
- Ability to export the collected VAT, by rates, to a CSV file
|
||||
- Refactored the extended prices' frontend code to allow future customization
|
||||
- Fix a bug: the amount label in not correctly shown in the extended prices modal
|
||||
- Fix a bug: `extended_prices_in_same_day` apply the extended prices to each day
|
||||
|
||||
## v5.2.0 2021 December 23
|
||||
|
||||
- Ability to configure prices for spaces, by time slots different from the default hourly rate
|
||||
- Updated portuguese translation
|
||||
- Refactored the ReserveButton component to use the same user's data across all the component
|
||||
- First optimization the load time of the payment schedules list
|
||||
|
||||
## v5.1.13 2021 November 16
|
||||
|
||||
- Fix a bug: unable to run the setup/upgrade scripts as root
|
||||
@ -12,21 +37,21 @@
|
||||
## v5.1.11 2021 October 22
|
||||
|
||||
- Refactored subscription new/renew/free extend interfaces and API
|
||||
- Ability to configure data sources for preventing booking on overlapping slots
|
||||
- Updated production documentation
|
||||
- Updated SSO documentation
|
||||
- Improved stripe subscription process with better error handling
|
||||
- Ability to configure the data sources of the booking prevention on overlapping slots
|
||||
- Updated the production documentation
|
||||
- Updated the SSO documentation
|
||||
- Improved the stripe subscription process with better error handling
|
||||
- The upgrade script will check and report the ability to access the hub API
|
||||
- Fix a bug: canceled training reservation is not marked as this in admin/edit members/trainings
|
||||
- Fix a bug: canceled training reservation is not marked as this in admin > edit members > trainings
|
||||
- Fix a bug: users can set their birthdate in the future
|
||||
- Fix a bug: the upgrade script won't add environment variables that are already present anymore
|
||||
- Fix a bug: the upgrade script won't add anymore the environment variables that are already present
|
||||
- Fix a bug: admin cannot take or renew a subscription for a member from member/edit interface
|
||||
- Fix a bug: missing translations
|
||||
- Fix a bug: the upgrade script report an invalid version to upgrade to
|
||||
- Fix a bug: invalid amount provided to the PayZen payment gateway when using a currency with anything else than 2 decimals
|
||||
- Fix a bug: invalid amount provided to the PayZen payment gateway, when using a 0-decimal or a 3-decimal currency
|
||||
- Fix a bug: incorrect behavior for the setting "email confirmation required"
|
||||
- Fix a bug: invalid text shown when a member confirms a free cart
|
||||
- Fix a bug: 3DS confirmation is not asked when an admin is subscribing a user through a payment schedule using PayZen
|
||||
- Fix a bug: 3DS confirmation is not asked when an admin is subscribing a user through a payment schedule, using PayZen
|
||||
- Updated @rails/webpacker to 5.4.3
|
||||
- Updated react-refresh-webpack-plugin to 0.5.1
|
||||
- Updated react-refresh to 0.10.0
|
||||
@ -41,17 +66,17 @@
|
||||
|
||||
## v5.1.10 2021 October 04
|
||||
|
||||
- Fix a bug: the image of the about page is not using the image set in backoffice
|
||||
- Fix a bug: the image of the about page is not using the image set in the backoffice
|
||||
- Fix a bug: updated sassc to 2.4.0 to fix ruby runtime error on some CPU architectures (#270)
|
||||
- Fix a security issue: prevent HTML code edition in projects, to prevent XSS vulnerability (#293)
|
||||
- Fix a bug : cover image doesn't display in profile
|
||||
- Fix a bug : it redirects to home when we delete a machine record photo
|
||||
- Fix a bug: cover image doesn't display in profile
|
||||
- Fix a bug: fab-manager redirects to the home page when we delete a machine photo
|
||||
|
||||
## v5.1.9 2021 September 21
|
||||
|
||||
- Add a setting for the purchase and use of a prepaid pack is only possible for the user with a valid subscription
|
||||
- Fix a bug: unable to show plan name in calendar reservations
|
||||
- Fix a bug: book overlapping slot setting label name
|
||||
- Add a setting to restrict the purchase and use of a prepaid pack to users with a valid subscription
|
||||
- Fix a bug: unable to view the plans names in the reservation calendar
|
||||
- Fix a bug: label name of the book overlapping slot setting
|
||||
|
||||
## v5.1.8 2021 September 13
|
||||
|
||||
@ -266,7 +291,11 @@
|
||||
- [TODO DEPLOY] `rails fablab:maintenance:rebuild_stylesheet`
|
||||
- [TODO DEPLOY] `\curl -sSL https://raw.githubusercontent.com/sleede/fab-manager/master/scripts/rename-adminsys.sh | bash`
|
||||
|
||||
## v4.7.13 2020 June 11
|
||||
## v4.7.14 2021 September 30
|
||||
|
||||
- Fix a bug: updated sassc to 2.4.0 to fix ruby runtime error on some CPU architectures
|
||||
|
||||
## v4.7.13 2021 June 11
|
||||
|
||||
- Fix a bug: unable to process stripe payments with 3DS authentication
|
||||
|
||||
|
@ -70,6 +70,8 @@ class API::ExportsController < API::ApiController
|
||||
case type
|
||||
when 'acd'
|
||||
export = export.where('created_at > ?', Invoice.maximum('updated_at'))
|
||||
when 'vat'
|
||||
export = export.where('created_at > ?', Invoice.maximum('updated_at'))
|
||||
else
|
||||
raise ArgumentError, "Unknown type accounting/#{type}"
|
||||
end
|
||||
|
@ -4,8 +4,9 @@
|
||||
class API::PaymentSchedulesController < API::ApiController
|
||||
before_action :authenticate_user!
|
||||
before_action :set_payment_schedule, only: %i[download cancel]
|
||||
before_action :set_payment_schedule_item, only: %i[cash_check refresh_item pay_item]
|
||||
before_action :set_payment_schedule_item, only: %i[show_item cash_check confirm_transfer refresh_item pay_item]
|
||||
|
||||
# retrieve all payment schedules for the current user, paginated
|
||||
def index
|
||||
@payment_schedules = PaymentSchedule.where('invoicing_profile_id = ?', current_user.invoicing_profile.id)
|
||||
.includes(:invoicing_profile, :payment_schedule_items, :payment_schedule_objects)
|
||||
@ -15,6 +16,7 @@ class API::PaymentSchedulesController < API::ApiController
|
||||
.per(params[:size])
|
||||
end
|
||||
|
||||
# retrieve all payment schedules for all users. Filtering is supported
|
||||
def list
|
||||
authorize PaymentSchedule
|
||||
|
||||
@ -44,6 +46,15 @@ class API::PaymentSchedulesController < API::ApiController
|
||||
render json: attrs, status: :ok
|
||||
end
|
||||
|
||||
def confirm_transfer
|
||||
authorize @payment_schedule_item.payment_schedule
|
||||
PaymentScheduleService.new.generate_invoice(@payment_schedule_item, payment_method: 'transfer')
|
||||
attrs = { state: 'paid', payment_method: 'transfer' }
|
||||
@payment_schedule_item.update_attributes(attrs)
|
||||
|
||||
render json: attrs, status: :ok
|
||||
end
|
||||
|
||||
def refresh_item
|
||||
authorize @payment_schedule_item.payment_schedule
|
||||
PaymentScheduleItemWorker.new.perform(@payment_schedule_item.id)
|
||||
@ -62,6 +73,11 @@ class API::PaymentSchedulesController < API::ApiController
|
||||
end
|
||||
end
|
||||
|
||||
def show_item
|
||||
authorize @payment_schedule_item.payment_schedule
|
||||
render json: @payment_schedule_item, status: :ok
|
||||
end
|
||||
|
||||
def cancel
|
||||
authorize @payment_schedule
|
||||
|
||||
|
@ -4,6 +4,20 @@
|
||||
# Prices are used in reservations (Machine, Space)
|
||||
class API::PricesController < API::ApiController
|
||||
before_action :authenticate_user!
|
||||
before_action :set_price, only: %i[update destroy]
|
||||
|
||||
def create
|
||||
@price = Price.new(price_create_params)
|
||||
@price.amount *= 100
|
||||
|
||||
authorize @price
|
||||
|
||||
if @price.save
|
||||
render json: @price, status: :created
|
||||
else
|
||||
render json: @price.errors, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def index
|
||||
@prices = PriceService.list(params)
|
||||
@ -11,7 +25,6 @@ class API::PricesController < API::ApiController
|
||||
|
||||
def update
|
||||
authorize Price
|
||||
@price = Price.find(params[:id])
|
||||
price_parameters = price_params
|
||||
price_parameters[:amount] = price_parameters[:amount] * 100
|
||||
if @price.update(price_parameters)
|
||||
@ -21,6 +34,12 @@ class API::PricesController < API::ApiController
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @price
|
||||
@price.safe_destroy
|
||||
head :no_content
|
||||
end
|
||||
|
||||
def compute
|
||||
cs = CartService.new(current_user)
|
||||
cart = cs.from_hash(params)
|
||||
@ -29,7 +48,15 @@ class API::PricesController < API::ApiController
|
||||
|
||||
private
|
||||
|
||||
def set_price
|
||||
@price = Price.find(params[:id])
|
||||
end
|
||||
|
||||
def price_create_params
|
||||
params.require(:price).permit(:amount, :duration, :group_id, :plan_id, :priceable_id, :priceable_type)
|
||||
end
|
||||
|
||||
def price_params
|
||||
params.require(:price).permit(:amount)
|
||||
params.require(:price).permit(:amount, :duration)
|
||||
end
|
||||
end
|
||||
|
@ -4,6 +4,11 @@
|
||||
class RegistrationsController < Devise::RegistrationsController
|
||||
# POST /users.json
|
||||
def create
|
||||
# Is public registration allowed?
|
||||
unless Setting.get('public_registrations')
|
||||
render json: { errors: { signup: [t('errors.messages.registration_disabled')] } }, status: :forbidden and return
|
||||
end
|
||||
|
||||
# first check the recaptcha
|
||||
check = RecaptchaService.verify(params[:user][:recaptcha])
|
||||
render json: check['error-codes'], status: :unprocessable_entity and return unless check['success']
|
||||
|
@ -2,5 +2,8 @@
|
||||
|
||||
# Raised when an an error occurred with the PayZen payment gateway
|
||||
class PayzenError < PaymentGatewayError
|
||||
def details
|
||||
JSON.parse(message.gsub('=>', ':').gsub('nil', 'null'))
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
CancelScheduleResponse,
|
||||
CashCheckResponse, PayItemResponse,
|
||||
PaymentSchedule,
|
||||
PaymentScheduleIndexRequest, RefreshItemResponse
|
||||
PaymentScheduleIndexRequest, PaymentScheduleItem, RefreshItemResponse
|
||||
} from '../models/payment-schedule';
|
||||
|
||||
export default class PaymentScheduleAPI {
|
||||
@ -23,6 +23,16 @@ export default class PaymentScheduleAPI {
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async confirmTransfer (paymentScheduleItemId: number): Promise<CashCheckResponse> {
|
||||
const res: AxiosResponse = await apiClient.post(`/api/payment_schedules/items/${paymentScheduleItemId}/confirm_transfer`);
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async getItem (paymentScheduleItemId: number): Promise<PaymentScheduleItem> {
|
||||
const res: AxiosResponse = await apiClient.get(`/api/payment_schedules/items/${paymentScheduleItemId}`);
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async refreshItem (paymentScheduleItemId: number): Promise<RefreshItemResponse> {
|
||||
const res: AxiosResponse = await apiClient.post(`/api/payment_schedules/items/${paymentScheduleItemId}/refresh_item`);
|
||||
return res?.data;
|
||||
|
@ -14,11 +14,21 @@ export default class PriceAPI {
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async create (price: Price): Promise<Price> {
|
||||
const res: AxiosResponse<Price> = await apiClient.post('/api/prices', { price });
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async update (price: Price): Promise<Price> {
|
||||
const res: AxiosResponse<Price> = await apiClient.patch(`/api/prices/${price.id}`, { price });
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async destroy (priceId: number): Promise<void> {
|
||||
const res: AxiosResponse<void> = await apiClient.delete(`/api/prices/${priceId}`);
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
private static filtersToQuery (filters?: PriceIndexFilter): string {
|
||||
if (!filters) return '';
|
||||
|
||||
|
16
app/frontend/src/javascript/api/space.ts
Normal file
16
app/frontend/src/javascript/api/space.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import apiClient from './clients/api-client';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { Space } from '../models/space';
|
||||
|
||||
export default class SpaceAPI {
|
||||
static async index (): Promise<Array<any>> {
|
||||
const res: AxiosResponse<Array<Space>> = await apiClient.get('/api/spaces');
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async get (id: number): Promise<Space> {
|
||||
const res: AxiosResponse<Space> = await apiClient.get(`/api/spaces/${id}`);
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
}
|
@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface HtmlTranslateProps {
|
||||
trKey: string,
|
||||
options?: Record<string, string>
|
||||
options?: Record<string, string|number>
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { DocumentFilters } from '../document-filters';
|
||||
@ -38,13 +38,6 @@ const PaymentSchedulesList: React.FC<PaymentSchedulesListProps> = ({ currentUser
|
||||
// current filter, by date, for the schedules and the deadlines
|
||||
const [dateFilter, setDateFilter] = useState<Date>(null);
|
||||
|
||||
/**
|
||||
* When the component is loaded first, refresh the list of schedules to fill the first page.
|
||||
*/
|
||||
useEffect(() => {
|
||||
handleRefreshList();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Fetch from the API the payments schedules matching the given filters and reset the results table with the new schedules.
|
||||
*/
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { ReactEventHandler, useState } from 'react';
|
||||
import React, { ReactElement, ReactEventHandler, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Loader } from '../base/loader';
|
||||
import _ from 'lodash';
|
||||
@ -6,11 +6,11 @@ import { FabButton } from '../base/fab-button';
|
||||
import { FabModal } from '../base/fab-modal';
|
||||
import { UpdateCardModal } from '../payment/update-card-modal';
|
||||
import { StripeElements } from '../payment/stripe/stripe-elements';
|
||||
import { StripeConfirm } from '../payment/stripe/stripe-confirm';
|
||||
import { User, UserRole } from '../../models/user';
|
||||
import { PaymentSchedule, PaymentScheduleItem, PaymentScheduleItemState } from '../../models/payment-schedule';
|
||||
import PaymentScheduleAPI from '../../api/payment-schedule';
|
||||
import FormatLib from '../../lib/format';
|
||||
import { StripeConfirmModal } from '../payment/stripe/stripe-confirm-modal';
|
||||
|
||||
interface PaymentSchedulesTableProps {
|
||||
paymentSchedules: Array<PaymentSchedule>,
|
||||
@ -31,6 +31,8 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
const [showExpanded, setShowExpanded] = useState<Map<number, boolean>>(new Map());
|
||||
// is open, the modal dialog to confirm the cashing of a check?
|
||||
const [showConfirmCashing, setShowConfirmCashing] = useState<boolean>(false);
|
||||
// is open, the modal dialog to confirm a back transfer?
|
||||
const [showConfirmTransfer, setShowConfirmTransfer] = useState<boolean>(false);
|
||||
// is open, the modal dialog the resolve a pending card payment?
|
||||
const [showResolveAction, setShowResolveAction] = useState<boolean>(false);
|
||||
// the user cannot confirm the action modal (3D secure), unless he has resolved the pending action
|
||||
@ -46,6 +48,8 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
|
||||
// we want to display the card update button, only once. This is an association table keeping when we already shown one
|
||||
const cardUpdateButton = new Map<number, boolean>();
|
||||
// we want to display the cancel subscription button, only once. This is an association table keeping when we already shown one
|
||||
const subscriptionCancelButton = new Map<number, boolean>();
|
||||
|
||||
/**
|
||||
* Check if the requested payment schedule is displayed with its deadlines (PaymentScheduleItem) or without them
|
||||
@ -110,11 +114,26 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a button to cancel the given subscription, if the user is privileged enough
|
||||
*/
|
||||
const cancelSubscriptionButton = (schedule: PaymentSchedule): ReactElement => {
|
||||
if (isPrivileged() && !subscriptionCancelButton.get(schedule.id)) {
|
||||
subscriptionCancelButton.set(schedule.id, true);
|
||||
return (
|
||||
<FabButton onClick={handleCancelSubscription(schedule)}
|
||||
icon={<i className="fas fa-times" />}>
|
||||
{t('app.shared.schedules_table.cancel_subscription')}
|
||||
</FabButton>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the human-readable string for the status of the provided deadline.
|
||||
*/
|
||||
const formatState = (item: PaymentScheduleItem): JSX.Element => {
|
||||
let res = t(`app.shared.schedules_table.state_${item.state}`);
|
||||
const formatState = (item: PaymentScheduleItem, schedule: PaymentSchedule): JSX.Element => {
|
||||
let res = t(`app.shared.schedules_table.state_${item.state}${item.state === 'pending' ? '_' + schedule.payment_method : ''}`);
|
||||
if (item.state === PaymentScheduleItemState.Paid) {
|
||||
const key = `app.shared.schedules_table.method_${item.payment_method}`;
|
||||
res += ` (${t(key)})`;
|
||||
@ -138,12 +157,21 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
return downloadButton(TargetType.Invoice, item.invoice_id);
|
||||
case PaymentScheduleItemState.Pending:
|
||||
if (isPrivileged()) {
|
||||
return (
|
||||
<FabButton onClick={handleConfirmCheckPayment(item)}
|
||||
icon={<i className="fas fa-money-check" />}>
|
||||
{t('app.shared.schedules_table.confirm_payment')}
|
||||
</FabButton>
|
||||
);
|
||||
if (schedule.payment_method === 'transfer') {
|
||||
return (
|
||||
<FabButton onClick={handleConfirmTransferPayment(item)}
|
||||
icon={<i className="fas fa-university"/>}>
|
||||
{t('app.shared.schedules_table.confirm_payment')}
|
||||
</FabButton>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<FabButton onClick={handleConfirmCheckPayment(item)}
|
||||
icon={<i className="fas fa-money-check"/>}>
|
||||
{t('app.shared.schedules_table.confirm_payment')}
|
||||
</FabButton>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return <span>{t('app.shared.schedules_table.please_ask_reception')}</span>;
|
||||
}
|
||||
@ -161,28 +189,29 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
{t('app.shared.schedules_table.update_card')}
|
||||
</FabButton>
|
||||
);
|
||||
case PaymentScheduleItemState.GatewayCanceled:
|
||||
case PaymentScheduleItemState.Error:
|
||||
// if the payment schedule was canceled by the gateway, or
|
||||
// if the payment is in error, the schedule is over, and we can't update the card
|
||||
cardUpdateButton.set(schedule.id, true);
|
||||
if (isPrivileged()) {
|
||||
return (
|
||||
<FabButton onClick={handleCancelSubscription(schedule)}
|
||||
icon={<i className="fas fa-times" />}>
|
||||
{t('app.shared.schedules_table.cancel_subscription')}
|
||||
</FabButton>
|
||||
);
|
||||
} else {
|
||||
if (!isPrivileged()) {
|
||||
return <span>{t('app.shared.schedules_table.please_ask_reception')}</span>;
|
||||
}
|
||||
return cancelSubscriptionButton(schedule);
|
||||
case PaymentScheduleItemState.New:
|
||||
if (!cardUpdateButton.get(schedule.id)) {
|
||||
if (!cardUpdateButton.get(schedule.id) && schedule.payment_method === 'card') {
|
||||
cardUpdateButton.set(schedule.id, true);
|
||||
return (
|
||||
<FabButton onClick={handleUpdateCard(schedule)}
|
||||
icon={<i className="fas fa-credit-card" />}>
|
||||
{t('app.shared.schedules_table.update_card')}
|
||||
</FabButton>
|
||||
<span>
|
||||
<FabButton onClick={handleUpdateCard(schedule)}
|
||||
icon={<i className="fas fa-credit-card" />}>
|
||||
{t('app.shared.schedules_table.update_card')}
|
||||
</FabButton>
|
||||
{cancelSubscriptionButton(schedule)}
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
return cancelSubscriptionButton(schedule);
|
||||
}
|
||||
return <span />;
|
||||
default:
|
||||
@ -200,6 +229,15 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user's clicks on the "confirm transfer" button: show a confirmation modal
|
||||
*/
|
||||
const handleConfirmTransferPayment = (item: PaymentScheduleItem): ReactEventHandler => {
|
||||
return (): void => {
|
||||
setTempDeadline(item);
|
||||
toggleConfirmTransferModal();
|
||||
};
|
||||
};
|
||||
/**
|
||||
* After the user has confirmed that he wants to cash the check, update the API, refresh the list and close the modal.
|
||||
*/
|
||||
@ -212,6 +250,18 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* After the user has confirmed that he validates the tranfer, update the API, refresh the list and close the modal.
|
||||
*/
|
||||
const onTransferConfirmed = (): void => {
|
||||
PaymentScheduleAPI.confirmTransfer(tempDeadline.id).then((res) => {
|
||||
if (res.state === PaymentScheduleItemState.Paid) {
|
||||
refreshSchedulesTable();
|
||||
toggleConfirmTransferModal();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Refresh all payment schedules in the table
|
||||
*/
|
||||
@ -226,6 +276,13 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
setShowConfirmCashing(!showConfirmCashing);
|
||||
};
|
||||
|
||||
/**
|
||||
* Show/hide the modal dialog that enable to confirm the bank transfer for a given deadline.
|
||||
*/
|
||||
const toggleConfirmTransferModal = (): void => {
|
||||
setShowConfirmTransfer(!showConfirmTransfer);
|
||||
};
|
||||
|
||||
/**
|
||||
* Show/hide the modal dialog that trigger the card "action".
|
||||
*/
|
||||
@ -262,7 +319,8 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user's clicks on the "update card" button: show a modal to input a new card
|
||||
* Callback triggered when the user's clicks on the "update card" button: show a modal to input a new card.
|
||||
* If a payementScheduleItem is provided, the payment will be triggered after a successful update (see handleCardUpdateSuccess).
|
||||
*/
|
||||
const handleUpdateCard = (paymentSchedule: PaymentSchedule, item?: PaymentScheduleItem): ReactEventHandler => {
|
||||
return (): void => {
|
||||
@ -375,7 +433,7 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
{_.orderBy(p.items, 'due_date').map(item => <tr key={item.id}>
|
||||
<td>{FormatLib.date(item.due_date)}</td>
|
||||
<td>{FormatLib.price(item.amount)}</td>
|
||||
<td>{formatState(item)}</td>
|
||||
<td>{formatState(item, p)}</td>
|
||||
<td>{itemButtons(item, p)}</td>
|
||||
</tr>)}
|
||||
</tbody>
|
||||
@ -390,6 +448,7 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="modals">
|
||||
{/* Confirm the cashing of the current deadline by check */}
|
||||
<FabModal title={t('app.shared.schedules_table.confirm_check_cashing')}
|
||||
isOpen={showConfirmCashing}
|
||||
toggleModal={toggleConfirmCashingModal}
|
||||
@ -403,6 +462,21 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
})}
|
||||
</span>}
|
||||
</FabModal>
|
||||
{/* Confirm the bank transfer for the current deadline */}
|
||||
<FabModal title={t('app.shared.schedules_table.confirm_bank_transfer')}
|
||||
isOpen={showConfirmTransfer}
|
||||
toggleModal={toggleConfirmTransferModal}
|
||||
onConfirm={onTransferConfirmed}
|
||||
closeButton={true}
|
||||
confirmButton={t('app.shared.schedules_table.confirm_button')}>
|
||||
{tempDeadline && <span>
|
||||
{t('app.shared.schedules_table.confirm_bank_transfer_body', {
|
||||
AMOUNT: FormatLib.price(tempDeadline.amount),
|
||||
DATE: FormatLib.date(tempDeadline.due_date)
|
||||
})}
|
||||
</span>}
|
||||
</FabModal>
|
||||
{/* Cancel the subscription */}
|
||||
<FabModal title={t('app.shared.schedules_table.cancel_subscription')}
|
||||
isOpen={showCancelSubscription}
|
||||
toggleModal={toggleCancelSubscriptionModal}
|
||||
@ -412,14 +486,12 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
{t('app.shared.schedules_table.confirm_cancel_subscription')}
|
||||
</FabModal>
|
||||
<StripeElements>
|
||||
<FabModal title={t('app.shared.schedules_table.resolve_action')}
|
||||
isOpen={showResolveAction}
|
||||
{/* 3D secure confirmation */}
|
||||
{tempDeadline && <StripeConfirmModal isOpen={showResolveAction}
|
||||
toggleModal={toggleResolveActionModal}
|
||||
onConfirm={afterAction}
|
||||
confirmButton={t('app.shared.schedules_table.ok_button')}
|
||||
preventConfirm={isConfirmActionDisabled}>
|
||||
{tempDeadline && <StripeConfirm clientSecret={tempDeadline.client_secret} onResponse={toggleConfirmActionButton} />}
|
||||
</FabModal>
|
||||
onSuccess={afterAction}
|
||||
paymentScheduleItemId={tempDeadline.id} />}
|
||||
{/* Update credit card */}
|
||||
{tempSchedule && <UpdateCardModal isOpen={showUpdateCard}
|
||||
toggleModal={toggleUpdateCardModal}
|
||||
operator={operator}
|
||||
|
@ -8,9 +8,9 @@ import SettingAPI from '../../../api/setting';
|
||||
import { SettingName } from '../../../models/setting';
|
||||
import { PaymentModal } from '../payment-modal';
|
||||
import { PaymentSchedule } from '../../../models/payment-schedule';
|
||||
import { PaymentMethod } from '../../../models/payment';
|
||||
import { HtmlTranslate } from '../../base/html-translate';
|
||||
|
||||
const ALL_SCHEDULE_METHODS = ['card', 'check'] as const;
|
||||
const ALL_SCHEDULE_METHODS = ['card', 'check', 'transfer'] as const;
|
||||
type scheduleMethod = typeof ALL_SCHEDULE_METHODS[number];
|
||||
|
||||
/**
|
||||
@ -31,11 +31,7 @@ export const LocalPaymentForm: React.FC<GatewayFormProps> = ({ onSubmit, onSucce
|
||||
const [onlinePaymentModal, setOnlinePaymentModal] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (cart.payment_method === PaymentMethod.Card) {
|
||||
setMethod('card');
|
||||
} else {
|
||||
setMethod('check');
|
||||
}
|
||||
setMethod(cart.payment_method || 'check');
|
||||
}, [cart]);
|
||||
|
||||
/**
|
||||
@ -65,11 +61,7 @@ export const LocalPaymentForm: React.FC<GatewayFormProps> = ({ onSubmit, onSucce
|
||||
* Callback triggered when the user selects a payment method for the current payment schedule.
|
||||
*/
|
||||
const handleUpdateMethod = (option: selectOption) => {
|
||||
if (option.value === 'card') {
|
||||
updateCart(Object.assign({}, cart, { payment_method: PaymentMethod.Card }));
|
||||
} else {
|
||||
updateCart(Object.assign({}, cart, { payment_method: PaymentMethod.Other }));
|
||||
}
|
||||
updateCart(Object.assign({}, cart, { payment_method: option.value }));
|
||||
setMethod(option.value);
|
||||
};
|
||||
|
||||
@ -140,6 +132,7 @@ export const LocalPaymentForm: React.FC<GatewayFormProps> = ({ onSubmit, onSucce
|
||||
value={methodToOption(method)} />
|
||||
{method === 'card' && <p>{t('app.admin.local_payment.card_collection_info')}</p>}
|
||||
{method === 'check' && <p>{t('app.admin.local_payment.check_collection_info', { DEADLINES: paymentSchedule.items.length })}</p>}
|
||||
{method === 'transfer' && <HtmlTranslate trKey="app.admin.local_payment.transfer_collection_info" options={{ DEADLINES: paymentSchedule.items.length }} />}
|
||||
</div>
|
||||
<div className="full-schedule">
|
||||
<ul>
|
||||
|
@ -0,0 +1,55 @@
|
||||
import { StripeConfirm } from './stripe-confirm';
|
||||
import { FabModal } from '../../base/fab-modal';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PaymentScheduleAPI from '../../../api/payment-schedule';
|
||||
import { PaymentScheduleItem } from '../../../models/payment-schedule';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface StripeConfirmModalProps {
|
||||
isOpen: boolean,
|
||||
toggleModal: () => void,
|
||||
onSuccess: () => void,
|
||||
paymentScheduleItemId: number,
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal dialog that trigger a 3D secure confirmation for the given payment schedule item (deadline for a payment schedule).
|
||||
*/
|
||||
export const StripeConfirmModal: React.FC<StripeConfirmModalProps> = ({ isOpen, toggleModal, onSuccess, paymentScheduleItemId }) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
const [item, setItem] = useState<PaymentScheduleItem>(null);
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
PaymentScheduleAPI.getItem(paymentScheduleItemId).then(data => {
|
||||
setItem(data);
|
||||
});
|
||||
}, [paymentScheduleItemId]);
|
||||
|
||||
/**
|
||||
* Callback triggered when the confirm button was clicked in the modal.
|
||||
*/
|
||||
const onConfirmed = (): void => {
|
||||
togglePending();
|
||||
onSuccess();
|
||||
};
|
||||
|
||||
/**
|
||||
* Enable/disable the confirm button of the "action" modal
|
||||
*/
|
||||
const togglePending = (): void => {
|
||||
setIsPending(!isPending);
|
||||
};
|
||||
|
||||
return (
|
||||
<FabModal title={t('app.shared.schedules_table.resolve_action')}
|
||||
isOpen={isOpen}
|
||||
toggleModal={toggleModal}
|
||||
onConfirm={onConfirmed}
|
||||
confirmButton={t('app.shared.schedules_table.ok_button')}
|
||||
preventConfirm={isPending}>
|
||||
{item && <StripeConfirm clientSecret={item.client_secret} onResponse={togglePending} />}
|
||||
</FabModal>
|
||||
);
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
import React, { memo, useEffect, useState } from 'react';
|
||||
import { Elements } from '@stripe/react-stripe-js';
|
||||
import { loadStripe } from '@stripe/stripe-js';
|
||||
import { loadStripe, Stripe } from '@stripe/stripe-js';
|
||||
import { SettingName } from '../../../models/setting';
|
||||
import SettingAPI from '../../../api/setting';
|
||||
|
||||
@ -8,15 +8,17 @@ import SettingAPI from '../../../api/setting';
|
||||
* This component initializes the stripe's Elements tag with the API key
|
||||
*/
|
||||
export const StripeElements: React.FC = memo(({ children }) => {
|
||||
const [stripe, setStripe] = useState(undefined);
|
||||
const [stripe, setStripe] = useState<Promise<Stripe | null>>(undefined);
|
||||
|
||||
/**
|
||||
* When this component is mounted, we initialize the <Elements> tag with the Stripe's public key
|
||||
*/
|
||||
useEffect(() => {
|
||||
SettingAPI.get(SettingName.StripePublicKey).then(key => {
|
||||
const promise = loadStripe(key.value);
|
||||
setStripe(promise);
|
||||
if (key?.value) {
|
||||
const promise = loadStripe(key.value);
|
||||
setStripe(promise);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
@ -24,11 +24,7 @@ const UpdateCardModalComponent: React.FC<UpdateCardModalProps> = ({ isOpen, togg
|
||||
const [gateway, setGateway] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
if (schedule.gateway_subscription.classname.match(/^PayZen::/)) {
|
||||
setGateway('payzen');
|
||||
} else if (schedule.gateway_subscription.classname.match(/^Stripe::/)) {
|
||||
setGateway('stripe');
|
||||
}
|
||||
setGateway(schedule.gateway);
|
||||
}, [schedule]);
|
||||
|
||||
/**
|
||||
@ -44,7 +40,7 @@ const UpdateCardModalComponent: React.FC<UpdateCardModalProps> = ({ isOpen, togg
|
||||
|
||||
/**
|
||||
* Render the PayZen update-card modal
|
||||
*/ // 1
|
||||
*/
|
||||
const renderPayZenModal = (): ReactElement => {
|
||||
return <PayzenCardUpdateModal isOpen={isOpen}
|
||||
toggleModal={toggleModal}
|
||||
@ -58,15 +54,16 @@ const UpdateCardModalComponent: React.FC<UpdateCardModalProps> = ({ isOpen, togg
|
||||
*/
|
||||
|
||||
switch (gateway) {
|
||||
case 'stripe':
|
||||
case 'Stripe':
|
||||
return renderStripeModal();
|
||||
case 'payzen':
|
||||
case 'PayZen':
|
||||
return renderPayZenModal();
|
||||
case '':
|
||||
case undefined:
|
||||
return <div/>;
|
||||
default:
|
||||
onError(t('app.shared.update_card_modal.unexpected_error'));
|
||||
console.error(`[UpdateCardModal] unexpected gateway: ${schedule.gateway_subscription?.classname}`);
|
||||
console.error(`[UpdateCardModal] unexpected gateway: ${schedule.gateway} for schedule ${schedule.id}`);
|
||||
return <div />;
|
||||
}
|
||||
};
|
||||
|
@ -35,7 +35,7 @@ const EditPlanCategoryComponent: React.FC<EditPlanCategoryProps> = ({ onSuccess,
|
||||
|
||||
/**
|
||||
* The edit has been confirmed by the user.
|
||||
* Call the API to trigger the update of the temporary set plan-category
|
||||
* Call the API to trigger the update of the temporary set plan-category.
|
||||
*/
|
||||
const onEditConfirmed = (): void => {
|
||||
PlanCategoryAPI.update(tempCategory).then((updatedCategory) => {
|
||||
|
@ -1,12 +1,12 @@
|
||||
import React, { ReactNode, useState } from 'react';
|
||||
import { PrepaidPack } from '../../models/prepaid-pack';
|
||||
import { PrepaidPack } from '../../../models/prepaid-pack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabPopover } from '../base/fab-popover';
|
||||
import { FabPopover } from '../../base/fab-popover';
|
||||
import { CreatePack } from './create-pack';
|
||||
import PrepaidPackAPI from '../../api/prepaid-pack';
|
||||
import PrepaidPackAPI from '../../../api/prepaid-pack';
|
||||
import { DeletePack } from './delete-pack';
|
||||
import { EditPack } from './edit-pack';
|
||||
import FormatLib from '../../lib/format';
|
||||
import FormatLib from '../../../lib/format';
|
||||
|
||||
interface ConfigurePacksButtonProps {
|
||||
packsData: Array<PrepaidPack>,
|
@ -1,10 +1,10 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FabModal } from '../base/fab-modal';
|
||||
import { FabModal } from '../../base/fab-modal';
|
||||
import { PackForm } from './pack-form';
|
||||
import { PrepaidPack } from '../../models/prepaid-pack';
|
||||
import PrepaidPackAPI from '../../api/prepaid-pack';
|
||||
import { PrepaidPack } from '../../../models/prepaid-pack';
|
||||
import PrepaidPackAPI from '../../../api/prepaid-pack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabAlert } from '../base/fab-alert';
|
||||
import { FabAlert } from '../../base/fab-alert';
|
||||
|
||||
interface CreatePackProps {
|
||||
onSuccess: (message: string) => void,
|
@ -1,10 +1,10 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { FabModal } from '../base/fab-modal';
|
||||
import { Loader } from '../base/loader';
|
||||
import { PrepaidPack } from '../../models/prepaid-pack';
|
||||
import PrepaidPackAPI from '../../api/prepaid-pack';
|
||||
import { FabButton } from '../../base/fab-button';
|
||||
import { FabModal } from '../../base/fab-modal';
|
||||
import { Loader } from '../../base/loader';
|
||||
import { PrepaidPack } from '../../../models/prepaid-pack';
|
||||
import PrepaidPackAPI from '../../../api/prepaid-pack';
|
||||
|
||||
interface DeletePackProps {
|
||||
onSuccess: (message: string) => void,
|
@ -1,10 +1,10 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FabModal } from '../base/fab-modal';
|
||||
import { FabModal } from '../../base/fab-modal';
|
||||
import { PackForm } from './pack-form';
|
||||
import { PrepaidPack } from '../../models/prepaid-pack';
|
||||
import PrepaidPackAPI from '../../api/prepaid-pack';
|
||||
import { PrepaidPack } from '../../../models/prepaid-pack';
|
||||
import PrepaidPackAPI from '../../../api/prepaid-pack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { FabButton } from '../../base/fab-button';
|
||||
|
||||
interface EditPackProps {
|
||||
pack: PrepaidPack,
|
@ -1,25 +1,23 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { Loader } from '../base/loader';
|
||||
import { FabAlert } from '../base/fab-alert';
|
||||
import { HtmlTranslate } from '../base/html-translate';
|
||||
import MachineAPI from '../../api/machine';
|
||||
import GroupAPI from '../../api/group';
|
||||
import { IFablab } from '../../models/fablab';
|
||||
import { Machine } from '../../models/machine';
|
||||
import { Group } from '../../models/group';
|
||||
import { IApplication } from '../../models/application';
|
||||
import { EditablePrice } from './editable-price';
|
||||
import { Loader } from '../../base/loader';
|
||||
import { FabAlert } from '../../base/fab-alert';
|
||||
import { HtmlTranslate } from '../../base/html-translate';
|
||||
import MachineAPI from '../../../api/machine';
|
||||
import GroupAPI from '../../../api/group';
|
||||
import { Machine } from '../../../models/machine';
|
||||
import { Group } from '../../../models/group';
|
||||
import { IApplication } from '../../../models/application';
|
||||
import { EditablePrice } from '../editable-price';
|
||||
import { ConfigurePacksButton } from './configure-packs-button';
|
||||
import PriceAPI from '../../api/price';
|
||||
import { Price } from '../../models/price';
|
||||
import PrepaidPackAPI from '../../api/prepaid-pack';
|
||||
import { PrepaidPack } from '../../models/prepaid-pack';
|
||||
import PriceAPI from '../../../api/price';
|
||||
import { Price } from '../../../models/price';
|
||||
import PrepaidPackAPI from '../../../api/prepaid-pack';
|
||||
import { PrepaidPack } from '../../../models/prepaid-pack';
|
||||
import { useImmer } from 'use-immer';
|
||||
import FormatLib from '../../lib/format';
|
||||
import FormatLib from '../../../lib/format';
|
||||
|
||||
declare let Fablab: IFablab;
|
||||
declare const Application: IApplication;
|
||||
|
||||
interface MachinesPricingProps {
|
@ -1,11 +1,11 @@
|
||||
import React, { BaseSyntheticEvent } from 'react';
|
||||
import Select from 'react-select';
|
||||
import Switch from 'react-switch';
|
||||
import { PrepaidPack } from '../../models/prepaid-pack';
|
||||
import { PrepaidPack } from '../../../models/prepaid-pack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useImmer } from 'use-immer';
|
||||
import { FabInput } from '../base/fab-input';
|
||||
import { IFablab } from '../../models/fablab';
|
||||
import { FabInput } from '../../base/fab-input';
|
||||
import { IFablab } from '../../../models/fablab';
|
||||
|
||||
declare let Fablab: IFablab;
|
||||
|
@ -0,0 +1,86 @@
|
||||
import React, { ReactNode, useState } from 'react';
|
||||
import { Price } from '../../../models/price';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabPopover } from '../../base/fab-popover';
|
||||
import { CreateExtendedPrice } from './create-extended-price';
|
||||
import PriceAPI from '../../../api/price';
|
||||
import FormatLib from '../../../lib/format';
|
||||
import { EditExtendedPrice } from './edit-extended-price';
|
||||
import { DeleteExtendedPrice } from './delete-extended-price';
|
||||
|
||||
interface ConfigureExtendedPriceButtonProps {
|
||||
prices: Array<Price>,
|
||||
onError: (message: string) => void,
|
||||
onSuccess: (message: string) => void,
|
||||
groupId: number,
|
||||
priceableId: number,
|
||||
priceableType: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* This component is a button that shows the list of extendedPrices.
|
||||
* It also triggers modal dialogs to configure (add/edit/remove) extendedPrices.
|
||||
*/
|
||||
export const ConfigureExtendedPriceButton: React.FC<ConfigureExtendedPriceButtonProps> = ({ prices, onError, onSuccess, groupId, priceableId, priceableType }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [extendedPrices, setExtendedPrices] = useState<Array<Price>>(prices);
|
||||
const [showList, setShowList] = useState<boolean>(false);
|
||||
|
||||
/**
|
||||
* Return the number of minutes, user-friendly formatted
|
||||
*/
|
||||
const formatDuration = (minutes: number): string => {
|
||||
return t('app.admin.configure_extended_prices_button.extended_price_DURATION', { DURATION: minutes });
|
||||
};
|
||||
|
||||
/**
|
||||
* Open/closes the popover listing the existing packs
|
||||
*/
|
||||
const toggleShowList = (): void => {
|
||||
setShowList(!showList);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the extendedPrice was successfully created/deleted/updated.
|
||||
* We refresh the list of extendedPrices.
|
||||
*/
|
||||
const handleSuccess = (message: string) => {
|
||||
onSuccess(message);
|
||||
PriceAPI.index({ group_id: groupId, priceable_id: priceableId, priceable_type: priceableType })
|
||||
.then(data => setExtendedPrices(data.filter(p => p.duration !== 60)))
|
||||
.catch(error => onError(error));
|
||||
};
|
||||
|
||||
/**
|
||||
* Render the button used to trigger the "new extended price" modal
|
||||
*/
|
||||
const renderAddButton = (): ReactNode => {
|
||||
return <CreateExtendedPrice onSuccess={handleSuccess}
|
||||
onError={onError}
|
||||
groupId={groupId}
|
||||
priceableId={priceableId}
|
||||
priceableType={priceableType} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="configure-extended-prices-button">
|
||||
<button className="extended-prices-button" onClick={toggleShowList}>
|
||||
<i className="fas fa-stopwatch" />
|
||||
</button>
|
||||
{showList && <FabPopover title={t('app.admin.configure_extended_prices_button.extended_prices')} headerButton={renderAddButton()} className="fab-popover__right">
|
||||
<ul>
|
||||
{extendedPrices?.map(extendedPrice =>
|
||||
<li key={extendedPrice.id}>
|
||||
{formatDuration(extendedPrice.duration)} - {FormatLib.price(extendedPrice.amount)}
|
||||
<span className="extended-prices-actions">
|
||||
<EditExtendedPrice onSuccess={handleSuccess} onError={onError} price={extendedPrice} />
|
||||
<DeleteExtendedPrice onSuccess={handleSuccess} onError={onError} price={extendedPrice} />
|
||||
</span>
|
||||
</li>)}
|
||||
</ul>
|
||||
{extendedPrices?.length === 0 && <span>{t('app.admin.configure_extended_prices_button.no_extended_prices')}</span>}
|
||||
</FabPopover>}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,69 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FabModal } from '../../base/fab-modal';
|
||||
import { ExtendedPriceForm } from './extended-price-form';
|
||||
import { Price } from '../../../models/price';
|
||||
import PriceAPI from '../../../api/price';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabAlert } from '../../base/fab-alert';
|
||||
|
||||
interface CreateExtendedPriceProps {
|
||||
onSuccess: (message: string) => void,
|
||||
onError: (message: string) => void,
|
||||
groupId: number,
|
||||
priceableId: number,
|
||||
priceableType: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* This component shows a button.
|
||||
* When clicked, we show a modal dialog handing the process of creating a new extended price
|
||||
*/
|
||||
export const CreateExtendedPrice: React.FC<CreateExtendedPriceProps> = ({ onSuccess, onError, groupId, priceableId, priceableType }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
|
||||
/**
|
||||
* Open/closes the "new extended price" modal dialog
|
||||
*/
|
||||
const toggleModal = (): void => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user has validated the creation of the new extended price
|
||||
*/
|
||||
const handleSubmit = (extendedPrice: Price): void => {
|
||||
// set the already-known attributes of the new extended price
|
||||
const newExtendedPrice = Object.assign<Price, Price>({} as Price, extendedPrice);
|
||||
newExtendedPrice.group_id = groupId;
|
||||
newExtendedPrice.priceable_id = priceableId;
|
||||
newExtendedPrice.priceable_type = priceableType;
|
||||
|
||||
// create it on the API
|
||||
PriceAPI.create(newExtendedPrice)
|
||||
.then(() => {
|
||||
onSuccess(t('app.admin.create_extended_price.extended_price_successfully_created'));
|
||||
toggleModal();
|
||||
})
|
||||
.catch(error => onError(error));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="create-extended-price">
|
||||
<button className="add-price-button" onClick={toggleModal}><i className="fas fa-plus"/></button>
|
||||
<FabModal isOpen={isOpen}
|
||||
toggleModal={toggleModal}
|
||||
title={t('app.admin.create_extended_price.new_extended_price')}
|
||||
className="new-extended-price-modal"
|
||||
closeButton
|
||||
confirmButton={t('app.admin.create_extended_price.create_extended_price')}
|
||||
onConfirmSendFormId="new-extended-price">
|
||||
<FabAlert level="info">
|
||||
{t('app.admin.create_extended_price.new_extended_price_info', { TYPE: priceableType })}
|
||||
</FabAlert>
|
||||
<ExtendedPriceForm formId="new-extended-price" onSubmit={handleSubmit} />
|
||||
</FabModal>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,56 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabButton } from '../../base/fab-button';
|
||||
import { FabModal } from '../../base/fab-modal';
|
||||
import { Price } from '../../../models/price';
|
||||
import PriceAPI from '../../../api/price';
|
||||
|
||||
interface DeleteExtendedPriceProps {
|
||||
onSuccess: (message: string) => void,
|
||||
onError: (message: string) => void,
|
||||
price: Price,
|
||||
}
|
||||
|
||||
/**
|
||||
* This component shows a button.
|
||||
* When clicked, we show a modal dialog to ask the user for confirmation about the deletion of the provided extended price.
|
||||
*/
|
||||
export const DeleteExtendedPrice: React.FC<DeleteExtendedPriceProps> = ({ onSuccess, onError, price }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [deletionModal, setDeletionModal] = useState<boolean>(false);
|
||||
|
||||
/**
|
||||
* Opens/closes the deletion modal
|
||||
*/
|
||||
const toggleDeletionModal = (): void => {
|
||||
setDeletionModal(!deletionModal);
|
||||
};
|
||||
|
||||
/**
|
||||
* The deletion has been confirmed by the user.
|
||||
* Call the API to trigger the deletion of the temporary set extended price
|
||||
*/
|
||||
const onDeleteConfirmed = (): void => {
|
||||
PriceAPI.destroy(price.id).then(() => {
|
||||
onSuccess(t('app.admin.delete_extended_price.extended_price_deleted'));
|
||||
}).catch((error) => {
|
||||
onError(t('app.admin.delete_extended_price.unable_to_delete') + error);
|
||||
});
|
||||
toggleDeletionModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="delete-extended-price">
|
||||
<FabButton type='button' className="remove-price-button" icon={<i className="fa fa-trash" />} onClick={toggleDeletionModal} />
|
||||
<FabModal title={t('app.admin.delete_extended_price.delete_extended_price')}
|
||||
isOpen={deletionModal}
|
||||
toggleModal={toggleDeletionModal}
|
||||
closeButton={true}
|
||||
confirmButton={t('app.admin.delete_extended_price.confirm_delete')}
|
||||
onConfirm={onDeleteConfirmed}>
|
||||
<span>{t('app.admin.delete_extended_price.delete_confirmation')}</span>
|
||||
</FabModal>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,66 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FabModal } from '../../base/fab-modal';
|
||||
import { ExtendedPriceForm } from './extended-price-form';
|
||||
import { Price } from '../../../models/price';
|
||||
import PriceAPI from '../../../api/price';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabButton } from '../../base/fab-button';
|
||||
|
||||
interface EditExtendedPriceProps {
|
||||
price: Price,
|
||||
onSuccess: (message: string) => void,
|
||||
onError: (message: string) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* This component shows a button.
|
||||
* When clicked, we show a modal dialog handing the process of creating a new extended price
|
||||
*/
|
||||
export const EditExtendedPrice: React.FC<EditExtendedPriceProps> = ({ price, onSuccess, onError }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [extendedPriceData, setExtendedPriceData] = useState<Price>(price);
|
||||
|
||||
/**
|
||||
* Open/closes the "edit extended price" modal dialog
|
||||
*/
|
||||
const toggleModal = (): void => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
/**
|
||||
* When the user clicks on the edition button open te edition modal
|
||||
*/
|
||||
const handleRequestEdit = (): void => {
|
||||
toggleModal();
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user has validated the changes of the extended price
|
||||
*/
|
||||
const handleUpdate = (price: Price): void => {
|
||||
PriceAPI.update(price)
|
||||
.then(() => {
|
||||
onSuccess(t('app.admin.edit_extended_price.extended_price_successfully_updated'));
|
||||
setExtendedPriceData(price);
|
||||
toggleModal();
|
||||
})
|
||||
.catch(error => onError(error));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="edit-extended-price">
|
||||
<FabButton type='button' className="edit-price-button" icon={<i className="fas fa-edit" />} onClick={handleRequestEdit} />
|
||||
<FabModal isOpen={isOpen}
|
||||
toggleModal={toggleModal}
|
||||
title={t('app.admin.edit_extended_price.edit_extended_price')}
|
||||
className="edit-pack-modal"
|
||||
closeButton
|
||||
confirmButton={t('app.admin.edit_extended_price.confirm_changes')}
|
||||
onConfirmSendFormId="edit-extended-price">
|
||||
{extendedPriceData && <ExtendedPriceForm formId="edit-extended-price" onSubmit={handleUpdate} price={extendedPriceData} />}
|
||||
</FabModal>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,74 @@
|
||||
import React, { BaseSyntheticEvent } from 'react';
|
||||
import { Price } from '../../../models/price';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useImmer } from 'use-immer';
|
||||
import { FabInput } from '../../base/fab-input';
|
||||
import { IFablab } from '../../../models/fablab';
|
||||
|
||||
declare let Fablab: IFablab;
|
||||
|
||||
interface ExtendedPriceFormProps {
|
||||
formId: string,
|
||||
onSubmit: (price: Price) => void,
|
||||
price?: Price,
|
||||
}
|
||||
|
||||
/**
|
||||
* A form component to create/edit a extended price.
|
||||
* The form validation must be created elsewhere, using the attribute form={formId}.
|
||||
*/
|
||||
export const ExtendedPriceForm: React.FC<ExtendedPriceFormProps> = ({ formId, onSubmit, price }) => {
|
||||
const [extendedPriceData, updateExtendedPriceData] = useImmer<Price>(price || {} as Price);
|
||||
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
/**
|
||||
* Callback triggered when the user sends the form.
|
||||
*/
|
||||
const handleSubmit = (event: BaseSyntheticEvent): void => {
|
||||
event.preventDefault();
|
||||
onSubmit(extendedPriceData);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user inputs an amount for the current extended price.
|
||||
*/
|
||||
const handleUpdateAmount = (amount: string) => {
|
||||
updateExtendedPriceData(draft => {
|
||||
draft.amount = parseFloat(amount);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user inputs a number of minutes for the current extended price.
|
||||
*/
|
||||
const handleUpdateHours = (minutes: string) => {
|
||||
updateExtendedPriceData(draft => {
|
||||
draft.duration = parseInt(minutes, 10);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form id={formId} onSubmit={handleSubmit} className="extended-price-form">
|
||||
<label htmlFor="duration">{t('app.admin.extended_price_form.duration')} *</label>
|
||||
<FabInput id="duration"
|
||||
type="number"
|
||||
defaultValue={extendedPriceData?.duration || ''}
|
||||
onChange={handleUpdateHours}
|
||||
step={1}
|
||||
min={1}
|
||||
icon={<i className="fas fa-clock" />}
|
||||
required />
|
||||
<label htmlFor="amount">{t('app.admin.extended_price_form.amount')} *</label>
|
||||
<FabInput id="amount"
|
||||
type="number"
|
||||
step={0.01}
|
||||
min={0}
|
||||
defaultValue={extendedPriceData?.amount || ''}
|
||||
onChange={handleUpdateAmount}
|
||||
icon={<i className="fas fa-money-bill" />}
|
||||
addOn={Fablab.intl_currency}
|
||||
required />
|
||||
</form>
|
||||
);
|
||||
};
|
@ -0,0 +1,146 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { Loader } from '../../base/loader';
|
||||
import { FabAlert } from '../../base/fab-alert';
|
||||
import { HtmlTranslate } from '../../base/html-translate';
|
||||
import SpaceAPI from '../../../api/space';
|
||||
import GroupAPI from '../../../api/group';
|
||||
import { Group } from '../../../models/group';
|
||||
import { IApplication } from '../../../models/application';
|
||||
import { Space } from '../../../models/space';
|
||||
import { EditablePrice } from '../editable-price';
|
||||
import { ConfigureExtendedPriceButton } from './configure-extended-price-button';
|
||||
import PriceAPI from '../../../api/price';
|
||||
import { Price } from '../../../models/price';
|
||||
import { useImmer } from 'use-immer';
|
||||
import FormatLib from '../../../lib/format';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
interface SpacesPricingProps {
|
||||
onError: (message: string) => void,
|
||||
onSuccess: (message: string) => void,
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface to set and edit the prices of spaces-hours, per group
|
||||
*/
|
||||
const SpacesPricing: React.FC<SpacesPricingProps> = ({ onError, onSuccess }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [spaces, setSpaces] = useState<Array<Space>>(null);
|
||||
const [groups, setGroups] = useState<Array<Group>>(null);
|
||||
const [prices, updatePrices] = useImmer<Array<Price>>([]);
|
||||
|
||||
// retrieve the initial data
|
||||
useEffect(() => {
|
||||
SpaceAPI.index()
|
||||
.then(data => setSpaces(data))
|
||||
.catch(error => onError(error));
|
||||
GroupAPI.index({ disabled: false, admins: false })
|
||||
.then(data => setGroups(data))
|
||||
.catch(error => onError(error));
|
||||
PriceAPI.index({ priceable_type: 'Space', plan_id: null })
|
||||
.then(data => updatePrices(data))
|
||||
.catch(error => onError(error));
|
||||
}, []);
|
||||
|
||||
// duration of the example slot
|
||||
const EXEMPLE_DURATION = 20;
|
||||
|
||||
/**
|
||||
* Return the exemple price, formatted
|
||||
*/
|
||||
const examplePrice = (type: 'hourly_rate' | 'final_price'): string => {
|
||||
const hourlyRate = 10;
|
||||
|
||||
if (type === 'hourly_rate') {
|
||||
return FormatLib.price(hourlyRate);
|
||||
}
|
||||
|
||||
const price = (hourlyRate / 60) * EXEMPLE_DURATION;
|
||||
return FormatLib.price(price);
|
||||
};
|
||||
|
||||
/**
|
||||
* Find the default price (hourly rate) matching the given criterion
|
||||
*/
|
||||
const findPriceBy = (spaceId, groupId): Price => {
|
||||
return prices.find(price => price.priceable_id === spaceId && price.group_id === groupId && price.duration === 60);
|
||||
};
|
||||
|
||||
/**
|
||||
* Find prices matching the given criterion, except the default hourly rate
|
||||
*/
|
||||
const findExtendedPricesBy = (spaceId, groupId): Array<Price> => {
|
||||
return prices.filter(price => price.priceable_id === spaceId && price.group_id === groupId && price.duration !== 60);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the given price in the internal state
|
||||
*/
|
||||
const updatePrice = (price: Price): void => {
|
||||
updatePrices(draft => {
|
||||
const index = draft.findIndex(p => p.id === price.id);
|
||||
draft[index] = price;
|
||||
return draft;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user has confirmed to update a price
|
||||
*/
|
||||
const handleUpdatePrice = (price: Price): void => {
|
||||
PriceAPI.update(price)
|
||||
.then(() => {
|
||||
onSuccess(t('app.admin.spaces_pricing.price_updated'));
|
||||
updatePrice(price);
|
||||
})
|
||||
.catch(error => onError(error));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="spaces-pricing">
|
||||
<FabAlert level="warning">
|
||||
<p><HtmlTranslate trKey="app.admin.spaces_pricing.prices_match_space_hours_rates_html"/></p>
|
||||
<p><HtmlTranslate trKey="app.admin.spaces_pricing.prices_calculated_on_hourly_rate_html" options={{ DURATION: `${EXEMPLE_DURATION}`, RATE: examplePrice('hourly_rate'), PRICE: examplePrice('final_price') }} /></p>
|
||||
<p>{t('app.admin.spaces_pricing.you_can_override')}</p>
|
||||
<p>{t('app.admin.spaces_pricing.extended_prices')}</p>
|
||||
</FabAlert>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('app.admin.spaces_pricing.spaces')}</th>
|
||||
{groups?.map(group => <th key={group.id} className="group-name">{group.name}</th>)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{spaces?.map(space => <tr key={space.id}>
|
||||
<td>{space.name}</td>
|
||||
{groups?.map(group => <td key={group.id}>
|
||||
{prices.length && <EditablePrice price={findPriceBy(space.id, group.id)} onSave={handleUpdatePrice} />}
|
||||
<ConfigureExtendedPriceButton
|
||||
prices={findExtendedPricesBy(space.id, group.id)}
|
||||
onError={onError}
|
||||
onSuccess={onSuccess}
|
||||
groupId={group.id}
|
||||
priceableId={space.id}
|
||||
priceableType='Space' />
|
||||
</td>)}
|
||||
</tr>)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SpacesPricingWrapper: React.FC<SpacesPricingProps> = ({ onError, onSuccess }) => {
|
||||
return (
|
||||
<Loader>
|
||||
<SpacesPricing onError={onError} onSuccess={onSuccess} />
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
|
||||
Application.Components.component('spacesPricing', react2angular(SpacesPricingWrapper, ['onError', 'onSuccess']));
|
@ -407,10 +407,30 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state
|
||||
// check if slot is not in the past
|
||||
const today = new Date();
|
||||
if (Math.trunc((start.valueOf() - today) / (60 * 1000)) < 0) {
|
||||
growl.warning(_t('app.admin.calendar.event_in_the_past'));
|
||||
return uiCalendarConfig.calendars.calendar.fullCalendar('unselect');
|
||||
return dialogs.confirm({
|
||||
resolve: {
|
||||
object () {
|
||||
return {
|
||||
title: _t('app.admin.calendar.event_in_the_past'),
|
||||
msg: _t('app.admin.calendar.confirm_create_event_in_the_past')
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
function () { // confirmed
|
||||
startAvailabilityCreation(start, end);
|
||||
}, function () { // canceled
|
||||
uiCalendarConfig.calendars.calendar.fullCalendar('unselect');
|
||||
});
|
||||
}
|
||||
|
||||
startAvailabilityCreation(start, end);
|
||||
};
|
||||
|
||||
/**
|
||||
* Start the process to create a new availability between the given start and end datetimes
|
||||
*/
|
||||
const startAvailabilityCreation = function (start, end) {
|
||||
// check that the selected slot is an multiple of SLOT_MULTIPLE (ie. not decimal)
|
||||
const slots = Math.trunc((end.valueOf() - start.valueOf()) / (60 * 1000)) / SLOT_MULTIPLE;
|
||||
if (!Number.isInteger(slots)) {
|
||||
|
@ -92,6 +92,15 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
|
||||
active: false,
|
||||
templateUrl: '/admin/invoices/settings/editVAT.html'
|
||||
},
|
||||
multiVAT: {
|
||||
rateMachine: '',
|
||||
rateSpace: '',
|
||||
rateTraining: '',
|
||||
rateEvent: '',
|
||||
rateSubscription: '',
|
||||
editTemplateUrl: '/admin/invoices/settings/editMultiVAT.html',
|
||||
historyTemplateUrl: '/admin/invoices/settings/multiVATHistory.html'
|
||||
},
|
||||
text: {
|
||||
content: ''
|
||||
},
|
||||
@ -217,6 +226,14 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
|
||||
// Is shown the modal dialog to select a payment gateway
|
||||
$scope.openSelectGatewayModal = false;
|
||||
|
||||
/**
|
||||
* Return the VAT rate applicable to the machine reservations
|
||||
* @return {number}
|
||||
*/
|
||||
$scope.getMachineExampleRate = function () {
|
||||
return $scope.invoice.multiVAT.rateMachine || $scope.invoice.VAT.rate;
|
||||
};
|
||||
|
||||
/**
|
||||
* Change the invoices ordering criterion to the one provided
|
||||
* @param orderBy {string} ordering criterion
|
||||
@ -446,6 +463,9 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
|
||||
active () {
|
||||
return $scope.invoice.VAT.active;
|
||||
},
|
||||
multiVAT () {
|
||||
return $scope.invoice.multiVAT;
|
||||
},
|
||||
rateHistory () {
|
||||
return Setting.get({ name: 'invoice_VAT-rate', history: true }).$promise;
|
||||
},
|
||||
@ -453,13 +473,74 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
|
||||
return Setting.get({ name: 'invoice_VAT-active', history: true }).$promise;
|
||||
}
|
||||
},
|
||||
controller: ['$scope', '$uibModalInstance', 'rate', 'active', 'rateHistory', 'activeHistory', function ($scope, $uibModalInstance, rate, active, rateHistory, activeHistory) {
|
||||
controller: ['$scope', '$uibModalInstance', 'rate', 'active', 'rateHistory', 'activeHistory', 'multiVAT', function ($scope, $uibModalInstance, rate, active, rateHistory, activeHistory, multiVAT) {
|
||||
$scope.rate = rate;
|
||||
$scope.isSelected = active;
|
||||
$scope.history = [];
|
||||
|
||||
$scope.ok = function () { $uibModalInstance.close({ rate: $scope.rate, active: $scope.isSelected }); };
|
||||
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
|
||||
$scope.editMultiVAT = function () {
|
||||
const editMultiVATModalInstance = $uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: multiVAT.editTemplateUrl,
|
||||
size: 'lg',
|
||||
resolve: {
|
||||
rate () {
|
||||
return $scope.rate;
|
||||
},
|
||||
multiVAT () {
|
||||
return multiVAT;
|
||||
}
|
||||
},
|
||||
controller: ['$scope', '$uibModalInstance', 'rate', 'multiVAT', function ($scope, $uibModalInstance, rate, multiVAT) {
|
||||
$scope.rate = rate;
|
||||
$scope.multiVAT = multiVAT;
|
||||
|
||||
$scope.ok = function () { $uibModalInstance.close({ multiVAT: $scope.multiVAT }); };
|
||||
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
|
||||
|
||||
$scope.showMultiRateHistory = function (rateType) {
|
||||
$uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: multiVAT.historyTemplateUrl,
|
||||
size: 'lg',
|
||||
resolve: {
|
||||
rateHistory () {
|
||||
return Setting.get({ name: `invoice_VAT-rate_${rateType}`, history: true }).$promise;
|
||||
}
|
||||
},
|
||||
controller: ['$scope', '$uibModalInstance', 'rateHistory', function ($scope, $uibModalInstance, rateHistory) {
|
||||
$scope.history = [];
|
||||
|
||||
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
|
||||
|
||||
const initialize = function () {
|
||||
rateHistory.setting.history.forEach(function (rate) {
|
||||
$scope.history.push({ date: rate.created_at, rate: rate.value, user: rate.user });
|
||||
});
|
||||
};
|
||||
|
||||
initialize();
|
||||
}]
|
||||
});
|
||||
};
|
||||
}]
|
||||
});
|
||||
return editMultiVATModalInstance.result.then(function (result) {
|
||||
['Machine', 'Space', 'Training', 'Event', 'Subscription'].forEach(rateType => {
|
||||
Setting.update({ name: `invoice_VAT-rate_${rateType}` }, { value: result.multiVAT[`rate${rateType}`] + '' }, function (data) {
|
||||
return growl.success(_t('app.admin.invoices.VAT_rate_successfully_saved'));
|
||||
}
|
||||
, function (error) {
|
||||
if (error.status === 304) return;
|
||||
|
||||
growl.error(_t('app.admin.invoices.an_error_occurred_while_saving_the_VAT_rate'));
|
||||
console.error(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const initialize = function () {
|
||||
rateHistory.setting.history.forEach(function (rate) {
|
||||
@ -943,6 +1024,11 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
|
||||
$scope.invoice.text.content = settings.invoice_text;
|
||||
$scope.invoice.VAT.rate = parseFloat(settings['invoice_VAT-rate']);
|
||||
$scope.invoice.VAT.active = (settings['invoice_VAT-active'] === 'true');
|
||||
$scope.invoice.multiVAT.rateMachine = settings['invoice_VAT-rate_Machine'] ? parseFloat(settings['invoice_VAT-rate_Machine']) : '';
|
||||
$scope.invoice.multiVAT.rateSpace = settings['invoice_VAT-rate_Space'] ? parseFloat(settings['invoice_VAT-rate_Space']) : '';
|
||||
$scope.invoice.multiVAT.rateTraining = settings['invoice_VAT-rate_Training'] ? parseFloat(settings['invoice_VAT-rate_Training']) : '';
|
||||
$scope.invoice.multiVAT.rateEvent = settings['invoice_VAT-rate_Event'] ? parseFloat(settings['invoice_VAT-rate_Event']) : '';
|
||||
$scope.invoice.multiVAT.rateSubscription = settings['invoice_VAT-rate_Subscription'] ? parseFloat(settings['invoice_VAT-rate_Subscription']) : '';
|
||||
$scope.invoice.number.model = settings['invoice_order-nb'];
|
||||
$scope.invoice.code.model = settings['invoice_code-value'];
|
||||
$scope.invoice.code.active = (settings['invoice_code-active'] === 'true');
|
||||
@ -1328,6 +1414,16 @@ Application.Controllers.controller('AccountingExportModalController', ['$scope',
|
||||
decimalSeparator: ',',
|
||||
exportInvoicesAtZero: false,
|
||||
columns: ['journal_code', 'date', 'account_code', 'account_label', 'piece', 'line_label', 'debit_origin', 'credit_origin', 'debit_euro', 'credit_euro', 'lettering']
|
||||
},
|
||||
vat: {
|
||||
format: 'csv',
|
||||
encoding: 'UTF-8',
|
||||
separator: ';',
|
||||
dateFormat: '%Y-%m-%d',
|
||||
labelMaxLength: 'N/A',
|
||||
decimalSeparator: '.',
|
||||
exportInvoicesAtZero: false,
|
||||
columns: ['start_date', 'end_date', 'vat_rate', 'amount']
|
||||
}
|
||||
};
|
||||
|
||||
@ -1347,6 +1443,7 @@ Application.Controllers.controller('AccountingExportModalController', ['$scope',
|
||||
|
||||
// binding to radio button "export to"
|
||||
$scope.exportTarget = {
|
||||
type: null,
|
||||
software: null,
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
|
@ -461,7 +461,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state',
|
||||
*/
|
||||
$scope.findPriceBy = function (prices, machineId, groupId) {
|
||||
for (const price of Array.from(prices)) {
|
||||
if ((price.priceable_id === machineId) && (price.group_id === groupId)) {
|
||||
if ((price.priceable_id === machineId) && (price.group_id === groupId) && (price.duration === 60)) {
|
||||
return price;
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,18 @@
|
||||
'use strict';
|
||||
|
||||
Application.Controllers.controller('HeaderController', ['$scope', '$rootScope', '$state',
|
||||
function ($scope, $rootScope, $state) {
|
||||
Application.Controllers.controller('HeaderController', ['$scope', '$rootScope', '$state', 'settingsPromise',
|
||||
function ($scope, $rootScope, $state, settingsPromise) {
|
||||
$scope.aboutPage = ($state.current.name === 'app.public.about');
|
||||
|
||||
$rootScope.$on('$stateChangeStart', function (event, toState) {
|
||||
$scope.aboutPage = (toState.name === 'app.public.about');
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the current state of the public registration setting (allowed/blocked).
|
||||
*/
|
||||
$scope.registrationEnabled = function () {
|
||||
return settingsPromise.public_registrations === 'true';
|
||||
};
|
||||
}
|
||||
]);
|
||||
|
@ -13,7 +13,7 @@
|
||||
/**
|
||||
* Navigation controller. List the links availables in the left navigation pane and their icon.
|
||||
*/
|
||||
Application.Controllers.controller('MainNavController', ['$scope', function ($scope) {
|
||||
Application.Controllers.controller('MainNavController', ['$scope', 'settingsPromise', function ($scope, settingsPromise) {
|
||||
// Common links (public application)
|
||||
$scope.navLinks = [
|
||||
{
|
||||
@ -172,5 +172,12 @@ Application.Controllers.controller('MainNavController', ['$scope', function ($sc
|
||||
authorizedRoles: ['admin']
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current state of the public registration setting (allowed/blocked).
|
||||
*/
|
||||
$scope.registrationEnabled = function () {
|
||||
return settingsPromise.public_registrations === 'true';
|
||||
};
|
||||
}
|
||||
]);
|
||||
|
@ -4,7 +4,8 @@ export enum PaymentScheduleItemState {
|
||||
RequirePaymentMethod = 'requires_payment_method',
|
||||
RequireAction = 'requires_action',
|
||||
Paid = 'paid',
|
||||
Error = 'error'
|
||||
Error = 'error',
|
||||
GatewayCanceled = 'gateway_canceled'
|
||||
}
|
||||
|
||||
export enum PaymentMethod {
|
||||
@ -26,7 +27,7 @@ export interface PaymentSchedule {
|
||||
id: number,
|
||||
total: number,
|
||||
reference: string,
|
||||
payment_method: string,
|
||||
payment_method: 'card' | 'transfer' | '',
|
||||
items: Array<PaymentScheduleItem>,
|
||||
created_at: Date,
|
||||
chained_footprint: boolean,
|
||||
@ -43,9 +44,7 @@ export interface PaymentSchedule {
|
||||
first_name: string,
|
||||
last_name: string,
|
||||
},
|
||||
gateway_subscription: {
|
||||
classname: string
|
||||
}
|
||||
gateway: 'PayZen' | 'Stripe',
|
||||
}
|
||||
|
||||
export interface PaymentScheduleIndexRequest {
|
||||
|
@ -18,7 +18,8 @@ export interface IntentConfirmation {
|
||||
|
||||
export enum PaymentMethod {
|
||||
Card = 'card',
|
||||
Other = ''
|
||||
Check = 'check',
|
||||
Transfer = 'transfer'
|
||||
}
|
||||
|
||||
export type CartItem = { reservation: Reservation }|
|
||||
|
@ -11,7 +11,8 @@ export interface Price {
|
||||
plan_id: number,
|
||||
priceable_type: string,
|
||||
priceable_id: number,
|
||||
amount: number
|
||||
amount: number,
|
||||
duration?: number // in minutes
|
||||
}
|
||||
|
||||
export interface ComputePriceResult {
|
||||
|
@ -20,6 +20,11 @@ export enum SettingName {
|
||||
InvoiceOrderNb = 'invoice_order-nb',
|
||||
InvoiceVATActive = 'invoice_VAT-active',
|
||||
InvoiceVATRate = 'invoice_VAT-rate',
|
||||
InvoiceVATRateMachine = 'invoice_VAT-rate_Machine',
|
||||
InvoiceVATRateTraining = 'invoice_VAT-rate_Training',
|
||||
InvoiceVATRateSpace = 'invoice_VAT-rate_Space',
|
||||
InvoiceVATRateEvent = 'invoice_VAT-rate_Event',
|
||||
InvoiceVATRateSubscription = 'invoice_VAT-rate_Subscription',
|
||||
InvoiceText = 'invoice_text',
|
||||
InvoiceLegals = 'invoice_legals',
|
||||
BookingWindowStart = 'booking_window_start',
|
||||
@ -111,7 +116,9 @@ export enum SettingName {
|
||||
PublicAgendaModule = 'public_agenda_module',
|
||||
RenewPackThreshold = 'renew_pack_threshold',
|
||||
PackOnlyForSubscription = 'pack_only_for_subscription',
|
||||
OverlappingCategories = 'overlapping_categories'
|
||||
OverlappingCategories = 'overlapping_categories',
|
||||
ExtendedPricesInSameDay = 'extended_prices_in_same_day',
|
||||
PublicRegistrations = 'public_registrations'
|
||||
}
|
||||
|
||||
export type SettingValue = string|boolean|number;
|
||||
|
15
app/frontend/src/javascript/models/space.ts
Normal file
15
app/frontend/src/javascript/models/space.ts
Normal file
@ -0,0 +1,15 @@
|
||||
|
||||
export interface Space {
|
||||
id: number,
|
||||
name: string,
|
||||
description: string,
|
||||
slug: string,
|
||||
default_places: number,
|
||||
disabled: boolean,
|
||||
space_image: string,
|
||||
space_file_attributes?: {
|
||||
id: number,
|
||||
attachment: string,
|
||||
attachement_url: string,
|
||||
}
|
||||
}
|
@ -38,7 +38,8 @@ angular.module('application.router', ['ui.router'])
|
||||
logoFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'logo-file' }).$promise; }],
|
||||
logoBlackFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'logo-black-file' }).$promise; }],
|
||||
sharedTranslations: ['Translations', function (Translations) { return Translations.query(['app.shared', 'app.public.common']).$promise; }],
|
||||
modulesPromise: ['Setting', function (Setting) { return Setting.query({ names: "['spaces_module', 'plans_module', 'invoicing_module', 'wallet_module', 'statistics_module', 'trainings_module', 'public_agenda_module']" }).$promise; }]
|
||||
modulesPromise: ['Setting', function (Setting) { return Setting.query({ names: "['spaces_module', 'plans_module', 'invoicing_module', 'wallet_module', 'statistics_module', 'trainings_module', 'public_agenda_module']" }).$promise; }],
|
||||
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['public_registrations']" }).$promise; }]
|
||||
},
|
||||
onEnter: ['$rootScope', 'logoFile', 'logoBlackFile', 'modulesPromise', 'CSRF', function ($rootScope, logoFile, logoBlackFile, modulesPromise, CSRF) {
|
||||
// Retrieve Anti-CSRF tokens from cookies
|
||||
@ -869,7 +870,8 @@ angular.module('application.router', ['ui.router'])
|
||||
resolve: {
|
||||
settings: ['Setting', function (Setting) {
|
||||
return Setting.query({
|
||||
names: "['invoice_legals', 'invoice_text', 'invoice_VAT-rate', 'invoice_VAT-active', 'invoice_order-nb', 'invoice_code-value', " +
|
||||
names: "['invoice_legals', 'invoice_text', 'invoice_VAT-rate', 'invoice_VAT-rate_Machine', 'invoice_VAT-rate_Training', 'invoice_VAT-rate_Space', " +
|
||||
"'invoice_VAT-rate_Event', 'invoice_VAT-rate_Subscription', 'invoice_VAT-active', 'invoice_order-nb', 'invoice_code-value', " +
|
||||
"'invoice_code-active', 'invoice_reference', 'invoice_logo', 'accounting_journal_code', 'accounting_card_client_code', " +
|
||||
"'accounting_card_client_label', 'accounting_wallet_client_code', 'accounting_wallet_client_label', 'invoicing_module', " +
|
||||
"'accounting_other_client_code', 'accounting_other_client_label', 'accounting_wallet_code', 'accounting_wallet_label', " +
|
||||
@ -1080,7 +1082,8 @@ angular.module('application.router', ['ui.router'])
|
||||
"'reminder_delay', 'visibility_yearly', 'visibility_others', 'wallet_module', 'trainings_module', " +
|
||||
"'display_name_enable', 'machines_sort_by', 'fab_analytics', 'statistics_module', 'address_required', " +
|
||||
"'link_name', 'home_content', 'home_css', 'phone_required', 'upcoming_events_shown', 'public_agenda_module'," +
|
||||
"'renew_pack_threshold', 'pack_only_for_subscription', 'overlapping_categories']"
|
||||
"'renew_pack_threshold', 'pack_only_for_subscription', 'overlapping_categories', 'public_registrations'," +
|
||||
"'extended_prices_in_same_day']"
|
||||
}).$promise;
|
||||
}],
|
||||
privacyDraftsPromise: ['Setting', function (Setting) { return Setting.get({ name: 'privacy_draft', history: true }).$promise; }],
|
||||
|
@ -57,12 +57,18 @@
|
||||
@import "modules/machines/machines-filters";
|
||||
@import "modules/machines/required-training-modal";
|
||||
@import "modules/user/avatar";
|
||||
@import "modules/pricing/machines-pricing";
|
||||
@import "modules/pricing/editable-price";
|
||||
@import "modules/pricing/configure-packs-button";
|
||||
@import "modules/pricing/pack-form";
|
||||
@import "modules/pricing/delete-pack";
|
||||
@import "modules/pricing/edit-pack";
|
||||
@import "modules/pricing/machines/machines-pricing";
|
||||
@import "modules/pricing/machines/configure-packs-button";
|
||||
@import "modules/pricing/machines/pack-form";
|
||||
@import "modules/pricing/machines/delete-pack";
|
||||
@import "modules/pricing/machines/edit-pack";
|
||||
@import "modules/pricing/machines/create-pack";
|
||||
@import "modules/pricing/spaces/configure-extended-prices-button";
|
||||
@import "modules/pricing/spaces/create-extended-price";
|
||||
@import "modules/pricing/spaces/delete-extended-price";
|
||||
@import "modules/pricing/spaces/edit-extended-price";
|
||||
@import "modules/pricing/spaces/spaces-pricing";
|
||||
@import "modules/settings/check-list-setting";
|
||||
@import "modules/prepaid-packs/propose-packs-modal";
|
||||
@import "modules/prepaid-packs/packs-summary";
|
||||
|
@ -367,3 +367,7 @@ table.export-table-template {
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.multi-vat-rate-input {
|
||||
width: 90% !important;
|
||||
}
|
||||
|
@ -18,13 +18,6 @@
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
.popover-title {
|
||||
.add-pack-button {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.popover-content {
|
||||
ul {
|
@ -0,0 +1,7 @@
|
||||
.create-pack {
|
||||
.add-pack-button {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 10px;
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
.configure-extended-prices-button {
|
||||
display: inline-block;
|
||||
margin-left: 6px;
|
||||
position: relative;
|
||||
|
||||
.extended-prices-button {
|
||||
border: 1px solid #d0cccc;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
box-shadow: 0 1px 1px 0 #abaaaa;
|
||||
|
||||
&:hover {
|
||||
background-color: #b9b9b9;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.popover-content {
|
||||
ul {
|
||||
padding-left: 19px;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
&::before {
|
||||
content: '\f466';
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
position: absolute;
|
||||
left: 11px;
|
||||
font-weight: 800;
|
||||
font-size: 12px;
|
||||
vertical-align: middle;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.extended-prices-actions button {
|
||||
font-size: 10px;
|
||||
vertical-align: middle;
|
||||
line-height: 10px;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
.create-extended-price {
|
||||
.add-price-button {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 10px;
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
.delete-extended-price {
|
||||
display: inline;
|
||||
|
||||
.remove-price-button {
|
||||
background-color: #cb1117;
|
||||
color: white;
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
.edit-extended-price {
|
||||
display: inline-block;
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
.extended-price-form {
|
||||
.interval-inputs {
|
||||
display: flex;
|
||||
|
||||
.select-interval {
|
||||
min-width: 49%;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
.spaces-pricing {
|
||||
.fab-alert {
|
||||
margin: 15px 0;
|
||||
}
|
||||
table {
|
||||
overflow-y: scroll;
|
||||
thead > tr > th:first-child {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
thead > tr > th.group-name {
|
||||
width: 20%;
|
||||
text-transform: uppercase;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
thead > tr > th {
|
||||
vertical-align: bottom;
|
||||
border-bottom: 2px solid #ddd;
|
||||
padding: 8px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
tbody > tr > td {
|
||||
padding: 8px;
|
||||
line-height: 1.5;
|
||||
vertical-align: top;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
}
|
||||
}
|
@ -42,11 +42,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h4 class="control-label m-l" translate>{{ 'app.admin.invoices.export_to' }}</h4>
|
||||
<h4 class="control-label m-l" translate>{{ 'app.admin.invoices.export_what' }}</h4>
|
||||
<div class="form-group m-l-lg">
|
||||
<label for="acd">
|
||||
<label for="vat" class="block">
|
||||
<input type="radio" name="vat" id="vat" ng-model="exportTarget.software" ng-value="'vat'" ng-click="fillSettings('vat')" required/>
|
||||
{{ 'app.admin.invoices.export_VAT' | translate }}
|
||||
</label>
|
||||
<label for="acd" class="block">
|
||||
<input type="radio" name="acd" id="acd" ng-model="exportTarget.software" ng-value="'acd'" ng-click="fillSettings('acd')" required/>
|
||||
{{ 'app.admin.invoices.acd' | translate }}
|
||||
{{ 'app.admin.invoices.export_to_ACD' | translate }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -54,12 +54,12 @@
|
||||
</tr>
|
||||
|
||||
<tr class="invoice-vat invoice-editable vat-line italic" ng-click="openEditVAT()" ng-show="invoice.VAT.active">
|
||||
<td>{{ 'app.admin.invoices.including_VAT' | translate }} {{invoice.VAT.rate}} %</td>
|
||||
<td>{{30-(30/(invoice.VAT.rate/100+1)) | currency}}</td>
|
||||
<td translate translate-values="{RATE:getMachineExampleRate(), AMOUNT:(30.0 | currency)}">{{ 'app.admin.invoices.including_VAT' }}</td>
|
||||
<td>{{30-(30/(getMachineExampleRate()/100+1)) | currency}}</td>
|
||||
</tr>
|
||||
<tr class="invoice-ht vat-line italic" ng-show="invoice.VAT.active">
|
||||
<td translate>{{ 'app.admin.invoices.including_total_excluding_taxes' }}</td>
|
||||
<td>{{30/(invoice.VAT.rate/100+1) | currency}}</td>
|
||||
<td>{{30/(getMachineExampleRate()/100+1) | currency}}</td>
|
||||
</tr>
|
||||
<tr class="invoice-payed vat-line bold" ng-show="invoice.VAT.active">
|
||||
<td translate>{{ 'app.admin.invoices.including_amount_payed_on_ordering' }}</td>
|
||||
|
@ -0,0 +1,57 @@
|
||||
<div class="custom-invoice">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title" translate>{{ 'app.admin.invoices.multiVAT' }}</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<uib-alert type="warning">
|
||||
<p class="text-sm">
|
||||
<i class="fa fa-warning"></i>
|
||||
<span ng-bind-html="'app.admin.invoices.multi_VAT_notice' | translate:{ RATE: rate }"></span>
|
||||
</p>
|
||||
</uib-alert>
|
||||
<div class="form-group">
|
||||
<label for="vatRateMachine" class="control-label" translate>{{ 'app.admin.invoices.VAT_rate_machine' }}</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">% </span>
|
||||
<input id="vatRateMachine" type="number" ng-model="multiVAT.rateMachine" class="form-control multi-vat-rate-input" min="0" max="100"/>
|
||||
<button class="btn pull-right" ng-click="showMultiRateHistory('Machine')"><i class="fa fa-history"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="vatRateSpace" class="control-label" translate>{{ 'app.admin.invoices.VAT_rate_space' }}</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">% </span>
|
||||
<input id="vatRateSpace" type="number" ng-model="multiVAT.rateSpace" class="form-control multi-vat-rate-input" min="0" max="100"/>
|
||||
<button class="btn pull-right" ng-click="showMultiRateHistory('Space')"><i class="fa fa-history"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="vatRateTraining" class="control-label" translate>{{ 'app.admin.invoices.VAT_rate_training' }}</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">% </span>
|
||||
<input id="vatRateTraining" type="number" ng-model="multiVAT.rateTraining" class="form-control multi-vat-rate-input" min="0" max="100"/>
|
||||
<button class="btn pull-right" ng-click="showMultiRateHistory('Training')"><i class="fa fa-history"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="vatRateEvent" class="control-label" translate>{{ 'app.admin.invoices.VAT_rate_event' }}</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">% </span>
|
||||
<input id="vatRateEvent" type="number" ng-model="multiVAT.rateEvent" class="form-control multi-vat-rate-input" min="0" max="100"/>
|
||||
<button class="btn pull-right" ng-click="showMultiRateHistory('Event')"><i class="fa fa-history"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="vatRateSubscription" class="control-label" translate>{{ 'app.admin.invoices.VAT_rate_subscription' }}</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">% </span>
|
||||
<input id="vatRateSubscription" type="number" ng-model="multiVAT.rateSubscription" class="form-control multi-vat-rate-input" min="0" max="100"/>
|
||||
<button class="btn pull-right" ng-click="showMultiRateHistory('Subscription')"><i class="fa fa-history"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-warning" ng-click="ok()" translate>{{ 'app.shared.buttons.confirm' }}</button>
|
||||
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
|
||||
</div>
|
||||
</div>
|
@ -22,6 +22,12 @@
|
||||
<input id="vatRate" type="number" ng-model="rate" class="form-control" min="0" max="100"/>
|
||||
</div>
|
||||
</div>
|
||||
<uib-alert type="warning" ng-show="isSelected">
|
||||
<p class="text-sm">
|
||||
<i class="fa fa-warning"></i>
|
||||
<span>{{ 'app.admin.invoices.VAT_notice' | translate }}</span>
|
||||
</p>
|
||||
</uib-alert>
|
||||
|
||||
<div class="m-t-lg">
|
||||
<h4 translate>{{ 'app.admin.invoices.VAT_history' }}</h4>
|
||||
@ -48,6 +54,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-warning pull-left" ng-click="editMultiVAT()" ng-show="isSelected" translate>{{ 'app.admin.invoices.edit_multi_VAT_button' }}</button>
|
||||
<button class="btn btn-warning" ng-click="ok()" translate>{{ 'app.shared.buttons.confirm' }}</button>
|
||||
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
|
||||
</div>
|
||||
|
@ -0,0 +1,32 @@
|
||||
<div class="custom-invoice">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title" translate>{{ 'app.admin.invoices.VAT_history' }}</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div>
|
||||
<table class="table scrollable-3-cols">
|
||||
<thead>
|
||||
<tr>
|
||||
<th translate>{{ 'app.admin.invoices.VAT_rate' }}</th>
|
||||
<th translate>{{ 'app.admin.invoices.changed_at' }}</th>
|
||||
<th translate>{{ 'app.admin.invoices.changed_by' }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="value in history | orderBy:'-date'">
|
||||
<td>
|
||||
<span class="no-user-label" ng-show="value.enabled === false" translate>{{'app.admin.invoices.VAT_disabled'}}</span>
|
||||
<span class="no-user-label" ng-show="value.enabled === true" translate>{{'app.admin.invoices.VAT_enabled'}}</span>
|
||||
<span ng-show="value.rate">{{value.rate}}</span>
|
||||
</td>
|
||||
<td>{{value.date | amDateFormat:'L LT'}}</td>
|
||||
<td>{{value.user.name}}<span class="no-user-label" ng-hide="value.user" translate>{{ 'app.admin.invoices.deleted_user' }}</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
|
||||
</div>
|
||||
</div>
|
@ -1,29 +1 @@
|
||||
<div class="alert alert-warning m-t">
|
||||
<p ng-bind-html="'app.admin.pricing.these_prices_match_space_hours_rates_html' | translate"></p>
|
||||
<p ng-bind-html="'app.admin.pricing.prices_calculated_on_hourly_rate_html' | translate:{ DURATION:slotDuration, RATE: examplePrice('hourly_rate'), PRICE: examplePrice('final_price') }"></p>
|
||||
<p translate>{{ 'app.admin.pricing.you_can_override' }}</p>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:20%" translate>{{ 'app.admin.pricing.spaces' }}</th>
|
||||
<th style="width:20%" ng-repeat="group in enabledGroups">
|
||||
<span class="text-u-c text-sm">{{group.name}}</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="space in enabledSpaces">
|
||||
<td>
|
||||
{{ space.name }}
|
||||
</td>
|
||||
<td ng-repeat="group in enabledGroups">
|
||||
<span editable-number="findPriceBy(spacesPrices, space.id, group.id).amount"
|
||||
e-step="any"
|
||||
onbeforesave="updatePrice($data, findPriceBy(spacesPrices, space.id, group.id))">
|
||||
{{ findPriceBy(spacesPrices, space.id, group.id).amount | currency}}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<spaces-pricing on-success="onSuccess" on-error="onError">
|
||||
|
@ -412,6 +412,18 @@
|
||||
<span class="font-sbold" translate>{{ 'app.admin.settings.account_creation' }}</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<h3 class="m-l" translate>{{ 'app.admin.settings.general.public_registrations' }}</h3>
|
||||
<p class="alert alert-warning m-h-md" translate>
|
||||
{{ 'app.admin.settings.general.public_registrations_info' }}
|
||||
</p>
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<boolean-setting name="public_registrations"
|
||||
settings="allSettings"
|
||||
label="app.admin.settings.general.public_registrations_allowed">
|
||||
</boolean-setting>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h3 class="m-l" translate>{{ 'app.admin.settings.phone' }}</h3>
|
||||
<p class="alert alert-warning m-h-md" translate>
|
||||
|
@ -117,6 +117,28 @@
|
||||
required="true">
|
||||
</number-setting>
|
||||
</div>
|
||||
|
||||
<div class="section-separator"></div>
|
||||
<div class="row">
|
||||
<h3 class="m-l" translate>{{ 'app.admin.settings.pack_only_for_subscription_info' }}</h3>
|
||||
<p class="alert alert-warning m-h-md" ng-bind-html="'app.admin.settings.pack_only_for_subscription_info_html' | translate"></p>
|
||||
<boolean-setting name="pack_only_for_subscription"
|
||||
settings="allSettings"
|
||||
label="app.admin.settings.pack_only_for_subscription"
|
||||
classes="m-l">
|
||||
</boolean-setting>
|
||||
</div>
|
||||
|
||||
<div class="section-separator"></div>
|
||||
<div class="row">
|
||||
<h3 class="m-l" translate>{{ 'app.admin.settings.extended_prices' }}</h3>
|
||||
<p class="alert alert-warning m-h-md" ng-bind-html="'app.admin.settings.extended_prices_info_html' | translate"></p>
|
||||
<boolean-setting name="extended_prices_in_same_day"
|
||||
settings="allSettings"
|
||||
label="app.admin.settings.extended_prices_in_same_day"
|
||||
classes="m-l">
|
||||
</boolean-setting>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -170,6 +192,8 @@
|
||||
label="app.admin.settings.show_event"
|
||||
classes="m-l"></boolean-setting>
|
||||
</div>
|
||||
|
||||
<div class="section-separator"></div>
|
||||
<div class="row">
|
||||
<h3 class="m-l" translate>{{ 'app.admin.settings.display_invite_to_renew_pack' }}</h3>
|
||||
<p class="alert alert-warning m-h-md" ng-bind-html="'app.admin.settings.packs_threshold_info_html' | translate"></p>
|
||||
@ -182,14 +206,5 @@
|
||||
step="0.01">
|
||||
</number-setting>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h3 class="m-l" translate>{{ 'app.admin.settings.pack_only_for_subscription_info' }}</h3>
|
||||
<p class="alert alert-warning m-h-md" ng-bind-html="'app.admin.settings.pack_only_for_subscription_info_html' | translate"></p>
|
||||
<boolean-setting name="pack_only_for_subscription"
|
||||
settings="allSettings"
|
||||
label="app.admin.settings.pack_only_for_subscription"
|
||||
classes="m-l">
|
||||
</boolean-setting>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -53,7 +53,7 @@
|
||||
<li><a class="text-black pointer" ng-click="logout($event)"><i class="fa fa-power-off"></i> {{ 'app.public.common.sign_out' | translate }}</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li ng-if="!isAuthenticated()"><a class="font-sbold label text-md pointer" ng-click="signup($event)"><i class="fa fa-rocket"></i> {{ 'app.public.common.sign_up' | translate }}</a></li>
|
||||
<li ng-if="!isAuthenticated() && registrationEnabled()"><a class="font-sbold label text-md pointer" ng-click="signup($event)"><i class="fa fa-rocket"></i> {{ 'app.public.common.sign_up' | translate }}</a></li>
|
||||
<li ng-if="!isAuthenticated()">
|
||||
<a class="font-sbold label text-md pointer" ng-click="login($event)"><i class="fa fa-sign-in"></i> {{ 'app.public.common.sign_in' | translate }}</a>
|
||||
</li>
|
||||
|
@ -7,12 +7,12 @@
|
||||
<nav class="nav-primary hidden-xs">
|
||||
<ul class="nav nav-main m-t-xs" data-ride="collapse">
|
||||
<!-- Disconnected user menu for small devices -->
|
||||
<li class="hidden-sm hidden-md hidden-lg" ng-if-start="!isAuthenticated()">
|
||||
<li class="hidden-sm hidden-md hidden-lg" ng-if="!isAuthenticated() && registrationEnabled()">
|
||||
<a class="auto pointer" ng-click="signup($event)">
|
||||
<i class="fa fa-rocket"></i> <span translate>{{ 'app.public.common.sign_up' }}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="hidden-sm hidden-md hidden-lg" ng-if-end>
|
||||
<li class="hidden-sm hidden-md hidden-lg" ng-if="!isAuthenticated()">
|
||||
<a class="auto pointer" ng-click="login($event)">
|
||||
<i class="fa fa-sign-in"></i> <span translate>{{ 'app.public.common.sign_in' }}</span>
|
||||
</a>
|
||||
|
@ -34,7 +34,12 @@ class AccountingPeriod < ApplicationRecord
|
||||
def invoices_with_vat(invoices)
|
||||
vat_service = VatHistoryService.new
|
||||
invoices.map do |i|
|
||||
{ invoice: i, vat_rate: vat_service.invoice_vat(i) / 100.0 }
|
||||
vat_rate_group = {}
|
||||
i.invoice_items.each do |item|
|
||||
vat_type = item.invoice_item_type
|
||||
vat_rate_group[vat_type] = vat_service.invoice_item_vat(item) / 100.0 unless vat_rate_group[vat_type]
|
||||
end
|
||||
{ invoice: i, vat_rate: vat_rate_group }
|
||||
end
|
||||
end
|
||||
|
||||
@ -70,7 +75,7 @@ class AccountingPeriod < ApplicationRecord
|
||||
end
|
||||
|
||||
def price_without_taxe(invoice)
|
||||
invoice[:invoice].total - (invoice[:invoice].total * invoice[:vat_rate])
|
||||
invoice[:invoice].invoice_items.map(&:net_amount).sum
|
||||
end
|
||||
|
||||
def compute_totals
|
||||
|
@ -3,7 +3,7 @@
|
||||
MINUTES_PER_HOUR = 60.0
|
||||
SECONDS_PER_MINUTE = 60.0
|
||||
|
||||
GET_SLOT_PRICE_DEFAULT_OPTS = { has_credits: false, elements: nil, is_division: true, prepaid: { minutes: 0 } }.freeze
|
||||
GET_SLOT_PRICE_DEFAULT_OPTS = { has_credits: false, elements: nil, is_division: true, prepaid: { minutes: 0 }, custom_duration: nil }.freeze
|
||||
|
||||
# A generic reservation added to the shopping cart
|
||||
class CartItem::Reservation < CartItem::BaseItem
|
||||
@ -16,7 +16,6 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
end
|
||||
|
||||
def price
|
||||
base_amount = @reservable.prices.find_by(group_id: @customer.group_id, plan_id: @plan.try(:id)).amount
|
||||
is_privileged = @operator.privileged? && @operator.id != @customer.id
|
||||
prepaid = { minutes: PrepaidPackService.minutes_available(@customer, @reservable) }
|
||||
|
||||
@ -24,11 +23,14 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
amount = 0
|
||||
|
||||
hours_available = credits
|
||||
@slots.each_with_index do |slot, index|
|
||||
amount += get_slot_price(base_amount, slot, is_privileged,
|
||||
elements: elements,
|
||||
has_credits: (index < hours_available),
|
||||
prepaid: prepaid)
|
||||
grouped_slots.values.each do |slots|
|
||||
prices = applicable_prices(slots)
|
||||
slots.each_with_index do |slot, index|
|
||||
amount += get_slot_price_from_prices(prices, slot, is_privileged,
|
||||
elements: elements,
|
||||
has_credits: (index < hours_available),
|
||||
prepaid: prepaid)
|
||||
end
|
||||
end
|
||||
|
||||
{ elements: elements, amount: amount }
|
||||
@ -61,6 +63,36 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
0
|
||||
end
|
||||
|
||||
##
|
||||
# Group the slots by date, if the extended_prices_in_same_day option is set to true
|
||||
##
|
||||
def grouped_slots
|
||||
return { all: @slots } unless Setting.get('extended_prices_in_same_day')
|
||||
|
||||
@slots.group_by { |slot| slot[:start_at].to_date }
|
||||
end
|
||||
|
||||
##
|
||||
# Compute the price of a single slot, according to the list of applicable prices.
|
||||
# @param prices {{ prices: Array<{price: Price, duration: number}> }} list of prices to use with the current reservation
|
||||
# @see get_slot_price
|
||||
##
|
||||
def get_slot_price_from_prices(prices, slot, is_privileged, options = {})
|
||||
options = GET_SLOT_PRICE_DEFAULT_OPTS.merge(options)
|
||||
|
||||
slot_minutes = (slot[:end_at].to_time - slot[:start_at].to_time) / SECONDS_PER_MINUTE
|
||||
price = prices[:prices].find { |p| p[:duration] <= slot_minutes && p[:duration].positive? }
|
||||
price = prices[:prices].first if price.nil?
|
||||
hourly_rate = (price[:price].amount.to_f / price[:price].duration) * MINUTES_PER_HOUR
|
||||
|
||||
# apply the base price to the real slot duration
|
||||
real_price = get_slot_price(hourly_rate, slot, is_privileged, options)
|
||||
|
||||
price[:duration] -= slot_minutes
|
||||
|
||||
real_price
|
||||
end
|
||||
|
||||
##
|
||||
# Compute the price of a single slot, according to the base price and the ability for an admin
|
||||
# to offer the slot.
|
||||
@ -103,6 +135,33 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
real_price
|
||||
end
|
||||
|
||||
# We determine the list of prices applicable to current reservation
|
||||
# The longest available price is always used in priority.
|
||||
# Eg. If the reservation is for 12 hours, and there are prices for 3 hours, 7 hours,
|
||||
# and the base price (1 hours), we use the 7 hours price, then 3 hours price, and finally the base price twice (7+3+1+1 = 12).
|
||||
# All these prices are returned to be applied to the reservation.
|
||||
def applicable_prices(slots)
|
||||
total_duration = slots.map { |slot| (slot[:end_at].to_time - slot[:start_at].to_time) / SECONDS_PER_MINUTE }.reduce(:+)
|
||||
rates = { prices: [] }
|
||||
|
||||
remaining_duration = total_duration
|
||||
while remaining_duration.positive?
|
||||
max_duration = @reservable.prices.where(group_id: @customer.group_id, plan_id: @plan.try(:id))
|
||||
.where(Price.arel_table[:duration].lteq(remaining_duration))
|
||||
.maximum(:duration)
|
||||
max_duration = 60 if max_duration.nil?
|
||||
max_duration_price = @reservable.prices.find_by(group_id: @customer.group_id, plan_id: @plan.try(:id), duration: max_duration)
|
||||
|
||||
current_duration = [remaining_duration, max_duration].min
|
||||
rates[:prices].push(price: max_duration_price, duration: current_duration)
|
||||
|
||||
remaining_duration -= current_duration
|
||||
end
|
||||
|
||||
rates[:prices].sort! { |a, b| b[:duration] <=> a[:duration] }
|
||||
rates
|
||||
end
|
||||
|
||||
##
|
||||
# Compute the number of remaining hours in the users current credits (for machine or space)
|
||||
##
|
||||
|
@ -27,7 +27,7 @@ class InvoiceItem < Footprintable
|
||||
def net_amount
|
||||
# deduct VAT
|
||||
vat_service = VatHistoryService.new
|
||||
vat_rate = vat_service.invoice_vat(invoice)
|
||||
vat_rate = vat_service.invoice_item_vat(self)
|
||||
Rational(amount_after_coupon / (vat_rate / 100.00 + 1)).round.to_f
|
||||
end
|
||||
|
||||
@ -36,6 +36,19 @@ class InvoiceItem < Footprintable
|
||||
amount_after_coupon - net_amount
|
||||
end
|
||||
|
||||
# return invoice item type (Machine/Training/Space/Event/Subscription) used to determine the VAT rate
|
||||
def invoice_item_type
|
||||
if object_type == Reservation.name
|
||||
object.try(:reservable_type) || ''
|
||||
elsif [Subscription.name, OfferDay.name].include? object_type
|
||||
Subscription.name
|
||||
elsif object_type == StatisticProfilePrepaidPack.name
|
||||
object.prepaid_pack.priceable_type
|
||||
else
|
||||
''
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def log_changes
|
||||
|
@ -58,6 +58,7 @@ class NotificationType
|
||||
notify_admin_payment_schedule_failed
|
||||
notify_member_payment_schedule_failed
|
||||
notify_admin_payment_schedule_check_deadline
|
||||
notify_admin_payment_schedule_transfer_deadline
|
||||
]
|
||||
# deprecated:
|
||||
# - notify_member_subscribed_plan_is_changed
|
||||
|
@ -14,9 +14,10 @@ class PaymentScheduleItem < Footprintable
|
||||
|
||||
def payment_intent
|
||||
return unless payment_gateway_object
|
||||
return unless payment_gateway_object.gateway_object.gateway == 'Stripe'
|
||||
|
||||
stp_invoice = payment_gateway_object.gateway_object.retrieve
|
||||
Stripe::PaymentIntent.retrieve(stp_invoice.payment_intent, api_key: Setting.get('stripe_secret_key')) # FIXME, maybe this is only used for stripe?
|
||||
Stripe::PaymentIntent.retrieve(stp_invoice.payment_intent, api_key: Setting.get('stripe_secret_key'))
|
||||
end
|
||||
|
||||
def self.columns_out_of_footprint
|
||||
|
@ -7,5 +7,9 @@ class Price < ApplicationRecord
|
||||
belongs_to :priceable, polymorphic: true
|
||||
|
||||
validates :priceable, :group_id, :amount, presence: true
|
||||
validates :priceable_id, uniqueness: { scope: %i[priceable_type plan_id group_id] }
|
||||
validates :priceable_id, uniqueness: { scope: %i[priceable_type plan_id group_id duration] }
|
||||
|
||||
def safe_destroy
|
||||
destroy unless duration == 60
|
||||
end
|
||||
end
|
||||
|
@ -28,6 +28,11 @@ class Setting < ApplicationRecord
|
||||
invoice_order-nb
|
||||
invoice_VAT-active
|
||||
invoice_VAT-rate
|
||||
invoice_VAT-rate_Machine
|
||||
invoice_VAT-rate_Training
|
||||
invoice_VAT-rate_Space
|
||||
invoice_VAT-rate_Event
|
||||
invoice_VAT-rate_Subscription
|
||||
invoice_text
|
||||
invoice_legals
|
||||
booking_window_start
|
||||
@ -121,7 +126,9 @@ class Setting < ApplicationRecord
|
||||
public_agenda_module
|
||||
renew_pack_threshold
|
||||
pack_only_for_subscription
|
||||
overlapping_categories] }
|
||||
overlapping_categories
|
||||
extended_prices_in_same_day
|
||||
public_registrations] }
|
||||
# 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
|
||||
|
@ -230,10 +230,12 @@ class PDF::Invoice < Prawn::Document
|
||||
|
||||
# TVA
|
||||
vat_service = VatHistoryService.new
|
||||
vat_rate = vat_service.invoice_vat(invoice)
|
||||
if vat_rate != 0
|
||||
vat_rate_group = vat_service.invoice_vat(invoice)
|
||||
if total_vat != 0
|
||||
data += [[I18n.t('invoices.total_including_all_taxes'), number_to_currency(total)]]
|
||||
data += [[I18n.t('invoices.including_VAT_RATE', RATE: vat_rate), number_to_currency(total_vat / 100.00)]]
|
||||
vat_rate_group.each do |_type, rate|
|
||||
data += [[I18n.t('invoices.including_VAT_RATE', RATE: rate[:vat_rate], AMOUNT: number_to_currency(rate[:amount] / 100.00)), number_to_currency(rate[:total_vat] / 100.00)]]
|
||||
end
|
||||
data += [[I18n.t('invoices.including_total_excluding_taxes'), number_to_currency(total_ht / 100.00)]]
|
||||
data += [[I18n.t('invoices.including_amount_payed_on_ordering'), number_to_currency(total)]]
|
||||
|
||||
@ -252,23 +254,25 @@ class PDF::Invoice < Prawn::Document
|
||||
row(0).font_style = :bold
|
||||
column(1).style align: :right
|
||||
|
||||
if Setting.get('invoice_VAT-active')
|
||||
if total_vat != 0
|
||||
# Total incl. taxes
|
||||
row(-1).style align: :right
|
||||
row(-1).background_color = 'E4E4E4'
|
||||
row(-1).font_style = :bold
|
||||
# including VAT xx%
|
||||
row(-2).style align: :right
|
||||
row(-2).background_color = 'E4E4E4'
|
||||
row(-2).font_style = :italic
|
||||
vat_rate_group.size.times do |i|
|
||||
# including VAT xx%
|
||||
row(-2 - i).style align: :right
|
||||
row(-2 - i).background_color = 'E4E4E4'
|
||||
row(-2 - i).font_style = :italic
|
||||
end
|
||||
# including total excl. taxes
|
||||
row(-3).style align: :right
|
||||
row(-3).background_color = 'E4E4E4'
|
||||
row(-3).font_style = :italic
|
||||
row(-3 - vat_rate_group.size + 1).style align: :right
|
||||
row(-3 - vat_rate_group.size + 1).background_color = 'E4E4E4'
|
||||
row(-3 - vat_rate_group.size + 1).font_style = :italic
|
||||
# including amount payed on ordering
|
||||
row(-4).style align: :right
|
||||
row(-4).background_color = 'E4E4E4'
|
||||
row(-4).font_style = :bold
|
||||
row(-4 - vat_rate_group.size + 1).style align: :right
|
||||
row(-4 - vat_rate_group.size + 1).background_color = 'E4E4E4'
|
||||
row(-4 - vat_rate_group.size + 1).font_style = :bold
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -2,13 +2,13 @@
|
||||
|
||||
# Check the access policies for API::PaymentSchedulesController
|
||||
class PaymentSchedulePolicy < ApplicationPolicy
|
||||
%w[list? cash_check? cancel?].each do |action|
|
||||
%w[list? cash_check? confirm_transfer? cancel?].each do |action|
|
||||
define_method action do
|
||||
user.admin? || user.manager?
|
||||
end
|
||||
end
|
||||
|
||||
%w[refresh_item? download? pay_item?].each do |action|
|
||||
%w[refresh_item? download? pay_item? show_item?].each do |action|
|
||||
define_method action do
|
||||
user.admin? || user.manager? || (record.invoicing_profile.user_id == user.id)
|
||||
end
|
||||
|
@ -2,6 +2,14 @@
|
||||
|
||||
# Check the access policies for API::PricesController
|
||||
class PricePolicy < ApplicationPolicy
|
||||
def create?
|
||||
user.admin? && record.duration != 60
|
||||
end
|
||||
|
||||
def destroy?
|
||||
user.admin? && record.duration != 60
|
||||
end
|
||||
|
||||
def update?
|
||||
user.admin?
|
||||
end
|
||||
|
@ -28,7 +28,7 @@ class SettingPolicy < ApplicationPolicy
|
||||
end
|
||||
|
||||
##
|
||||
# Every settings that anyone can read. The other settings are restricted for admins.
|
||||
# List of settings that anyone can read. The other settings are restricted for admins.
|
||||
# This list must be manually updated if a new setting should be world-readable
|
||||
##
|
||||
def self.public_whitelist
|
||||
@ -40,11 +40,11 @@ class SettingPolicy < ApplicationPolicy
|
||||
recaptcha_site_key feature_tour_display disqus_shortname allowed_cad_extensions openlab_app_id openlab_default
|
||||
online_payment_module stripe_public_key confirmation_required wallet_module trainings_module address_required
|
||||
payment_gateway payzen_endpoint payzen_public_key public_agenda_module renew_pack_threshold statistics_module
|
||||
pack_only_for_subscription overlapping_categories]
|
||||
pack_only_for_subscription overlapping_categories public_registrations]
|
||||
end
|
||||
|
||||
##
|
||||
# Every settings that only admins can read.
|
||||
# List of settings that only admins can read.
|
||||
# This blacklist is automatically generated from the public_whitelist above.
|
||||
##
|
||||
def self.public_blacklist
|
||||
|
@ -16,7 +16,6 @@ class AccountingExportService
|
||||
@label_max_length = 50
|
||||
@export_zeros = false
|
||||
@journal_code = Setting.get('accounting_journal_code') || ''
|
||||
@date_format = date_format
|
||||
@columns = columns
|
||||
end
|
||||
|
||||
@ -134,9 +133,9 @@ class AccountingExportService
|
||||
|
||||
# Generate the "VAT" row, which contains the credit to the VAT account, with VAT amount only
|
||||
def vat_row(invoice)
|
||||
rate = VatHistoryService.new.invoice_vat(invoice)
|
||||
total = invoice.invoice_items.map(&:net_amount).sum
|
||||
# we do not render the VAT row if it was disabled for this invoice
|
||||
return nil if rate.zero?
|
||||
return nil if total == invoice.total
|
||||
|
||||
row(
|
||||
invoice,
|
||||
|
@ -23,6 +23,11 @@ class PaymentGatewayService
|
||||
@gateway.create_subscription(payment_schedule, *args)
|
||||
end
|
||||
|
||||
def cancel_subscription(payment_schedule)
|
||||
gateway = service_for_payment_schedule(payment_schedule)
|
||||
gateway.cancel_subscription(payment_schedule)
|
||||
end
|
||||
|
||||
def create_user(user_id)
|
||||
@gateway.create_user(user_id)
|
||||
end
|
||||
@ -52,7 +57,7 @@ class PaymentGatewayService
|
||||
private
|
||||
|
||||
def service_for_payment_schedule(payment_schedule)
|
||||
service = case payment_schedule.gateway_subscription.klass
|
||||
service = case payment_schedule.gateway_subscription&.klass
|
||||
when /^PayZen::/
|
||||
require 'pay_zen/service'
|
||||
PayZen::Service
|
||||
|
@ -127,7 +127,7 @@ class PaymentScheduleService
|
||||
# @param filters {Hash} allowed filters: reference, customer, date.
|
||||
##
|
||||
def self.list(page, size, filters = {})
|
||||
ps = PaymentSchedule.includes(:invoicing_profile, :payment_schedule_items)
|
||||
ps = PaymentSchedule.includes(:invoicing_profile, :payment_schedule_items, :payment_schedule_objects)
|
||||
.joins(:invoicing_profile)
|
||||
.order('payment_schedules.created_at DESC')
|
||||
.page(page)
|
||||
@ -158,6 +158,8 @@ class PaymentScheduleService
|
||||
end
|
||||
|
||||
def self.cancel(payment_schedule)
|
||||
PaymentGatewayService.new.cancel_subscription(payment_schedule)
|
||||
|
||||
# cancel all item where state != paid
|
||||
payment_schedule.ordered_items.each do |item|
|
||||
next if item.state == 'paid'
|
||||
|
@ -61,10 +61,7 @@ class PrepaidPackService
|
||||
|
||||
## Total number of prepaid minutes available
|
||||
def minutes_available(user, priceable)
|
||||
is_pack_only_for_subscription = Setting.find_by(name: "pack_only_for_subscription")&.value
|
||||
if is_pack_only_for_subscription == 'true' && !user.subscribed_plan
|
||||
return 0
|
||||
end
|
||||
return 0 if Setting.get('pack_only_for_subscription') && !user.subscribed_plan
|
||||
|
||||
user_packs = user_packs(user, priceable)
|
||||
total_available = user_packs.map { |up| up.prepaid_pack.minutes }.reduce(:+) || 0
|
||||
|
97
app/services/vat_export_service.rb
Normal file
97
app/services/vat_export_service.rb
Normal file
@ -0,0 +1,97 @@
|
||||
# frozen_string_literal: false
|
||||
|
||||
# Provides the routine to export the collected VAT data to a CSV file.
|
||||
class VatExportService
|
||||
include ActionView::Helpers::NumberHelper
|
||||
|
||||
attr_reader :encoding, :format, :separator, :date_format, :columns, :decimal_separator
|
||||
|
||||
def initialize(columns, encoding: 'UTF-8', format: 'CSV', separator: ';')
|
||||
@encoding = encoding
|
||||
@format = format
|
||||
@separator = separator
|
||||
@decimal_separator = '.'
|
||||
@date_format = '%Y-%m-%d'
|
||||
@columns = columns
|
||||
end
|
||||
|
||||
def set_options(decimal_separator: ',', date_format: '%d/%m/%Y', label_max_length: nil, export_zeros: nil)
|
||||
@decimal_separator = decimal_separator
|
||||
@date_format = date_format
|
||||
end
|
||||
|
||||
def export(start_date, end_date, file)
|
||||
# build CSV content
|
||||
content = header_row
|
||||
invoices = Invoice.where('created_at >= ? AND created_at <= ?', start_date, end_date).order('created_at ASC')
|
||||
vat_totals = compute_vat_totals(invoices)
|
||||
content << generate_rows(vat_totals, start_date, end_date)
|
||||
|
||||
# write content to file
|
||||
File.open(file, "w:#{encoding}") { |f| f.puts content.encode(encoding, invalid: :replace, undef: :replace) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def header_row
|
||||
row = ''
|
||||
columns.each do |column|
|
||||
row << I18n.t("vat_export.#{column}") << separator
|
||||
end
|
||||
"#{row}\n"
|
||||
end
|
||||
|
||||
def generate_rows(vat_totals, start_date, end_date)
|
||||
rows = ''
|
||||
|
||||
vat_totals.each do |rate, total|
|
||||
next if rate.zero?
|
||||
|
||||
rows += "#{row(
|
||||
start_date,
|
||||
end_date,
|
||||
rate,
|
||||
total
|
||||
)}\n"
|
||||
end
|
||||
|
||||
rows
|
||||
end
|
||||
|
||||
def compute_vat_totals(invoices)
|
||||
vat_total = []
|
||||
service = VatHistoryService.new
|
||||
invoices.each do |i|
|
||||
puts "processing invoice #{i.id}..." unless Rails.env.test?
|
||||
vat_total.push service.invoice_vat(i)
|
||||
end
|
||||
|
||||
vat_total.map(&:values).flatten.group_by { |tot| tot[:vat_rate] }.map { |k, v| [k, v.map { |t| t[:total_vat] }.reduce(:+)] }.to_h
|
||||
end
|
||||
|
||||
# Generate a row of the export, filling the configured columns with the provided values
|
||||
def row(start_date, end_date, vat_rate, amount)
|
||||
row = ''
|
||||
columns.each do |column|
|
||||
case column
|
||||
when 'start_date'
|
||||
row << DateTime.parse(start_date).strftime(date_format)
|
||||
when 'end_date'
|
||||
row << DateTime.parse(end_date).strftime(date_format)
|
||||
when 'vat_rate'
|
||||
row << vat_rate.to_s
|
||||
when 'amount'
|
||||
row << format_number(amount / 100.0)
|
||||
else
|
||||
puts "Unsupported column: #{column}"
|
||||
end
|
||||
row << separator
|
||||
end
|
||||
row
|
||||
end
|
||||
|
||||
# Format the given number as a string, using the configured separator
|
||||
def format_number(num)
|
||||
number_to_currency(num, unit: '', separator: decimal_separator, delimiter: '', precision: 2)
|
||||
end
|
||||
end
|
@ -2,30 +2,42 @@
|
||||
|
||||
# Provides the VAT rate in use at the given date
|
||||
class VatHistoryService
|
||||
# return the VAT rate for the given Invoice/Avoir
|
||||
# @return the VAT rate for the given Invoice
|
||||
def invoice_vat(invoice)
|
||||
if invoice.is_a?(Avoir)
|
||||
vat_rate(invoice.avoir_date)
|
||||
vat_rate_group = {}
|
||||
invoice.invoice_items.each do |item|
|
||||
vat_type = item.invoice_item_type
|
||||
vat_rate_group[vat_type] = { vat_rate: invoice_item_vat(item), total_vat: 0, amount: 0 } unless vat_rate_group[vat_type]
|
||||
vat_rate_group[vat_type][:total_vat] += item.vat
|
||||
vat_rate_group[vat_type][:amount] += item.amount.to_i
|
||||
end
|
||||
vat_rate_group
|
||||
end
|
||||
|
||||
# return the VAT rate for the given InvoiceItem
|
||||
def invoice_item_vat(invoice_item)
|
||||
if invoice_item.invoice.is_a?(Avoir)
|
||||
vat_rate(invoice_item.invoice.avoir_date, invoice_item.invoice_item_type)
|
||||
else
|
||||
vat_rate(invoice.created_at)
|
||||
vat_rate(invoice_item.invoice.created_at, invoice_item.invoice_item_type)
|
||||
end
|
||||
end
|
||||
|
||||
# return the VAT rate for the given date
|
||||
def vat_rate(date)
|
||||
@vat_rates = vat_history if @vat_rates.nil?
|
||||
# return the VAT rate for the given date and vat type
|
||||
def vat_rate(date, vat_rate_type)
|
||||
vat_rates = vat_history(vat_rate_type)
|
||||
|
||||
first_rate = @vat_rates.first
|
||||
first_rate = vat_rates.first
|
||||
return first_rate[:rate] if date < first_rate[:date]
|
||||
|
||||
@vat_rates.each_index do |i|
|
||||
return @vat_rates[i][:rate] if date >= @vat_rates[i][:date] && (@vat_rates[i + 1].nil? || date < @vat_rates[i + 1][:date])
|
||||
vat_rates.each_index do |i|
|
||||
return vat_rates[i][:rate] if date >= vat_rates[i][:date] && (vat_rates[i + 1].nil? || date < vat_rates[i + 1][:date])
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def vat_history
|
||||
def vat_history(vat_rate_type)
|
||||
chronology = []
|
||||
end_date = DateTime.current
|
||||
Setting.find_by(name: 'invoice_VAT-active').history_values.order(created_at: 'DESC').each do |v|
|
||||
@ -33,15 +45,65 @@ class VatHistoryService
|
||||
end_date = v.created_at
|
||||
end
|
||||
chronology.push(start: DateTime.new(0), end: end_date, enabled: false)
|
||||
# now chronology contains something like one of the following:
|
||||
# - [{start: 0000-01-01, end: now, enabled: false}] => VAT was never enabled
|
||||
# - [
|
||||
# {start: fab-manager initial setup date, end: now, enabled: true},
|
||||
# {start: 0000-01-01, end: fab-manager initial setup date, enabled: false}
|
||||
# ] => VAT was enabled from the beginning
|
||||
# - [
|
||||
# {start: [date disabled], end: now, enabled: false},
|
||||
# {start: [date enable], end: [date disabled], enabled: true},
|
||||
# {start: fab-manager initial setup date, end: [date enabled], enabled: false},
|
||||
# {start: 0000-01-01, end: fab-manager initial setup date, enabled: false}
|
||||
# ] => VAT was enabled at some point, and disabled at some other point later
|
||||
|
||||
date_rates = []
|
||||
Setting.find_by(name: 'invoice_VAT-rate').history_values.order(created_at: 'ASC').each do |rate|
|
||||
range = chronology.select { |p| rate.created_at.to_i.between?(p[:start].to_i, p[:end].to_i) }.first
|
||||
date = range[:enabled] ? rate.created_at : range[:end]
|
||||
date_rates.push(date: date, rate: rate.value.to_i)
|
||||
end
|
||||
chronology.reverse_each do |period|
|
||||
date_rates.push(date: period[:start], rate: 0) unless period[:enabled]
|
||||
if vat_rate_type.present?
|
||||
vat_rate_by_type = Setting.find_by(name: "invoice_VAT-rate_#{vat_rate_type}")&.history_values&.order(created_at: 'ASC')
|
||||
first_vat_rate_by_type = vat_rate_by_type&.select { |v| v.value.present? }&.first
|
||||
if first_vat_rate_by_type
|
||||
# before the first VAT rate was defined for the given type, the general VAT rate is used
|
||||
vat_rate_history_values = Setting.find_by(name: 'invoice_VAT-rate')
|
||||
.history_values.where('created_at < ?', first_vat_rate_by_type.created_at)
|
||||
.order(created_at: 'ASC').to_a
|
||||
# after that, the VAT rate for the given type is used
|
||||
vat_rate_by_type = Setting.find_by(name: "invoice_VAT-rate_#{vat_rate_type}")
|
||||
.history_values.where('created_at >= ?', first_vat_rate_by_type.created_at)
|
||||
.order(created_at: 'ASC')
|
||||
vat_rate_by_type.each do |rate|
|
||||
if rate.value.blank?
|
||||
# if, at some point in the history, a blank rate was set, the general VAT rate is used instead
|
||||
vat_rate = Setting.find_by(name: 'invoice_VAT-rate')
|
||||
.history_values.where('created_at < ?', rate.created_at)
|
||||
.order(created_at: 'DESC')
|
||||
.first
|
||||
rate.value = vat_rate.value
|
||||
end
|
||||
vat_rate_history_values.push(rate)
|
||||
end
|
||||
else
|
||||
# if no VAT rate is defined for the given type, the general VAT rate is always used
|
||||
vat_rate_history_values = Setting.find_by(name: 'invoice_VAT-rate').history_values.order(created_at: 'ASC').to_a
|
||||
end
|
||||
|
||||
# Now we have all the rates history, we can build the final chronology, depending on whether VAT was enabled or not
|
||||
vat_rate_history_values.each do |rate|
|
||||
# when the VAT rate was enabled, set the date it was enabled and the rate
|
||||
range = chronology.select { |p| rate.created_at.to_i.between?(p[:start].to_i, p[:end].to_i) }.first
|
||||
date = range[:enabled] ? rate.created_at : range[:end]
|
||||
date_rates.push(date: date, rate: rate.value.to_i)
|
||||
end
|
||||
chronology.reverse_each do |period|
|
||||
# when the VAT rate was disabled, set the date it was disabled and rate=0
|
||||
date_rates.push(date: period[:start], rate: 0) unless period[:enabled]
|
||||
end
|
||||
else
|
||||
# if no VAT rate type is given, we return rate=0 from 0000-01-01
|
||||
date_rates.push(date: chronology[-1][:start], rate: 0)
|
||||
end
|
||||
|
||||
# finally, we return the chronology, sorted by dates (ascending)
|
||||
date_rates.sort_by { |k| k[:date] }
|
||||
end
|
||||
end
|
||||
|
@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.title notification.notification_type
|
||||
json.description t('.schedule_deadline', DATE: I18n.l(notification.attached_object.due_date.to_date),
|
||||
REFERENCE: notification.attached_object.payment_schedule.reference)
|
@ -18,13 +18,9 @@ json.main_object do
|
||||
json.id payment_schedule.main_object.object_id
|
||||
end
|
||||
if payment_schedule.gateway_subscription
|
||||
json.gateway_subscription do
|
||||
# this attribute is used to known which gateway should we interact with, in the front-end
|
||||
json.classname payment_schedule.gateway_subscription.klass
|
||||
end
|
||||
# this attribute is used to known which gateway should we interact with, in the front-end
|
||||
json.gateway json.classname payment_schedule.gateway_subscription.gateway
|
||||
end
|
||||
json.items payment_schedule.payment_schedule_items do |item|
|
||||
json.extract! item, :id, :due_date, :state, :invoice_id, :payment_method
|
||||
json.amount item.amount / 100.00
|
||||
json.client_secret item.payment_intent.client_secret if item.payment_gateway_object && item.state == 'requires_action'
|
||||
json.partial! 'api/payment_schedules/payment_schedule_item', item: item
|
||||
end
|
||||
|
@ -0,0 +1,4 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.extract! item, :id, :due_date, :state, :invoice_id, :payment_method
|
||||
json.amount item.amount / 100.00
|
6
app/views/api/payment_schedules/show_item.json.jbuilder
Normal file
6
app/views/api/payment_schedules/show_item.json.jbuilder
Normal file
@ -0,0 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.partial! 'api/payment_schedules/payment_schedule_item', item: @payment_schedule_item
|
||||
if @payment_schedule_item.payment_gateway_object && @payment_schedule_item.state == 'requires_action'
|
||||
json.client_secret @payment_schedule_item.payment_intent.client_secret
|
||||
end
|
@ -1,2 +1,4 @@
|
||||
json.extract! price, :id, :group_id, :plan_id, :priceable_type, :priceable_id
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.extract! price, :id, :group_id, :plan_id, :priceable_type, :priceable_id, :duration
|
||||
json.amount price.amount / 100.0
|
||||
|
@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.price @amount[:total] / 100.00
|
||||
json.price_without_coupon @amount[:before_coupon] / 100.00
|
||||
if @amount[:elements]
|
||||
|
3
app/views/api/prices/create.json.jbuilder
Normal file
3
app/views/api/prices/create.json.jbuilder
Normal file
@ -0,0 +1,3 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.partial! 'api/prices/price', price: @price
|
@ -1 +1,3 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.partial! 'api/prices/price', collection: @prices, as: :price
|
||||
|
@ -1 +1,3 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.partial! 'api/prices/price', price: @price
|
||||
|
@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.array!(@spaces) do |space|
|
||||
json.extract! space, :id, :name, :description, :slug, :default_places, :disabled
|
||||
json.space_image space.space_image.attachment.medium.url if space.space_image
|
||||
|
@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.extract! @space, :id, :name, :description, :characteristics, :created_at, :updated_at, :slug, :default_places, :disabled
|
||||
json.space_image @space.space_image.attachment.large.url if @space.space_image
|
||||
json.space_files_attributes @space.space_files do |f|
|
||||
@ -9,4 +11,4 @@ end
|
||||
# using the space in the space_show screen
|
||||
# json.space_projects @space.projects do |p|
|
||||
# json.extract! p, :slug, :name
|
||||
# end
|
||||
# end
|
||||
|
@ -34,7 +34,7 @@ json.invoices do
|
||||
json.id item.object_id
|
||||
json.main item.main
|
||||
end
|
||||
json.partial! 'archive/vat', price: item.amount, vat_rate: invoice[:vat_rate]
|
||||
json.partial! 'archive/vat', price: item.amount, vat_rate: invoice[:vat_rate][item.invoice_item_type]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -0,0 +1,10 @@
|
||||
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
|
||||
|
||||
<p>
|
||||
<%= t('.body.remember',
|
||||
REFERENCE: @attached_object.payment_schedule.reference,
|
||||
AMOUNT: number_to_currency(@attached_object.amount / 100.00),
|
||||
DATE: I18n.l(@attached_object.due_date, format: :long)) %>
|
||||
<%= t('.body.date') %>
|
||||
</p>
|
||||
<p><%= t('.body.confirm') %></p>
|
@ -10,7 +10,8 @@ class AccountingExportWorker
|
||||
raise SecurityError, 'Not allowed to export' unless export.user.admin?
|
||||
|
||||
data = JSON.parse(export.query)
|
||||
service = AccountingExportService.new(
|
||||
service = export.export_type == 'vat' ? VatExportService : AccountingExportService
|
||||
service = service.new(
|
||||
data['columns'],
|
||||
encoding: data['encoding'], format: export.extension, separator: export.key
|
||||
)
|
||||
|
@ -22,8 +22,8 @@ class PaymentScheduleItemWorker
|
||||
### Cards
|
||||
PaymentGatewayService.new.process_payment_schedule_item(psi)
|
||||
elsif psi.state == 'new'
|
||||
### Check (only new deadlines, to prevent spamming)
|
||||
NotificationCenter.call type: 'notify_admin_payment_schedule_check_deadline',
|
||||
### Check/Bank transfer (only new deadlines, to prevent spamming)
|
||||
NotificationCenter.call type: "notify_admin_payment_schedule_#{psi.payment_schedule.payment_method}_deadline",
|
||||
receiver: User.admins_and_managers,
|
||||
attached_object: psi
|
||||
psi.update_attributes(state: 'pending')
|
||||
|
@ -98,7 +98,8 @@ de:
|
||||
delete_this_slot: "Nur diesen Slot"
|
||||
delete_this_and_next: "Diesen Slot und die folgenden"
|
||||
delete_all: "Alle Slots"
|
||||
event_in_the_past: "In der Vergangenheit kann kein Slot erstellt werden."
|
||||
event_in_the_past: "Create a slot in the past"
|
||||
confirm_create_event_in_the_past: "You are about to create a slot in the past. Are you sure you want to do this? Members will not be able to book this slot."
|
||||
edit_event: "Veranstaltung bearbeiten"
|
||||
view_reservations: "Reservierungen anzeigen"
|
||||
legend: "Legende"
|
||||
@ -368,6 +369,13 @@ de:
|
||||
status_enabled: "Aktiviert"
|
||||
status_disabled: "Deaktiviert"
|
||||
status_all: "Alle"
|
||||
spaces_pricing:
|
||||
prices_match_space_hours_rates_html: "The prices below match one hour of space reservation, <strong>without subscription</strong>."
|
||||
prices_calculated_on_hourly_rate_html: "All the prices will be automatically calculated based on the hourly rate defined here.<br/><em>For example</em>, if you define an hourly rate at {RATE}: a slot of {DURATION} minutes, will be charged <strong>{PRICE}</strong>."
|
||||
you_can_override: "You can override this duration for each availability you create in the agenda. The price will then be adjusted accordingly."
|
||||
extended_prices: "Moreover, you can define extended prices which will apply in priority over the hourly rate below. Extended prices allow you, for example, to set a favorable price for a booking of several hours."
|
||||
spaces: "Spaces"
|
||||
price_updated: "Price successfully updated"
|
||||
machines_pricing:
|
||||
prices_match_machine_hours_rates_html: "Die unten aufgeführten Preise entsprechen einer Stunde Maschinengebrauch, <strong>ohne Abonnement</strong>."
|
||||
prices_calculated_on_hourly_rate_html: "Alle Preise werden automatisch nach dem hier definierten Stundensatz berechnet.<br/><em>Zum Beispiel</em> wird bei einem veranschlagten Stundensatz von {RATE} ein Slot von {DURATION} Minuten, zum Preis von <strong>{PRICE}</strong> berechnet."
|
||||
@ -378,6 +386,13 @@ de:
|
||||
packs: "Prepaid packs"
|
||||
no_packs: "No packs for now"
|
||||
pack_DURATION: "{DURATION} hours"
|
||||
configure_extended_prices_button:
|
||||
extended_prices: "Extended prices"
|
||||
no_extended_prices: "No extended price for now"
|
||||
extended_price_DURATION: "{DURATION} minutes"
|
||||
extended_price_form:
|
||||
duration: "Duration (minutes)"
|
||||
amount: "Price"
|
||||
pack_form:
|
||||
hours: "Hours"
|
||||
amount: "Price"
|
||||
@ -404,6 +419,21 @@ de:
|
||||
edit_pack: "Edit the pack"
|
||||
confirm_changes: "Confirm changes"
|
||||
pack_successfully_updated: "The prepaid pack was successfully updated."
|
||||
create_extended_price:
|
||||
new_extended_price: "New extended price"
|
||||
new_extended_price_info: "Extended prices allows you to define prices based on custom durations, instead of the default hourly rates."
|
||||
create_extended_price: "Create extended price"
|
||||
extended_price_successfully_created: "The new extended price was successfully created."
|
||||
delete_extended_price:
|
||||
extended_price_deleted: "The extended price was successfully deleted."
|
||||
unable_to_delete: "Unable to delete the extended price: "
|
||||
delete_extended_price: "Delete the extended price"
|
||||
confirm_delete: "Delete"
|
||||
delete_confirmation: "Are you sure you want to delete this extended price?"
|
||||
edit_extended_price:
|
||||
edit_extended_price: "Edit the extended price"
|
||||
confirm_changes: "Confirm changes"
|
||||
extended_price_successfully_updated: "The extended price was successfully updated."
|
||||
#ajouter un code promotionnel
|
||||
coupons_new:
|
||||
add_a_coupon: "Gutschein hinzufügen"
|
||||
@ -466,13 +496,14 @@ de:
|
||||
details: "Details"
|
||||
amount: "Betrag"
|
||||
machine_booking-3D_printer: "Maschinen-Buchung - 3D-Drucker"
|
||||
training_booking-3D_print: "Training booking - initiation to 3d printing"
|
||||
total_amount: "Gesamtbetrag"
|
||||
total_including_all_taxes: "Gesamtpreis inkl. Steuern"
|
||||
VAT_disabled: "MwSt. deaktiviert"
|
||||
VAT_enabled: "MwSt. aktiviert"
|
||||
including_VAT: "Inklusive MwSt."
|
||||
including_VAT: "Inklusive MwSt. {RATE}% von {AMOUNT}"
|
||||
including_total_excluding_taxes: "Gesamtbetrag zzgl. Steuern"
|
||||
including_amount_payed_on_ordering: "Inklusive bei Bestellung bezahlter Betrag"
|
||||
including_amount_payed_on_ordering: "Inklusive bei Bestellung gezahlter Betrag"
|
||||
settlement_by_debit_card_on_DATE_at_TIME_for_an_amount_of_AMOUNT: "Begleichung mit Debitkarte am {DATE} um {TIME}, über den Betrag von {AMOUNT}"
|
||||
important_notes: "Wichtige Hinweise"
|
||||
address_and_legal_information: "Adresse und rechtliche Informationen"
|
||||
@ -522,6 +553,15 @@ de:
|
||||
enable_VAT: "MwSt. aktivieren"
|
||||
VAT_rate: "MwSt.-Satz"
|
||||
VAT_history: "MwSt.-Sätze Historie"
|
||||
VAT_notice: "This parameter configures the general case of the VAT rate and applies to everything sold by the Fablab. It is possible to override this parameter by setting a specific VAT rate for each object."
|
||||
edit_multi_VAT_button: "More options"
|
||||
multiVAT: "Advanced VAT"
|
||||
multi_VAT_notice: "<strong>Please note</strong>: The current general rate is {RATE}%. Here you can define different VAT rates for each category.</br></br>For example, you can override this value, only for machine reservations, by filling in the corresponding field below. If no value is filled in, the general rate will apply."
|
||||
VAT_rate_machine: "Machine reservation"
|
||||
VAT_rate_space: "Space reservation"
|
||||
VAT_rate_training: "Training reservation"
|
||||
VAT_rate_event: "Event reservation"
|
||||
VAT_rate_subscription: "Subscription"
|
||||
changed_at: "Geändert am"
|
||||
changed_by: "Von"
|
||||
deleted_user: "Gelöschter Nutzer"
|
||||
@ -640,9 +680,10 @@ de:
|
||||
codes_customization_success: "Anpassung der Abrechnungscodes erfolgreich gespeichert."
|
||||
unexpected_error_occurred: "Beim Speichern der Codes ist ein unerwarteter Fehler aufgetreten. Bitte versuchen Sie es später erneut."
|
||||
export_accounting_data: "Abrechnungsdaten exportieren"
|
||||
export_to: "In die Buchhaltungssoftware exportieren"
|
||||
export_what: "What do you want to export?"
|
||||
export_VAT: "Export the collected VAT"
|
||||
export_to_ACD: "Export all data to the accounting software ACD"
|
||||
export_is_running: "Export wird ausgeführt. Sie werden nach Fertigstellung benachrichtigt."
|
||||
acd: "ACD"
|
||||
export_form_date: "Exportieren ab"
|
||||
export_to_date: "Exportieren bis"
|
||||
format: "Dateiformat"
|
||||
@ -665,6 +706,10 @@ de:
|
||||
debit_euro: "Soll in Euro"
|
||||
credit_euro: "Guthaben in Euro"
|
||||
lettering: "Beschriftung"
|
||||
start_date: "Start date"
|
||||
end_date: "End date"
|
||||
vat_rate: "VAT rate"
|
||||
amount: "Total amount"
|
||||
payment:
|
||||
payment_settings: "Bezahlungseinstellungen"
|
||||
online_payment: "Online-Bezahlung"
|
||||
@ -1235,6 +1280,10 @@ de:
|
||||
pack_only_for_subscription_info_html: "If this option is activated, the purchase and use of a prepaid pack is only possible for the user with a valid subscription."
|
||||
pack_only_for_subscription: "Subscription valid for purchase and use of a prepaid pack"
|
||||
pack_only_for_subscription_info: "Make subscription mandatory for prepaid packs"
|
||||
extended_prices: "Extended prices"
|
||||
extended_prices_info_html: "Spaces can have different prices depending on the cumulated duration of the booking. You can choose if this apply to all bookings or only to those starting within the same day."
|
||||
extended_prices_in_same_day: "Extended prices in the same day"
|
||||
public_registrations: "Public registrations"
|
||||
overlapping_options:
|
||||
training_reservations: "Trainings"
|
||||
machine_reservations: "Machines"
|
||||
@ -1261,6 +1310,9 @@ de:
|
||||
name: "Name"
|
||||
created_at: "Erstellungsdatum"
|
||||
updated_at: "Datum der letzten Aktualisierung"
|
||||
public_registrations: "Public registrations"
|
||||
public_registrations_info: "Allow everyone to register a new account on the platform. If disabled, only administrators and managers can create new accounts."
|
||||
public_registrations_allowed: "Public registrations allowed"
|
||||
help: "Hilfe"
|
||||
feature_tour: "Feature-Tour"
|
||||
feature_tour_info_html: "<p>Wenn sich ein Administrator oder Manager anmeldet, wird beim jeweils ersten Besuch eines Abschnitts der Anwendung die Feature-Tour ausgelöst. Sie können dieses Verhalten auf einen der folgenden Werte ändern:</p><ul><li>« Einmal », um das Standardverhalten beizubehalten.</li><li>« Pro Sitzung », um die Tour jedes Mal anzuzeigen, wenn die Anwendung erneut geöffnet wird.</li><li>« Nur manuell », deaktiviert die automatische Anzeige der Touren. Es ist weiterhin möglich, sie durch Drücken der F1-Taste oder durch Klicken auf « Hilfe » im Benutzermenu zu starten.</li></ul>"
|
||||
@ -1399,8 +1451,10 @@ de:
|
||||
payment_method: "Payment method"
|
||||
method_card: "Online by card"
|
||||
method_check: "By check"
|
||||
method_transfer: "By bank transfer"
|
||||
card_collection_info: "By validating, you'll be prompted for the member's card number. This card will be automatically charged at the deadlines."
|
||||
check_collection_info: "By validating, you confirm that you have {DEADLINES} checks, allowing you to collect all the monthly payments."
|
||||
transfer_collection_info: "<p>By validating, you confirm that you set up {DEADLINES} bank direct debits, allowing you to collect all the monthly payments.</p><p><strong>Please note:</strong> the bank transfers are not automatically handled by Fab-manager.</p>"
|
||||
online_payment_disabled: "Online payment is not available. You cannot collect this payment schedule by online card."
|
||||
check_list_setting:
|
||||
save: 'Save'
|
||||
|
@ -98,7 +98,8 @@ en:
|
||||
delete_this_slot: "Only this slot"
|
||||
delete_this_and_next: "This slot and the following"
|
||||
delete_all: "All slots"
|
||||
event_in_the_past: "Unable to create a slot in the past."
|
||||
event_in_the_past: "Create a slot in the past"
|
||||
confirm_create_event_in_the_past: "You are about to create a slot in the past. Are you sure you want to do this? Members will not be able to book this slot."
|
||||
edit_event: "Edit the event"
|
||||
view_reservations: "View reservations"
|
||||
legend: "Legend"
|
||||
@ -368,6 +369,13 @@ en:
|
||||
status_enabled: "Enabled"
|
||||
status_disabled: "Disabled"
|
||||
status_all: "All"
|
||||
spaces_pricing:
|
||||
prices_match_space_hours_rates_html: "The prices below match one hour of space reservation, <strong>without subscription</strong>."
|
||||
prices_calculated_on_hourly_rate_html: "All the prices will be automatically calculated based on the hourly rate defined here.<br/><em>For example</em>, if you define an hourly rate at {RATE}: a slot of {DURATION} minutes, will be charged <strong>{PRICE}</strong>."
|
||||
you_can_override: "You can override this duration for each availability you create in the agenda. The price will then be adjusted accordingly."
|
||||
extended_prices: "Moreover, you can define extended prices which will apply in priority over the hourly rate below. Extended prices allow you, for example, to set a favorable price for a booking of several hours."
|
||||
spaces: "Spaces"
|
||||
price_updated: "Price successfully updated"
|
||||
machines_pricing:
|
||||
prices_match_machine_hours_rates_html: "The prices below match one hour of machine usage, <strong>without subscription</strong>."
|
||||
prices_calculated_on_hourly_rate_html: "All the prices will be automatically calculated based on the hourly rate defined here.<br/><em>For example</em>, if you define an hourly rate at {RATE}: a slot of {DURATION} minutes, will be charged <strong>{PRICE}</strong>."
|
||||
@ -378,6 +386,13 @@ en:
|
||||
packs: "Prepaid packs"
|
||||
no_packs: "No packs for now"
|
||||
pack_DURATION: "{DURATION} hours"
|
||||
configure_extended_prices_button:
|
||||
extended_prices: "Extended prices"
|
||||
no_extended_prices: "No extended price for now"
|
||||
extended_price_DURATION: "{DURATION} minutes"
|
||||
extended_price_form:
|
||||
duration: "Duration (minutes)"
|
||||
amount: "Price"
|
||||
pack_form:
|
||||
hours: "Hours"
|
||||
amount: "Price"
|
||||
@ -404,6 +419,21 @@ en:
|
||||
edit_pack: "Edit the pack"
|
||||
confirm_changes: "Confirm changes"
|
||||
pack_successfully_updated: "The prepaid pack was successfully updated."
|
||||
create_extended_price:
|
||||
new_extended_price: "New extended price"
|
||||
new_extended_price_info: "Extended prices allows you to define prices based on custom durations, instead of the default hourly rates."
|
||||
create_extended_price: "Create extended price"
|
||||
extended_price_successfully_created: "The new extended price was successfully created."
|
||||
delete_extended_price:
|
||||
extended_price_deleted: "The extended price was successfully deleted."
|
||||
unable_to_delete: "Unable to delete the extended price: "
|
||||
delete_extended_price: "Delete the extended price"
|
||||
confirm_delete: "Delete"
|
||||
delete_confirmation: "Are you sure you want to delete this extended price?"
|
||||
edit_extended_price:
|
||||
edit_extended_price: "Edit the extended price"
|
||||
confirm_changes: "Confirm changes"
|
||||
extended_price_successfully_updated: "The extended price was successfully updated."
|
||||
#ajouter un code promotionnel
|
||||
coupons_new:
|
||||
add_a_coupon: "Add a coupon"
|
||||
@ -466,13 +496,14 @@ en:
|
||||
details: "Details"
|
||||
amount: "Amount"
|
||||
machine_booking-3D_printer: "Machine booking - 3D printer"
|
||||
training_booking-3D_print: "Training booking - initiation to 3d printing"
|
||||
total_amount: "Total amount"
|
||||
total_including_all_taxes: "Total incl. all taxes"
|
||||
VAT_disabled: "VAT disabled"
|
||||
VAT_enabled: "VAT enabled"
|
||||
including_VAT: "Including VAT"
|
||||
including_VAT: "Including VAT {RATE}% of {AMOUNT}"
|
||||
including_total_excluding_taxes: "Including Total excl. taxes"
|
||||
including_amount_payed_on_ordering: "Including Amount payed on ordering"
|
||||
including_amount_payed_on_ordering: "Including amount payed on ordering"
|
||||
settlement_by_debit_card_on_DATE_at_TIME_for_an_amount_of_AMOUNT: "Settlement by debit card on {DATE} at {TIME}, for an amount of {AMOUNT}"
|
||||
important_notes: "Important notes"
|
||||
address_and_legal_information: "Address and legal information"
|
||||
@ -522,6 +553,15 @@ en:
|
||||
enable_VAT: "Enable VAT"
|
||||
VAT_rate: "VAT rate"
|
||||
VAT_history: "VAT rates history"
|
||||
VAT_notice: "This parameter configures the general case of the VAT rate and applies to everything sold by the Fablab. It is possible to override this parameter by setting a specific VAT rate for each object."
|
||||
edit_multi_VAT_button: "More options"
|
||||
multiVAT: "Advanced VAT"
|
||||
multi_VAT_notice: "<strong>Please note</strong>: The current general rate is {RATE}%. Here you can define different VAT rates for each category.</br></br>For example, you can override this value, only for machine reservations, by filling in the corresponding field below. If no value is filled in, the general rate will apply."
|
||||
VAT_rate_machine: "Machine reservation"
|
||||
VAT_rate_space: "Space reservation"
|
||||
VAT_rate_training: "Training reservation"
|
||||
VAT_rate_event: "Event reservation"
|
||||
VAT_rate_subscription: "Subscription"
|
||||
changed_at: "Changed at"
|
||||
changed_by: "By"
|
||||
deleted_user: "Deleted user"
|
||||
@ -640,9 +680,10 @@ en:
|
||||
codes_customization_success: "Customization of the accounting codes successfully saved."
|
||||
unexpected_error_occurred: "An unexpected error occurred while saving the codes. Please try again later."
|
||||
export_accounting_data: "Export accounting data"
|
||||
export_to: "Export to the accounting software"
|
||||
export_what: "What do you want to export?"
|
||||
export_VAT: "Export the collected VAT"
|
||||
export_to_ACD: "Export all data to the accounting software ACD"
|
||||
export_is_running: "Export is running. You'll be notified when it's ready."
|
||||
acd: "ACD"
|
||||
export_form_date: "Export from"
|
||||
export_to_date: "Export until"
|
||||
format: "File format"
|
||||
@ -665,6 +706,10 @@ en:
|
||||
debit_euro: "Euro debit"
|
||||
credit_euro: "Euro credit"
|
||||
lettering: "Lettering"
|
||||
start_date: "Start date"
|
||||
end_date: "End date"
|
||||
vat_rate: "VAT rate"
|
||||
amount: "Total amount"
|
||||
payment:
|
||||
payment_settings: "Payment settings"
|
||||
online_payment: "Online payment"
|
||||
@ -1235,6 +1280,10 @@ en:
|
||||
pack_only_for_subscription_info_html: "If this option is activated, the purchase and use of a prepaid pack is only possible for the user with a valid subscription."
|
||||
pack_only_for_subscription: "Subscription valid for purchase and use of a prepaid pack"
|
||||
pack_only_for_subscription_info: "Make subscription mandatory for prepaid packs"
|
||||
extended_prices: "Extended prices"
|
||||
extended_prices_info_html: "Spaces can have different prices depending on the cumulated duration of the booking. You can choose if this apply to all bookings or only to those starting within the same day."
|
||||
extended_prices_in_same_day: "Extended prices in the same day"
|
||||
public_registrations: "Public registrations"
|
||||
overlapping_options:
|
||||
training_reservations: "Trainings"
|
||||
machine_reservations: "Machines"
|
||||
@ -1261,6 +1310,9 @@ en:
|
||||
name: "Name"
|
||||
created_at: "Creation date"
|
||||
updated_at: "Last update date"
|
||||
public_registrations: "Public registrations"
|
||||
public_registrations_info: "Allow everyone to register a new account on the platform. If disabled, only administrators and managers can create new accounts."
|
||||
public_registrations_allowed: "Public registrations allowed"
|
||||
help: "Help"
|
||||
feature_tour: "Feature tour"
|
||||
feature_tour_info_html: "<p>When an administrator or a manager in logged-in, a feature tour will be triggered the first time he/she visits each section of the application. You can change this behavior to one of the following values:</p><ul><li>« Once » to keep the default behavior.</li><li>« By session » to display the tours each time you reopen the application.</li><li>« Manual trigger » to prevent displaying the tours automatically. It'll still be possible to trigger them by pressing the F1 key or by clicking on « Help » in the user's menu.</li></ul>"
|
||||
@ -1399,8 +1451,10 @@ en:
|
||||
payment_method: "Payment method"
|
||||
method_card: "Online by card"
|
||||
method_check: "By check"
|
||||
method_transfer: "By bank transfer"
|
||||
card_collection_info: "By validating, you'll be prompted for the member's card number. This card will be automatically charged at the deadlines."
|
||||
check_collection_info: "By validating, you confirm that you have {DEADLINES} checks, allowing you to collect all the monthly payments."
|
||||
transfer_collection_info: "<p>By validating, you confirm that you set up {DEADLINES} bank direct debits, allowing you to collect all the monthly payments.</p><p><strong>Please note:</strong> the bank transfers are not automatically handled by Fab-manager.</p>"
|
||||
online_payment_disabled: "Online payment is not available. You cannot collect this payment schedule by online card."
|
||||
check_list_setting:
|
||||
save: 'Save'
|
||||
|
@ -98,7 +98,8 @@ es:
|
||||
delete_this_slot: "Only this slot"
|
||||
delete_this_and_next: "This slot and the following"
|
||||
delete_all: "All slots"
|
||||
event_in_the_past: "Unable to create a slot in the past."
|
||||
event_in_the_past: "Create a slot in the past"
|
||||
confirm_create_event_in_the_past: "You are about to create a slot in the past. Are you sure you want to do this? Members will not be able to book this slot."
|
||||
edit_event: "Edit the event"
|
||||
view_reservations: "Ver reservas"
|
||||
legend: "Leyenda"
|
||||
@ -368,6 +369,13 @@ es:
|
||||
status_enabled: "Enabled"
|
||||
status_disabled: "Disabled"
|
||||
status_all: "All"
|
||||
spaces_pricing:
|
||||
prices_match_space_hours_rates_html: "The prices below match one hour of space reservation, <strong>without subscription</strong>."
|
||||
prices_calculated_on_hourly_rate_html: "All the prices will be automatically calculated based on the hourly rate defined here.<br/><em>For example</em>, if you define an hourly rate at {RATE}: a slot of {DURATION} minutes, will be charged <strong>{PRICE}</strong>."
|
||||
you_can_override: "You can override this duration for each availability you create in the agenda. The price will then be adjusted accordingly."
|
||||
extended_prices: "Moreover, you can define extended prices which will apply in priority over the hourly rate below. Extended prices allow you, for example, to set a favorable price for a booking of several hours."
|
||||
spaces: "Spaces"
|
||||
price_updated: "Price successfully updated"
|
||||
machines_pricing:
|
||||
prices_match_machine_hours_rates_html: "The prices below match one hour of machine usage, <strong>without subscription</strong>."
|
||||
prices_calculated_on_hourly_rate_html: "All the prices will be automatically calculated based on the hourly rate defined here.<br/><em>For example</em>, if you define an hourly rate at {RATE}: a slot of {DURATION} minutes, will be charged <strong>{PRICE}</strong>."
|
||||
@ -378,6 +386,13 @@ es:
|
||||
packs: "Prepaid packs"
|
||||
no_packs: "No packs for now"
|
||||
pack_DURATION: "{DURATION} hours"
|
||||
configure_extended_prices_button:
|
||||
extended_prices: "Extended prices"
|
||||
no_extended_prices: "No extended price for now"
|
||||
extended_price_DURATION: "{DURATION} minutes"
|
||||
extended_price_form:
|
||||
duration: "Duration (minutes)"
|
||||
amount: "Price"
|
||||
pack_form:
|
||||
hours: "Hours"
|
||||
amount: "Price"
|
||||
@ -404,6 +419,21 @@ es:
|
||||
edit_pack: "Edit the pack"
|
||||
confirm_changes: "Confirm changes"
|
||||
pack_successfully_updated: "The prepaid pack was successfully updated."
|
||||
create_extended_price:
|
||||
new_extended_price: "New extended price"
|
||||
new_extended_price_info: "Extended prices allows you to define prices based on custom durations, instead of the default hourly rates."
|
||||
create_extended_price: "Create extended price"
|
||||
extended_price_successfully_created: "The new extended price was successfully created."
|
||||
delete_extended_price:
|
||||
extended_price_deleted: "The extended price was successfully deleted."
|
||||
unable_to_delete: "Unable to delete the extended price: "
|
||||
delete_extended_price: "Delete the extended price"
|
||||
confirm_delete: "Delete"
|
||||
delete_confirmation: "Are you sure you want to delete this extended price?"
|
||||
edit_extended_price:
|
||||
edit_extended_price: "Edit the extended price"
|
||||
confirm_changes: "Confirm changes"
|
||||
extended_price_successfully_updated: "The extended price was successfully updated."
|
||||
#ajouter un code promotionnel
|
||||
coupons_new:
|
||||
add_a_coupon: "Añadir un cupón"
|
||||
@ -466,11 +496,12 @@ es:
|
||||
details: "Detalles"
|
||||
amount: "Cantidad"
|
||||
machine_booking-3D_printer: "Reserva de la máquina- Impresora 3D"
|
||||
training_booking-3D_print: "Training booking - initiation to 3d printing"
|
||||
total_amount: "Cantidad total"
|
||||
total_including_all_taxes: "Total incl. todos los impuestos"
|
||||
VAT_disabled: "IVA desactivado"
|
||||
VAT_enabled: "IVA activado"
|
||||
including_VAT: "Incluido IVA"
|
||||
including_VAT: "Incluido IVA {RATE}% de {AMOUNT}"
|
||||
including_total_excluding_taxes: "Incluido Total excl. impuestos"
|
||||
including_amount_payed_on_ordering: "Incluido el monto pagado en el pedido"
|
||||
settlement_by_debit_card_on_DATE_at_TIME_for_an_amount_of_AMOUNT: "Liquidación por tarjeta de débito el {DATE} a las {TIME}, por una cantidad de {AMOUNT}"
|
||||
@ -522,6 +553,15 @@ es:
|
||||
enable_VAT: "Habilitar IVA"
|
||||
VAT_rate: "Ratio IVA"
|
||||
VAT_history: "Historial de ratios de IVA"
|
||||
VAT_notice: "This parameter configures the general case of the VAT rate and applies to everything sold by the Fablab. It is possible to override this parameter by setting a specific VAT rate for each object."
|
||||
edit_multi_VAT_button: "More options"
|
||||
multiVAT: "Advanced VAT"
|
||||
multi_VAT_notice: "<strong>Please note</strong>: The current general rate is {RATE}%. Here you can define different VAT rates for each category.</br></br>For example, you can override this value, only for machine reservations, by filling in the corresponding field below. If no value is filled in, the general rate will apply."
|
||||
VAT_rate_machine: "Machine reservation"
|
||||
VAT_rate_space: "Space reservation"
|
||||
VAT_rate_training: "Training reservation"
|
||||
VAT_rate_event: "Event reservation"
|
||||
VAT_rate_subscription: "Subscription"
|
||||
changed_at: "Cambiado en"
|
||||
changed_by: "Por"
|
||||
deleted_user: "Usario eliminado"
|
||||
@ -640,9 +680,10 @@ es:
|
||||
codes_customization_success: "Customization of accounting codes successfully saved."
|
||||
unexpected_error_occurred: "An unexpected error occurred while saving the codes. Please try again later."
|
||||
export_accounting_data: "Export accounting data"
|
||||
export_to: "Export to the accounting software"
|
||||
export_what: "What do you want to export?"
|
||||
export_VAT: "Export the collected VAT"
|
||||
export_to_ACD: "Export all data to the accounting software ACD"
|
||||
export_is_running: "Exportando, será notificado cuando esté listo."
|
||||
acd: "ACD"
|
||||
export_form_date: "Export from"
|
||||
export_to_date: "Export until"
|
||||
format: "File format"
|
||||
@ -665,6 +706,10 @@ es:
|
||||
debit_euro: "Euro debit"
|
||||
credit_euro: "Euro credit"
|
||||
lettering: "Lettering"
|
||||
start_date: "Start date"
|
||||
end_date: "End date"
|
||||
vat_rate: "VAT rate"
|
||||
amount: "Total amount"
|
||||
payment:
|
||||
payment_settings: "Payment settings"
|
||||
online_payment: "Online payment"
|
||||
@ -1235,6 +1280,10 @@ es:
|
||||
pack_only_for_subscription_info_html: "If this option is activated, the purchase and use of a prepaid pack is only possible for the user with a valid subscription."
|
||||
pack_only_for_subscription: "Subscription valid for purchase and use of a prepaid pack"
|
||||
pack_only_for_subscription_info: "Make subscription mandatory for prepaid packs"
|
||||
extended_prices: "Extended prices"
|
||||
extended_prices_info_html: "Spaces can have different prices depending on the cumulated duration of the booking. You can choose if this apply to all bookings or only to those starting within the same day."
|
||||
extended_prices_in_same_day: "Extended prices in the same day"
|
||||
public_registrations: "Public registrations"
|
||||
overlapping_options:
|
||||
training_reservations: "Trainings"
|
||||
machine_reservations: "Machines"
|
||||
@ -1261,6 +1310,9 @@ es:
|
||||
name: "Name"
|
||||
created_at: "Creation date"
|
||||
updated_at: "Last update date"
|
||||
public_registrations: "Public registrations"
|
||||
public_registrations_info: "Allow everyone to register a new account on the platform. If disabled, only administrators and managers can create new accounts."
|
||||
public_registrations_allowed: "Public registrations allowed"
|
||||
help: "Help"
|
||||
feature_tour: "Feature tour"
|
||||
feature_tour_info_html: "<p>When an administrator or a manager in logged-in, a feature tour will be triggered the first time he/she visits each section of the application. You can change this behavior to one of the following values:</p><ul><li>« Once » to keep the default behavior.</li><li>« By session » to display the tours each time you reopen the application.</li><li>« Manual trigger » to prevent displaying the tours automatically. It'll still be possible to trigger them by pressing the F1 key or by clicking on « Help » in the user's menu.</li></ul>"
|
||||
@ -1399,8 +1451,10 @@ es:
|
||||
payment_method: "Payment method"
|
||||
method_card: "Online by card"
|
||||
method_check: "By check"
|
||||
method_transfer: "By bank transfer"
|
||||
card_collection_info: "By validating, you'll be prompted for the member's card number. This card will be automatically charged at the deadlines."
|
||||
check_collection_info: "By validating, you confirm that you have {DEADLINES} checks, allowing you to collect all the monthly payments."
|
||||
transfer_collection_info: "<p>By validating, you confirm that you set up {DEADLINES} bank direct debits, allowing you to collect all the monthly payments.</p><p><strong>Please note:</strong> the bank transfers are not automatically handled by Fab-manager.</p>"
|
||||
online_payment_disabled: "Online payment is not available. You cannot collect this payment schedule by online card."
|
||||
check_list_setting:
|
||||
save: 'Save'
|
||||
|
@ -98,7 +98,8 @@ fr:
|
||||
delete_this_slot: "Uniquement ce créneau"
|
||||
delete_this_and_next: "Ce créneau et tous les suivants"
|
||||
delete_all: "Tous les créneaux"
|
||||
event_in_the_past: "Impossible de créer un créneau dans le passé."
|
||||
event_in_the_past: "Créer un créneau dans le passé"
|
||||
confirm_create_event_in_the_past: "Vous êtes sur le point de créer un créneau dans le passé. Êtes-vous sûr de vouloir faire cela ? Les membres ne pourront pas réserver ce créneau."
|
||||
edit_event: "Modifier l'événement"
|
||||
view_reservations: "Voir les réservations"
|
||||
legend: "Légende"
|
||||
@ -368,6 +369,13 @@ fr:
|
||||
status_enabled: "Actifs"
|
||||
status_disabled: "Désactivés"
|
||||
status_all: "Tous"
|
||||
spaces_pricing:
|
||||
prices_match_space_hours_rates_html: "Les tarifs ci-dessous correspondent à une heure de réservation d'espace, <strong>sans abonnement</strong>."
|
||||
prices_calculated_on_hourly_rate_html: "Tous les prix seront automatiquement calculés par rapport au tarif horaire défini ici.<br/><em>Par exemple</em>, si vous définissez un tarif horaire à {RATE} : un créneau de {DURATION} minutes, sera facturé <strong>{PRICE}</strong>."
|
||||
you_can_override: "Vous pouvez surcharger cette durée pour chaque disponibilité que vous créez dans l'agenda. Le prix sera alors ajusté en conséquence."
|
||||
extended_prices: "De plus, vous pouvez définir des prix étendus qui prévaudront sur le tarif horaire ci-dessous. Les prix étendus vous permettent, par exemple, de fixer un prix favorable pour une réservation de plusieurs heures."
|
||||
spaces: "Espaces"
|
||||
price_updated: "Le prix a bien été mis à jour"
|
||||
machines_pricing:
|
||||
prices_match_machine_hours_rates_html: "Les tarifs ci-dessous correspondent à une heure d'utilisation machine, <strong>sans abonnement</strong>."
|
||||
prices_calculated_on_hourly_rate_html: "Tous les prix seront automatiquement calculés par rapport au tarif horaire défini ici.<br/><em>Par exemple</em>, si vous définissez un tarif horaire à {RATE} : un créneau de {DURATION} minutes, sera facturé <strong>{PRICE}</strong>."
|
||||
@ -378,6 +386,13 @@ fr:
|
||||
packs: "Packs prépayés"
|
||||
no_packs: "Aucun pack pour le moment"
|
||||
pack_DURATION: "{DURATION} heures"
|
||||
configure_extended_prices_button:
|
||||
extended_prices: "Prix étendus"
|
||||
no_extended_prices: "Aucun prix étendu pour l'instant"
|
||||
extended_price_DURATION: "{DURATION} minutes"
|
||||
extended_price_form:
|
||||
duration: "Durée (minutes)"
|
||||
amount: "Prix"
|
||||
pack_form:
|
||||
hours: "Heures"
|
||||
amount: "Prix"
|
||||
@ -404,6 +419,21 @@ fr:
|
||||
edit_pack: "Modifier le pack"
|
||||
confirm_changes: "Valider les modifications"
|
||||
pack_successfully_updated: "Le pack prépayé a bien été mis à jour."
|
||||
create_extended_price:
|
||||
new_extended_price: "Nouveau prix étendu"
|
||||
new_extended_price_info: "Les prix étendus vous permettent de définir des prix basés sur des durées personnalisées, au lieu du tarif horaire par défaut."
|
||||
create_extended_price: "Créer un prix étendu"
|
||||
extended_price_successfully_created: "Le nouveau prix étendu a bien été créé."
|
||||
delete_extended_price:
|
||||
extended_price_deleted: "Le prix étendu a bien été supprimé."
|
||||
unable_to_delete: "Impossible de supprimer le prix étendu : "
|
||||
delete_extended_price: "Supprimer le prix étendu"
|
||||
confirm_delete: "Supprimer"
|
||||
delete_confirmation: "Êtes-vous sûr de vouloir supprimer ce prix étendu ?"
|
||||
edit_extended_price:
|
||||
edit_extended_price: "Modifier le prix étendu"
|
||||
confirm_changes: "Valider les modifications"
|
||||
extended_price_successfully_updated: "Le prix étendu a bien été mis à jour."
|
||||
#ajouter un code promotionnel
|
||||
coupons_new:
|
||||
add_a_coupon: "Ajouter un code promotionnel"
|
||||
@ -466,11 +496,12 @@ fr:
|
||||
details: "Détails"
|
||||
amount: "Montant"
|
||||
machine_booking-3D_printer: "Réservation Machine - Imprimante 3D"
|
||||
training_booking-3D_print: "Réservation de formation - initiation à l'impression 3D"
|
||||
total_amount: "Montant total"
|
||||
total_including_all_taxes: "Total TTC"
|
||||
VAT_disabled: "TVA désactivée"
|
||||
VAT_enabled: "TVA activée"
|
||||
including_VAT: "Dont TVA"
|
||||
including_VAT: "Dont TVA {RATE} % de {AMOUNT}"
|
||||
including_total_excluding_taxes: "Dont total HT"
|
||||
including_amount_payed_on_ordering: "Dont montant payé à la commande"
|
||||
settlement_by_debit_card_on_DATE_at_TIME_for_an_amount_of_AMOUNT: "Règlement effectué par carte bancaire le {DATE} à {TIME}, pour un montant de {AMOUNT}"
|
||||
@ -522,6 +553,15 @@ fr:
|
||||
enable_VAT: "Activer la TVA"
|
||||
VAT_rate: "Taux de TVA"
|
||||
VAT_history: "Historique des taux de TVA"
|
||||
VAT_notice: "Ce paramètre configure le cas général du taux de TVA et s'applique à tout ce qui est vendu par le Fablab. Il est possible de surcharger ce paramètre en définissant un taux de TVA spécifique pour chaque objet."
|
||||
edit_multi_VAT_button: "Plus d'options"
|
||||
multiVAT: "TVA avancée"
|
||||
multi_VAT_notice: "<strong>Veuillez noter</strong> : Le taux général actuel est de {RATE} %. Ici, vous pouvez définir des taux de TVA différents pour chaque catégorie.</br></br>Par exemple, vous pouvez surcharger cette valeur, uniquement pour les réservations de machines, en remplissant le champ correspondant ci-dessous. Si aucune valeur n'est remplie, le tarif général s'appliquera."
|
||||
VAT_rate_machine: "Réservation de machines"
|
||||
VAT_rate_space: "Réservation d'espaces"
|
||||
VAT_rate_training: "Réservation de formations"
|
||||
VAT_rate_event: "Réservation d'événements"
|
||||
VAT_rate_subscription: "Abonnements"
|
||||
changed_at: "Changé le"
|
||||
changed_by: "Par"
|
||||
deleted_user: "Utilisateur supprimé"
|
||||
@ -640,9 +680,10 @@ fr:
|
||||
codes_customization_success: "La personnalisation des codes comptables a bien été enregistrée."
|
||||
unexpected_error_occurred: "Une erreur inattendue est survenue lors de l’enregistrement des codes. Veuillez réessayer plus tard."
|
||||
export_accounting_data: "Exporter les données comptables"
|
||||
export_to: "Exporter vers le logiciel comptable"
|
||||
export_what: "Que voulez-vous exporter ?"
|
||||
export_VAT: "Exporter la TVA collectée"
|
||||
export_to_ACD: "Exporter toutes les données vers le logiciel de comptabilité ACD"
|
||||
export_is_running: "L'export est en cours. Vous serez notifié lorsqu'il sera prêt."
|
||||
acd: "ACD"
|
||||
export_form_date: "Exporter depuis le"
|
||||
export_to_date: "Exporter jusqu'au"
|
||||
format: "Format de fichier"
|
||||
@ -665,6 +706,10 @@ fr:
|
||||
debit_euro: "Débit euro"
|
||||
credit_euro: "Crédit euro"
|
||||
lettering: "Lettrage"
|
||||
start_date: "Date de début"
|
||||
end_date: "Date de fin"
|
||||
vat_rate: "Taux de TVA"
|
||||
amount: "Montant total"
|
||||
payment:
|
||||
payment_settings: "Paramètres de paiement"
|
||||
online_payment: "Paiement en ligne"
|
||||
@ -1235,6 +1280,10 @@ fr:
|
||||
pack_only_for_subscription_info_html: "Si cette option est activée, l'achat et l'utilisation d'un pack prépayé n'est possible que pour l'utilisateur possédant un abonnement en cours de validité."
|
||||
pack_only_for_subscription: "Abonnement valide pour l'achat et l'utilisation d'un pack prépayé"
|
||||
pack_only_for_subscription_info: "Rendre l'abonnement obligatoire pour les packs prépayés"
|
||||
extended_prices: "Prix étendus"
|
||||
extended_prices_info_html: "Les espaces peuvent avoir des prix différents selon la durée cumulée de la réservation. Vous pouvez choisir si cela s'applique à toutes les réservations ou seulement à celles qui commencent dans la même journée."
|
||||
extended_prices_in_same_day: "Prix étendus le même jour"
|
||||
public_registrations: "Inscriptions publiques"
|
||||
overlapping_options:
|
||||
training_reservations: "Formations"
|
||||
machine_reservations: "Machines"
|
||||
@ -1261,6 +1310,9 @@ fr:
|
||||
name: "Nom"
|
||||
created_at: "Date de création"
|
||||
updated_at: "Date de mise à jour"
|
||||
public_registrations: "Inscriptions publiques"
|
||||
public_registrations_info: "Permettre à tout le monde de créer un nouveau compte sur la plateforme. Si désactivé, seuls les administrateurs et les gestionnaires peuvent créer de nouveaux comptes."
|
||||
public_registrations_allowed: "Inscriptions publiques autorisées"
|
||||
help: "Aide"
|
||||
feature_tour: "Visite guidée des fonctionnalités"
|
||||
feature_tour_info_html: "<p>Lorsque un administrateur ou un gestionnaire est connecté, une visite guidée des fonctionnalités se déclenchera lors de la première visite de chaque section de l'application. Vous pouvez modifier ce comportement pour une des valeur suivantes :</p><ul><li>« Une fois » pour garder le comportement par défaut.</li><li>« Par session » pour afficher les visites guidées chaque fois que l'application est ouverte de nouveau.</li><li>« Lancement manuel » pour éviter l'affichage automatique des visites guidées. Il sera toujours possible de les déclencher en appuyant sur F1 ou en cliquant sur « Aide » dans le menu utilisateur.</li></ul>"
|
||||
@ -1399,8 +1451,10 @@ fr:
|
||||
payment_method: "Moyen de paiement"
|
||||
method_card: "Carte bancaire en ligne"
|
||||
method_check: "Par chèques"
|
||||
method_transfer: "Par virement bancaire"
|
||||
card_collection_info: "En validant, vous serez invité à saisir les informations de carte bancaire du membre. Cette carte sera prélevée automatiquement aux échéances."
|
||||
check_collection_info: "En validant, vous confirmez être en possession de {DEADLINES} chèques permettant d'encaisser l'ensemble des mensualité."
|
||||
transfer_collection_info: "<p>En validant, vous confirmez avoir mis en place {DEADLINES} prélèvements bancaires, vous permettant de percevoir toutes les mensualités.</p><p><strong>Veuillez noter :</strong> les prélèvements bancaires ne sont pas automatiquement gérés par Fab-manager.</p>"
|
||||
online_payment_disabled: "Le paiement en ligne n'est pas disponible. Vous ne pouvez pas encaisser cet échéancier de paiement en utilisant la carte bancaire en ligne."
|
||||
check_list_setting:
|
||||
save: 'Enregistrer'
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user