rawbotz 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -5
  3. data/Gemfile +2 -0
  4. data/README.md +60 -6
  5. data/Rakefile +3 -2
  6. data/agpl-3.0.txt +661 -0
  7. data/exe/bcrypt_pw +10 -0
  8. data/exe/rawbotz_abort_orders +28 -0
  9. data/exe/rawbotz_process_order_queue +12 -8
  10. data/exe/rawbotz_stock_update +10 -1
  11. data/exe/rawbotz_update_local_products +11 -3
  12. data/exe/rawbotz_update_remote_products +27 -4
  13. data/lib/rawbotz.rb +13 -5
  14. data/lib/rawbotz/app.rb +36 -2
  15. data/lib/rawbotz/cli/order_result_table.rb +13 -0
  16. data/lib/rawbotz/helpers/format_helper.rb +14 -0
  17. data/lib/rawbotz/helpers/icon_helper.rb +108 -22
  18. data/lib/rawbotz/helpers/order_item_color_helper.rb +17 -0
  19. data/lib/rawbotz/helpers/resource_link_helper.rb +3 -0
  20. data/lib/rawbotz/mail_template.rb +42 -7
  21. data/lib/rawbotz/models/sales.rb +21 -0
  22. data/lib/rawbotz/models/stock.rb +25 -0
  23. data/lib/rawbotz/models/stock_product.rb +146 -0
  24. data/lib/rawbotz/models/stock_product_factory.rb +64 -0
  25. data/lib/rawbotz/processors/order_linker.rb +50 -0
  26. data/lib/rawbotz/{order_processor.rb → processors/order_processor.rb} +14 -3
  27. data/lib/rawbotz/processors/organic_product_deliveries_csv.rb +47 -0
  28. data/lib/rawbotz/{product_updater.rb → processors/product_updater.rb} +71 -6
  29. data/lib/rawbotz/processors/stock_processor.rb +67 -0
  30. data/lib/rawbotz/public/rawbotz.css +35 -1
  31. data/lib/rawbotz/public/rawbotz.js +0 -1
  32. data/lib/rawbotz/remote_shop.rb +3 -0
  33. data/lib/rawbotz/routes.rb +3 -0
  34. data/lib/rawbotz/routes/non_remote_orders.rb +129 -29
  35. data/lib/rawbotz/routes/orders.rb +55 -4
  36. data/lib/rawbotz/routes/orders/stock.rb +75 -0
  37. data/lib/rawbotz/routes/product_links.rb +1 -1
  38. data/lib/rawbotz/routes/products.rb +19 -22
  39. data/lib/rawbotz/routes/remote_shop.rb +1 -1
  40. data/lib/rawbotz/routes/stock.rb +58 -0
  41. data/lib/rawbotz/routes/suppliers.rb +9 -4
  42. data/lib/rawbotz/sales_data.rb +13 -0
  43. data/lib/rawbotz/version.rb +1 -1
  44. data/lib/rawbotz/views/_hide_unhide_button.haml +1 -1
  45. data/lib/rawbotz/views/_menu.haml +9 -0
  46. data/lib/rawbotz/views/index.haml +1 -1
  47. data/lib/rawbotz/views/layout.haml +0 -1
  48. data/lib/rawbotz/views/order/_head.haml +64 -19
  49. data/lib/rawbotz/views/order/_item_table.haml +19 -3
  50. data/lib/rawbotz/views/order/_order_actions.haml +26 -0
  51. data/lib/rawbotz/views/order/link_to_remote.haml +30 -0
  52. data/lib/rawbotz/views/order/non_remote.haml +120 -26
  53. data/lib/rawbotz/views/order/packlist.haml +62 -5
  54. data/lib/rawbotz/views/order/packlist.pdf.haml +35 -0
  55. data/lib/rawbotz/views/order/stock.haml +116 -0
  56. data/lib/rawbotz/views/order/view.haml +41 -21
  57. data/lib/rawbotz/views/orders/_order_table.haml +82 -0
  58. data/lib/rawbotz/views/orders/index.haml +41 -28
  59. data/lib/rawbotz/views/orders/menu.haml +2 -2
  60. data/lib/rawbotz/views/orders/non_remotes.haml +14 -3
  61. data/lib/rawbotz/views/pdf_layout.haml +44 -0
  62. data/lib/rawbotz/views/product/view.haml +64 -11
  63. data/lib/rawbotz/views/products/index.haml +1 -1
  64. data/lib/rawbotz/views/products/table.haml +5 -1
  65. data/lib/rawbotz/views/remote_cart/index.haml +10 -0
  66. data/lib/rawbotz/views/remote_order/view.haml +6 -4
  67. data/lib/rawbotz/views/remote_orders/index.haml +16 -13
  68. data/lib/rawbotz/views/stock/index.haml +119 -0
  69. data/lib/rawbotz/views/stock/menu.haml +20 -0
  70. data/lib/rawbotz/views/stock_product/_value_table_line.haml +22 -0
  71. data/lib/rawbotz/views/stock_product/value_table.haml +28 -0
  72. data/lib/rawbotz/views/stockexplorer/stockexplorer.haml +157 -0
  73. data/lib/rawbotz/views/supplier/orders/table.haml +26 -0
  74. data/lib/rawbotz/views/supplier/view.haml +56 -10
  75. data/rawbotz.gemspec +13 -9
  76. metadata +86 -17
  77. data/lib/rawbotz/views/order/new.haml +0 -22
