spree_batch_capture 0.1.0 → 0.1.1
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.
- 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: []
|