Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions app/controllers/manage/quotes_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# frozen_string_literal: true

module Manage
class QuotesController < BaseController
load_and_authorize_resource :booking
load_and_authorize_resource :quote, through: :booking, shallow: true

def index
@quotes = @quotes.where(booking: { organisation: current_organisation })
.includes(:organisation).ordered.with_attached_pdf
@quotes = @quotes.where(booking: @booking) if @booking.present?
@quotes = @quotes.kept if @booking.blank?

respond_with :manage, @quotes
end

def show
@booking = @quote.booking
respond_to do |format|
format.pdf do
redirect_to url_for(@quote.pdf)
end
end
end

def new
@quote = QuoteFactory.new(@booking).build(suggest_items: true, **quote_params)
respond_with :manage, @booking, @quote
end

def edit
@booking = @quote.booking
respond_with :manage, @quote
end

def create
@booking = @quote.booking
@quote.save
respond_with :manage, @quote, location: -> { manage_booking_quotes_path(@quote.booking) }
end

def update
@booking = @quote.booking
@quote.update(quote_params) unless @quote.discarded?
respond_with :manage, @quote, location: -> { manage_booking_quotes_path(@quote.booking) }
end

def destroy
@quote.discarded? || !@quote.sent? ? @quote.destroy : @quote.discard!
respond_with :manage, @quote, location: -> { manage_booking_quotes_path(@quote.booking) }
end

private

def quote_params
QuoteParams.new(params[:quote]).permitted
end
end
end
2 changes: 1 addition & 1 deletion app/controllers/manage/rich_text_templates_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def rich_text_templates_by_booking_action

