Skip to content

Commit

Permalink
Shop, add automatic invoicing setting
Browse files Browse the repository at this point in the history
This patch adds a new setting to the shop, allowing to automatically
invoice orders after/before a certain delay following/preceding the
delivery date.

Close #100
  • Loading branch information
thibaudgg committed Aug 11, 2023
1 parent 6b01628 commit 304b124
Show file tree
Hide file tree
Showing 18 changed files with 167 additions and 18 deletions.
2 changes: 2 additions & 0 deletions app/admin/acp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
:shop_order_maximum_weight_in_kg, :shop_order_minimal_amount,
:shop_member_percentages,
:shop_delivery_open_delay_in_days, :shop_delivery_open_last_day_end_time,
:shop_order_automatic_invoicing_delay_in_days,
:recurring_billing_wday, :currency_code,
:send_closed_invoice,
:open_renewal_reminder_sent_after_in_days,
Expand Down Expand Up @@ -317,6 +318,7 @@
f.input :shop_delivery_open_last_day_end_time, as: :time_picker, input_html: {
value: f.object.shop_delivery_open_last_day_end_time&.strftime('%H:%M')
}
f.input :shop_order_automatic_invoicing_delay_in_days
translated_input(f, :shop_invoice_infos,
hint: t('formtastic.hints.acp.shop_invoice_info'),
required: false)
Expand Down
20 changes: 18 additions & 2 deletions app/admin/shop/order.rb
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,9 @@
end
end

sidebar t('active_admin.sidebars.shop_status'), if: -> { params.dig(:q, :delivery_id_eq).present? }, only: :index do
sidebar t('active_admin.sidebars.shop_status'), if: -> { params.dig(:q, :_delivery_gid_eq).present? }, only: :index do
div class: 'content' do
delivery = Delivery.find(params[:q][:delivery_id_eq])
delivery = GlobalID::Locator.locate(params[:q][:_delivery_gid_eq])
if delivery == Delivery.shop_open.next
if delivery.shop_open?
span t('active_admin.sidebars.shop_open_until_html', date: l(delivery.date, format: :long), end_date: l(delivery.shop_closing_at, format: :long))
Expand All @@ -127,6 +127,22 @@
end
end

sidebar t('active_admin.sidebars.billing'), if: -> { params.dig(:q, :_delivery_gid_eq).present? }, only: :index do
div class: 'actions' do
handbook_icon_link('shop', anchor: 'facturation')
end

div class: 'content' do
if delay = Current.acp.shop_order_automatic_invoicing_delay_in_days
delivery = GlobalID::Locator.locate(params[:q][:_delivery_gid_eq])
date = delivery.date + delay.days
span t('shop.orders_automatic_invoicing', date: l(date, format: :long))
else
span t('shop.orders_manual_invoicing')
end
end
end

sidebar_handbook_link('shop#commandes')

show do |order|
Expand Down
1 change: 1 addition & 0 deletions app/assets/stylesheets/active_admin.css.erb
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@ span.delivery_note input[type="submit"] {
font-size: 16px;
font-weight: 600;
margin: 30px 0 0 0;
scroll-margin-top: 60px;
}

.handbook #main_content p {
Expand Down
9 changes: 9 additions & 0 deletions app/jobs/billing/shop_order_auto_invoicer_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module Billing
class ShopOrderAutoInvoicerJob < ApplicationJob
queue_as :low

def perform(order)
order.auto_invoice!
end
end
end
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module Scheduled
class BillingInvoicerJob < BaseJob
class BillingMembersInvoicerJob < BaseJob
def perform
return unless Current.acp.recurring_billing_wday == Date.current.wday

Expand Down
12 changes: 12 additions & 0 deletions app/jobs/scheduled/billing_shop_orders_auto_invoicer_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module Scheduled
class BillingShopOrdersAutoInvoicerJob < BaseJob
def perform
return unless Current.acp.feature?('shop')
return unless Current.acp.shop_order_automatic_invoicing_delay_in_days

