Skip to content

Commit

Permalink
More options for supplier emails
Browse files Browse the repository at this point in the history
Previous behavior:
The order email sent to the supplier will be sent as a copy (CC) to the user* and the supplier will be request to send any replies to that user*.
*user: If sent by clicking on the "Send to supplier" button, the user who clicked the button; if sent automatically by end action, the user who created the order.

New behavior:
In Administration/Configuration, there's a new tab "Suppliers" where admins can set options for communication with suppliers.
The default by migration is: The order email sent to the supplier will be sent as a copy (CC) to the associated users* and the supplier will be requested to send any replies to them.
*associated users: both a) the user who created the order and b) (unless it was auto-sent) the user who clicked the button "Send to supplier."
The copy of the email to the supplier can be changed to blind copy (BCC, default for new instances) or no copy at all.
If a reply-to address is set, the supplier will be requested to send any replies to that address instead.
If the "send reply copy" option below is checked, there will be multiple reply-to addresses: the specified address and the associated user(s)

Old behavior:
If not a single article has been ordered, the empty order will be sent to the supplier anyway (unless a minimum order quantity has been set and the respective end action been selected.)

New behavior:
Not to disrupt any workflows, this behavior remains the same if "Close the order and send it to the supplier" selected, but is pointed out now by the affix "(even if nothing has been ordered.)"
There's a new option "Close the order and send it to the supplier unless nothing has been ordered." This checks if at least one article has been ordered (i.e. 1 box filled.)
The behavior of "Close the order and send it to the supplier if the minimum quantity has been reached" is changed slightly: It also checks if at least one article has been ordered. This makes it a good general option that fulfills most use cases, so you don't have to memorize whether a minimum order quantity has been set for each supplier.