def rich_text_templates_by_document # rubocop:disable Metrics/MethodLength
{
Invoices::Offer => @rich_text_templates.where(key: :invoices_offer_text),
Quote => @rich_text_templates.where(key: :quote_text),
Invoices::Deposit => @rich_text_templates.where(key: :invoices_deposit_text),
Invoices::Invoice => @rich_text_templates.where(key: :invoices_invoice_text),
Invoices::LateNotice => @rich_text_templates.where(key: :invoices_late_notice_text),
Expand Down
29 changes: 19 additions & 10 deletions app/domain/booking_actions/email_contract.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ class EmailContract < Base

delegate :contract, to: :booking

def invoke!(invoice_ids: invoices.map(&:id), current_user: nil)
def invoke!(invoice_ids: invoices.map(&:id), quote_ids: quotes.map(&:id), current_user: nil)
invoices = self.invoices.where(id: invoice_ids)
mail = send_tenant_notification(invoices)
quotes = self.quotes.where(id: quote_ids)
mail = send_tenant_notification(invoices, quotes)

send_operator_notification(invoices)
send_operator_notification(invoices, quotes)

Result.success redirect_proc: mail&.autodeliver_with_redirect_proc
end
Expand All @@ -25,40 +26,48 @@ def invokable_with(current_user: nil)
return unless invokable?(current_user:)

invoice_ids = invoices.map(&:to_param)
quote_ids = quotes.map(&:to_param)
if invoices.any?
{ label: translate(:label_with_invoice), params: { invoice_ids: } }
{ label: translate(:label_with_invoice), params: { invoice_ids:, quote_ids: } }
else
{ label: translate(:label_without_invoice), confirm: translate(:confirm), params: { invoice_ids: [] } }
{ label: translate(:label_without_invoice), confirm: translate(:confirm),
params: { invoice_ids: [], quote_ids: [] } }
end
end

def invoices
@invoices ||= booking.invoices.kept.unsent
end

def quotes
@quotes ||= booking.quotes.unsent
end

def invoke_schema
Dry::Schema.Params do
optional(:invoice_ids).array(:string)
optional(:quote_ids).array(:string)
end
end

protected

def send_tenant_notification(invoices)
context = { contract:, invoices: }
def send_tenant_notification(invoices, quotes)
context = { contract:, invoices:, quotes: }
MailTemplate.use!(:email_contract_notification, booking, to: :tenant, context:) do |mail|
mail.attach :contract, invoices
mail.save!
invoices.each { it.update!(sent_with_notification: mail) }
quotes.each { it.update!(sent_with_notification: mail) }
contract.update!(sent_with_notification: mail)
end
end

def send_operator_notification(invoices)
context = { contract:, invoices: }
def send_operator_notification(invoices, quotes)
context = { contract:, invoices:, quotes: }
Notification.dedup(booking, to: %i[billing home_handover home_return]) do |to|
MailTemplate.use(:operator_email_contract_notification, booking, to:, context:)&.tap do |mail|
mail.attach contract, invoices
mail.attach contract, invoices, quotes
mail.autodeliver!
end
end
Expand Down
47 changes: 0 additions & 47 deletions app/domain/booking_actions/email_offer.rb

This file was deleted.

47 changes: 47 additions & 0 deletions app/domain/booking_actions/email_quote.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# frozen_string_literal: true

module BookingActions
class EmailQuote < Base
use_mail_template(:email_quote_notification, context: %i[booking quote], optional: true, autodeliver: false)

def invoke!(quote_id:, current_user: nil)
quote = booking.organisation.quotes.find_by(id: quote_id)
mail = send_tenant_notification(quote)

Result.success redirect_proc: mail&.autodeliver_with_redirect_proc
end

def invokable?(quote_id:, current_user: nil)
booking.notifications_enabled && booking.tenant.email.present? && unsent_quotes.exists?(id: quote_id)
end

def invokable_with(current_user: nil)
unsent_quotes.filter_map do |quote|
next unless invokable?(quote_id: quote.id, current_user:)

{ label:, params: { quote_id: quote.to_param } }
end
end

def invoke_schema
Dry::Schema.Params do
optional(:quote_id).filled(:string)
end
end

protected

def send_tenant_notification(quote)
context = { quote: }
MailTemplate.use!(:email_quote_notification, booking, to: :tenant, context:).tap do |mail|
mail.attach(quote)
mail.save!
quote.update!(sent_at: Time.zone.now)
end
end

def unsent_quotes
booking.quotes.kept.unsent
end
end
end
2 changes: 1 addition & 1 deletion app/domain/booking_flows/default.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def self.manage_actions # rubocop:disable Metrics/MethodLength
mark_contract_sent: BookingActions::MarkContractSent,
mark_invoices_refunded: BookingActions::MarkInvoicesRefunded,
email_invoice: BookingActions::EmailInvoice,
email_offer: BookingActions::EmailOffer,
email_quote: BookingActions::EmailQuote,
postpone_deadline: BookingActions::PostponeDeadline,
mark_contract_signed: BookingActions::MarkContractSigned,
commit_request: BookingActions::CommitRequest,
Expand Down
2 changes: 1 addition & 1 deletion app/domain/booking_states/provisional_request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class ProvisionalRequest < Base
include Rails.application.routes.url_helpers

def checklist
BookingStateChecklistItem.prepare(:offer_created, booking:)
BookingStateChecklistItem.prepare(:quote_created, booking:)
end

def self.to_sym
Expand Down
3 changes: 2 additions & 1 deletion app/models/booking.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class Booking < ApplicationRecord # rubocop:disable Metrics/ClassLength
ROLES = (%i[organisation tenant booking_agent] + OperatorResponsibility::RESPONSIBILITIES.keys).freeze
LIMIT = ENV.fetch('RECORD_LIMIT', 250)
DEFAULT_INCLUDES = [:organisation, :state_transitions, :invoices, :contracts, :payments, :booking_agent,
:category, :logs, :home,
:category, :logs, :home, :quotes,
{ tenant: :organisation, deadline: :booking, occupancies: :occupiable,
agent_booking: %i[booking_agent organisation],
booking_question_responses: :booking_question }].freeze
Expand All @@ -64,6 +64,7 @@ class Booking < ApplicationRecord # rubocop:disable Metrics/ClassLength
foreign_key: :booking_category_id

has_many :invoices, dependent: :destroy
has_many :quotes, dependent: :destroy
has_many :payments, dependent: :destroy
has_many :notifications, dependent: :destroy, inverse_of: :booking, autosave: true, validate: false
has_many :usages, -> { ordered }, dependent: :destroy, inverse_of: :booking
Expand Down
4 changes: 3 additions & 1 deletion app/models/booking_condition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ def initialize_copy(origin)
end

def self.one_of
StoreModel.one_of { |json| subtypes[(json[:type].presence || json['type'].presence)&.to_sym] || BookingCondition }
StoreModel.one_of do |json|
subtypes[(json&.[](:type).presence || json&.[]('type').presence)&.to_sym] || BookingCondition
end
end

def self.options_for_select(organisation)
Expand Down
12 changes: 6 additions & 6 deletions app/models/booking_state_checklist_item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,17 +70,17 @@ class BookingStateChecklistItem
BookingStateChecklistItem.new(key: :deposit_created, context: { booking: }, checked:, url:)
end,

offer_created: lambda do |booking|
return if booking.organisation.rich_text_templates.enabled.by_key(:invoices_offer_text).blank?
quote_created: lambda do |booking|
return if booking.organisation.rich_text_templates.enabled.by_key(:quote_text).blank?

checked = Invoices::Offer.of(booking).kept.exists?
checked = booking.quotes.kept.exists?
url = if checked
proc { manage_booking_invoices_path(it.booking) }
proc { manage_booking_quotes_path(it.booking) }
else
proc { new_manage_booking_invoice_path(it.booking, invoice: { type: Invoices::Offer.model_name.to_s }) }
proc { new_manage_booking_quote_path(it.booking) }
end

BookingStateChecklistItem.new(key: :offer_created, context: { booking: }, checked:, url:)
BookingStateChecklistItem.new(key: :quote_created, context: { booking: }, checked:, url:)
end,

responsibilities_assigned: lambda do |booking|
Expand Down
5 changes: 2 additions & 3 deletions app/models/invoice.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,11 @@ class Invoice < ApplicationRecord
attribute :items, Invoice::Item.one_of.to_array_type

scope :ordered, -> { order(payable_until: :ASC, created_at: :ASC) }
scope :not_offer, -> { where.not(type: 'Invoices::Offer') }
scope :sent, -> { where.not(sent_at: nil) }
scope :unsent, -> { kept.where(sent_at: nil) }
scope :overdue, ->(at = Time.zone.today) { kept.not_offer.where(arel_table[:payable_until].lteq(at)) }
scope :overdue, ->(at = Time.zone.today) { kept.where(arel_table[:payable_until].lteq(at)) }
scope :of, ->(booking) { where(booking:) }
scope :unsettled, -> { kept.not_offer.where(status: %i[outstanding refund]) }
scope :unsettled, -> { kept.where(status: %i[outstanding refund]) }
scope :with_default_includes, -> { includes(%i[payments organisation]) }

accepts_nested_attributes_for :items, reject_if: :all_blank, allow_destroy: true
Expand Down
3 changes: 0 additions & 3 deletions app/models/invoice/factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ class Invoice
class Factory
RichTextTemplate.define(:invoices_deposit_text, context: %i[booking invoice])
RichTextTemplate.define(:invoices_invoice_text, context: %i[booking invoice])
RichTextTemplate.define(:invoices_offer_text, context: %i[booking invoice])
RichTextTemplate.define(:invoices_late_notice_text, context: %i[booking invoice])

def initialize(booking)
Expand Down Expand Up @@ -44,8 +43,6 @@ def prepare_to_supersede(invoice)
end

def payment_info_type(invoice)
return if invoice.type.to_s == Invoices::Offer.to_s

booking = invoice.booking
country_code = booking.tenant&.country_code&.upcase
return PaymentInfos::ForeignPaymentInfo if country_code && country_code != booking.organisation.country_code
Expand Down
16 changes: 10 additions & 6 deletions app/models/invoice/item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class Item
attribute :deposit_id
attribute :vat_category_id

delegate :booking, :organisation, to: :invoice, allow_nil: true
delegate :booking, :organisation, to: :parent, allow_nil: true

validates :type, presence: true, inclusion: { in: ->(_) { Invoice::Item.subtypes.keys.map(&:to_s) } }
validates :id, presence: true
Expand All @@ -38,15 +38,19 @@ def type
end

def invoice
parent
parent if parent.is_a?(Invoice)
end

def quote
parent if parent.is_a?(Quote)
end

def invoice=(value)
self.parent = value
end

def usage
@usage ||= invoice&.booking&.usages&.find_by(id: usage_id) if usage_id.present?
@usage ||= parent&.booking&.usages&.find_by(id: usage_id) if usage_id.present?
end

def tarif
Expand All @@ -62,7 +66,7 @@ def vat_category=(value)
end

def vat_category
@vat_category ||= invoice&.organisation&.vat_categories&.find_by(id: vat_category_id) if vat_category_id.present?
@vat_category ||= parent&.organisation&.vat_categories&.find_by(id: vat_category_id) if vat_category_id.present?
end

def calculated_amount
Expand All @@ -75,7 +79,7 @@ def vat_breakdown

def accounting_cost_center_nr
@accounting_cost_center_nr ||= if super.to_s == 'home'
invoice.booking.home&.settings&.accounting_cost_center_nr.presence
parent.booking.home&.settings&.accounting_cost_center_nr.presence
else
super.presence
end
Expand All @@ -87,7 +91,7 @@ def to_sum(sum)

def self.one_of
StoreModel.one_of do |json|
subtypes[(json[:type].presence || json['type'].presence)&.to_sym] || Item
subtypes[(json&.[](:type).presence || json&.[]('type').presence)&.to_sym] || Item
end
end
end
Expand Down
Loading