Shop::Order.pending.find_each do |order|
Billing::ShopOrderInvoicerJob.perform_later(order)
end
end
end
end
2 changes: 2 additions & 0 deletions app/models/acp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ class ACP < ApplicationRecord
numericality: { greater_than_or_equal_to: 1, allow_nil: true }
validates :shop_order_minimal_amount,
numericality: { greater_than_or_equal_to: 1, allow_nil: true }
validates :shop_order_automatic_invoicing_delay_in_days,
numericality: { only_integer: true, allow_nil: true }
validates :member_form_mode, presence: true, inclusion: { in: MEMBER_FORM_MODES }
validates :member_profession_form_mode, presence: true, inclusion: { in: INPUT_FORM_MODES }
validates :member_come_from_form_mode, presence: true, inclusion: { in: INPUT_FORM_MODES }
Expand Down
10 changes: 10 additions & 0 deletions app/models/shop/order.rb
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,16 @@ def unconfirm!
end
end

def auto_invoice!
delay = Current.acp.shop_order_automatic_invoicing_delay_in_days
return unless delay
return unless can_invoice?

if (Date.today - delivery.date).to_i >= delay
invoice!
end
end

def invoice!
invalid_transition(:invoice!) unless can_invoice?

Expand Down
4 changes: 3 additions & 1 deletion app/views/handbook/shop.md.erb
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ Un bulletin de livraison (PDF), n'incluant pas le prix des produits, est disponi

### Facturation

Une fois les commandes préparées, les commandes doivent être **manuellement** facturées (possibilité de toutes les facturer en un coup via une action groupée). La facture est automatiquement envoyée par email au membre.
Par défaut, une fois les commandes préparées, les commandes doivent être **manuellement** facturées (possibilité de toutes les facturer en un coup via une action groupée). La facture est ensuite automatiquement envoyée par email au membre.