@@ -0,0 +1,17 @@
1
+ module Rawbotz
2
+ module Helpers
3
+ module OrderItemColorHelper
4
+ def order_item_class order_item
5
+ if order_item.ordered? && order_item.all_ordered?
6
+ "perfect_order"
7
+ elsif order_item.out_of_stock?
8
+ "out_of_stock"
9
+ elsif order_item.not_ordered?
10
+ "not_ordered"
11
+ else
12
+ "partly_available"
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -39,6 +39,9 @@ module Rawbotz
39
39
  "[no supplier]"
40
40
  end
41
41
  end
42
+ def order_link order
43
+ "<a href=\"/order/#{order.id}\">Order #{order.id} (#{order.supplier.try(:name) || "no supplier"})</a>"
44
+ end
42
45
  end
43
46
  end
44
47
  end
@@ -3,27 +3,62 @@ module Rawbotz
3
3
  # Substitute certain patterns in template
4
4
  def self.consume template, order
5
5
  result = ""
6
- result = template.gsub(/SUPPLIERNAME/, order[:supplier][:name])
6
+ result = template.gsub(/SUPPLIERNAME/, order.supplier[:name])
7
+ result = result.gsub(/PUBLIC_COMMENT/, order.public_comment || "")
7
8
 
8
9
  lines = result.split("\n")
9
10
  subject, lines = lines.partition{|l| l.start_with?("SUBJECT=")}
10
11
  product_line = lines.detect{|l| l.start_with?("* ")}
11
- order_lines = order[:order_items].map do |oi|
12
+ order_lines = order.order_items.select{|o|o.num_wished.to_i != 0}.map do |oi|
12
13
  next if product_line.nil?
13
14
  order_item_line = product_line[2..-1]
14
- order_item_line.gsub!(/SUPPLIERSKU/, oi[:local_product][:supplier_sku].to_s)
15
+ order_item_line.gsub!(/SUPPLIERSKU/, oi.local_product[:supplier_sku].to_s)
15
16
  order_item_line.gsub!(/QTY/, oi[:num_wished].to_s)
16
- if oi[:local_product][:packsize].to_s != ""
17
- order_item_line.gsub!(/NUM_PACKS/, (oi[:num_wished] / oi[:local_product][:packsize].to_f).to_s)
17
+ if oi.local_product[:packsize].to_s != ""
18
+ order_item_line.gsub!(/NUM_PACKS/, "%g" % (oi[:num_wished] / oi.local_product[:packsize].to_f))
18
19
  else
19
20
  order_item_line.gsub!(/NUM_PACKS/, '')
20
21
  end
