cirandas.net

ref: master

plugins/orders_cycle/models/orders_cycle_plugin/cycle.rb


  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
class OrdersCyclePlugin::Cycle < ApplicationRecord

  attr_accessible :profile, :status, :name, :description, :opening_message

  attr_accessible :start, :finish, :delivery_start, :delivery_finish
  attr_accessible :start_date, :start_time, :finish_date, :finish_time, :delivery_start_date, :delivery_start_time, :delivery_finish_date, :delivery_finish_time,

  Statuses = %w[edition orders purchases receipts separation delivery closing]
  DbStatuses = %w[new] + Statuses
  UserStatuses = Statuses

  # which status the sales are on each cycle status
  SaleStatusMap = {
    'edition'    => nil,
    'orders'     => :ordered,
    'purchases'  => :accepted,
    'receipts'   => :accepted,
    'separation' => :accepted,
    'delivery'   => :separated,
    'closing'    => :delivered,
  }
  # which status the purchases are on each cycle status
  PurchaseStatusMap = {
    'edition'    => nil,
    'orders'     => nil,
    'purchases'  => :draft,
    'receipts'   => :ordered,
    'separation' => :received,
    'delivery'   => :received,
    'closing'    => :received,
  }

  belongs_to :profile

  has_many :delivery_options, -> {
    where "delivery_plugin_options.owner_type = 'OrdersCyclePlugin::Cycle'"
  }, class_name: 'DeliveryPlugin::Option', dependent: :destroy, as: :owner
  has_many :delivery_methods, through: :delivery_options, source: :delivery_method

  has_many :cycle_orders, -> { order 'id ASC' }, class_name: 'OrdersCyclePlugin::CycleOrder', foreign_key: :cycle_id, dependent: :destroy

  # cannot use :order because of months/years scope
  has_many :sales, through: :cycle_orders, source: :sale
  has_many :purchases, through: :cycle_orders, source: :purchase

  has_many :cycle_products, foreign_key: :cycle_id, class_name: 'OrdersCyclePlugin::CycleProduct', dependent: :destroy
  has_many :products, -> {
    includes(:from_2x_products, :from_products, {profile: :domains})
  }, through: :cycle_products, class_name: 'OrdersCyclePlugin::OfferedProduct', source: :product

  has_many :consumers, -> { distinct.reorder 'name ASC' }, through: :sales, source: :consumer
  has_many :suppliers, -> { group 'suppliers_plugin_suppliers.id' }, through: :products
  has_many :orders_suppliers, -> { reorder 'name ASC' }, through: :sales, source: :profile

  has_many :from_products, -> { distinct.reorder 'name ASC' }, through: :products
  has_many :supplier_products, -> { distinct.reorder 'name ASC' }, through: :products
  has_many :product_categories, -> { distinct.reorder 'name ASC' }, through: :products

  has_many :orders_confirmed, -> { reorder('id ASC').where 'orders_plugin_orders.ordered_at IS NOT NULL' },
    through: :cycle_orders, source: :sale

  has_many :items_selled, through: :sales, source: :items
  has_many :items_purchased, through: :purchases, source: :items
  # DEPRECATED
  has_many :items, through: :orders_confirmed

  has_many :ordered_suppliers, through: :orders_confirmed, source: :suppliers

  has_many :ordered_offered_products, -> { distinct.includes :suppliers }, through: :orders_confirmed, source: :offered_products
  has_many :ordered_distributed_products, -> { distinct.includes :suppliers }, through: :orders_confirmed, source: :distributed_products
  has_many :ordered_supplier_products, -> { distinct.includes :suppliers }, through: :orders_confirmed, source: :supplier_products

  has_many :volunteers_periods, class_name: 'VolunteersPlugin::Period', as: :owner
  has_many :volunteers, through: :volunteers_periods, source: :profile
  attr_accessible :volunteers_periods_attributes
  accepts_nested_attributes_for :volunteers_periods, allow_destroy: true

  scope :has_volunteers_periods, -> { distinct.joins :volunteers_periods }

  # status scopes
  scope :on_edition, -> { where status: 'edition' }
  scope :on_orders, -> { where status: 'orders' }
  scope :on_purchases, -> { where status: 'purchases' }
  scope :on_separation, -> { where status: 'separation' }
  scope :on_delivery, -> { where status: 'delivery' }
  scope :on_closing, -> { where status: 'closing' }

  scope :defuncts, -> { where "status = 'new' AND created_at < ?", 2.days.ago }
  scope :not_new, -> { where "status <> 'new'" }
  scope :on_orders, -> {
    where "status = 'orders' AND ( (start <= :now AND finish IS NULL) OR (start <= :now AND finish >= :now) )", now: DateTime.now
  }
  scope :not_on_orders, -> {
    where "NOT (status = 'orders' AND ( (start <= :now AND finish IS NULL) OR (start <= :now AND finish >= :now) ) )", now: DateTime.now
  }
  scope :opened, -> { where "status <> 'new' AND status <> 'closing'" }
  scope :closing, -> { where "status = 'closing'" }
  scope :by_status, -> (status) { where status: status }

  scope :months, -> { order('month DESC').select 'DISTINCT(EXTRACT(months FROM start)) as month' }
  scope :years, -> { order('year DESC').select 'DISTINCT(EXTRACT(YEAR FROM start)) as year' }

  scope :by_month, -> (month) {
    where 'EXTRACT(month FROM start) <= :month AND EXTRACT(month FROM finish) >= :month', month: month
  }
  scope :by_year, -> (year) {
    where 'EXTRACT(year FROM start) <= :year AND EXTRACT(year FROM finish) >= :year', year: year
  }
  scope :by_range, -> (range) {
    where 'start BETWEEN :start AND :finish OR finish BETWEEN :start AND :finish', start: range.first, finish: range.last
  }

  # FINANCIAL CALLBACKS
  if defined? FinancialPlugin
    has_many :financial_transactions, class_name: "FinancialPlugin::Transaction", as: :target
  end

  validates_presence_of :profile
  validates_presence_of :name, if: :not_new?
  validates_presence_of :start, if: :not_new?
  # FIXME: The user frequenqly forget about this, and this will crash the app in some places, so don't enable this
  #validates_presence_of :delivery_options, unless: :new_or_edition?
  validates_inclusion_of :status, in: DbStatuses, if: :not_new?
  validates_numericality_of :margin_percentage, allow_nil: true, if: :not_new?
  validate :validate_orders_dates, if: :not_new?
  validate :validate_delivery_dates, if: :not_new?

  before_save :step_new
  before_validation :update_orders_status
  before_save :add_products_on_edition_state
  after_create :delay_purge_profile_defuncts

  extend CodeNumbering::ClassMethods
  code_numbering :code, scope: Proc.new { self.profile.orders_cycles }

  extend OrdersPlugin::DateRangeAttr::ClassMethods
  date_range_attr :start, :finish
  date_range_attr :delivery_start, :delivery_finish

  extend SplitDatetime::SplitMethods
  split_datetime :start
  split_datetime :finish
  split_datetime :delivery_start
  split_datetime :delivery_finish

  serialize :data, Hash

  def name_with_code
    I18n.t('orders_cycle_plugin.models.cycle.code_name') % {
      code: code, name: name
    }
  end
  def total_price_consumer_ordered
    self.items.sum :price_consumer_ordered
  end

  def status
    self['status'] = 'closing' if self['status'] == 'closed'
    self['status']
  end

  def step
    self.status = DbStatuses[DbStatuses.index(self.status)+1]
  end
  def step_back
    self.status = DbStatuses[DbStatuses.index(self.status)-1]
  end

  def passed_by? status
    DbStatuses.index(self.status) > DbStatuses.index(status) rescue false
  end

  def new?
    self.status == 'new'
  end
  def not_new?
    self.status != 'new'
  end
  def open?
    !self.closing?
  end
  def closing?
    self.status == 'closing'
  end
  def edition?
    self.status == 'edition'
  end
  def new_or_edition?
    self.status == 'new' or self.status == 'edition'
  end
  def after_orders?
    now = DateTime.now
    status == 'orders' && self.finish < now
  end
  def before_orders?
    now = DateTime.now
    status == 'orders' && self.start >= now
  end
  def orders?
    now = DateTime.now
    status == 'orders' && ( (self.start <= now && self.finish.nil?) || (self.start <= now && self.finish >= now) )
  end
  def delivery?
    now = DateTime.now
    status == 'delivery' && ( (self.delivery_start <= now && self.delivery_finish.nil?) || (self.delivery_start <= now && self.delivery_finish >= now) )
  end

  def may_order? consumer
    self.orders? and consumer.present? and consumer.in? profile.members
  end

  def consumer_previous_orders consumer
    self.profile.orders_cycles_sales.where(consumer_id: consumer.id).
      where('orders_cycle_plugin_cycle_orders.cycle_id <> ?', self.id)
  end

  def products_for_order
    self.products.unarchived.alphabetically.with_price
  end

  def supplier_products_by_suppliers orders = self.sales.ordered
    OrdersCyclePlugin::Order.supplier_products_by_suppliers orders
  end

  def generate_purchases sales = self.sales.ordered
    return self.purchases if self.purchases.present?

    sales.each do |sale|
      sale.add_purchases_items_without_delay
    end

    self.purchases true
  end
  def regenerate_purchases sales = self.sales.ordered
    self.purchases.destroy_all
    self.generate_purchases sales
  end

  def add_products
    return if self.products.count > 0
    scope = self.profile.products.supplied.unarchived.available
    scope = scope.in_stock if defined? StockPlugin

    ApplicationRecord.transaction do
      scope.find_each batch_size: 20 do |product|
        self.add_product product
      end
    end
  end

  def add_product product
    OrdersCyclePlugin::OfferedProduct.create_from product, self
  end

  def add_products_job
    @add_products_job ||= Delayed::Job.find_by id: self.data[:add_products_job_id]
  end

  def transactions_for_report
    {
      order:   financial_transactions.orders,
      payment: financial_transactions.payments,
      income:  financial_transactions.manual.inputs,
      expense: financial_transactions.manual.outputs,
    }
  end
  protected

  def add_products_on_edition_state
    return unless self.status_was == 'new'
    job = self.delay.add_products
    self.data[:add_products_job_id] = job.id
  end

  def step_new
    return if self.new_record?
    self.step if self.new?
  end

  def update_sales_status from, to
    sales = self.sales.where(status: from.to_s)
    sales.each do |sale|
      sale.update status: to.to_s
    end
  end

  def update_purchases_status from, to
    purchases = self.purchases.where(status: from.to_s)
    purchases.each do |purchase|
      purchase.update status: to.to_s
    end
  end

  def update_orders_status
    return if self.new? or self.status_was == "new"
    return if self.status_was == self.status

    # Don't rewind confirmed sales
    unless self.status_was == 'orders' and self.status == 'edition'
      sale_status_was = SaleStatusMap[self.status_was]
      new_sale_status = SaleStatusMap[self.status]
      self.delay.update_sales_status sale_status_was, new_sale_status unless sale_status_was == new_sale_status
    end

    # Don't rewind confirmed purchases
    unless self.status_was == 'receipts' and self.status == 'purchases'
      purchase_status_was = PurchaseStatusMap[self.status_was]
      new_purchase_status = PurchaseStatusMap[self.status]
      self.delay.update_purchases_status purchase_status_was, new_purchase_status unless purchase_status_was == new_purchase_status
    end
  end

  def validate_orders_dates
    return if self.new? or self.finish.nil?
    errors.add :base, (I18n.t('orders_cycle_plugin.models.cycle.invalid_orders_period')) unless self.start < self.finish
  end

  def validate_delivery_dates
    return if self.new? or delivery_start.nil? or delivery_finish.nil?
    errors.add :base, I18n.t('orders_cycle_plugin.models.cycle.invalid_delivery_peri') unless delivery_start < delivery_finish
    errors.add :base, I18n.t('orders_cycle_plugin.models.cycle.delivery_period_befor') unless finish <= delivery_start
  end

  def purge_profile_defuncts
    self.class.where(profile_id: self.profile_id).defuncts.destroy_all
  end

  def delay_purge_profile_defuncts
    self.delay.purge_profile_defuncts
  end

end