class Job < ApplicationRecord attr_accessor :current_user belongs_to :costumer, class_name: "User", optional: true, counter_cache: :costumer_jobs_count, inverse_of: :costumer_jobs belongs_to :operator, class_name: "User", optional: true, counter_cache: :operator_jobs_count, inverse_of: :operator_jobs belongs_to :creator, class_name: "User", optional: true, counter_cache: :created_jobs_count, inverse_of: :created_jobs belongs_to :cashier, class_name: "User", optional: true, counter_cache: :cashed_jobs_count, inverse_of: :cashed_jobs has_one_attached :pdf, dependent: :purge validates_presence_of :costumer_firstname, :costumer_lastname, :pdf validates_presence_of :cost_center, if: :intern validates :privacy_policy, acceptance: true, unless: :created_by_operator? validates :number_of_plans_a0, :number_of_plans_a1, :number_of_plans_a2, :number_of_plans_a3, :costum_qm_plan, numericality: { greater_than_or_equal_to: 0 } validate :acceptable_pdf before_save :update_status_changed_at, if: :will_save_change_to_status? before_save :update_user_status_infos, if: -> { will_save_change_to_status? || new_record? } before_save :set_cost_qm before_save :calc_cost, if: :printed_pages_changes? before_validation :set_costumer_infos, unless: :created_by_operator?, on: :create # TODO: works only when job is created. Should move analyzer to activestorage : # https://discuss.rubyonrails.org/t/active-storage-in-production-lessons-learned-and-in-depth-look-at-how-it-works/83289 # https://redgreen.no/2021/01/24/custom-analyzer-for-activestorage.html # Possible alternative # https://github.com/shrinerb/shrine after_create_commit :analyze_pdf # NOTE: Multiple status if paing before brinting? enum :status, { open: "open", printing: "printing", pickup: "pickup", paid: "paid", canceled: "canceled" } AVAILABLE_PAGE_FORMATS = [ :a0, :a1, :a2, :a3 ] # scope :created_as_operator, -> { where created_as_operator: true } # scope :created_as_costumer, -> { where created_as_operator: false } scope :not_canceled, -> { !canceled } # NOTE: only named status are returned because of WHERE/IN clause for the enum values scope :in_status_order, -> { in_order_of(:status, %w[open printing pickup paid canceled]) } scope :created_today, -> { created_on_day(Time.now) } scope :created_on_day, lambda { |date| where("created_at >= ? AND created_at <= ?", date.beginning_of_day, date.end_of_day) } scope :updated_today, -> { updated_on_day(Time.now) } scope :updated_on_day, lambda { |date| where("updated_at >= ? AND updated_at <= ?", date.beginning_of_day, date.end_of_day) } scope :status_changed_today, -> { status_changed_on_day(Time.now) } scope :status_changed_on_day, lambda { |date| where("status_changed_at >= ? AND status_changed_at <= ?", date.beginning_of_day, date.end_of_day) } scope :created_by_costumer, -> { not(:created_by_operator) } # Returns all jobs with status: open print pickup and jobs from today with status: paid canceled # paid: only updated_at today # canceled: only updated_at today def self.currently_working_on # NOTE: use Time.now instead of Date.today to take the timezone into account where(status: %i[open printing pickup]) .or(Job.where(status: %i[paid canceled]) .where("status_changed_at >= ?", Time.now.beginning_of_day)) # .in_status_order .order(created_at: :asc) # .order(:costumer_firstname, :costumer_lastname) .with_attached_pdf # scope from activestorage for .includes(pdf_attachment: :blob) # .references(:pdf_attachment, :blob) # creates big join table end def self.paidcanceled Job.where(status: %i[paid canceled]) end def costumer_fullname [ costumer_firstname, " ", costumer_lastname ].join end def acceptable_pdf return unless pdf.attached? acceptable_types = [ "application/pdf" ] errors.add(:pdf, "is too big") unless pdf.blob.byte_size <= 100.megabyte errors.add(:pdf, "must be a PDF") unless acceptable_types.include?(pdf.content_type) end def able_to_cancel? open? || printing? # TODO: different check for operator and admin end # cancel job only if it is still open def canceled! if able_to_cancel? self.status = :canceled self.printed_at = nil self.paid_at = nil save end end def increment_page(din) return false unless AVAILABLE_PAGE_FORMATS.include?(din.to_sym) public_send("number_of_plans_#{din}=", public_send("number_of_plans_#{din}") + 1) if respond_to? "number_of_plans_#{din}=" save end def decrement_page(din) return false unless AVAILABLE_PAGE_FORMATS.include?(din.to_sym) public_send("number_of_plans_#{din}=", public_send("number_of_plans_#{din}") - 1) if respond_to? "number_of_plans_#{din}=" save end def self.report_to_csv(jobs) columns_readable = [ "ID", "Kunde Vorname", "Kunde Nachname", "Kassierer Vorname", "Kassierer Nachname", "bezahlt am", "Betrag" ] CSV.generate(col_sep: ";") do |csv| csv << columns_readable jobs.each do |job| # csv << job.attributes.values_at(*columns) csv << [ job.id, job.costumer_firstname, job.costumer_lastname, job.cashier_firstname, job.cashier_lastname, job.paid_at.localtime.strftime("%Y-%m-%d"), job.cost.to_s + " €" ] end end end def self.ransackable_attributes(auth_object = nil) [ "created_at", "id", "costumer_firstname", "costumer_lastname", "pdf.", "created_by_operator", "number_of_plans_a0", "number_of_plans_a1", "number_of_plans_a2", "number_of_plans_a3", "costum_qm_plan", "cost", "status" ] end def self.ransackable_associations(auth_object = nil) [ "pdf_blob" ] end private def printed_pages_changes? costum_qm_plan_changed? || number_of_plans_a0_changed? || number_of_plans_a1_changed? || number_of_plans_a2_changed? || number_of_plans_a3_changed? end def update_status_changed_at self.status_changed_at = Time.now end def calc_cost self.cost = (number_of_plans_a0 * cost_qm) + (number_of_plans_a1 * cost_qm / 2) + (number_of_plans_a2 * cost_qm / 4) + (number_of_plans_a3 * cost_qm / 8) + (costum_qm_plan * cost_qm).round(2) end def set_cost_qm # TODO: get cost from global settings self.cost_qm = 7 end def analyze_pdf # return unless pdf.attached? && pdf.new_record? self.number_of_plans_a0 = 0 self.number_of_plans_a1 = 0 self.number_of_plans_a2 = 0 self.number_of_plans_a3 = 0 self.costum_qm_plan = 0 # TODO: add any check if attachment has changed # pdfs.each do |pdf| pdf.blob.open do |file| # file = ActiveStorage::Blob.service.path_for(pdf.key).to_s pdf_analyzer = Services::PdfAnalyzer.new(file) pdf_analyzer.analyze self.number_of_plans_a0 += pdf_analyzer.pages_a0 self.number_of_plans_a1 += pdf_analyzer.pages_a1 self.number_of_plans_a2 += pdf_analyzer.pages_a2 self.number_of_plans_a3 += pdf_analyzer.pages_a3 self.costum_qm_plan += pdf_analyzer.costum_qm end # end calc_cost save end def set_costumer_infos self.costumer = current_user unless self.costumer self.costumer_firstname = costumer.firstname self.costumer_lastname = costumer.lastname end def set_operator_infos self.operator = current_user unless self.operator self.operator_firstname = operator.firstname self.operator_lastname = operator.lastname end def clear_operator_infos self.operator = nil self.operator_firstname = nil self.operator_lastname = nil end def operator_infos_set? self.operator && self.operator_firstname && self.operator_lastname end def set_cashier_infos self.paid_at = Time.now self.cashier = current_user unless self.cashier self.cashier_firstname = cashier.firstname self.cashier_lastname = cashier.lastname end def clear_cashier_infos self.paid_at = nil self.cashier = nil self.cashier_firstname = nil self.cashier_lastname = nil end def reset_operator_and_cashier_infos clear_operator_infos clear_cashier_infos end def update_user_status_infos case status.to_sym when :open reset_operator_and_cashier_infos self.printed_at = nil self.paid_at = nil when :printing clear_cashier_infos set_operator_infos self.printed_at = nil when :pickup clear_cashier_infos self.printed_at = Time.now unless self.printed_at when :paid set_operator_infos unless operator_infos_set? self.printed_at = Time.now unless self.printed_at set_cashier_infos self.paid_at = Time.now end end def reset_to_status_open? status_changed? && open? end end