21
- order_item_line.gsub!(/PACKSIZE/, oi[:local_product][:packsize].to_s)
22
- order_item_line.gsub!(/PRODUCTNAME/, oi[:local_product][:supplier_prod_name] || oi[:local_product][:name].to_s)
22
+ order_item_line.gsub!(/PACKSIZE/, oi.local_product[:packsize].to_s)
23
+ if oi.local_product[:supplier_prod_name].to_s != ""
24
+ order_item_line.gsub!(/PRODUCTNAME/, oi.local_product[:supplier_prod_name])
25
+ else
26
+ order_item_line.gsub!(/PRODUCTNAME/, oi.local_product[:name].to_s)
27
+ end
23
28
  order_item_line
29
+ #SUPPLIERSKU
30
+ #SUPPLIERPRODUCTNAME
24
31
  end
25
32
  lines[lines.find_index(product_line)] = order_lines if product_line
26
33
  lines.flatten.join("\n")
27
34
  end
35
+
36
+ def self.extract_subject template, order
37
+ result = ""
38
+ result = template.gsub(/SUPPLIERNAME/, order.supplier[:name])
39
+
40
+ lines = result.split("\n")
41
+ subject, lines = lines.partition{|l| l.start_with?("SUBJECT=")}
42
+ if !subject.empty?
43
+ subject[0][8..-1]
44
+ else
45
+ "Order"
46
+ end
47
+ end
48
+
49
+ def self.create_mailto_url supplier, order
50
+ mail_preview = self.consume supplier[:order_template], order
51
+ subject = self.extract_subject supplier[:order_template], order
52
+ to_mail = (supplier[:email] || "").split[0]
53
+ if supplier[:email].to_s == ""
54
+ cc_mail = ""
55
+ else
56
+ cc_mail = supplier[:email].split[1..-1].map{|m| "cc=#{m}"}.join("&")
57
+ end
58
+ if cc_mail.to_s != ""
59
+ cc_mail += "&"
60
+ end
61
+ "mailto:#{to_mail}?#{cc_mail}Subject=#{subject}&body=%s" % URI::escape(mail_preview).gsub(/&/,'%26')
62
+ end
28
63
  end
29
64
  end