TO DO: (help welcome!)
- The "send reply copy" checkbox should only collapse if the email field above is filled. I didn't manage to solve this (yet)
- I could only test the "unless nothing ordered" code indirectly, as the end actions didn't work in my local environment. The check worked in another method, but it should be tested if this exact code actually works (both for auto_close_and_send_min_quantity and auto_close_and_send_unless_empty.)
- I tried to update the tests (since the min_order_quantity test didn't work anymore) and add more, but they don't work yet -- no email gets sent. I either made a mistake in the tests (didn't really grasp the "let" etc. logic yet) or the emailing function is broken for some reason.
  • Loading branch information
twothreenine committed Apr 28, 2024
1 parent 8b4c36e commit 6d77ed5
Show file tree
Hide file tree
Showing 12 changed files with 149 additions and 21 deletions.
2 changes: 1 addition & 1 deletion .rubocop_todo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ Metrics/BlockNesting:
# Offense count: 18
# Configuration parameters: CountComments, CountAsOne.
Metrics/ClassLength:
Max: 294
Max: 310

# Offense count: 51
# Configuration parameters: AllowedMethods, AllowedPatterns.
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/admin/configs_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def update

# Set configuration tab names as `@tabs`
def get_tabs
@tabs = %w[foodcoop payment tasks messages layout language security others]
@tabs = %w[foodcoop payment tasks messages suppliers layout language security others]
# allow engines to modify this list
engines = Rails::Engine.subclasses.map(&:instance).select { |e| e.respond_to?(:configuration) }
engines.each { |e| e.configuration(@tabs, self) }
Expand Down
6 changes: 6 additions & 0 deletions app/lib/foodsoft_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ module DistributionStrategy
NO_AUTOMATIC_DISTRIBUTION = 'no_automatic_distribution'
end

module MailOrderResultCopyToUser
NO_COPY = 'no_copy'
CC = 'cc'
BCC = 'bcc'
end

class << self
# Load and initialize foodcoop configuration file.
# @param filename [String] Override configuration file
Expand Down
21 changes: 18 additions & 3 deletions app/mailers/mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,25 @@ def order_result_supplier(user, order, options = {})
@order = order
@supplier = order.supplier

associated_users =
if user == order.created_by
format_user_address(user)
else
[format_user_address(user), format_user_address(order.created_by)].join(', ')
end
reply_to_users = associated_users unless FoodsoftConfig[:order_result_email_reply_to] && !FoodsoftConfig[:order_result_email_reply_copy_to_user]
users_cc = associated_users if FoodsoftConfig[:mail_order_result_copy_to_user] == FoodsoftConfig::MailOrderResultCopyToUser::CC
users_bcc = associated_users if FoodsoftConfig[:mail_order_result_copy_to_user] == FoodsoftConfig::MailOrderResultCopyToUser::BCC

add_order_result_attachments order, options

subject = I18n.t('mailer.order_result_supplier.subject', name: order.supplier.name)
subject += " (#{I18n.t('activerecord.attributes.order.pickup')}: #{format_date(order.pickup)})" if order.pickup

mail to: order.supplier.email,
cc: user,
reply_to: user,
cc: users_cc,
bcc: users_bcc,
reply_to: [FoodsoftConfig[:order_result_email_reply_to], reply_to_users].join(', '),
subject: subject
end

Expand Down Expand Up @@ -119,7 +130,7 @@ def mail(args)

%i[bcc cc reply_to sender to].each do |k|
user = args[k]
args[k] = format_address(user.email, show_user(user)) if user.is_a? User
args[k] = format_user_address(user) if user.is_a? User
end

if contact_email = FoodsoftConfig[:contact][:email]
Expand Down Expand Up @@ -165,6 +176,10 @@ def additonal_welcome_text(user); end

private

def format_user_address(user)
format_address(user.email, show_user(user))
end

def format_address(email, name)
address = Mail::Address.new email
address.display_name = name
Expand Down
7 changes: 5 additions & 2 deletions app/models/order.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class Order < ApplicationRecord
belongs_to :updated_by, class_name: 'User', foreign_key: 'updated_by_user_id'
belongs_to :created_by, class_name: 'User', foreign_key: 'created_by_user_id'

enum end_action: { no_end_action: 0, auto_close: 1, auto_close_and_send: 2, auto_close_and_send_min_quantity: 3 }
enum end_action: { no_end_action: 0, auto_close: 1, auto_close_and_send: 2, auto_close_and_send_unless_empty: 4, auto_close_and_send_min_quantity: 3 }
enum transport_distribution: { skip: 0, ordergroup: 1, price: 2, articles: 3 }

# Validations
Expand Down Expand Up @@ -316,7 +316,10 @@ def do_end_action!
send_to_supplier!(created_by)
elsif auto_close_and_send_min_quantity?
finish!(created_by)
send_to_supplier!(created_by) if sum >= supplier.min_order_quantity.to_r
send_to_supplier!(created_by) if sum >= supplier.min_order_quantity.to_r && !order_articles.ordered.empty?
elsif auto_close_and_send_unless_empty?
finish!(created_by)
send_to_supplier!(created_by) unless order_articles.ordered.empty?
end
end

Expand Down
8 changes: 8 additions & 0 deletions app/views/admin/configs/_tab_suppliers.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
%fieldset
%label
%h4= t '.communication_with_suppliers'
- mail_order_result_copy_to_user_options = FoodsoftConfig::MailOrderResultCopyToUser.constants.map { |c| FoodsoftConfig::MailOrderResultCopyToUser.const_get(c) }
= config_input form, :mail_order_result_copy_to_user, as: :select, collection: mail_order_result_copy_to_user_options,
include_blank: false, input_html: {class: 'input-xxlarge'}, value_method: ->(s){ s }, label_method: ->(s){ t("config.keys.mail_order_result_copy_to_user_options.#{s}") }
= config_input form, :order_result_email_reply_to, as: :string, input_html: {class: 'input-xlarge', placeholder: "#{@cfg[:name]} <#{@cfg[:contact][:email]}>"}
= config_input form, :order_result_email_reply_copy_to_user, as: :boolean
13 changes: 13 additions & 0 deletions config/app_config.yml.SAMPLE
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,19 @@ default: &defaults
# email address to be used as sender
email_sender: foodsoft@foodcoop.test


# Options for communication between suppliers, the foodcoop, and order-associated users:
# Associated users are a) the user who created the order and b) (unless it was auto-sent) the user who clicked the button "Send to supplier."

# Mail order results only to the supplier (no_copy), as copy to associated user(s) (cc), or as blind copy to associated user(s) (bcc).
mail_order_result_copy_to_user: bcc

# Enter an email address if you want to request your suppliers to send any replies to that address instead of the associated users':
# order_result_email_reply_to: Foodcoop <orders@foodcoop.test>
# If you want replies to be sent to both the specified reply-to address and the associated users':
# order_result_email_reply_copy_to_user: true


# domain to be used for reply emails
#reply_email_domain: reply.foodcoop.test

Expand Down
18 changes: 16 additions & 2 deletions config/locales/de.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,9 @@ de:
end_action: Endeaktion
end_actions:
auto_close: Bestellung beenden
auto_close_and_send: Bestellung beenden und an Lieferantin schicken
auto_close_and_send_min_quantity: Bestellung beenden und an Lieferantin schicken sofern die Mindestbestellmenge erreicht wurde
auto_close_and_send: Bestellung beenden und an Lieferantin schicken (auch wenn nichts bestellt wurde)
auto_close_and_send_min_quantity: Bestellung beenden und an Lieferantin schicken, sofern die Mindestbestellmenge erreicht wurde (und mind. 1 Artikel bestellt)
auto_close_and_send_unless_empty: Bestellung beenden und an Lieferantin schicken, außer es wurde nichts bestellt
no_end_action: Keine automatische Aktion
ends: Endet am
name: Lieferant
Expand Down Expand Up @@ -316,6 +317,8 @@ de:
pdf_title: PDF-Dokumente
tab_messages:
emails_title: E-Mails versenden
tab_suppliers:
communication_with_suppliers: Kommunikation mit Lieferant:innen
tab_payment:
schedule_title: Bestellschema
tab_security:
Expand Down Expand Up @@ -603,6 +606,9 @@ de:
email_from: E-Mails werden so aussehen, als ob sie von dieser Adresse gesendet wurden. Kann leer gelassen werden, um die Kontaktadresse der Foodcoop zu benutzen.
email_replyto: Setze diese Adresse, wenn Du Antworten auf Foodsoft E-Mails auf eine andere, als die oben angegebene Absenderadresse bekommen möchtest.
email_sender: E-Mails werden so aussehen, als ob sie von dieser Adresse versendet wurden. Um zu vermeiden, dass E-Mails dadurch als Spam eingeordnet werden, muss der Webserver möglicherweise im SPF Eintrag der Domain der E-Mail Adresse eingetragen werden.
mail_order_result_copy_to_user: Wenn eine Bestellung an eine Lieferant:in gesendet wird, wird eine (Blind-)Kopie der E-Mail an die zugehörige Benutzer:innen gesendet. Diese sind a) die Benutzer:in, die die Bestellung eröffnet hat und b) (außer bei automatischer Aussendung) die Benutzer:in, die auf den "An Lieferantin schicken"-Button geklickt hat.
order_result_email_reply_to: Gib eine E-Mail-Adresse ein, falls du die Lieferant:innen bitten möchtest, Antworten ggf. an jene Adresse zu schicken anstatt an die der zugehörigen Benutzer:innen (die die Bestellung erstellt bzw. auf den "An Lieferantin schicken"-Button geklickt haben)
order_result_email_reply_copy_to_user: Wenn aktiviert, werden die Lieferant:innen gebeten Antworten ggf. sowohl an die angegebene Adresse, als auch an die Benutzer:in, die die Bestellung erstellt hat, als auch ggf. an die Benutzer:in, die auf den "An Lieferantin schicken"-Button geklickt hat, zu schicken.
help_url: Link zur Dokumentationsseite
homepage: Webseite der Foodcoop
ignore_browser_locale: Ignoriere die Sprache des Computers des Anwenders, wenn der Anwender noch keine Sprache gewählt hat.
Expand Down Expand Up @@ -660,6 +666,13 @@ de:
email_from: Absenderadresse
email_replyto: Antwortadresse
email_sender: Senderadresse
mail_order_result_copy_to_user: E-Mail mit Bestellergebnis ...
mail_order_result_copy_to_user_options:
no_copy: nur an Lieferant:in senden
cc: als Kopie (CC) an zugehörige Benutzer:in(nen) senden
bcc: als Blindkopie (BCC) an zugehörige Benutzer:in(nen) senden
order_result_email_reply_to: Antwortadresse
order_result_email_reply_copy_to_user: Antwort auch an zugehörige Benutzer:in(nen)
help_url: URL Dokumentation
homepage: Webseite
ignore_browser_locale: Browsersprache ignorieren
Expand Down Expand Up @@ -701,6 +714,7 @@ de:
layout: Layout
list: Liste
messages: Nachrichten
suppliers: Lieferant:innen
others: Sonstiges
payment: Finanzen
security: Sicherheit
Expand Down
18 changes: 16 additions & 2 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,9 @@ en:
end_action: End action
end_actions:
auto_close: Close the order
auto_close_and_send: Close the order and send it to the supplier
auto_close_and_send_min_quantity: Close the order and send it to the supplier if the minimum quantity has been reached
auto_close_and_send: Close the order and send it to the supplier (even if nothing has been ordered)
auto_close_and_send_min_quantity: Close the order and send it to the supplier if the minimum quantity has been reached (and min. 1 article ordered)
auto_close_and_send_unless_empty: Close the order and send it to the supplier unless nothing has been ordered
no_end_action: No automatic action
ends: Ends at
name: Supplier
Expand Down Expand Up @@ -316,6 +317,8 @@ en:
pdf_title: PDF documents
tab_messages:
emails_title: Sending email
tab_suppliers:
communication_with_suppliers: Communication with suppliers
tab_payment:
schedule_title: Ordering schedule
tab_security:
Expand Down Expand Up @@ -603,6 +606,9 @@ en:
email_from: Emails will appear to be from this email address. Leave empty to use the foodcoop's contact address.
email_replyto: Set this when you want to receive replies from emails sent by Foodsoft on a different address than the above.
email_sender: Emails will appear to be sent from this email address. To avoid emails sent being classified as spam, the webserver may need to be registered in the SPF record of the email address's domain.
mail_order_result_copy_to_user: When an order is sent to the supplier, a (blind) copy of the email will be sent to the associated users. Those are a) the user who created the order and b) (unless it was auto-sent) the user who clicked the button "Send to supplier."
order_result_email_reply_to: Enter an email address if you want to request your suppliers to send any replies to that address instead of the associated users' (who created the order / clicked the "Send to supplier" button.)
order_result_email_reply_copy_to_user: If enabled, your suppliers will be requested to send any replies both to the specified reply address, as to the user who created the order, as, if given, to the user who clicked the "Send to supplier" button.
help_url: Documentation website.
homepage: Website of your foodcoop.
ignore_browser_locale: Ignore the language of user's computer when the user has not chosen a language yet.
Expand Down Expand Up @@ -660,6 +666,13 @@ en:
email_from: From address
email_replyto: Reply-to address
email_sender: Sender address
mail_order_result_copy_to_user: Mail order result ...
mail_order_result_copy_to_user_options:
no_copy: only to the supplier
cc: as copy (CC) to associated user(s)
bcc: as blind copy (BCC) to associated user(s)
order_result_email_reply_to: Reply-to address
order_result_email_reply_copy_to_user: Send reply copy to associated user(s)
help_url: Documentation URL
homepage: Homepage
ignore_browser_locale: Ignore browser language
Expand Down Expand Up @@ -701,6 +714,7 @@ en:
layout: Layout
list: List
messages: Messages
suppliers: Suppliers
others: Other
payment: Finances
security: Security
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class AddMailOrderResultCopyToUserSetting < ActiveRecord::Migration[7.0]
def up
FoodsoftConfig[:mail_order_result_copy_to_user] = FoodsoftConfig::MailOrderResultCopyToUser::CC
end

