1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2024-12-01 12:24:28 +01:00

Merge branch 'monthly-payment' into staging

This commit is contained in:
Sylvain 2021-02-10 13:37:57 +01:00
commit 92b1fe165f
12 changed files with 60 additions and 13 deletions

View File

@ -1,6 +1,7 @@
# Changelog Fab-manager # Changelog Fab-manager
## Next release ## Next release
- Payment schedules on subscriptions
- Refactored theme builder to use scss files - Refactored theme builder to use scss files
- Updated stripe gem to 5.29.0 - Updated stripe gem to 5.29.0
- Architecture documentation - Architecture documentation

View File

@ -735,11 +735,21 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
placement: 'left' placement: 'left'
}); });
} }
if (settings.invoicing_module === 'true') {
uitour.createStep({
selector: '.invoices-management .payment-schedules-list',
stepId: 'payment-schedules',
order: 5,
title: _t('app.admin.tour.invoices.payment-schedules.title'),
content: _t('app.admin.tour.invoices.payment-schedules.content'),
placement: 'bottom'
});
}
if (AuthService.isAuthorized('admin')) { if (AuthService.isAuthorized('admin')) {
uitour.createStep({ uitour.createStep({
selector: '.invoices-management .invoices-settings', selector: '.invoices-management .invoices-settings',
stepId: 'settings', stepId: 'settings',
order: 5, order: 6,
title: _t('app.admin.tour.invoices.settings.title'), title: _t('app.admin.tour.invoices.settings.title'),
content: _t('app.admin.tour.invoices.settings.content'), content: _t('app.admin.tour.invoices.settings.content'),
placement: 'bottom' placement: 'bottom'
@ -747,7 +757,7 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
uitour.createStep({ uitour.createStep({
selector: '.invoices-management .accounting-codes-tab', selector: '.invoices-management .accounting-codes-tab',
stepId: 'codes', stepId: 'codes',
order: 6, order: 7,
title: _t('app.admin.tour.invoices.codes.title'), title: _t('app.admin.tour.invoices.codes.title'),
content: _t('app.admin.tour.invoices.codes.content'), content: _t('app.admin.tour.invoices.codes.content'),
placement: 'bottom' placement: 'bottom'
@ -755,7 +765,7 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
uitour.createStep({ uitour.createStep({
selector: '.heading .export-accounting-button', selector: '.heading .export-accounting-button',
stepId: 'export', stepId: 'export',
order: 7, order: 8,
title: _t('app.admin.tour.invoices.export.title'), title: _t('app.admin.tour.invoices.export.title'),
content: _t('app.admin.tour.invoices.export.content'), content: _t('app.admin.tour.invoices.export.content'),
placement: 'bottom' placement: 'bottom'
@ -763,7 +773,7 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
uitour.createStep({ uitour.createStep({
selector: '.invoices-management .payment-settings', selector: '.invoices-management .payment-settings',
stepId: 'payment', stepId: 'payment',
order: 8, order: 9,
title: _t('app.admin.tour.invoices.payment.title'), title: _t('app.admin.tour.invoices.payment.title'),
content: _t('app.admin.tour.invoices.payment.content'), content: _t('app.admin.tour.invoices.payment.content'),
placement: 'bottom', placement: 'bottom',
@ -772,7 +782,7 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
uitour.createStep({ uitour.createStep({
selector: '.heading .close-accounting-periods-button', selector: '.heading .close-accounting-periods-button',
stepId: 'periods', stepId: 'periods',
order: 9, order: 10,
title: _t('app.admin.tour.invoices.periods.title'), title: _t('app.admin.tour.invoices.periods.title'),
content: _t('app.admin.tour.invoices.periods.content'), content: _t('app.admin.tour.invoices.periods.content'),
placement: 'bottom', placement: 'bottom',
@ -782,7 +792,7 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
uitour.createStep({ uitour.createStep({
selector: 'body', selector: 'body',
stepId: 'conclusion', stepId: 'conclusion',
order: 10, order: 11,
title: _t('app.admin.tour.conclusion.title'), title: _t('app.admin.tour.conclusion.title'),
content: _t('app.admin.tour.conclusion.content'), content: _t('app.admin.tour.conclusion.content'),
placement: 'bottom', placement: 'bottom',
@ -790,7 +800,7 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
}); });
// on step change, change the active tab if needed // on step change, change the active tab if needed
uitour.on('stepChanged', function (nextStep) { uitour.on('stepChanged', function (nextStep) {
if (nextStep.stepId === 'list' || nextStep.stepId === 'settings') { if (nextStep.stepId === 'list' || nextStep.stepId === 'refund') {
$scope.tabs.active = 0; $scope.tabs.active = 0;
} }
if (nextStep.stepId === 'settings') { if (nextStep.stepId === 'settings') {
@ -802,6 +812,9 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
if (nextStep.stepId === 'payment') { if (nextStep.stepId === 'payment') {
$scope.tabs.active = 3; $scope.tabs.active = 3;
} }
if (nextStep.stepId === 'payment-schedules') {
$scope.tabs.active = 4;
}
}); });
// on tour end, save the status in database // on tour end, save the status in database
uitour.on('ended', function () { uitour.on('ended', function () {

View File

@ -34,7 +34,7 @@
<ng-include src="'/admin/invoices/list.html'"></ng-include> <ng-include src="'/admin/invoices/list.html'"></ng-include>
</uib-tab> </uib-tab>
<uib-tab heading="{{ 'app.admin.invoices.payment_schedules_list' | translate }}" ng-show="$root.modules.invoicing" index="4"> <uib-tab heading="{{ 'app.admin.invoices.payment_schedules_list' | translate }}" ng-show="$root.modules.invoicing" index="4" class="payment-schedules-list">
<payment-schedules-list current-user="currentUser" /> <payment-schedules-list current-user="currentUser" />
</uib-tab> </uib-tab>

View File

@ -51,6 +51,12 @@ class PaymentSchedule < PaymentDocument
invoicing_profile.user invoicing_profile.user
end end
# for debug & used by rake task "fablab:maintenance:regenerate_schedules"
def regenerate_pdf
pdf = ::PDF::PaymentSchedule.new(self).render
File.binwrite(file, pdf)
end
def check_footprint def check_footprint
payment_schedule_items.map(&:check_footprint).all? && footprint == compute_footprint payment_schedule_items.map(&:check_footprint).all? && footprint == compute_footprint
end end

View File

@ -85,6 +85,12 @@ class Subscription < ApplicationRecord
statistic_profile.user statistic_profile.user
end end
def original_payment_schedule
return payment_schedule if payment_schedule
PaymentScheduleItem.where("cast(details->>'subscription_id' AS int) = ?", id).first&.payment_schedule
end
private private
def notify_member_subscribed_plan def notify_member_subscribed_plan

View File

@ -53,7 +53,7 @@ class Subscriptions::Subscribe
expiration_date: new_expiration_date expiration_date: new_expiration_date
) )
if new_sub.save if new_sub.save
schedule = subscription.payment_schedule schedule = subscription.original_payment_schedule
details = Price.compute(true, new_sub.user, nil, [], plan_id: subscription.plan_id) details = Price.compute(true, new_sub.user, nil, [], plan_id: subscription.plan_id)
payment = if schedule payment = if schedule
generate_schedule(subscription: new_sub, generate_schedule(subscription: new_sub,

View File

@ -254,7 +254,8 @@ h5:after {
color: $secondary-text-color; color: $secondary-text-color;
} }
.pricing-panel .content .wrap { .pricing-panel .plan-card .content .wrap,
.pricing-panel .plan-card .content .wrap-monthly {
border-color: $secondary; border-color: $secondary;
} }

View File

@ -824,7 +824,7 @@ en:
credits_will_remain_unchanged: "The balance of free credits (training / machines / spaces) of the user will remain unchanged." credits_will_remain_unchanged: "The balance of free credits (training / machines / spaces) of the user will remain unchanged."
you_intentionally_decide_to_extend_the_user_s_subscription_by_charging_him_again_for_his_current_subscription: "You intentionally decide to extend the user's subscription by charging him again for his current subscription." you_intentionally_decide_to_extend_the_user_s_subscription_by_charging_him_again_for_his_current_subscription: "You intentionally decide to extend the user's subscription by charging him again for his current subscription."
credits_will_be_reset: "The balance of free credits (training / machines / spaces) of the user will be reset, unused credits will be lost." credits_will_be_reset: "The balance of free credits (training / machines / spaces) of the user will be reset, unused credits will be lost."
payment_scheduled: "If the previous subscription was charged through a payment schedule, this one will be charged the same way." payment_scheduled: "If the previous subscription was charged through a payment schedule, this one will be charged the same way, the first deadline being charged right now, then each following month."
until_expiration_date: "Until (expiration date):" until_expiration_date: "Until (expiration date):"
you_successfully_changed_the_expiration_date_of_the_user_s_subscription: "You successfully changed the expiration date of the user's subscription" you_successfully_changed_the_expiration_date_of_the_user_s_subscription: "You successfully changed the expiration date of the user's subscription"
a_problem_occurred_while_saving_the_date: "A problem occurred while saving the date." a_problem_occurred_while_saving_the_date: "A problem occurred while saving the date."
@ -1332,6 +1332,9 @@ en:
refund: refund:
title: "Credit note" title: "Credit note"
content: "Allows you to generate a credit note for the invoice on this line or some of its sub-elements. <strong>Warning:</strong> This will only generate the accounting document, the actual refund of the user will always be your responsibility." content: "Allows you to generate a credit note for the invoice on this line or some of its sub-elements. <strong>Warning:</strong> This will only generate the accounting document, the actual refund of the user will always be your responsibility."
payment-schedules:
title: "Payment schedules"
content: "<p>Some subscription plans may be configured to allow the members to pay them with a monthly payment schedule.</p><p>Here you can view all existing payment schedules and manage their deadlines.</p><p>Click on [+] at the beginning of a row to display all deadlines associated with a payment schedule, and run some actions on them.</p>"
settings: settings:
title: "Settings" title: "Settings"
content: "<p>Here you can modify the parameters for invoices generation. Click on the item you are interested in to start editing.</p><p>In particular, this is where you can set if you are subject to VAT and the applicable rate.</p>" content: "<p>Here you can modify the parameters for invoices generation. Click on the item you are interested in to start editing.</p><p>In particular, this is where you can set if you are subject to VAT and the applicable rate.</p>"

View File

@ -824,7 +824,7 @@ fr:
credits_will_remain_unchanged: "Le solde de crédits gratuits (formations/machines/espaces) de l'utilisateur restera inchangé." credits_will_remain_unchanged: "Le solde de crédits gratuits (formations/machines/espaces) de l'utilisateur restera inchangé."
you_intentionally_decide_to_extend_the_user_s_subscription_by_charging_him_again_for_his_current_subscription: "Vous décidez délibérément d'étendre l'abonnement de l'utilisateur en lui faisant repayer le prix de l'abonnement qu'il possède actuellement." you_intentionally_decide_to_extend_the_user_s_subscription_by_charging_him_again_for_his_current_subscription: "Vous décidez délibérément d'étendre l'abonnement de l'utilisateur en lui faisant repayer le prix de l'abonnement qu'il possède actuellement."
credits_will_be_reset: "Le solde de crédits gratuits (formations/machines/espaces) de l'utilisateur sera remis à zéro, ses crédits non utilisés seront perdu." credits_will_be_reset: "Le solde de crédits gratuits (formations/machines/espaces) de l'utilisateur sera remis à zéro, ses crédits non utilisés seront perdu."
payment_scheduled: "Si l'abonnement précédent a été facturé via un échéancier de paiement mensualisé, celui-ci sera facturé de la même façon." payment_scheduled: "Si l'abonnement précédent a été facturé via un échéancier de paiement mensualisé, celui-ci sera facturé de la même façon, la première échéance étant facturée immédiatement, puis chaque mois suivant."
until_expiration_date: "Jusqu'à (date d'expiration) :" until_expiration_date: "Jusqu'à (date d'expiration) :"
you_successfully_changed_the_expiration_date_of_the_user_s_subscription: "Vous avez bien modifié la date d'expiration de l'abonnement de l'utilisateur" you_successfully_changed_the_expiration_date_of_the_user_s_subscription: "Vous avez bien modifié la date d'expiration de l'abonnement de l'utilisateur"
a_problem_occurred_while_saving_the_date: "Il y a eu un problème lors de l'enregistrement de la date." a_problem_occurred_while_saving_the_date: "Il y a eu un problème lors de l'enregistrement de la date."
@ -1332,6 +1332,9 @@ fr:
refund: refund:
title: "Avoir" title: "Avoir"
content: "Permet de générer un avoir sur la facture de cette ligne ou certains de ses sous-éléments. <strong>Attention :</strong> Cela générera uniquement le document comptable, le remboursement effectif de l'utilisateur restera toujours à votre charge." content: "Permet de générer un avoir sur la facture de cette ligne ou certains de ses sous-éléments. <strong>Attention :</strong> Cela générera uniquement le document comptable, le remboursement effectif de l'utilisateur restera toujours à votre charge."
payment-schedules:
title: "Échéanciers de paiement"
content: "<p>Des formules d'abonnement peuvent être configurées pour permettre aux membres de les payer grâce à un échéancier de paiement mensuel.</p><p>Ici, vous pouvez voir tous les échéanciers de paiement existants et gérer leurs échéances.</p><p>Cliquez sur [+] au début d'une ligne pour afficher toutes les échéances associées à un échéancier, et effectuer des actions sur celles-ci.</p>"
settings: settings:
title: "Paramètres" title: "Paramètres"
content: "<p>Ici vous pourrez modifier les paramètres de génération des factures. Cliquez sur l'élément qui vous intéresse pour commencer l'édition.</p><p>C'est notamment ici que vous pourrez définir si vous êtes assujettis à la TVA et à quel taux.</p>" content: "<p>Ici vous pourrez modifier les paramètres de génération des factures. Cliquez sur l'élément qui vous intéresse pour commencer l'édition.</p><p>C'est notamment ici que vous pourrez définir si vous êtes assujettis à la TVA et à quel taux.</p>"

View File

@ -80,3 +80,4 @@ This is currently not supported, because of some PostgreSQL specific instruction
- `db/migrate/20200623141305_update_search_vector_of_projects.rb` defines a PL/pgSQL function (`fill_search_vector_for_project()`) and create an SQL trigger for this function; - `db/migrate/20200623141305_update_search_vector_of_projects.rb` defines a PL/pgSQL function (`fill_search_vector_for_project()`) and create an SQL trigger for this function;
- `db/migrate/20200629123011_update_pg_trgm.rb` is using [ALTER EXTENSION](https://www.postgresql.org/docs/10/sql-alterextension.html); - `db/migrate/20200629123011_update_pg_trgm.rb` is using [ALTER EXTENSION](https://www.postgresql.org/docs/10/sql-alterextension.html);
- `db/migrate/20201027101809_create_payment_schedule_items.rb` is using [jsonb](https://www.postgresql.org/docs/9.4/static/datatype-json.html); - `db/migrate/20201027101809_create_payment_schedule_items.rb` is using [jsonb](https://www.postgresql.org/docs/9.4/static/datatype-json.html);
- `app/models/subscription.rb@original_payment_schedule` is using `->>` and `cast(... AS int)` for querying a with jsonb column.

View File

@ -17,6 +17,19 @@ namespace :fablab do
puts '-> Done' puts '-> Done'
end end
task :regenerate_schedules, %i[year month] => :environment do |_task, args|
year = args.year || Time.current.year
month = args.month || Time.current.month
start_date = Time.zone.local(year.to_i, month.to_i, 1)
end_date = start_date.next_month
puts "-> Start regenerate the payment schedules PDF between #{I18n.l start_date, format: :long} and " \
"#{I18n.l end_date - 1.minute, format: :long}"
schedules = PaymentSchedule.where('created_at >= :start_date AND created_at < :end_date', start_date: start_date, end_date: end_date)
.order(created_at: :asc)
schedules.each(&:regenerate_pdf)
puts '-> Done'
end
desc 'recreate every versions of images' desc 'recreate every versions of images'
task build_images_versions: :environment do task build_images_versions: :environment do
Project.find_each do |project| Project.find_each do |project|

View File

@ -26,7 +26,7 @@ add_mount()
{ {
# shellcheck disable=SC2016 # shellcheck disable=SC2016
# we don't want to expand ${PWD} # we don't want to expand ${PWD}
yq w docker-compose.yml "services.$SERVICE.volumes[+]" '- ${PWD}/payment_schedules:/usr/src/app/payment_schedules' yq w -i docker-compose.yml "services.$SERVICE.volumes[+]" '${PWD}/payment_schedules:/usr/src/app/payment_schedules'
} }
proceed() proceed()