@@ -0,0 +1,21 @@
1
+ require 'active_model'
2
+ require 'date'
3
+
4
+ module Rawbotz
5
+ module Models
6
+ class Sales
7
+ include ActiveModel::Model # convenience
8
+
9
+ def self.daily_since product_id, num_days=30
10
+ RawgentoDB::Query.sales_daily_between(product_id,
11
+ Date.today, Date.today - num_days)
12
+ end
13
+
14
+ def self.monthly_since product_id, num_months=12
15
+ # Todo create a date num_month month ago (vs x*30)
16
+ RawgentoDB::Query.sales_monthly_between(product_id,
17
+ Date.today, Date.today - (num_months * 30)).uniq
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,25 @@
1
+ require 'active_model'
2
+
3
+ module Rawbotz
4
+ module Models
5
+ class Stock
6
+ include ActiveModel::Model # convenience
7
+
8
+ def self.all_stock
9
+ stock = {}
10
+ RawgentoDB::Query.stock.each {|s| stock[s.product_id] = s.qty}
11
+ stock
12
+ end
13
+
14
+ # Returns a map of product_id to RawgentoDB::ProductQty .
15
+ def self.stock_for product_ids
16
+ stock = {}
17
+ # Find ruby idiomatic way to do that (probably map{}.to_h)
18
+ RawgentoDB::Query.stock_for(product_ids).each do |s|
19
+ stock[s.product_id] = s.qty
20
+ end
21
+ stock
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,146 @@
1
+ require 'active_model'
2
+
3
+ module Rawbotz
4
+ module Models
5
+ class UnsupportedNumberOfDaysError < StandardError
6
+ end
7
+
8
+ class StockProduct
9
+ include ActiveModel::Model # convenience
10
+
11
+ attr_accessor :product, :current_stock,
12
+ :sales_last_30, :sales_last_60, :sales_last_90, :sales_last_365,
13
+ :num_days_first_sale, :mem_corrected_sales
14
+
15
+ delegate :name, to: :product
16
+
17
+ def expected_stock_lifetime
18
+ if @current_stock.to_i == 0 || sales_per_day.nil? || sales_per_day == 0.0
19
+ return 0.0
20
+ end
21
+ @current_stock / sales_per_day
22
+ end
23
+
24
+ def real_sales num_days
25
+ if num_days == 30
26
+ @sales_last_30
27
+ elsif num_days == 60
28
+ @sales_last_60
29
+ elsif num_days == 90
30
+ @sales_last_90
31
+ elsif num_days == 365
32
+ @sales_last_365
33
+ else
34
+ raise UnsupportedNumberOfDaysError
35
+ end
36
+ end
37
+
38
+ def corrected_sales num_days, per_days: num_days
39
+ factor = num_days.to_f / per_days
40
+ sales = nil
41
+ if num_days == 30
42
+ sales = corrected_sales_last_30
43
+ elsif num_days == 60
44
+ sales = corrected_sales_last_60
45
+ elsif num_days == 90
46
+ sales = corrected_sales_last_90
47
+ elsif num_days == 365
48
+ sales = corrected_sales_last_365
49
+ else
50
+ raise UnsupportedNumberOfDaysError
51
+ end
52
+ return nil if sales.nil?
53
+ sales / factor
54
+ end
55
+
56
+ # We should also extrapolate (out of-) stock days!
57
+
58
+ def corrected_sales_last_30
59
+ @mem_corrected_sales ||= {}
60
+ @mem_corrected_sales[30] ||= calculate_corrected_sales(30)
61
+ end
62
+
63
+ def corrected_sales_last_60
64
+ @mem_corrected_sales ||= {}
65
+ @mem_corrected_sales[60] ||= calculate_corrected_sales(60)
66
+ end
67
+
68
+ def corrected_sales_last_90
69
+ @mem_corrected_sales ||= {}
70
+ @mem_corrected_sales[90] ||= calculate_corrected_sales(90)
71
+ end
72
+
73
+ def corrected_sales_last_365
74
+ @mem_corrected_sales ||= {}
75
+ @mem_corrected_sales[365] ||= calculate_corrected_sales(365)
76
+ end
77
+
78
+ def days_since_first_stock_date
79
+ #@mem_days_since_first_stock_date ||=
80
+ return 0 if !@product.first_stock_record.present?
81
+ first_stock_date = @product.first_stock_record.created_at.to_date
82
+ Date.today - first_stock_date
83
+ end
84
+
85
+ def out_of_stock_days num_days
86
+ @product.out_of_stock_days_since(Date.today - num_days)
87
+ end
88
+
89
+ def sales_per_day
90
+ case sales_per_day_base
91
+ when 365
92
+ @sales_last_365 / days_in_stock(365).to_f
93
+ when 90
94
+ @sales_last_90 / days_in_stock(90).to_f
95
+ when 60
96
+ @sales_last_60 / days_in_stock(60).to_f
97
+ when 30
98
+ return nil if @sales_last_30.nil?
99
+ return nil if days_in_stock(30).to_i == 0
100
+ @sales_last_30 / days_in_stock(30).to_f
101
+ else
102
+ raise UnsupportedNumberOfDaysError
103
+ end
104
+ end
105
+
106
+ def sales_per_day_base
107
+ product_days_first_stock = @product.days_since_first_stock
108
+ #first saleproduct_days_first_stock = @product.days_since_first_stock
109
+ if @sales_last_365.present? && product_days_first_stock >= 365 && num_days_first_sale >= 365 && days_in_stock(365) != 0
110
+ return 365
111
+ elsif @sales_last_90.present? && product_days_first_stock >= 90 && num_days_first_sale >= 90 && days_in_stock(90) != 0
112
+ return 90
113
+ elsif @sales_last_60.present? && product_days_first_stock >= 60 && num_days_first_sale >= 60 && days_in_stock(60) != 0
114
+ return 60
115
+ #elsif @sales_last_30.present?
116
+ else
117
+ # today - last sale ?
118
+ return 30
119
+ end
120
+ end
121
+
122
+ private
123
+
124
+ # For easier memoization
125
+ def calculate_corrected_sales num_days
126
+ return nil if days_in_stock(num_days).to_i == 0
127
+ return nil if real_sales(num_days).nil?
128
+ if days_in_stock(num_days).to_i != 0
129
+ real_sales(num_days) * factor_days_in_stock(num_days)
130
+ else
131
+ 0
132
+ end
133
+ end
134
+
135
+ # From the last num_days, how many days was the product in stock?
136
+ def days_in_stock num_days
137
+ num_days - out_of_stock_days(num_days)
138
+ end
139
+
140
+ # Factor to extrapolate sales, e.g. 1/2 of time out of stock makes factor 2.0
141
+ def factor_days_in_stock num_days
142
+ num_days.to_f / days_in_stock(num_days)
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,64 @@
1
+ module Rawbotz
2
+ module Models
3
+ module StockProductFactory
4
+ include RawgentoModels
5
+ include RawgentoDB
6
+
7
+ def self.create_single product
8
+ product_id = product.product_id
9
+ sales_30 = Query.num_sales_since(Date.today - 30, product_id)
10
+ sales_60 = Query.num_sales_since(Date.today - 60, product_id)
11
+ sales_90 = Query.num_sales_since(Date.today - 90, product_id)
12
+ sales_365 = Query.num_sales_since(Date.today - 365, product_id)
13
+
14
+ beginning_of_time = StockItem.order(created_at: :asc).pluck(:created_at).first.to_date
15
+
16
+ sales = Query.num_sales_since(beginning_of_time, product_id)
17
+ first_sales = Query.first_sales(product_id)
18
+ # Default to today instead of nil date
19
+ first_sales.default = Date.today
20
+
21
+ # stock product has trouble when empty values ...
22
+ stock = Query.stock.find {|s| s.product_id = product_id}.qty
23
+ StockProduct.new(product: LocalProduct.unscoped.find_by(product_id: product_id),
24
+ current_stock: stock[product_id],
25
+ sales_last_30: sales_30[product_id]&.qty ,
26
+ sales_last_60: sales_60[product_id]&.qty ,
27
+ sales_last_90: sales_90[product_id]&.qty ,
28
+ sales_last_365: sales_365[product_id]&.qty ,
29
+ num_days_first_sale: Date.today - first_sales[product_id].to_date
30
+ )
31
+
32
+ end
33
+
34
+ def self.create suppliers
35
+ product_ids = LocalProduct.unscoped.where(supplier_id: suppliers.map(&:id), active: true).pluck(:product_id)
36
+
37
+ sales_30 = Query.num_sales_since(Date.today - 30, product_ids)
38
+ sales_60 = Query.num_sales_since(Date.today - 60, product_ids)
39
+ sales_90 = Query.num_sales_since(Date.today - 90, product_ids)
40
+ sales_365 = Query.num_sales_since(Date.today - 365, product_ids)
41
+
42
+ beginning_of_time = StockItem.order(created_at: :asc).first.created_at.to_date
43
+
44
+ sales = Query.num_sales_since(beginning_of_time, product_ids)
45
+ first_sales = Query.first_sales(product_ids)
46
+ # Default to today instead of nil date
47
+ first_sales.default = Date.today
48
+ stock = {}
49
+
50
+ Query.stock.each {|s| stock[s.product_id] = s.qty}
51
+ product_ids.map do |product_id|
52
+ StockProduct.new(product: LocalProduct.unscoped.find_by(product_id: product_id),
53
+ current_stock: stock[product_id],
54
+ sales_last_30: sales_30[product_id]&.qty,
55
+ sales_last_60: sales_60[product_id]&.qty,
56
+ sales_last_90: sales_90[product_id]&.qty,
57
+ sales_last_365: sales_365[product_id]&.qty,
58
+ num_days_first_sale: Date.today - first_sales[product_id].to_date
59
+ )
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,50 @@
1
+ module Rawbotz
2
+ class OrderLinker
3
+ include RawgentoModels
4
+
5
+ OrderItemLine = Struct.new(:remote_product_name,
6
+ :qty_ordered,
7
+ :qty_refunded)
8
+ OrderItemRecord = Struct.new(:order_item,
9
+ :qty_ordered,
10
+ :qty_refunded)
11
+
12
+ attr_accessor :order, :orphans, :refunds
13
+
14
+ def initialize(order)
15
+ @order = order
16
+ end
17
+
18
+ # Links a local rawbotz order to a remote (magento) order.
19
+ #
20
+ # Set @orphans (remote order items that do not directly match,
21
+ # @refunds (maps not only refunded order_item to OrderItemLine)
22
+ # and @matched_order_items
23
+ def link!
24
+ # My, thats a hairy regex
25
+ shop_order_id = @order.remote_order_link[/\d+/]
26
+ @remote_order_lines = Rawbotz.mech.products_from_order(shop_order_id).map do |line|
27
+ OrderItemLine.new(line[0],
28
+ line[2],
29
+ line[3])
30
+ end
31
+ matches, orphans = @remote_order_lines.partition{|line| !order_item(line).nil?}
32
+ @matched_order_items = matches.map{|line| order_item line}
33
+ @orphans = orphans
34
+ @refunds = @matched_order_items.map do |oi|
35
+ [oi, @remote_order_lines.find{|l| l.remote_product_name == oi.local_product.remote_product.name}]
36
+ end.to_h
37
+ end
38
+
39
+ private
40
+ # Get the order item of the order, or nil.
41
+ def order_item order_item_line
42
+ remote_product = RemoteProduct.find_by name: order_item_line.remote_product_name
43
+ return nil if remote_product.blank?
44
+ return nil if remote_product.local_product.blank?
45
+
46
+ @order.order_items.processible.where(local_product: remote_product.local_product).first
47
+ end
48
+ end
49
+ end
50
+
@@ -1,8 +1,12 @@
1
+ require 'date'
2
+
1
3
  module Rawbotz
