OSDN Git Service

CSS site title fix
[elecoma/elecoma.git] / lib / totalizer.rb
1 # -*- coding: utf-8 -*-
2   def parse_date_select params, name
3     return nil unless params and not params['%s(1i)' % name].blank?
4     args = (1..3).map {|i| params["%s(%di)" % [name, i]]}.reject(&:blank?).map(&:to_i)
5     args << 1 if args.length < 3
6     Time.local(*args)
7   end
8
9 require 'gruff'
10
11 class TotalizerBase
12 end
13
14 TotalizerBase.instance_eval { include AddCSVDownload }
15
16 # 名前が気にいらない
17 class Totalizer < TotalizerBase
18   COLUMN_NAMES = {
19     'term' => '期間',
20     'male' => '男性',
21     'female' => '女性',
22     'member_male' => "男性\n(会員)",
23     'member_female' => "女性\n(会員)",
24     'guest_male' => "男性\n(非会員)",
25     'guest_female' => "女性\n(非会員)",
26
27     'product_code' => '商品番号',
28     'product_name' => '商品名',
29     'items' => '点数',
30     'unit_price' => '単価',
31     'price' => '金額(税込)',
32     'sale_start_at' => '販売開始期間',
33
34     'age' => '年齢',
35     'job' => '職業',
36     'kind' => '区分',
37
38     'position' => '順位',
39     'count' => '購入件数',
40     'sum' => '購入合計',
41     'average' => '購入平均',
42
43     'subtotal' => '小計',
44     'total' => '購入合計',
45     'payment_total' => '支払い合計',
46     'discount' => '値引',
47     'charge' => '手数料',
48     'deliv_fee' => '送料',
49     'use_point' => '使用ポイント',
50   }
51   WHERE_CLAUSE = "orders.received_at between :date_from and :date_to and orders.retailer_id = :retailer_id "
52
53   attr_accessor :columns, :title, :total, :links, :default_type
54
55   def Totalizer.get_instance type
56     name = "#{type}_totalizer".classify
57     Object.const_defined?(name) and Object.const_get(name).new
58   end
59
60   def labels
61     columns.map { |i| COLUMN_NAMES[i] }
62   end
63
64   def get_records(params)
65     conditions = get_conditions params
66     return nil unless conditions[:date_from] and conditions[:date_to]
67     @records = search(conditions)
68   end
69
70   def get_conditions params
71     @type = params[:type]
72     if params[:search][:by_date]
73       date_from = parse_date_select(params[:search], 'date_from')
74       date_to = parse_date_select(params[:search], 'date_to')
75     else
76       date_from = parse_date_select(params[:search], 'month')
77       date_to = Time.local(date_from.year, date_from.month, 1) + 1.month - 1.day if date_from
78     end
79     date_to &&= Time.local(date_to.year, date_to.month, date_to.day, 23, 59, 59)
80     { :date_from => date_from, :date_to => date_to, :retailer_id => params[:search][:retailer_id] }
81   end
82
83   def self.list_for_csv(params)
84     @records = self.new.get_records(params)
85   end
86
87   def graph
88     nil
89   end
90
91   private
92
93   def init_graph(klass)
94     g = klass.new(640)
95     g.font = Pathname.new(RAILS_ROOT).join('lib', 'sazanami-gothic.ttf').to_s
96     g
97   end
98
99   def self.get_csv_settings(columns=nil)
100     [self.new.columns, self.new.labels.map{|c|c.sub("\n", '')}]
101   end
102
103   def self.csv_output_setting_name
104     "total"
105   end
106
107 end
108
109 class TermTotalizer < Totalizer
110   def initialize
111     super
112     @title = '期間別集計'
113     @columns = %w(term count male female subtotal discount charge deliv_fee total average)
114     @links = %w(日別 day 月別 month 年別 year 曜日別 wday 時間別 hour)
115     @default_type = 'day'
116   end
117
118   def get_conditions params
119     conds = super
120     @helper = Helper.new(params[:type])
121     conds
122   end
123
124   def graph
125     @records or return nil
126     return nil if @records == []
127     g = init_graph(Gruff::Line)
128     g.title = self.title
129     g.data('価格', @records.map{|r| r['total']})
130     labels = {}
131     # ラベルは適当に間引いて設定
132     (0...@records.size).step([@records.size/3, 1].max)[0...-1].each do |i|
133       labels[i] = @records[i]['term']
134     end
135     # 最後のは必ず付ける
136     labels[@records.size-1] = @records.last['term']
137     g.labels = labels
138     return g.to_blob
139   end
140
141   def search conditions
142     records = OrderDelivery.find_by_sql([<<-EOS, conditions])
143       select
144         #{@helper.columns 'received_at'},
145         '' as term,
146         count(*) as count,
147         sum(male) as male,
148         sum(female) as female,
149         sum(member_male) as member_male,
150         sum(member_female) as member_female,
151         sum(guest_male) as guest_male,
152         sum(guest_female) as guest_female,
153         sum(total) as total,
154         sum(subtotal) as subtotal,
155         sum(payment_total) as payment_total,
156         sum(discount) as discount,
157         sum(deliv_fee) as deliv_fee,
158         sum(charge) as charge,
159         sum(use_point) as use_point,
160         round(avg(total)) as average
161       from (
162         select
163           case when (customers.sex = #{System::MALE} or (customers.sex is null and order_deliveries.sex = #{System::MALE})) then 1 else 0 end as male,
164           case when (customers.sex = #{System::FEMALE} or (customers.sex is null and order_deliveries.sex = #{System::FEMALE})) then 1 else 0 end as female,
165           case when customers.sex = #{System::MALE} then 1 else 0 end as member_male,
166           case when customers.sex = #{System::FEMALE} then 1 else 0 end as member_female,
167           case when customers.sex is null and order_deliveries.sex = #{System::MALE} then 1 else 0 end as guest_male,
168           case when customers.sex is null and order_deliveries.sex = #{System::FEMALE} then 1 else 0 end as guest_female,
169           subtotal, total, payment_total,
170           discount, deliv_fee, charge, use_point,
171           orders.received_at
172         from order_deliveries
173           join orders on orders.id = order_deliveries.order_id
174           left outer join customers on customers.id = orders.customer_id
175         where #{WHERE_CLAUSE}
176       ) as t1
177       group by term, #{@helper.group_by_clause}
178       order by term, #{@helper.group_by_clause}
179     EOS
180     # 期間
181     records.each do | record |
182       record.term = @helper.term_of record
183     end
184     # 歯抜けを埋める
185     all_term = @helper.all_term conditions[:date_from], conditions[:date_to]
186     record_hash = records.index_by(&:term)
187     # term を回して records に無ければ、全部 0 の Hash で埋める
188     records = all_term.map do | term |
189       if record_hash[term]
190         record_hash[term]
191       else
192         r = Hash.new(0) # デフォルト値0
193         r['term'] = term
194         r
195       end
196     end
197
198     # 合計行
199     @total = Hash.new(0)
200     records.each do | record |
201       columns.each do | key |
202         total[key] ||= 0
203         total[key] += record[key].to_i
204       end
205     end
206     @total['term'] = '合計'
207     if @total['count'] != 0
208       @total['average'] = @total['total'] / @total['count']
209     else
210       @total['average'] = 0
211     end
212     records
213   end
214
215   class Helper
216     def initialize type
217       @type = type || 'day'
218       case @type
219       when 'day'
220         @fields = ['year', 'month', 'day', 'dow']
221         @format = '%04d/%02d/%02d(%s)'
222       when 'month'
223         @fields = ['year', 'month']
224         @format = '%02d/%02d月'
225       when 'year'
226         @fields = ['year']
227         @format = '%02d年'
228       when 'wday'
229         @fields = ['dow']
230         @format = '%s曜日'
231       when 'hour'
232         @fields = ['hour']
233         @format = '%d時'
234       end
235     end
236
237     def group_by_clause
238       @fields.join(', ')
239     end
240
241     def columns date_column_name
242       #offset = "%d" % Time.zone.utc_offset
243       dow_column_interval = "%s" % [date_column_name]#+ #{MergeAdapterUtil.interval_second(offset)}" % [date_column_name]
244       dow_column = "#{MergeAdapterUtil.day_of_week(dow_column_interval)} as dow"
245       @fields.map do | f |
246         unless f == 'dow'
247           "extract(%s from %s ) as %s" % [f, date_column_name, f]
248         else
249           dow_column
250         end
251       end.join(",\n")
252     end
253
254     def term_of record
255       @format % @fields.map do | f |
256         v = record[f]
257         f == 'dow' ? %w(日 月 火 水 木 金 土)[v.to_i] : v
258       end
259     end
260
261     def date_to_record d
262       {'year'=>d.year,'month'=>d.month,'day'=>d.day,'dow'=>d.wday}
263     end
264
265     def all_term date_from, date_to
266       case @type
267       when 'day'
268         terms = []
269         d = date_from
270         (( ( date_to - date_from ) / 1.day).ceil ).times do
271            terms << term_of(date_to_record(d))
272            d = d.advance(:days=>1)
273         end
274
275         terms
276       when 'month'
277         terms = []
278         d = date_from
279         while d.year < date_to.year || d.month <= date_to.month
280           terms << term_of(date_to_record(d))
281           d = Time.local(d.year, d.month + 1)
282         end
283         terms
284       when 'year'
285         terms = []
286         d = date_from
287         while d.year <= date_to.year
288           terms << term_of(date_to_record(d))
289           d = Time.local(d.year + 1)
290         end
291         terms
292       when 'wday'
293         (0..6).map{ |dow| term_of({'dow'=>dow}) }
294       when 'hour'
295         (0..23).map{ |hour| term_of({'hour'=>hour}) }
296       end
297     end
298   end
299 end
300
301 class ProductTotalizer < Totalizer
302   def initialize
303     super
304     @title = '商品別集計'
305     @columns = %w(position product_code product_name count items unit_price price sale_start_at)
306     @links = %w(全体 all 会員 member 非会員 nomember)
307     @default_type = 'all'
308   end
309   def get_conditions params
310     conds = super
311     conds[:activate] =
312       case params[:type]
313       when 'member'
314         [Customer::KARITOUROKU, Customer::TOUROKU, Customer::TEISHI]
315       when 'nomember'
316         nil
317       else
318         [Customer::KARITOUROKU, Customer::TOUROKU, Customer::TEISHI]
319       end
320     @member = params[:type]
321     conds[:sale_start_from] = parse_date_select(params[:search], 'sale_start_from')
322     conds[:sale_start_to] = parse_date_select(params[:search], 'sale_start_to')
323     conds
324   end
325   def graph
326     @records or return nil
327     g = init_graph(Gruff::Pie)
328     g.title = self.title
329     g.legend_box_size = g.legend_font_size = 14
330     g.zero_degree = -90.0
331     g.sort = false
332     others = 0
333     @records.each_with_index do |r, i|
334       if i < 6
335         g.data(r['product_name'], r['price'].to_i)
336       else
337         others += r['price'].to_i
338       end
339     end
340     if others > 0
341       g.data('その他', others)
342     end
343     return g.to_blob
344   end
345   def search conditions
346     records = OrderDetail.find_by_sql([<<-EOS, conditions])
347       select
348         order_details.id,
349         1 as position,
350         product_code,
351         product_name,
352         count(*) as count,
353         sum(quantity) as items,
354         order_details.price as unit_price,
355         sum(order_details.price*quantity) as price,
356         products.sale_start_at as sale_start_at
357       from order_details
358         join order_deliveries on order_deliveries.id = order_details.order_delivery_id
359         join orders on orders.id = order_deliveries.order_id
360         left outer join customers on customers.id = orders.customer_id
361         join product_styles on product_styles.id = order_details.product_style_id
362         join products on products.id = product_styles.product_id
363       where #{WHERE_CLAUSE}
364   #{if @member == 'member'
365     " and (customers.activate in (:activate)) "
366   elsif @member == 'nomember'
367     " and (customers.activate is null) "
368   else
369     " and (customers.activate in (:activate) or customers.activate is null) "
370   end}
371         and ((:sale_start_from is null or :sale_start_to is null)
372              or products.sale_start_at between :sale_start_from and :sale_start_to)
373       group by order_details.id, product_code, product_name, unit_price, products.sale_start_at
374       order by price desc
375     EOS
376     # position の振り直し & 販売開始日を Date に
377     records.zip((1..records.size).to_a) do | r, i |
378       r.position = i
379       r.sale_start_at = Date.parse(r.sale_start_at)
380     end
381     records
382   end
383 end
384
385 class AgeTotalizer < Totalizer
386   # 上限, 表示名
387   AGES = [
388     [nil, '未回答'],
389     [10, '0~9歳'],
390     [20, '10~19歳'],
391     [30, '20~29歳'],
392     [40, '30~39歳'],
393     [50, '40~49歳'],
394     [60, '50~59歳'],
395     [70, '60~69歳'],
396     [:else, '70歳~']
397   ]
398   def initialize
399     super
400     @title = '年代別集計'
401     @columns = %w(age count subtotal discount charge deliv_fee total average)
402     #@links = %w(全体 all 会員 member 非会員 nomember)
403     @links = %w(全体 all) # 今のところ非会員購入はできないので
404     @default_type = 'all'
405   end
406   def get_conditions params
407     conds = super
408     conds
409   end
410   def graph
411     @records or return nil
412     g = init_graph(Gruff::SideBar)
413     g.title = self.title
414     g.sort = false
415     @records.each do |r|
416       g.data(r['age'], r['payment_total'].to_i)
417     end
418     g.labels = {0=>' '}
419     return g.to_blob
420   end
421   def search conditions
422     age_when_else = AGES.map do | v, label |
423       if v == :else
424         "else '%s'" % label
425       elsif v.nil?
426         "when ((customers.birthday is null and customers.id is not null) or (customers.id is null and order_deliveries.birthday is null)) then '%s'" % label
427       else
428         "when extract(year from #{MergeAdapterUtil.age('customers.birthday')}) < %d then '%s'" % [v, label]
429         "when extract(year from #{MergeAdapterUtil.age('order_deliveries.birthday')}) < %d then '%s'" % [v, label]
430       end
431     end.join("\n")
432     records = OrderDetail.find_by_sql([<<-EOS, conditions])
433       select
434         age,
435         count(*) as count,
436         sum(total) as total,
437         sum(subtotal) as subtotal,
438         sum(payment_total) as payment_total,
439         sum(discount) as discount,
440         sum(deliv_fee) as deliv_fee,
441         sum(charge) as charge,
442         sum(use_point) as use_point,
443         round(avg(total)) as average
444       from (
445         select
446           case
447             #{age_when_else}
448           end as age,
449           total,
450           subtotal,
451           payment_total,
452           discount,
453           deliv_fee,
454           charge,
455           use_point
456         from order_deliveries
457           join orders on orders.id = order_deliveries.order_id
458           left outer join customers on customers.id = orders.customer_id
459         where #{WHERE_CLAUSE}
460       ) as t1
461       group by age
462       order by age
463     EOS
464     # 整列する
465     record_hash = records.index_by(&:age)
466     AGES.map{|_, age| record_hash[age] || Hash.new(0).merge('age'=>age) }
467   end
468 end
469
470 class JobTotalizer < Totalizer
471   def initialize
472     super
473     @title = '職業別集計'
474     @columns = %w(position job count subtotal discount charge deliv_fee total average)
475     @links = %w(全体 all)
476     @default_type = 'all'
477   end
478   def get_conditions params
479     conds = super
480   end
481   def graph
482     @records or return nil
483     g = init_graph(Gruff::Pie)
484     g.title = self.title
485     @records.reject{|r| r['payment_total'].to_i.zero?}.each do |r|
486       g.data(r['job'], r['payment_total'].to_i)
487     end
488     return g.to_blob
489   end
490   def search conditions
491     records = OrderDetail.find_by_sql([<<-EOS, conditions])
492       select
493         1 as position,
494         occupations.name as job,
495         count(*) as count,
496         sum(total) as total,
497         sum(subtotal) as subtotal,
498         sum(payment_total) as payment_total,
499         sum(discount) as discount,
500         sum(deliv_fee) as deliv_fee,
501         sum(charge) as charge,
502         sum(use_point) as use_point,
503         round(avg(total)) as average
504       from 
505       ( select 
506         case
507           when customers.occupation_id is not null then customers.occupation_id
508           when customers.occupation_id is null then order_deliveries.occupation_id
509         end as occupation_id,
510         total as total,
511         subtotal as subtotal,
512         payment_total as payment_total,
513         discount as discount,
514         deliv_fee as deliv_fee,
515         charge as charge,
516         use_point as use_point
517       from order_deliveries
518         join orders on orders.id = order_deliveries.order_id
519         left outer join customers on customers.id = orders.customer_id
520         left join occupations on occupations.id = customers.occupation_id
521       where #{WHERE_CLAUSE}
522       ) as t1
523         left join occupations on occupations.id = t1.occupation_id
524       group by occupations.name
525       order by total desc
526     EOS
527     record_hash = records.index_by(&:job)
528     records = Occupation.find(:all).map do | occupation |
529       job = occupation.name
530       if record_hash.has_key?(job)
531         record_hash[job]
532       else
533         h = Hash.new(0)
534         h['job'] = job
535         h
536       end
537     end.sort_by do |record|
538       -record['total'].to_i
539     end
540     records.zip((1..records.size).to_a) do | r, i |
541       r['position'] = i
542     end
543     records
544   end
545 end
546
547 class MemberTotalizer < Totalizer
548   def initialize
549     super
550     @title = '会員別集計'
551     @columns = %w(kind count subtotal discount charge deliv_fee total average)
552   end
553   def get_conditions params
554     conds = super
555   end
556   def graph
557     @records or return nil
558     g = init_graph(Gruff::Pie)
559     g.title = self.title
560     @records.reject{|r| r['payment_total'].to_i.zero?}.each do |r|
561       g.data(r['kind'], r['payment_total'].to_i)
562     end
563     return g.to_blob
564   end
565   def search conditions
566     records = OrderDetail.find_by_sql([<<-EOS, conditions])
567       select
568         case when customers.sex = #{System::MALE} then '会員男性'
569              when customers.sex = #{System::FEMALE} then '会員女性'
570              when customers.sex is null and order_deliveries.sex = #{System::MALE} then '非会員男性'
571              when customers.sex is null and order_deliveries.sex = #{System::FEMALE} then '非会員女性'
572         end as kind,
573         count(*) as count,
574         sum(total) as total,
575         sum(subtotal) as subtotal,
576         sum(payment_total) as payment_total,
577         sum(discount) as discount,
578         sum(deliv_fee) as deliv_fee,
579         sum(charge) as charge,
580         sum(use_point) as use_point,
581         round(avg(total)) as average
582       from order_deliveries
583         join orders on orders.id = order_deliveries.order_id
584         left outer join customers on customers.id = orders.customer_id
585       where #{WHERE_CLAUSE}
586       group by kind
587       order by total desc
588     EOS
589     record_hash = records.index_by(&:kind)
590
591     # TODO: ここの即値はなんとかしたい
592     records = %w(会員男性 会員女性 非会員男性 非会員女性).map do |kind|
593       if record_hash.has_key?(kind)
594         record_hash[kind]
595       else
596         h = Hash.new(0)
597         h['kind'] = kind
598         h
599       end
600     end.sort_by do |record|
601       -record['total'].to_i
602     end
603   end
604 end