spree_batch_capture 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/app/models/order_export.rb +7 -0
- data/app/uploaders/export_uploader.rb +5 -0
- data/config/initializers/carrierwave.rb +8 -0
- data/db/migrate/20111207214102_create_order_exports.rb +18 -0
- data/lib/spree_batch_capture/formatter/simple_csv.rb +127 -0
- data/lib/spree_batch_capture/scope_serializer.rb +36 -0
- data/lib/spree_batch_capture/worker/capture.rb +1 -1
- data/lib/spree_batch_capture/worker/export.rb +182 -0
- data/lib/spree_batch_capture/worker/ship.rb +52 -0
- metadata +52 -11
@@ -0,0 +1,18 @@
|
|
1
|
+
class CreateOrderExports < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :order_exports do |t|
|
4
|
+
t.integer :user_id
|
5
|
+
t.datetime :requested_at
|
6
|
+
t.datetime :completed_at
|
7
|
+
t.integer :count_of_orders
|
8
|
+
t.string :exported_orders
|
9
|
+
t.string :export_errors
|
10
|
+
|
11
|
+
t.timestamps
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.down
|
16
|
+
drop_table :order_exports
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module SpreeBatchCapture::Formatter
|
2
|
+
|
3
|
+
class SimpleCsv
|
4
|
+
|
5
|
+
# Takes an order and returns 1 or more csv rows
|
6
|
+
def self.transform(order)
|
7
|
+
return "" if order.nil? || order.id.nil?
|
8
|
+
|
9
|
+
line_items = LineItem.includes(:order => [:ship_address, :bill_address], :variant => :product).where(:order_id => order.id)
|
10
|
+
|
11
|
+
content = ""
|
12
|
+
if line_items && line_items.count > 0
|
13
|
+
content = CSV.generate do |csv|
|
14
|
+
|
15
|
+
line_items.each do |line_item|
|
16
|
+
csv << values(line_item)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
return content
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.header(line_item)
|
24
|
+
head = []
|
25
|
+
sections.each do |section|
|
26
|
+
head << extract_field_names(section, line_item)
|
27
|
+
end
|
28
|
+
Rails.logger.debug "Exporting Header: #{head.flatten}:: #{head.flatten.to_csv}"
|
29
|
+
return head.flatten.to_csv
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def self.order_fields(line_item)
|
36
|
+
return [
|
37
|
+
{ "OrderNumber" => line_item.order.number },
|
38
|
+
{ "OrderTotal" => line_item.order.total },
|
39
|
+
{ "OrderState" => line_item.order.state },
|
40
|
+
{ "ShipmentState" => line_item.order.shipment_state },
|
41
|
+
{ "PaymentState" => line_item.order.payment_state },
|
42
|
+
{ "EmailAddress" => line_item.order.email }
|
43
|
+
]
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.shipping_address_fields(line_item)
|
47
|
+
return address_for("Shipping", line_item.order.ship_address)
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.billing_address_fields(line_item)
|
51
|
+
return address_for("Billing", line_item.order.bill_address)
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.address_for(prefix, address)
|
55
|
+
# In case an address doesn't exist, return nil values
|
56
|
+
address = Address.default if address.nil?
|
57
|
+
return [
|
58
|
+
{ "#{prefix}FirstName" => address.firstname },
|
59
|
+
{ "#{prefix}LastName" => address.lastname },
|
60
|
+
{ "#{prefix}Address1" => address.address1 },
|
61
|
+
{ "#{prefix}Address2" => address.address2 },
|
62
|
+
{ "#{prefix}City" => address.city },
|
63
|
+
{ "#{prefix}State" => address.state_text },
|
64
|
+
{ "#{prefix}ZipCode" => address.zipcode },
|
65
|
+
{ "#{prefix}Country" => address.country.name }
|
66
|
+
]
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.product_fields(line_item)
|
70
|
+
return [{ "ProductName" => line_item.variant.product.name }]
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.variant_fields(line_item)
|
74
|
+
return [
|
75
|
+
{ "SKU" => line_item.variant.sku },
|
76
|
+
{ "VariantWeight" => line_item.variant.weight },
|
77
|
+
{ "VariantHeight" => line_item.variant.height },
|
78
|
+
{ "VariantWidth" => line_item.variant.width },
|
79
|
+
{ "VariantDepth" => line_item.variant.depth }
|
80
|
+
]
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.line_item_fields(line_item)
|
84
|
+
return [
|
85
|
+
{ "Quantity" => line_item.quantity },
|
86
|
+
{ "LineItemPrice" => line_item.price }
|
87
|
+
]
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.sections
|
92
|
+
return [
|
93
|
+
:order_fields,
|
94
|
+
:shipping_address_fields,
|
95
|
+
:billing_address_fields,
|
96
|
+
:product_fields,
|
97
|
+
:variant_fields,
|
98
|
+
:line_item_fields
|
99
|
+
]
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.values(line_item)
|
103
|
+
values = []
|
104
|
+
sections.each do |section|
|
105
|
+
values << extract_field_values(section, line_item)
|
106
|
+
end
|
107
|
+
return values.flatten
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.extract_field_names(section, line_item)
|
111
|
+
self.send(section, line_item).map{ |h| h.keys.first }
|
112
|
+
end
|
113
|
+
|
114
|
+
def self.extract_field_values(section, line_item)
|
115
|
+
self.send(section, line_item).map{ |h| h.values.first }
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.value(section, key, line_item)
|
119
|
+
section_arr = self.send(section, line_item)
|
120
|
+
hash = section_arr.find{|h| h[key] != nil}
|
121
|
+
|
122
|
+
return hash.nil? ? nil : hash[key]
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module SpreeBatchCapture
|
2
|
+
class ScopeSerializer
|
3
|
+
|
4
|
+
def self.from_scope(relation)
|
5
|
+
raise "Invalid scope" unless relation.kind_of? ActiveRecord::Relation
|
6
|
+
{
|
7
|
+
:where => relation.where_values,
|
8
|
+
:joins => relation.joins_values,
|
9
|
+
:order => relation.order_values,
|
10
|
+
:group => relation.group_values,
|
11
|
+
:limit => relation.limit_value,
|
12
|
+
:from => relation.from_value,
|
13
|
+
:select => relation.select_values,
|
14
|
+
:includes => relation.includes_values,
|
15
|
+
:having => relation.having_values,
|
16
|
+
:offset_value => relation.offset_value,
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.to_scope(scope_hash)
|
21
|
+
scope_hash.symbolize_keys!
|
22
|
+
return Order
|
23
|
+
.where(scope_hash[:where])
|
24
|
+
.joins(scope_hash[:joins])
|
25
|
+
.order(scope_hash[:order])
|
26
|
+
.group(scope_hash[:group])
|
27
|
+
.limit(scope_hash[:limit])
|
28
|
+
.from(scope_hash[:from])
|
29
|
+
.select(scope_hash[:select])
|
30
|
+
.includes(scope_hash[:includes])
|
31
|
+
.having(scope_hash[:having])
|
32
|
+
.offset(scope_hash[:offset])
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -7,7 +7,7 @@ module SpreeBatchCapture
|
|
7
7
|
return true unless order.payment_state == "balance_due"
|
8
8
|
|
9
9
|
if order.payments.nil? || ( order.payments && order.payments.empty? )
|
10
|
-
raise UnexpectedWorkerError.new "Attempted to capture an order without any payments."
|
10
|
+
raise Error::UnexpectedWorkerError.new "Attempted to capture an order without any payments."
|
11
11
|
else
|
12
12
|
|
13
13
|
order.payments.each do |payment|
|
@@ -0,0 +1,182 @@
|
|
1
|
+
require 'digest/sha1'
|
2
|
+
require 'zip/zip'
|
3
|
+
|
4
|
+
module SpreeBatchCapture
|
5
|
+
class Worker::Export < Worker::Base
|
6
|
+
|
7
|
+
# expected options:
|
8
|
+
# :orders
|
9
|
+
# :user_id
|
10
|
+
# :requested_at
|
11
|
+
# :mark_as_shipped (optional, defaults to false)
|
12
|
+
#
|
13
|
+
# :orders is a hash that can be any of the following
|
14
|
+
# {:scope => {:joins => [], :where => [], ...} ## There is a helper for this (see self.from_scope and self.to_scope)
|
15
|
+
# {:orders => []} ## Array of ids
|
16
|
+
# {:where => []} ## Conditional where clause
|
17
|
+
# {:sql => "select * from orders"} ## Any valid sql
|
18
|
+
# If more than one key is specified, only the first will be used in the order above.
|
19
|
+
def self.run(options)
|
20
|
+
Rails.logger.debug "Starting Export Worker..."
|
21
|
+
|
22
|
+
orders = get_orders(options[:orders])
|
23
|
+
|
24
|
+
default_attrs = {
|
25
|
+
:requested_at => options[:requested_at],
|
26
|
+
:user_id => options[:user_id]
|
27
|
+
}
|
28
|
+
|
29
|
+
if orders && orders.count > 0
|
30
|
+
export_file = export(orders, Formatter::SimpleCsv)
|
31
|
+
Rails.logger.debug "Exported orders to #{export_file}"
|
32
|
+
begin
|
33
|
+
oe = OrderExport.create(default_attrs.merge({
|
34
|
+
:count_of_orders => orders.count
|
35
|
+
}))
|
36
|
+
|
37
|
+
if export_file
|
38
|
+
oe.exported_orders = File.open(export_file)
|
39
|
+
else
|
40
|
+
oe.export_errors = I18n.t(:unable_to_export)
|
41
|
+
oe.count_of_orders = 0
|
42
|
+
end
|
43
|
+
|
44
|
+
oe.completed_at = Time.now
|
45
|
+
oe.save!
|
46
|
+
|
47
|
+
options[:successful_orders] = orders.map(&:id) if options[:mark_as_shipped]
|
48
|
+
|
49
|
+
rescue => e
|
50
|
+
if oe
|
51
|
+
oe.count_of_orders = 0
|
52
|
+
oe.export_errors = I18n.t(:unable_to_export)
|
53
|
+
oe.completed_at = Time.now
|
54
|
+
oe.save!
|
55
|
+
end
|
56
|
+
ensure
|
57
|
+
Rails.logger.debug "DELETING FILE: #{export_file}"
|
58
|
+
FileUtils.rm(export_file) if export_file && File.exist?(export_file)
|
59
|
+
end
|
60
|
+
|
61
|
+
else
|
62
|
+
OrderExport.create(default_attrs.merge({
|
63
|
+
:count_of_orders => 0,
|
64
|
+
:export_errors => I18n.t(:no_orders_for_export)
|
65
|
+
}))
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
return true
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
def self.after_run(success, options)
|
74
|
+
if success && options[:mark_as_shipped]
|
75
|
+
options[:successful_orders].each do |order_id|
|
76
|
+
Worker::Ship.enqueue({ :order_id => order_id })
|
77
|
+
end
|
78
|
+
end
|
79
|
+
return true
|
80
|
+
end
|
81
|
+
|
82
|
+
# Gets the orders based on a set of params.
|
83
|
+
# see run for more information
|
84
|
+
def self.get_orders(order_criteria)
|
85
|
+
orders = []
|
86
|
+
order_criteria.symbolize_keys!
|
87
|
+
if order_criteria[:scope]
|
88
|
+
orders = ScopeSerializer.to_scope(order_criteria[:scope])
|
89
|
+
elsif order_criteria[:orders]
|
90
|
+
orders = Order.where("id in (?)", order_criteria[:orders])
|
91
|
+
elsif order_criteria[:where]
|
92
|
+
orders = Order.where(order_criteria[:where])
|
93
|
+
elsif order_criteria[:sql]
|
94
|
+
orders = Order.find_by_sql(order_criteria[:sql])
|
95
|
+
else
|
96
|
+
return false
|
97
|
+
end
|
98
|
+
|
99
|
+
return orders
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.worker_klass_name
|
104
|
+
return "Export"
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.default_queue_name
|
108
|
+
return "export"
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
# Allows overriding of the default
|
113
|
+
# filename prefix. All filenames are as follows:
|
114
|
+
# prefix_sha1.csv[.gz]
|
115
|
+
def self.filename_prefix
|
116
|
+
"export"
|
117
|
+
end
|
118
|
+
|
119
|
+
# Temporary directory used for creating the export files.
|
120
|
+
# Can be overridden to provide a custom path
|
121
|
+
def self.temp_directory
|
122
|
+
File.join(Rails.root.to_s, 'tmp', 'export')
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
# Exports the orders and returns a file on the local filesystem.
|
128
|
+
# Requires a formatter such as SpreeBatchCapture::Formatter::SimpleCsv
|
129
|
+
# Accepts a zip flag to zip the file.
|
130
|
+
def self.export(orders, formatter, zip=true)
|
131
|
+
begin
|
132
|
+
|
133
|
+
Rails.logger.debug "Exporting file..."
|
134
|
+
|
135
|
+
export_file = open_file(zip) do |f|
|
136
|
+
f.print formatter.header(orders.first.line_items.first)
|
137
|
+
orders.each do |order|
|
138
|
+
Rails.logger.debug "Exporting order: #{order.number}"
|
139
|
+
f.print formatter.transform(order)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
Rails.logger.debug "Export completed successfully."
|
144
|
+
return export_file
|
145
|
+
|
146
|
+
rescue => e
|
147
|
+
Rails.logger.debug ("Export failed: #{e.message}")
|
148
|
+
return false
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
|
153
|
+
def self.open_file(zip)
|
154
|
+
base_dir = temp_directory
|
155
|
+
FileUtils.mkdir_p(base_dir)
|
156
|
+
base_filename = generate_file_name
|
157
|
+
filename = File.join(base_dir, base_filename)
|
158
|
+
|
159
|
+
if zip
|
160
|
+
zip_filename = filename.gsub("csv", "zip")
|
161
|
+
Zip::ZipFile.open(zip_filename, Zip::ZipFile::CREATE) do |zipfile|
|
162
|
+
zipfile.get_output_stream(base_filename) { |f| yield(f) }
|
163
|
+
end
|
164
|
+
else
|
165
|
+
File.open(filename, "w") do |f|
|
166
|
+
yield(f)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
return zip ? zip_filename : filename
|
171
|
+
end
|
172
|
+
|
173
|
+
# Generates a unique filename which includes
|
174
|
+
# the full path.
|
175
|
+
def self.generate_file_name
|
176
|
+
sha1 = Digest::SHA1.hexdigest "#{filename_prefix}::#{Time.now.to_i}::#{rand(100)}"
|
177
|
+
return "#{filename_prefix}_#{sha1[1..10]}.csv"
|
178
|
+
end
|
179
|
+
|
180
|
+
|
181
|
+
end
|
182
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
class SpreeBatchCapture::Worker::Ship < SpreeBatchCapture::Worker::Base
|
2
|
+
|
3
|
+
# expecting an order id like this:
|
4
|
+
# {:order => order_obj}
|
5
|
+
# where order_obj is set in before_run from
|
6
|
+
# {:order_id => 1}
|
7
|
+
def self.run(options)
|
8
|
+
|
9
|
+
order = options[:order]
|
10
|
+
|
11
|
+
result = false
|
12
|
+
|
13
|
+
if order && order.respond_to?(:fulfill)
|
14
|
+
result = order.fulfill
|
15
|
+
elsif order && order.shipments.count > 0
|
16
|
+
order.shipments.each do |shipment|
|
17
|
+
unless shipment.shipped?
|
18
|
+
result = shipment.ship!
|
19
|
+
if result
|
20
|
+
shipment.shipped_at = Time.now
|
21
|
+
shipment.save
|
22
|
+
end
|
23
|
+
|
24
|
+
break unless result # if something went wrong, don't try to continue
|
25
|
+
else
|
26
|
+
# already shipped so we'll just say we were successful.
|
27
|
+
result = true
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
order.update!
|
32
|
+
|
33
|
+
else
|
34
|
+
# Nothing to do so we'll just proceed as if everything was ok
|
35
|
+
result = true
|
36
|
+
end
|
37
|
+
|
38
|
+
return result
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.before_run(options)
|
42
|
+
options[:order] = Order.find_by_id options[:order_id]
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.worker_klass_name
|
46
|
+
return "Ship"
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.default_queue_name
|
50
|
+
return "ship"
|
51
|
+
end
|
52
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: spree_batch_capture
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,22 +9,22 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2011-12-
|
12
|
+
date: 2011-12-15 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: spree_core
|
16
|
-
requirement: &
|
16
|
+
requirement: &6467000 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
|
-
- -
|
19
|
+
- - ~>
|
20
20
|
- !ruby/object:Gem::Version
|
21
21
|
version: 0.60.4
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *6467000
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: resque
|
27
|
-
requirement: &
|
27
|
+
requirement: &6466200 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ~>
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: 1.19.0
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *6466200
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: resque-lock
|
38
|
-
requirement: &
|
38
|
+
requirement: &6464520 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ~>
|
@@ -43,25 +43,66 @@ dependencies:
|
|
43
43
|
version: 1.0.0
|
44
44
|
type: :runtime
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *6464520
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: carrierwave
|
49
|
+
requirement: &6462680 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.5.8
|
55
|
+
type: :runtime
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *6462680
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: fog
|
60
|
+
requirement: &6461280 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ~>
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: 1.1.1
|
66
|
+
type: :runtime
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *6461280
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubyzip
|
71
|
+
requirement: &6459040 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ~>
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: 0.9.0
|
77
|
+
type: :runtime
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: *6459040
|
47
80
|
description: Adds batch processing to Spree. Includes batch capture and batch export
|
48
81
|
of orders.
|
49
|
-
email:
|
82
|
+
email: mike.farmer@gmail.com
|
50
83
|
executables: []
|
51
84
|
extensions: []
|
52
85
|
extra_rdoc_files: []
|
53
86
|
files:
|
87
|
+
- app/uploaders/export_uploader.rb
|
88
|
+
- app/models/order_export.rb
|
54
89
|
- config/routes.rb
|
90
|
+
- config/initializers/carrierwave.rb
|
55
91
|
- lib/spree_batch_capture_hooks.rb
|
56
92
|
- lib/tasks/install.rake
|
57
93
|
- lib/tasks/spree_batch_capture.rake
|
58
94
|
- lib/spree_batch_capture/worker/bogus.rb
|
95
|
+
- lib/spree_batch_capture/worker/export.rb
|
59
96
|
- lib/spree_batch_capture/worker/capture.rb
|
60
97
|
- lib/spree_batch_capture/worker/base.rb
|
98
|
+
- lib/spree_batch_capture/worker/ship.rb
|
99
|
+
- lib/spree_batch_capture/formatter/simple_csv.rb
|
61
100
|
- lib/spree_batch_capture/error.rb
|
101
|
+
- lib/spree_batch_capture/scope_serializer.rb
|
62
102
|
- lib/spree_batch_capture.rb
|
63
103
|
- db/migrate/20111129223646_add_last_processing_error_to_payments.rb
|
64
|
-
|
104
|
+
- db/migrate/20111207214102_create_order_exports.rb
|
105
|
+
homepage: http://github.com/vitrue/spree_batch_capture
|
65
106
|
licenses: []
|
66
107
|
post_install_message:
|
67
108
|
rdoc_options: []
|