2
4
  class OrderProcessor
3
5
  def initialize(order, logger=Logger.new("/dev/null"))
4
6
  @order = order
5
7
  @logger = logger
8
+ @form_token = YAML::load_file(
9
+ Rawbotz::conf_file_path)["remote_shop"]["form_token"]
6
10
  end
7
11
 
8
12
  # Yield items, if block given
@@ -15,8 +19,10 @@ module Rawbotz
15
19
  log_product_handling item
16
20
 
17
21
  begin
18
- ordered_qty = mech.add_to_cart! item.remote_product_id, item.num_wished
19
- rescue
22
+ ordered_qty = mech.add_to_cart! item.remote_product_id, item.num_wished, @form_token
23
+ rescue Exception => e
24
+ STDERR.puts e.message.to_s
25
+ STDERR.puts e.backtrace
20
26
  ordered_qty = nil
21
27
  item.update(state: "error")
22
28
  end
@@ -31,11 +37,12 @@ module Rawbotz
31
37
  @logger.warn(item.attributes)
32
38
  end
33
39
  end
40
+ @order.update(ordered_at: DateTime.now)
34
41
  end
35
42
 
36
43
  # Returns diff -> perfect: [], ...
37
44
  def check_against_cart
38
- diff = {perfect: [], under_avail: [], extra: [], modified: [], miss: []}.to_h
45
+ diff = {perfect: [], under_avail: [], extra: [], modified: [], miss: [], error: []}.to_h
39
46
  # have logger
40
47
  mech = Rawbotz::new_mech
41
48
  mech.login
@@ -65,6 +72,10 @@ module Rawbotz
65
72
  diff[:extra] << [name, qty]
66
73
  end
67
74
 
75
+ @order.order_items.where(state: "error").find_each do |item|
76
+ diff[:error] << [item.local_product.name]
77
+ end
78
+
68
79
  diff
69
80
  end
70
81