From 237eefd0d6ead70adac1ae299e411261f9335292 Mon Sep 17 00:00:00 2001 From: twothreenine Date: Wed, 24 Apr 2024 10:27:14 +0200 Subject: [PATCH] More options for supplier emails 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: - The "send reply copy" checkbox should only collapse if the email field above is filled. I didn't manage to solve this (yet) -- can somebody help me? - 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.) --- .rubocop_todo.yml | 2 +- app/controllers/admin/configs_controller.rb | 2 +- app/lib/foodsoft_config.rb | 6 ++ app/mailers/mailer.rb | 21 ++++++- app/models/order.rb | 7 ++- .../admin/configs/_tab_suppliers.html.haml | 8 +++ config/app_config.yml.SAMPLE | 13 ++++ config/locales/de.yml | 18 +++++- config/locales/en.yml | 18 +++++- ..._mail_order_result_copy_to_user_setting.rb | 11 ++++ db/schema.rb | 2 +- spec/models/order_spec.rb | 59 ++++++++++++++++--- 12 files changed, 146 insertions(+), 21 deletions(-) create mode 100644 app/views/admin/configs/_tab_suppliers.html.haml create mode 100644 db/migrate/20240424015646_add_mail_order_result_copy_to_user_setting.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 6ce621036..a8aa9fa70 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -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. diff --git a/app/controllers/admin/configs_controller.rb b/app/controllers/admin/configs_controller.rb index adc1586c9..69cb09073 100644 --- a/app/controllers/admin/configs_controller.rb +++ b/app/controllers/admin/configs_controller.rb @@ -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) } diff --git a/app/lib/foodsoft_config.rb b/app/lib/foodsoft_config.rb index 6162da765..154d01eb8 100644 --- a/app/lib/foodsoft_config.rb +++ b/app/lib/foodsoft_config.rb @@ -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 diff --git a/app/mailers/mailer.rb b/app/mailers/mailer.rb index c6eb0e3a8..99d8799e8 100644 --- a/app/mailers/mailer.rb +++ b/app/mailers/mailer.rb @@ -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 @@ -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] @@ -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 diff --git a/app/models/order.rb b/app/models/order.rb index ada62e598..6f879749c 100644 --- a/app/models/order.rb +++ b/app/models/order.rb @@ -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 @@ -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 diff --git a/app/views/admin/configs/_tab_suppliers.html.haml b/app/views/admin/configs/_tab_suppliers.html.haml new file mode 100644 index 000000000..379e997fb --- /dev/null +++ b/app/views/admin/configs/_tab_suppliers.html.haml @@ -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 diff --git a/config/app_config.yml.SAMPLE b/config/app_config.yml.SAMPLE index bcebd6528..0bc74168b 100644 --- a/config/app_config.yml.SAMPLE +++ b/config/app_config.yml.SAMPLE @@ -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 + # 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 diff --git a/config/locales/de.yml b/config/locales/de.yml index 6678c6383..85af605c4 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -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 @@ -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: @@ -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. @@ -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 @@ -701,6 +714,7 @@ de: layout: Layout list: Liste messages: Nachrichten + suppliers: Lieferant:innen others: Sonstiges payment: Finanzen security: Sicherheit diff --git a/config/locales/en.yml b/config/locales/en.yml index 248ecf590..f2709ed82 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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 @@ -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: @@ -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. @@ -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 @@ -701,6 +714,7 @@ en: layout: Layout list: List messages: Messages + suppliers: Suppliers others: Other payment: Finances security: Security diff --git a/db/migrate/20240424015646_add_mail_order_result_copy_to_user_setting.rb b/db/migrate/20240424015646_add_mail_order_result_copy_to_user_setting.rb new file mode 100644 index 000000000..f7c1a4865 --- /dev/null +++ b/db/migrate/20240424015646_add_mail_order_result_copy_to_user_setting.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index a71edb91c..aa9781481 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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 diff --git a/spec/models/order_spec.rb b/spec/models/order_spec.rb index 18a478b4d..c15c7222a 100644 --- a/spec/models/order_spec.rb +++ b/spec/models/order_spec.rb @@ -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 @@ -163,6 +154,56 @@ 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 + 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) + 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) }