item.rb
10.3 KB
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
class OrdersPlugin::Item < ActiveRecord::Base
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
belongs_to :sale, class_name: '::OrdersPlugin::Sale', foreign_key: :order_id, touch: true
belongs_to :purchase, class_name: '::OrdersPlugin::Purchase', foreign_key: :order_id, touch: true
belongs_to :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
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 include: [: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
# 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
extend CurrencyHelper::ClassMethods
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: {},
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[:price] = self.send "price_#{data_field}_as_currency_number"
status_data[:new_price] = quantity * new_price if new_price
status_data[:flags][:filled] = true
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
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