Il est également possible de configurer la **facturation automatique** des commandes, depuis [les paramètres de l'épicerie](/settings#shop), après ou avant un certain nombre de jours suivant ou précédant la date de livraison.

> Si une commande déjà facturée est modifiée, la facture existante est automatiquement annulée et une nouvelle facture devra être générée et envoyée.

Expand Down
4 changes: 4 additions & 0 deletions config/locales/activerecord.yml
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,10 @@ _:
_de: Verfügbare Prozentsätze für Mitglieder
_fr: Pourcentages disponibles pour les membres
_it: Percentuali disponibili per i soci
shop_order_automatic_invoicing_delay_in_days:
_de: Automatische Rechnungsstellung vor/nach der Lieferung (in Tagen)
_fr: Facturation automatique avant/après la livraison (en jours)
_it: Fatturazione automatica prima/dopo la consegna (in giorni)
shop_order_maximum_weight_in_kg:
_de: Maximales Gewicht einer Bestellung (in kg)
_fr: Poids maximal d'une commande (en kg)
Expand Down
4 changes: 4 additions & 0 deletions config/locales/formtastic.yml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,10 @@ _:
_de: Durch Komma getrennt. Ermöglicht es Mitgliedern, den Endpreis seiner Bestellung selbst um die verfügbaren Prozentsätze zu erhöhen (Unterstützung) oder zu senken (Rabatt).
_fr: Séparés par une virgule. Permet aux membres d'augmenter (soutien) ou de diminuer (rabais) lui-même le prix final de sa commande par les pourcentages disponibles.
_it: Separati da una virgola. Consente ai soci di aumentare (supporto) o diminuire (sconto) il prezzo finale dell'ordine in base alle percentuali disponibili.
shop_order_automatic_invoicing_delay_in_days:
_de: Ermöglicht es Ihnen, die Bestellungen X Tage nach oder vor (-X) dem Lieferdatum automatisch zu fakturieren. Lassen Sie das Feld leer, um die Bestellungen manuell zu fakturieren.
_fr: Permet de facturer automatiquement, durant la nuit, les commandes X jours après ou avant (-X) la date de livraison. Laisser vide pour facturer manuellement les commandes.
_it: Permette di fatturare automaticamente, durante la notte, gli ordini X giorni dopo o prima (-X) la data di consegna. Lasciare vuoto per fatturare manualmente gli ordini.
shop_order_maximum_weight_in_kg:
_de: Leer lassen, wenn keine Begrenzung.
_fr: Laisser vide si aucune limite.
Expand Down
24 changes: 12 additions & 12 deletions config/locales/mail_template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ _:
_de: "Bestätigte Aktivität \U0001F389"
_fr: "Activité validée \U0001F389"
_it: "Attività confermata \U0001F389"
invoice_created:
_de: 'Neue Rechnung #{{ invoice.number }}'
_fr: 'Nouvelle facture #{{ invoice.number }}'
_it: 'Nuova fattura #{{ invoice.number }}'
invoice_cancelled:
_de: 'Stornierte Rechnung #{{ invoice.number }}'
_fr: 'Facture annulée #{{ invoice.number }}'
_it: 'Fattura annullata #{{ invoice.number }}'
invoice_created:
_de: 'Neue Rechnung #{{ invoice.number }}'
_fr: 'Nouvelle facture #{{ invoice.number }}'
_it: 'Nuova fattura #{{ invoice.number }}'
invoice_overdue_notice:
_de: "Mahnung #{{ invoice.overdue_notices_count }} der Rechnung #{{ invoice.number }} \U0001F62C"
_fr: "Rappel #{{ invoice.overdue_notices_count }} de la facture #{{ invoice.number }} \U0001F62C"
Expand Down Expand Up @@ -58,14 +58,14 @@ _:
_de: Wird gesendet, wenn die Teilnahme an einer Aktivität bestätigt wird.
_fr: Envoyé quand une participation à une activité est validée.
_it: Inviato quando viene convalidata una partecipazione a un'attività.
invoice_created:
_de: Wird gesendet, wenn eine Rechnung automatisch erstellt wird oder wenn die Schaltfläche "Senden" einer manuell erstellten Rechnung gedrückt wird.
_fr: Envoyé lors de la création automatique d'une facture ou quand le bouton "Envoyer" d'une facture créée manuellement est pressé.
_it: Inviato durante la creazione automatica di una fattura o quando si preme il pulsante "Invia" di una fattura creata manualmente.
invoice_cancelled:
_de: Wird gesendet, wenn eine noch offene Rechnung storniert wird.
_fr: Envoyé quand une facture encore ouverte est annulée.
_it: Inviato quando una fattura ancora aperta viene annullata.
invoice_created:
_de: Wird gesendet, wenn eine Rechnung automatisch erstellt wird oder wenn die Schaltfläche "Senden" einer manuell erstellten Rechnung gedrückt wird.
_fr: Envoyé lors de la création automatique d'une facture ou quand le bouton "Envoyer" d'une facture créée manuellement est pressé.
_it: Inviato durante la creazione automatica di una fattura o quando si preme il pulsante "Invia" di una fattura creata manualmente.
invoice_overdue_notice:
_de: Wird automatisch gesendet, wenn eine Rechnung 35 Tage nach ihrer Erstellung oder letzten Mahnung noch offen ist.</br><i>Deaktiviert, wenn die Zahlungen nicht automatisch verarbeitet werden (EBICS).</i>
_fr: Envoyé automatiquement quand une facture est toujours ouverte 35 jours après sa création ou son dernier rappel.</br><i>Désactivé quand les paiements ne sont pas traités automatiquement (EBICS).</i>
Expand Down Expand Up @@ -103,14 +103,14 @@ _:
_de: Bestätigte Aktivität
_fr: Activité validée
_it: Attività confermata
invoice_created:
_de: Neue Rechnung
_fr: Nouvelle facture
_it: Nuova fattura
invoice_cancelled:
_de: Stornierte Rechnung
_fr: Facture annulée
_it: Fattura annullata
invoice_created:
_de: Neue Rechnung
_fr: Nouvelle facture
_it: Nuova fattura
invoice_overdue_notice:
_de: Mahnung der Rechnung
_fr: Rappel facture
Expand Down
8 changes: 8 additions & 0 deletions config/locales/shop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ _:
_de: keine Bestellung
_fr: aucune commande
_it: nessun ordine
orders_automatic_invoicing:
_de: Die zu bereitenden Bestellungen werden in der Nacht vom %{date} automatisch fakturiert.
_fr: Les commandes à préparer seront automatiquement facturées, durant la nuit, à partir du %{date}.
_it: Gli ordini da preparare verranno fatturati automaticamente, durante la notte, a partire dal %{date}.
orders_manual_invoicing:
_de: Automatische Rechnungsstellung ist nicht konfiguriert, Bestellungen müssen manuell fakturiert werden.
_fr: La facturation automatique n'est pas paramétrée, les commandes à préparer doivent être facturées manuellement.
_it: La fatturazione automatica non è configurata, gli ordini devono essere fatturati manualmente.
percentage:
negative:
_de: Rabatt %{percentage}%
Expand Down
8 changes: 7 additions & 1 deletion config/sidekiq.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,14 @@
cron: '0 0 5 * * *'
queue: low
class: ScheduledJob
args: Scheduled::BillingInvoicerJob
args: Scheduled::BillingMembersInvoicerJob
description: "Automatically create and send new invoices"
billing_shop_orders:
cron: '0 45 4 * * *'
queue: low
class: ScheduledJob
args: Scheduled::BillingShopOrdersAutoInvoicerJob
description: "Automatically invoice shop orders"
billing_payments:
cron: '0 30 4 * * *'
queue: low
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddShopOrderAutomaticInvoicingDelayInDays < ActiveRecord::Migration[7.0]
def change
add_column :acps, :shop_order_automatic_invoicing_delay_in_days, :integer
end
end
3 changes: 2 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[7.0].define(version: 2023_07_07_091231) do
ActiveRecord::Schema[7.0].define(version: 2023_08_10_153313) do
# These are extensions that must be enabled in order to support this database
enable_extension "hstore"
enable_extension "plpgsql"
Expand Down Expand Up @@ -110,6 +110,7 @@
t.string "basket_complements_member_order_mode", default: "deliveries_count_desc", null: false
t.string "depots_member_order_mode", default: "price_asc", null: false
t.string "deliveries_cycles_member_order_mode", default: "deliveries_count_desc", null: false
t.integer "shop_order_automatic_invoicing_delay_in_days"
t.index ["host"], name: "index_acps_on_host"
t.index ["tenant_name"], name: "index_acps_on_tenant_name"
end
Expand Down
3 changes: 3 additions & 0 deletions spec/factories/shop/orders.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,8 @@
trait :pending do
state { Shop::Order::PENDING_STATE }
end
trait :invoiced do
state { Shop::Order::INVOICED_STATE }
end
end
end
64 changes: 64 additions & 0 deletions spec/models/shop/order_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,70 @@
end
end

describe '#auto_invoice!' do
specify 'auto invoice after delivery date' do
delivery = create(:delivery, date: 3.days.from_now)
order = create(:shop_order, :pending, delivery_gid: delivery.gid)
Current.acp.update!(shop_order_automatic_invoicing_delay_in_days: 3)

travel 5.days do
expect { order.auto_invoice! }.not_to change { order.reload.state }
end

travel 6.days do
expect {
order.auto_invoice!
}.to change { order.reload.state }.from('pending').to('invoiced')
end
end

specify 'auto invoice before delivery date' do
delivery = create(:delivery, date: Date.today)
order = create(:shop_order, :pending, delivery_gid: delivery.gid)
Current.acp.update!(shop_order_automatic_invoicing_delay_in_days: -2)

travel -3.days do
expect { order.auto_invoice! }.not_to change { order.reload.state }
end

travel -2.days do
expect { order.auto_invoice! }
.to change { order.reload.state }.from('pending').to('invoiced')
end
end

specify 'auto invoice the delivery date' do
delivery = create(:delivery, date: Date.today)
order = create(:shop_order, :pending, delivery_gid: delivery.gid)
Current.acp.update!(shop_order_automatic_invoicing_delay_in_days: 0)

expect { order.auto_invoice! }
.to change { order.reload.state }.from('pending').to('invoiced')
end

specify 'do nothing when no delay configured' do
delivery = create(:delivery, date: Date.today)
order = create(:shop_order, :pending, delivery_gid: delivery.gid)
Current.acp.update!(shop_order_automatic_invoicing_delay_in_days: nil)

expect { order.auto_invoice! }.not_to change { order.reload.state }
end

specify 'do nothing for cart order' do
Current.acp.update!(shop_order_automatic_invoicing_delay_in_days: 0)

order = create(:shop_order, :cart)
expect { order.auto_invoice! }.not_to change { order.reload.state }
end

specify 'do nothing for invoiced order' do
Current.acp.update!(shop_order_automatic_invoicing_delay_in_days: 0)

order = create(:shop_order, :invoiced)
expect { order.auto_invoice! }.not_to change { order.reload.state }
end
end

describe '#invoice!' do
specify 'create an invoice and set state to invoiced' do
product = create(:shop_product,
Expand Down

0 comments on commit 304b124

Please sign in to comment.