def down
FoodsoftConfig[:mail_order_result_copy_to_user] = nil
FoodsoftConfig[:order_result_email_reply_to] = nil
FoodsoftConfig[:order_result_email_reply_copy_to_user] = nil
end
end
2 changes: 1 addition & 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: 2024_01_26_111615) do
ActiveRecord::Schema[7.0].define(version: 2024_04_24_015646) do
create_table "action_text_rich_texts", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t|
t.string "name", null: false
t.text "body", size: :long
Expand Down
62 changes: 53 additions & 9 deletions spec/models/order_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,6 @@
end
end

it 'sends mail if min_order_quantity has been reached' do
create(:user, groups: [create(:ordergroup)])
create(:order, created_by: user, starts: Date.yesterday, ends: 1.hour.ago,
end_action: :auto_close_and_send_min_quantity)

Order.finish_ended!
expect(ActionMailer::Base.deliveries.count).to eq 1
end

it 'needs a supplier' do
expect(build(:order, supplier: nil)).to be_invalid
end
Expand Down Expand Up @@ -163,6 +154,59 @@
end
end

describe 'with end_action auto_close_and_send' do
let!(:order) { create(:order, created_by: user, starts: Date.yesterday, ends: 1.hour.ago, end_action: :auto_close_and_send) }

