cirandas.net

ref: master

plugins/orders/models/orders_plugin/item.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
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
class OrdersPlugin::Item < ApplicationRecord

  attr_accessible :order, :sale, :purchase,
    :product, :product_id,
    :price, :name

  # flag used by items to compare them with products
  attr_accessor :product_diff

  Statuses     = %w[ordered accepted separated delivered received]
  DbStatuses   = %w[draft planned cancelled] + Statuses
  UserStatuses = %w[open forgotten planned cancelled] + Statuses
  StatusText = {}; UserStatuses.map do |status|
    StatusText[status] = "orders_plugin.models.order.statuses.#{status}"
  end

  # should be Order, but can't reference it here so it would create a cyclic reference
  StatusAccessMap = {
    'ordered'   => :consumer,
    'accepted'  => :supplier,
    'separated' => :supplier,
    'delivered' => :supplier,
    'received'  => :consumer,
  }
  StatusDataMap = {}; StatusAccessMap.each do |status, access|
    StatusDataMap[status] = "#{access}_#{status}"
  end
  StatusDataMap.each do |status, data|
    quantity = "quantity_#{data}".to_sym
    price = "price_#{data}".to_sym

    attr_accessible quantity
    attr_accessible price
  end

  serialize :data

  belongs_to :order, class_name: '::OrdersPlugin::Order', foreign_key: :order_id, touch: true, inverse_of: :items
  belongs_to :sale, class_name: '::OrdersPlugin::Sale', foreign_key: :order_id, touch: true, inverse_of: :items
  belongs_to :purchase, class_name: '::OrdersPlugin::Purchase', foreign_key: :order_id, touch: true, inverse_of: :items

  belongs_to :product, class_name: '::ProductsPlugin::Product'
  has_one :supplier, through: :product

  has_one :profile, through: :order
  has_one :consumer, through: :order

  # FIXME: don't work because of load order
  #if defined? SuppliersPlugin
    has_many :from_products, through: :product
    has_one :from_product, through: :product
    has_many :to_products, through: :product
    has_one :to_product, through: :product
    has_many :sources_supplier_products, through: :product
    has_one :sources_supplier_product, through: :product
    has_many :supplier_products, through: :product, :class_name => "ProductsPlugin::Product"
    has_one :supplier_product, through: :product
    has_many :suppliers, through: :product
    has_one :supplier, through: :product
  #end

  scope :ordered, -> { joins(:order).where 'orders_plugin_orders.status = ?', 'ordered' }
  scope :for_product, -> (product) { where product_id: product.id }

  default_scope -> { includes :product }

  validate :has_order
  validates_presence_of :product
  validates_inclusion_of :status, in: DbStatuses

  before_validation :set_defaults
  before_save :save_calculated_prices
  before_save :step_status
  before_create :sync_fields
  after_save :update_order

  extend CurrencyFields::ClassMethods

  # utility for other classes
  DefineTotals = proc do
    StatusDataMap.each do |status, data|
      quantity = "quantity_#{data}".to_sym
      price = "price_#{data}".to_sym

      self.send :define_method, "total_#{quantity}" do |items=nil|
        items ||= (self.ordered_items rescue nil) || self.items
        items.collect(&quantity).inject(0){ |sum, q| sum + q.to_f }
      end
      self.send :define_method, "total_#{price}" do |items=nil|
        items ||= (self.ordered_items rescue nil) || self.items
        items.collect(&price).inject(0){ |sum, p| sum + p.to_f }
      end

      has_number_with_locale "total_#{quantity}"
      has_currency "total_#{price}"
    end
  end
  has_currency :status_quantity

  has_currency :price
  StatusDataMap.each do |status, data|
    quantity = "quantity_#{data}"
    price = "price_#{data}"

    has_number_with_locale quantity
    has_currency price

    validates_numericality_of quantity, allow_nil: true
    validates_numericality_of price, allow_nil: true
  end

  # Attributes cached from product
  def name
    self[:name] || (self.product.name rescue nil)
  end
  def price
    self[:price] || (self.product.price_with_discount || 0 rescue nil)
  end
  def price_without_margins
    self.product.price_without_margins rescue self.price
  end
  def unit
    self.product.unit
  end
  def unit_name
    self.unit.singular if self.unit
  end
  def supplier
    self.product.supplier rescue self.order.profile.self_supplier
  end
  def supplier_name
    if self.product.supplier
      self.product.supplier.abbreviation_or_name
    else
      self.order.profile.short_name
    end
  end

  def calculated_status
    status = self.order.status
    index = Statuses.index status
    next_status = Statuses[index+1] if index
    next_quantity = "quantity_#{StatusDataMap[next_status]}" if next_status
    if next_status and self.send next_quantity then next_status else status end
  end
  def on_next_status?
    self.order.status != self.calculated_status
  end

  # product used for comparizon when repeating an order
  # override on subclasses
  def repeat_product
    self.product
  end

  def next_status_quantity_field actor_name
    status = StatusDataMap[self.order.next_status actor_name] || 'consumer_ordered'
    "quantity_#{status}"
  end
  def next_status_quantity actor_name
    self.send self.next_status_quantity_field(actor_name)
  end
  def next_status_quantity_set actor_name, value
    self.send "#{self.next_status_quantity_field actor_name}=", value
  end

  def status_quantity_field
    @status_quantity_field ||= begin
      status = StatusDataMap[self.status] || 'consumer_ordered'
      "quantity_#{status}"
    end
  end
  def status_price_field
    @status_price_field ||= begin
      status = StatusDataMap[self.status] || 'consumer_ordered'
      "price_#{status}"
    end
  end

  def status_quantity
    self.send self.status_quantity_field
  end
  def status_quantity= value
    self.send "#{self.status_quantity_field}=", value
  end

  def status_price
    self.send self.status_price_field
  end
  def status_price= value
    self.send "#{self.status_price_field}=", value
  end

  StatusDataMap.each do |status, data|
    quantity = "quantity_#{data}".to_sym
    price = "price_#{data}".to_sym

    define_method "calculated_#{price}" do
      self.price * self.send(quantity) rescue nil
    end

    define_method price do
      self[price] || self.send("calculated_#{price}")
    end
  end

  def quantity_price_data actor_name
    data = {flags: {}}
    statuses = ::OrdersPlugin::Order::Statuses
    statuses_data = data[:statuses] = {}

    current = statuses.index(self.status) || 0
    next_status = self.order.next_status actor_name
    next_index = statuses.index(next_status) || current + 1
    goto_next = actor_name == StatusAccessMap[next_status]

    new_price = nil
    # compare with product
    if self.product_diff
      if self.repeat_product and self.repeat_product.available
        if self.price != self.repeat_product.price
          new_price = self.repeat_product.price
          data[:new_price] = self.repeat_product.price_as_currency_number
        end
      else
        data[:flags][:unavailable] = true
      end
    end

    # Fetch data
    statuses.each.with_index do |status, i|
      data_field = StatusDataMap[status]
      access = StatusAccessMap[status]

      status_data = statuses_data[status] = {
        flags: {
          editable:     nil,
          not_modified: nil,
          empty:        nil,
          filled:       nil,
          overwritten:  nil,
          current:      nil,
          removed:      nil,
          admin:        nil,
        },

        price:     nil,
        new_price: nil,
        quantity:  nil,

        field:  data_field,
        access: access,
      }

      quantity = self.send "quantity_#{data_field}"
      if quantity.present?
        # quantity is used on <input type=number> so it should not be localized
        status_data[:quantity]           = quantity

        status_data[:flags][:removed] = true if status_data[:quantity].zero?
        status_data[:flags][:filled] = true

        status_data[:price]     = self.send "price_#{data_field}_as_currency_number"
        status_data[:new_price] = quantity * new_price if new_price
      else
        status_data[:flags][:empty] = true
      end

      if i == current
        status_data[:flags][:current] = true
      elsif i == next_index and goto_next
        status_data[:flags][:admin] = true
      end

      break if (if goto_next then i == next_index else i < next_index end)
    end

    # Set flags according to past/future data
    # Present flags are used as classes
    statuses_data.each.with_index do |(_status, status_data), i|
      prev_status_data = statuses_data[statuses[i-1]] unless i.zero?

      if prev_status_data
        if status_data[:quantity] == prev_status_data[:quantity]
          status_data[:flags][:not_modified] = true
        elsif status_data[:flags][:empty]
          # fill with previous status data
          status_data[:quantity] = prev_status_data[:quantity]

          status_data[:price] = prev_status_data[:price]
          status_data[:flags][:filled] = status_data[:flags].delete :empty
          status_data[:flags][:not_modified] = true
        end
      end
    end

    # reverse_each is necessary to set overwritten with intermediate not_modified
    statuses_data.reverse_each.with_index do |(_status, status_data), i|
      prev_status_data = statuses_data[statuses[-i-1]]
      if status_data[:not_modified] or
          (prev_status_data and prev_status_data[:flags][:filled] and status_data[:quantity] != prev_status_data[:quantity])
        status_data[:flags][:overwritten] = true
      end
    end

    # Set access
    statuses_data.each.with_index do |(status, status_data), i|
      #consumer_may_edit = actor_name == :consumer and status == 'ordered' and self.order.open?
      if StatusAccessMap[status] == actor_name
        status_data[:flags][:editable] = true
      end
      # only allow last status
      #status_data[:flags][:editable] = true if status_data[:access] == actor_name and (status_data[:flags][:admin] or self.order.open?)
    end

    data
  end

  def calculate_prices price
    self.price = price
    self.save_calculated_prices
  end

  # used by db/migrate/20150627232432_add_status_to_orders_plugin_item.rb
  def fill_status
    status = self.calculated_status
    return if self.status == status
    self.update_column :status, status
    self.order.update_column :building_next_status, true if self.order.status != status and not self.order.building_next_status
  end

  def update_order
    self.order.create_transaction
  end

  protected

  def save_calculated_prices
    StatusDataMap.each do |status, data|
      price = "price_#{data}".to_sym
      self.send "#{price}=", self.send("calculated_#{price}")
    end
  end

  def set_defaults
    self.status ||= Statuses.first
  end

  def step_status
    status = self.calculated_status
    return if self.status == status
    self.status = status
    self.order.update_column :building_next_status, true if self.order.status != status and not self.order.building_next_status
  end

  def has_order
    self.order or self.sale or self.purchase
  end

  def sync_fields
    self.name = self.product.name
    self.price = self.product.price
  end

end