it 'sends mail even if nothing ordered' do
Order.finish_ended!
order.reload
expect(ActionMailer::Base.deliveries.count).to eq 1
end
end

describe 'with end_action auto_close_and_send_min_quantity' do
let!(:order) { create(:order, created_by: user, starts: Date.yesterday, ends: 1.hour.ago, end_action: :auto_close_and_send_min_quantity, article_count: 1) }
let!(:oa) { order.order_articles.first }
let!(:go) { create(:group_order, order: order) }
let!(:goa) { create(:group_order_article, group_order: go, order_article: oa, quantity: 0) }

it 'does not send mail if nothing ordered' do
# TODO: call go.reload, oa.update_results! if that proves to be correct
Order.finish_ended!
order.reload
expect(ActionMailer::Base.deliveries.count).to eq 0
end

it 'sends mail if min_order_quantity has been reached' do # I think there isn't actually a min_order_quantity that is checked?!
goa.update_quantities(1, 0)
go.reload
oa.update_results!
Order.finish_ended!
order.reload
expect(ActionMailer::Base.deliveries.count).to eq 1
end
end

describe 'with end_action auto_close_and_send_unless_empty' do
let!(:order) { create(:order, created_by: user, starts: Date.yesterday, ends: 1.hour.ago, end_action: :auto_close_and_send_unless_empty, article_count: 1) }
let!(:oa) { order.order_articles.first }
let!(:go) { create(:group_order, order: order) }
let!(:goa) { create(:group_order_article, group_order: go, order_article: oa, quantity: 0) }

it 'does not send mail if nothing ordered' do
Order.finish_ended!
order.reload
expect(ActionMailer::Base.deliveries.count).to eq 0
end

it 'sends mail if something ordered' do
goa.update_quantities(1, 0)
Order.finish_ended!
order.reload
expect(ActionMailer::Base.deliveries.count).to eq 1
end
end

describe 'balancing charges correct amounts' do
let!(:transport) { rand(0.1..26.0).round(2) }
let!(:order) { create(:order, article_count: 1) }
Expand Down

0 comments on commit 6d77ed5

Please sign in to comment.