kylekthompson-shoppe 1.0.7
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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +55 -0
- data/Rakefile +36 -0
- data/app/assets/images/shoppe/chosen-sprite.png +0 -0
- data/app/assets/images/shoppe/chosen-sprite@2x.png +0 -0
- data/app/assets/images/shoppe/document.svg +1 -0
- data/app/assets/images/shoppe/icons/bag.svg +1 -0
- data/app/assets/images/shoppe/icons/balance.svg +1 -0
- data/app/assets/images/shoppe/icons/box.svg +1 -0
- data/app/assets/images/shoppe/icons/building.svg +1 -0
- data/app/assets/images/shoppe/icons/chart.svg +1 -0
- data/app/assets/images/shoppe/icons/chat.svg +1 -0
- data/app/assets/images/shoppe/icons/checkbox.svg +1 -0
- data/app/assets/images/shoppe/icons/checkbox2.svg +1 -0
- data/app/assets/images/shoppe/icons/cloud.svg +1 -0
- data/app/assets/images/shoppe/icons/cone.svg +1 -0
- data/app/assets/images/shoppe/icons/credit_card.svg +1 -0
- data/app/assets/images/shoppe/icons/currency.svg +1 -0
- data/app/assets/images/shoppe/icons/edit.svg +14 -0
- data/app/assets/images/shoppe/icons/flowchart.svg +1 -0
- data/app/assets/images/shoppe/icons/gift.svg +1 -0
- data/app/assets/images/shoppe/icons/globe.svg +1 -0
- data/app/assets/images/shoppe/icons/id.svg +1 -0
- data/app/assets/images/shoppe/icons/id2.svg +1 -0
- data/app/assets/images/shoppe/icons/locked.svg +1 -0
- data/app/assets/images/shoppe/icons/report.svg +1 -0
- data/app/assets/images/shoppe/icons/search.svg +1 -0
- data/app/assets/images/shoppe/icons/support.svg +1 -0
- data/app/assets/images/shoppe/icons/tags.svg +1 -0
- data/app/assets/images/shoppe/icons/toolbox.svg +1 -0
- data/app/assets/images/shoppe/icons/unlocked.svg +1 -0
- data/app/assets/images/shoppe/icons/wallet.svg +1 -0
- data/app/assets/images/shoppe/logo.svg +47 -0
- data/app/assets/images/shoppe/move.svg +1 -0
- data/app/assets/images/shoppe/shoppe.svg +25 -0
- data/app/assets/images/shoppe/square.svg +9 -0
- data/app/assets/images/shoppe/statuses/accepted.svg +14 -0
- data/app/assets/images/shoppe/statuses/paid.svg +16 -0
- data/app/assets/images/shoppe/statuses/received.svg +15 -0
- data/app/assets/images/shoppe/statuses/rejected.svg +14 -0
- data/app/assets/images/shoppe/statuses/shipped.svg +14 -0
- data/app/assets/images/shoppe/table-tear-off.png +0 -0
- data/app/assets/javascripts/shoppe/application.coffee +123 -0
- data/app/assets/javascripts/shoppe/chosen.jquery.js +1166 -0
- data/app/assets/javascripts/shoppe/jquery_ui.js +6 -0
- data/app/assets/javascripts/shoppe/mousetrap.js +9 -0
- data/app/assets/javascripts/shoppe/order_form.coffee +47 -0
- data/app/assets/stylesheets/shoppe/application.scss +601 -0
- data/app/assets/stylesheets/shoppe/chosen.scss +424 -0
- data/app/assets/stylesheets/shoppe/dialog.scss +25 -0
- data/app/assets/stylesheets/shoppe/elements.scss +79 -0
- data/app/assets/stylesheets/shoppe/printable.scss +67 -0
- data/app/assets/stylesheets/shoppe/reset.scss +93 -0
- data/app/assets/stylesheets/shoppe/sub.scss +106 -0
- data/app/assets/stylesheets/shoppe/variables.scss +1 -0
- data/app/controllers/shoppe/addresses_controller.rb +47 -0
- data/app/controllers/shoppe/application_controller.rb +46 -0
- data/app/controllers/shoppe/attachments_controller.rb +14 -0
- data/app/controllers/shoppe/countries_controller.rb +47 -0
- data/app/controllers/shoppe/customers_controller.rb +55 -0
- data/app/controllers/shoppe/dashboard_controller.rb +9 -0
- data/app/controllers/shoppe/delivery_service_prices_controller.rb +45 -0
- data/app/controllers/shoppe/delivery_services_controller.rb +47 -0
- data/app/controllers/shoppe/orders_controller.rb +119 -0
- data/app/controllers/shoppe/payments_controller.rb +33 -0
- data/app/controllers/shoppe/product_categories_controller.rb +47 -0
- data/app/controllers/shoppe/product_category_localisations_controller.rb +58 -0
- data/app/controllers/shoppe/product_localisations_controller.rb +58 -0
- data/app/controllers/shoppe/products_controller.rb +58 -0
- data/app/controllers/shoppe/sessions_controller.rb +34 -0
- data/app/controllers/shoppe/settings_controller.rb +16 -0
- data/app/controllers/shoppe/stock_level_adjustments_controller.rb +40 -0
- data/app/controllers/shoppe/tax_rates_controller.rb +49 -0
- data/app/controllers/shoppe/users_controller.rb +53 -0
- data/app/controllers/shoppe/variants_controller.rb +50 -0
- data/app/helpers/shoppe/application_helper.rb +60 -0
- data/app/helpers/shoppe/product_category_helper.rb +36 -0
- data/app/mailers/shoppe/order_mailer.rb +25 -0
- data/app/mailers/shoppe/user_mailer.rb +10 -0
- data/app/models/shoppe/address.rb +44 -0
- data/app/models/shoppe/country.rb +27 -0
- data/app/models/shoppe/customer.rb +41 -0
- data/app/models/shoppe/delivery_service.rb +33 -0
- data/app/models/shoppe/delivery_service_price.rb +31 -0
- data/app/models/shoppe/order.rb +100 -0
- data/app/models/shoppe/order/actions.rb +94 -0
- data/app/models/shoppe/order/billing.rb +105 -0
- data/app/models/shoppe/order/delivery.rb +229 -0
- data/app/models/shoppe/order/states.rb +69 -0
- data/app/models/shoppe/order_item.rb +239 -0
- data/app/models/shoppe/payment.rb +86 -0
- data/app/models/shoppe/product.rb +189 -0
- data/app/models/shoppe/product/product_attributes.rb +20 -0
- data/app/models/shoppe/product/variants.rb +51 -0
- data/app/models/shoppe/product_attribute.rb +66 -0
- data/app/models/shoppe/product_categorization.rb +14 -0
- data/app/models/shoppe/product_category.rb +75 -0
- data/app/models/shoppe/setting.rb +68 -0
- data/app/models/shoppe/stock_level_adjustment.rb +19 -0
- data/app/models/shoppe/tax_rate.rb +46 -0
- data/app/models/shoppe/user.rb +49 -0
- data/app/validators/permalink_validator.rb +7 -0
- data/app/views/layouts/shoppe/application.html.haml +34 -0
- data/app/views/layouts/shoppe/printable.html.haml +11 -0
- data/app/views/layouts/shoppe/sub.html.haml +10 -0
- data/app/views/shoppe/addresses/_form.html.haml +33 -0
- data/app/views/shoppe/addresses/edit.html.haml +5 -0
- data/app/views/shoppe/addresses/new.html.haml +5 -0
- data/app/views/shoppe/countries/_form.html.haml +35 -0
- data/app/views/shoppe/countries/edit.html.haml +6 -0
- data/app/views/shoppe/countries/index.html.haml +25 -0
- data/app/views/shoppe/countries/new.html.haml +7 -0
- data/app/views/shoppe/customers/_addresses.html.haml +20 -0
- data/app/views/shoppe/customers/_form.html.haml +30 -0
- data/app/views/shoppe/customers/_search_form.html.haml +13 -0
- data/app/views/shoppe/customers/edit.html.haml +5 -0
- data/app/views/shoppe/customers/index.html.haml +32 -0
- data/app/views/shoppe/customers/new.html.haml +5 -0
- data/app/views/shoppe/customers/show.html.haml +53 -0
- data/app/views/shoppe/delivery_service_prices/_form.html.haml +44 -0
- data/app/views/shoppe/delivery_service_prices/edit.html.haml +6 -0
- data/app/views/shoppe/delivery_service_prices/index.html.haml +23 -0
- data/app/views/shoppe/delivery_service_prices/new.html.haml +6 -0
- data/app/views/shoppe/delivery_services/_form.html.haml +38 -0
- data/app/views/shoppe/delivery_services/edit.html.haml +9 -0
- data/app/views/shoppe/delivery_services/index.html.haml +27 -0
- data/app/views/shoppe/delivery_services/new.html.haml +6 -0
- data/app/views/shoppe/order_mailer/accepted.text.erb +12 -0
- data/app/views/shoppe/order_mailer/received.text.erb +9 -0
- data/app/views/shoppe/order_mailer/rejected.text.erb +10 -0
- data/app/views/shoppe/order_mailer/shipped.text.erb +16 -0
- data/app/views/shoppe/orders/_form.html.haml +58 -0
- data/app/views/shoppe/orders/_order_details.html.haml +57 -0
- data/app/views/shoppe/orders/_order_items.html.haml +38 -0
- data/app/views/shoppe/orders/_order_items_form.html.haml +61 -0
- data/app/views/shoppe/orders/_payments_form.html.haml +15 -0
- data/app/views/shoppe/orders/_payments_table.html.haml +37 -0
- data/app/views/shoppe/orders/_search_form.html.haml +24 -0
- data/app/views/shoppe/orders/_status_bar.html.haml +35 -0
- data/app/views/shoppe/orders/despatch_note.html.haml +45 -0
- data/app/views/shoppe/orders/edit.html.haml +21 -0
- data/app/views/shoppe/orders/index.html.haml +39 -0
- data/app/views/shoppe/orders/new.html.haml +14 -0
- data/app/views/shoppe/orders/show.html.haml +25 -0
- data/app/views/shoppe/payments/refund.html.haml +13 -0
- data/app/views/shoppe/product_categories/_form.html.haml +50 -0
- data/app/views/shoppe/product_categories/edit.html.haml +8 -0
- data/app/views/shoppe/product_categories/index.html.haml +27 -0
- data/app/views/shoppe/product_categories/new.html.haml +6 -0
- data/app/views/shoppe/product_category_localisations/form.html.haml +29 -0
- data/app/views/shoppe/product_category_localisations/index.html.haml +26 -0
- data/app/views/shoppe/product_localisations/form.html.haml +32 -0
- data/app/views/shoppe/product_localisations/index.html.haml +26 -0
- data/app/views/shoppe/products/_form.html.haml +118 -0
- data/app/views/shoppe/products/_table.html.haml +42 -0
- data/app/views/shoppe/products/edit.html.haml +9 -0
- data/app/views/shoppe/products/import.html.haml +67 -0
- data/app/views/shoppe/products/index.html.haml +9 -0
- data/app/views/shoppe/products/new.html.haml +7 -0
- data/app/views/shoppe/sessions/new.html.haml +12 -0
- data/app/views/shoppe/sessions/reset.html.haml +12 -0
- data/app/views/shoppe/settings/edit.html.haml +19 -0
- data/app/views/shoppe/shared/error.html.haml +6 -0
- data/app/views/shoppe/stock_level_adjustments/index.html.haml +40 -0
- data/app/views/shoppe/tax_rates/form.html.haml +28 -0
- data/app/views/shoppe/tax_rates/index.html.haml +17 -0
- data/app/views/shoppe/user_mailer/new_password.text.erb +9 -0
- data/app/views/shoppe/users/_form.html.haml +27 -0
- data/app/views/shoppe/users/edit.html.haml +5 -0
- data/app/views/shoppe/users/index.html.haml +17 -0
- data/app/views/shoppe/users/new.html.haml +7 -0
- data/app/views/shoppe/variants/form.html.haml +68 -0
- data/app/views/shoppe/variants/index.html.haml +33 -0
- data/config/locales/de.yml +653 -0
- data/config/locales/en.yml +744 -0
- data/config/locales/es.yml +653 -0
- data/config/locales/pl.yml +662 -0
- data/config/locales/pt-BR.yml +655 -0
- data/config/routes.rb +53 -0
- data/db/countries.txt +252 -0
- data/db/migrate/20130926094549_create_shoppe_initial_schema.rb +175 -0
- data/db/migrate/20131024201501_add_address_type_to_shoppe_tax_rates.rb +5 -0
- data/db/migrate/20131024204815_create_shoppe_payments.rb +32 -0
- data/db/migrate/20131102143930_remove_default_on_order_item_weight.rb +9 -0
- data/db/migrate/20141013192427_create_shoppe_customers.rb +14 -0
- data/db/migrate/20141026175622_add_indexes_to_shoppe_order_items.rb +6 -0
- data/db/migrate/20141026175943_add_indexes_to_shoppe_orders.rb +7 -0
- data/db/migrate/20141026180333_add_indexes_to_shoppe_payments.rb +6 -0
- data/db/migrate/20141026180835_add_indexes_to_shoppe_product_attributes.rb +7 -0
- data/db/migrate/20141026180952_add_indexes_to_shoppe_product_categories.rb +5 -0
- data/db/migrate/20141026181040_add_indexes_to_shoppe_products.rb +8 -0
- data/db/migrate/20141026181312_add_indexes_to_shoppe_settings.rb +5 -0
- data/db/migrate/20141026181354_add_indexes_to_shoppe_stock_level_adjustments.rb +6 -0
- data/db/migrate/20141026181559_add_indexes_to_shoppe_users.rb +5 -0
- data/db/migrate/20141026181716_add_indexes_to_shoppe_delivery_services.rb +9 -0
- data/db/migrate/20141026181717_allow_multiple_shoppe_products_per_shoppe_product_category.rb +46 -0
- data/db/migrate/20141026181718_add_nested_to_product_categories.rb +30 -0
- data/db/migrate/20141027215005_create_shoppe_addresses.rb +17 -0
- data/db/migrate/20150315215633_add_customer_to_shoppe_orders.rb +5 -0
- data/db/migrate/20150513171350_create_shoppe_product_category_translation_table.rb +17 -0
- data/db/migrate/20150519173350_create_shoppe_product_translation_table.rb +18 -0
- data/db/schema.rb +308 -0
- data/db/seeds.rb +137 -0
- data/db/seeds_data/poe400.jpg +0 -0
- data/db/seeds_data/snom-870-blk.jpg +0 -0
- data/db/seeds_data/snom-870-grey.jpg +0 -0
- data/db/seeds_data/snom-mm2.jpg +0 -0
- data/db/seeds_data/spa303.jpg +0 -0
- data/db/seeds_data/t18p.jpg +0 -0
- data/db/seeds_data/t20p.jpg +0 -0
- data/db/seeds_data/t22p.jpg +0 -0
- data/db/seeds_data/t26p.jpg +0 -0
- data/db/seeds_data/t41pn.jpg +0 -0
- data/db/seeds_data/t46gn.jpg +0 -0
- data/db/seeds_data/w52p.jpg +0 -0
- data/db/seeds_data/yhs32.jpg +0 -0
- data/lib/shoppe.rb +60 -0
- data/lib/shoppe/associated_countries.rb +20 -0
- data/lib/shoppe/country_importer.rb +15 -0
- data/lib/shoppe/default_navigation.rb +21 -0
- data/lib/shoppe/engine.rb +55 -0
- data/lib/shoppe/error.rb +21 -0
- data/lib/shoppe/errors/inappropriate_delivery_service.rb +6 -0
- data/lib/shoppe/errors/insufficient_stock_to_fulfil.rb +15 -0
- data/lib/shoppe/errors/invalid_configuration.rb +6 -0
- data/lib/shoppe/errors/not_enough_stock.rb +15 -0
- data/lib/shoppe/errors/payment_declined.rb +6 -0
- data/lib/shoppe/errors/refund_failed.rb +6 -0
- data/lib/shoppe/errors/unorderable_item.rb +6 -0
- data/lib/shoppe/navigation_manager.rb +81 -0
- data/lib/shoppe/orderable_item.rb +39 -0
- data/lib/shoppe/settings.rb +26 -0
- data/lib/shoppe/settings_loader.rb +16 -0
- data/lib/shoppe/setup_generator.rb +10 -0
- data/lib/shoppe/version.rb +3 -0
- data/lib/shoppe/view_helpers.rb +16 -0
- data/lib/tasks/shoppe.rake +29 -0
- metadata +662 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
module Shoppe
|
|
2
|
+
class Order < ActiveRecord::Base
|
|
3
|
+
|
|
4
|
+
# An array of all the available statuses for an order
|
|
5
|
+
STATUSES = ['building', 'confirming', 'received', 'accepted', 'rejected', 'shipped']
|
|
6
|
+
|
|
7
|
+
# The Shoppe::User who accepted the order
|
|
8
|
+
#
|
|
9
|
+
# @return [Shoppe::User]
|
|
10
|
+
belongs_to :accepter, :class_name => 'Shoppe::User', :foreign_key => 'accepted_by'
|
|
11
|
+
|
|
12
|
+
# The Shoppe::User who rejected the order
|
|
13
|
+
#
|
|
14
|
+
# @return [Shoppe::User]
|
|
15
|
+
belongs_to :rejecter, :class_name => 'Shoppe::User', :foreign_key => 'rejected_by'
|
|
16
|
+
|
|
17
|
+
# Validations
|
|
18
|
+
validates :status, :inclusion => {:in => STATUSES}
|
|
19
|
+
|
|
20
|
+
# Set the status to building if we don't have a status
|
|
21
|
+
after_initialize { self.status = STATUSES.first if self.status.blank? }
|
|
22
|
+
|
|
23
|
+
# All orders which have been received
|
|
24
|
+
scope :received, -> {where("received_at is not null")}
|
|
25
|
+
|
|
26
|
+
# All orders which are currently pending acceptance/rejection
|
|
27
|
+
scope :pending, -> { where(:status => 'received') }
|
|
28
|
+
|
|
29
|
+
# All ordered ordered by their ID desending
|
|
30
|
+
scope :ordered, -> { order(:id => :desc)}
|
|
31
|
+
|
|
32
|
+
# Is this order still being built by the user?
|
|
33
|
+
#
|
|
34
|
+
# @return [Boolean]
|
|
35
|
+
def building?
|
|
36
|
+
self.status == 'building'
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Is this order in the user confirmation step?
|
|
40
|
+
#
|
|
41
|
+
# @return [Boolean]
|
|
42
|
+
def confirming?
|
|
43
|
+
self.status == 'confirming'
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Has this order been rejected?
|
|
47
|
+
#
|
|
48
|
+
# @return [Boolean]
|
|
49
|
+
def rejected?
|
|
50
|
+
!!self.rejected_at
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Has this order been accepted?
|
|
54
|
+
#
|
|
55
|
+
# @return [Boolean]
|
|
56
|
+
def accepted?
|
|
57
|
+
!!self.accepted_at
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Has the order been received?
|
|
61
|
+
#
|
|
62
|
+
# @return [Boolean]
|
|
63
|
+
def received?
|
|
64
|
+
!!self.received_at?
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
module Shoppe
|
|
2
|
+
class OrderItem < ActiveRecord::Base
|
|
3
|
+
|
|
4
|
+
self.table_name = 'shoppe_order_items'
|
|
5
|
+
|
|
6
|
+
# The associated order
|
|
7
|
+
#
|
|
8
|
+
# @return [Shoppe::Order]
|
|
9
|
+
belongs_to :order, :class_name => 'Shoppe::Order', :touch => true, :inverse_of => :order_items
|
|
10
|
+
|
|
11
|
+
# The item which has been ordered
|
|
12
|
+
belongs_to :ordered_item, :polymorphic => true
|
|
13
|
+
|
|
14
|
+
# Any stock level adjustments which have been made for this order item
|
|
15
|
+
has_many :stock_level_adjustments, :as => :parent, :dependent => :nullify, :class_name => 'Shoppe::StockLevelAdjustment'
|
|
16
|
+
|
|
17
|
+
# Validations
|
|
18
|
+
validates :quantity, :numericality => true
|
|
19
|
+
validates :ordered_item, :presence => true
|
|
20
|
+
|
|
21
|
+
validate do
|
|
22
|
+
unless in_stock?
|
|
23
|
+
errors.add :quantity, :too_high_quantity
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Before saving an order item which belongs to a received order, cache the pricing again if appropriate.
|
|
28
|
+
before_save do
|
|
29
|
+
if order.received? && (unit_price_changed? || unit_cost_price_changed? || tax_rate_changed? || tax_amount_changed?)
|
|
30
|
+
cache_pricing
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# After saving, if the order has been shipped, reallocate stock appropriate
|
|
35
|
+
after_save do
|
|
36
|
+
if order.shipped?
|
|
37
|
+
allocate_unallocated_stock!
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# This allows you to add a product to the scoped order. For example Order.first.order_items.add_product(...).
|
|
42
|
+
# This will either increase the quantity of the value in the order or create a new item if one does not
|
|
43
|
+
# exist already.
|
|
44
|
+
#
|
|
45
|
+
# @param ordered_item [Object] an object which implements the Shoppe::OrderableItem protocol
|
|
46
|
+
# @param quantity [Fixnum] the number of items to order
|
|
47
|
+
# @return [Shoppe::OrderItem]
|
|
48
|
+
def self.add_item(ordered_item, quantity = 1)
|
|
49
|
+
raise Errors::UnorderableItem, :ordered_item => ordered_item unless ordered_item.orderable?
|
|
50
|
+
transaction do
|
|
51
|
+
if existing = self.where(:ordered_item_id => ordered_item.id, :ordered_item_type => ordered_item.class.to_s).first
|
|
52
|
+
existing.increase!(quantity)
|
|
53
|
+
existing
|
|
54
|
+
else
|
|
55
|
+
new_item = self.create(:ordered_item => ordered_item, :quantity => 0)
|
|
56
|
+
new_item.increase!(quantity)
|
|
57
|
+
new_item
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Remove a product from an order. It will also ensure that the order's custom delivery
|
|
63
|
+
# service is updated if appropriate.
|
|
64
|
+
#
|
|
65
|
+
# @return [Shoppe::OrderItem]
|
|
66
|
+
def remove
|
|
67
|
+
transaction do
|
|
68
|
+
self.destroy!
|
|
69
|
+
self.order.remove_delivery_service_if_invalid
|
|
70
|
+
self
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Increases the quantity of items in the order by the number provided. Will raise an error if we don't have
|
|
75
|
+
# the stock to do this.
|
|
76
|
+
#
|
|
77
|
+
# @param quantity [Fixnum]
|
|
78
|
+
def increase!(amount = 1)
|
|
79
|
+
transaction do
|
|
80
|
+
self.quantity += amount
|
|
81
|
+
unless self.in_stock?
|
|
82
|
+
raise Shoppe::Errors::NotEnoughStock, :ordered_item => self.ordered_item, :requested_stock => self.quantity
|
|
83
|
+
end
|
|
84
|
+
self.save!
|
|
85
|
+
self.order.remove_delivery_service_if_invalid
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Decreases the quantity of items in the order by the number provided.
|
|
90
|
+
#
|
|
91
|
+
# @param amount [Fixnum]
|
|
92
|
+
def decrease!(amount = 1)
|
|
93
|
+
transaction do
|
|
94
|
+
self.quantity -= amount
|
|
95
|
+
self.quantity == 0 ? self.destroy : self.save!
|
|
96
|
+
self.order.remove_delivery_service_if_invalid
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# The total weight of the item
|
|
101
|
+
#
|
|
102
|
+
# @return [BigDecimal]
|
|
103
|
+
def weight
|
|
104
|
+
read_attribute(:weight) || ordered_item.try(:weight) || BigDecimal(0)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Return the total weight of the item
|
|
108
|
+
#
|
|
109
|
+
# @return [BigDecimal]
|
|
110
|
+
def total_weight
|
|
111
|
+
quantity * weight
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# The unit price for the item
|
|
115
|
+
#
|
|
116
|
+
# @return [BigDecimal]
|
|
117
|
+
def unit_price
|
|
118
|
+
read_attribute(:unit_price) || ordered_item.try(:price) || BigDecimal(0)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# The cost price for the item
|
|
122
|
+
#
|
|
123
|
+
# @return [BigDecimal]
|
|
124
|
+
def unit_cost_price
|
|
125
|
+
read_attribute(:unit_cost_price) || ordered_item.try(:cost_price) || BigDecimal(0)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# The tax rate for the item
|
|
129
|
+
#
|
|
130
|
+
# @return [BigDecimal]
|
|
131
|
+
def tax_rate
|
|
132
|
+
read_attribute(:tax_rate) || ordered_item.try(:tax_rate).try(:rate_for, self.order) || BigDecimal(0)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# The total tax for the item
|
|
136
|
+
#
|
|
137
|
+
# @return [BigDecimal]
|
|
138
|
+
def tax_amount
|
|
139
|
+
read_attribute(:tax_amount) || (self.sub_total / BigDecimal(100)) * self.tax_rate
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# The total cost for the product
|
|
143
|
+
#
|
|
144
|
+
# @return [BigDecimal]
|
|
145
|
+
def total_cost
|
|
146
|
+
quantity * unit_cost_price
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# The sub total for the product
|
|
150
|
+
#
|
|
151
|
+
# @return [BigDecimal]
|
|
152
|
+
def sub_total
|
|
153
|
+
quantity * unit_price
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# The total price including tax for the order line
|
|
157
|
+
#
|
|
158
|
+
# @return [BigDecimal]
|
|
159
|
+
def total
|
|
160
|
+
tax_amount + sub_total
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Cache the pricing for this order item
|
|
164
|
+
def cache_pricing
|
|
165
|
+
write_attribute :weight, self.weight
|
|
166
|
+
write_attribute :unit_price, self.unit_price
|
|
167
|
+
write_attribute :unit_cost_price, self.unit_cost_price
|
|
168
|
+
write_attribute :tax_rate, self.tax_rate
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Cache the pricing for this order item and save
|
|
172
|
+
def cache_pricing!
|
|
173
|
+
cache_pricing
|
|
174
|
+
save!
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Trigger when the associated order is confirmed. It handles caching the values
|
|
178
|
+
# of the monetary items and allocating stock as appropriate.
|
|
179
|
+
def confirm!
|
|
180
|
+
cache_pricing!
|
|
181
|
+
allocate_unallocated_stock!
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Trigger when the associated order is accepted
|
|
185
|
+
def accept!
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Trigged when the associated order is rejected..
|
|
189
|
+
def reject!
|
|
190
|
+
self.stock_level_adjustments.destroy_all
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Do we have the stock needed to fulfil this order?
|
|
194
|
+
#
|
|
195
|
+
# @return [Boolean]
|
|
196
|
+
def in_stock?
|
|
197
|
+
if self.ordered_item && self.ordered_item.stock_control?
|
|
198
|
+
self.ordered_item.stock >= unallocated_stock
|
|
199
|
+
else
|
|
200
|
+
true
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# How much stock remains to be allocated for this order?
|
|
205
|
+
#
|
|
206
|
+
# @return [Fixnum]
|
|
207
|
+
def unallocated_stock
|
|
208
|
+
self.quantity - allocated_stock
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# How much stock has been allocated to this item?
|
|
212
|
+
#
|
|
213
|
+
# @return [Fixnum]
|
|
214
|
+
def allocated_stock
|
|
215
|
+
0 - self.stock_level_adjustments.sum(:adjustment)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Validate the stock level against the product and update as appropriate. This method will be executed
|
|
219
|
+
# before an order is completed. If we have run out of this product, we will update the quantity to an
|
|
220
|
+
# appropriate level (or remove the order item) and return the object.
|
|
221
|
+
def validate_stock_levels
|
|
222
|
+
if in_stock?
|
|
223
|
+
false
|
|
224
|
+
else
|
|
225
|
+
self.quantity = self.ordered_item.stock
|
|
226
|
+
self.quantity == 0 ? self.destroy : self.save!
|
|
227
|
+
self
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Allocate any unallocated stock for this order item. There is no return value.
|
|
232
|
+
def allocate_unallocated_stock!
|
|
233
|
+
if self.ordered_item.stock_control? && self.unallocated_stock != 0
|
|
234
|
+
self.ordered_item.stock_level_adjustments.create!(:parent => self, :adjustment => 0 - self.unallocated_stock, :description => "Order ##{self.order.number}")
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
end
|
|
239
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
module Shoppe
|
|
2
|
+
class Payment < ActiveRecord::Base
|
|
3
|
+
|
|
4
|
+
# Additional callbacks
|
|
5
|
+
extend ActiveModel::Callbacks
|
|
6
|
+
define_model_callbacks :refund
|
|
7
|
+
|
|
8
|
+
# The associated order
|
|
9
|
+
#
|
|
10
|
+
# @return [Shoppe::Order]
|
|
11
|
+
belongs_to :order, :class_name => 'Shoppe::Order'
|
|
12
|
+
|
|
13
|
+
# An associated payment (only applies to refunds)
|
|
14
|
+
#
|
|
15
|
+
# @return [Shoppe::Payment]
|
|
16
|
+
belongs_to :parent, :class_name => "Shoppe::Payment", :foreign_key => "parent_payment_id"
|
|
17
|
+
|
|
18
|
+
# Validations
|
|
19
|
+
validates :amount, :numericality => true
|
|
20
|
+
validates :reference, :presence => true
|
|
21
|
+
validates :method, :presence => true
|
|
22
|
+
|
|
23
|
+
# Payments can have associated properties
|
|
24
|
+
key_value_store :properties
|
|
25
|
+
|
|
26
|
+
# Callbacks
|
|
27
|
+
after_create :cache_amount_paid
|
|
28
|
+
after_destroy :cache_amount_paid
|
|
29
|
+
before_destroy { self.parent.update_attribute(:amount_refunded, self.parent.amount_refunded + amount) if self.parent }
|
|
30
|
+
|
|
31
|
+
# Is this payment a refund?
|
|
32
|
+
#
|
|
33
|
+
# @return [Boolean]
|
|
34
|
+
def refund?
|
|
35
|
+
self.amount < BigDecimal(0)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Has this payment had any refunds taken from it?
|
|
39
|
+
#
|
|
40
|
+
# @return [Boolean]
|
|
41
|
+
def refunded?
|
|
42
|
+
self.amount_refunded > BigDecimal(0)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# How much of the payment can be refunded
|
|
46
|
+
#
|
|
47
|
+
# @return [BigDecimal]
|
|
48
|
+
def refundable_amount
|
|
49
|
+
refundable? ? (self.amount - self.amount_refunded) : BigDecimal(0)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Process a refund from this payment.
|
|
53
|
+
#
|
|
54
|
+
# @param amount [String] the amount which should be refunded
|
|
55
|
+
# @return [Boolean]
|
|
56
|
+
def refund!(amount)
|
|
57
|
+
run_callbacks :refund do
|
|
58
|
+
amount = BigDecimal(amount)
|
|
59
|
+
if refundable_amount >= amount
|
|
60
|
+
transaction do
|
|
61
|
+
self.class.create(:parent => self, :order_id => self.order_id, :amount => 0-amount, :method => self.method, :reference => reference)
|
|
62
|
+
self.update_attribute(:amount_refunded, self.amount_refunded + amount)
|
|
63
|
+
true
|
|
64
|
+
end
|
|
65
|
+
else
|
|
66
|
+
raise Shoppe::Errors::RefundFailed, :message => I18n.t('.refund_failed', refundable_amount: refundable_amount)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Return a transaction URL for viewing further information about this
|
|
72
|
+
# payment.
|
|
73
|
+
#
|
|
74
|
+
# @return [String]
|
|
75
|
+
def transaction_url
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def cache_amount_paid
|
|
82
|
+
self.order.update_attribute(:amount_paid, self.order.payments.sum(:amount))
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
require "roo"
|
|
2
|
+
require "globalize"
|
|
3
|
+
|
|
4
|
+
module Shoppe
|
|
5
|
+
class Product < ActiveRecord::Base
|
|
6
|
+
|
|
7
|
+
self.table_name = 'shoppe_products'
|
|
8
|
+
|
|
9
|
+
# Add dependencies for products
|
|
10
|
+
require_dependency 'shoppe/product/product_attributes'
|
|
11
|
+
require_dependency 'shoppe/product/variants'
|
|
12
|
+
|
|
13
|
+
# Products have a default_image and a data_sheet
|
|
14
|
+
attachment :default_image
|
|
15
|
+
attachment :data_sheet
|
|
16
|
+
|
|
17
|
+
# The product's categorizations
|
|
18
|
+
#
|
|
19
|
+
# @return [Shoppe::ProductCategorization]
|
|
20
|
+
has_many :product_categorizations, dependent: :destroy, class_name: 'Shoppe::ProductCategorization', inverse_of: :product
|
|
21
|
+
# The product's categories
|
|
22
|
+
#
|
|
23
|
+
# @return [Shoppe::ProductCategory]
|
|
24
|
+
has_many :product_categories, class_name: 'Shoppe::ProductCategory', through: :product_categorizations
|
|
25
|
+
|
|
26
|
+
# The product's tax rate
|
|
27
|
+
#
|
|
28
|
+
# @return [Shoppe::TaxRate]
|
|
29
|
+
belongs_to :tax_rate, :class_name => "Shoppe::TaxRate"
|
|
30
|
+
|
|
31
|
+
# Ordered items which are associated with this product
|
|
32
|
+
has_many :order_items, :dependent => :restrict_with_exception, :class_name => 'Shoppe::OrderItem', :as => :ordered_item
|
|
33
|
+
|
|
34
|
+
# Orders which have ordered this product
|
|
35
|
+
has_many :orders, :through => :order_items, :class_name => 'Shoppe::Order'
|
|
36
|
+
|
|
37
|
+
# Stock level adjustments for this product
|
|
38
|
+
has_many :stock_level_adjustments, :dependent => :destroy, :class_name => 'Shoppe::StockLevelAdjustment', :as => :item
|
|
39
|
+
|
|
40
|
+
# Validations
|
|
41
|
+
with_options :if => Proc.new { |p| p.parent.nil? } do |product|
|
|
42
|
+
product.validate :has_at_least_one_product_category
|
|
43
|
+
product.validates :description, :presence => true
|
|
44
|
+
product.validates :short_description, :presence => true
|
|
45
|
+
end
|
|
46
|
+
validates :name, :presence => true
|
|
47
|
+
validates :permalink, :presence => true, :uniqueness => true, :permalink => true
|
|
48
|
+
validates :sku, :presence => true
|
|
49
|
+
validates :weight, :numericality => true
|
|
50
|
+
validates :price, :numericality => true
|
|
51
|
+
validates :cost_price, :numericality => true, :allow_blank => true
|
|
52
|
+
|
|
53
|
+
# Before validation, set the permalink if we don't already have one
|
|
54
|
+
before_validation { self.permalink = self.name.parameterize if self.permalink.blank? && self.name.is_a?(String) }
|
|
55
|
+
|
|
56
|
+
# All active products
|
|
57
|
+
scope :active, -> { where(:active => true) }
|
|
58
|
+
|
|
59
|
+
# All featured products
|
|
60
|
+
scope :featured, -> {where(:featured => true)}
|
|
61
|
+
|
|
62
|
+
# Localisations
|
|
63
|
+
translates :name, :permalink, :description, :short_description
|
|
64
|
+
scope :ordered, -> { includes(:translations).order(:name) }
|
|
65
|
+
|
|
66
|
+
# Return the name of the product
|
|
67
|
+
#
|
|
68
|
+
# @return [String]
|
|
69
|
+
def full_name
|
|
70
|
+
self.parent ? "#{self.parent.name} (#{name})" : name
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Is this product orderable?
|
|
74
|
+
#
|
|
75
|
+
# @return [Boolean]
|
|
76
|
+
def orderable?
|
|
77
|
+
return false unless self.active?
|
|
78
|
+
return false if self.has_variants?
|
|
79
|
+
true
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# The price for the product
|
|
83
|
+
#
|
|
84
|
+
# @return [BigDecimal]
|
|
85
|
+
def price
|
|
86
|
+
# self.default_variant ? self.default_variant.price : read_attribute(:price)
|
|
87
|
+
self.default_variant ? self.default_variant.price : read_attribute(:price)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Is this product currently in stock?
|
|
91
|
+
#
|
|
92
|
+
# @return [Boolean]
|
|
93
|
+
def in_stock?
|
|
94
|
+
self.default_variant ? self.default_variant.in_stock? : (stock_control? ? stock > 0 : true)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Return the total number of items currently in stock
|
|
98
|
+
#
|
|
99
|
+
# @return [Fixnum]
|
|
100
|
+
def stock
|
|
101
|
+
self.stock_level_adjustments.sum(:adjustment)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Return the first product category
|
|
105
|
+
#
|
|
106
|
+
# @return [Shoppe::ProductCategory]
|
|
107
|
+
def product_category
|
|
108
|
+
self.product_categories.first rescue nil
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Search for products which include the given attributes and return an active record
|
|
112
|
+
# scope of these products. Chainable with other scopes and with_attributes methods.
|
|
113
|
+
# For example:
|
|
114
|
+
#
|
|
115
|
+
# Shoppe::Product.active.with_attribute('Manufacturer', 'Apple').with_attribute('Model', ['Macbook', 'iPhone'])
|
|
116
|
+
#
|
|
117
|
+
# @return [Enumerable]
|
|
118
|
+
def self.with_attributes(key, values)
|
|
119
|
+
product_ids = Shoppe::ProductAttribute.searchable.where(:key => key, :value => values).pluck(:product_id).uniq
|
|
120
|
+
where(:id => product_ids)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Imports products from a spreadsheet file
|
|
124
|
+
# Example:
|
|
125
|
+
#
|
|
126
|
+
# Shoppe:Product.import("path/to/file.csv")
|
|
127
|
+
def self.import(file)
|
|
128
|
+
spreadsheet = open_spreadsheet(file)
|
|
129
|
+
spreadsheet.default_sheet = spreadsheet.sheets.first
|
|
130
|
+
header = spreadsheet.row(1)
|
|
131
|
+
(2..spreadsheet.last_row).each do |i|
|
|
132
|
+
row = Hash[[header, spreadsheet.row(i)].transpose]
|
|
133
|
+
|
|
134
|
+
# Don't import products where the name is blank
|
|
135
|
+
unless row["name"].nil?
|
|
136
|
+
if product = find_by(name: row["name"])
|
|
137
|
+
# Dont import products with the same name but update quantities if they're not the same
|
|
138
|
+
qty = row["qty"].to_i
|
|
139
|
+
if qty > 0 && qty != product.stock
|
|
140
|
+
product.stock_level_adjustments.create!(description: I18n.t('shoppe.import'), adjustment: qty)
|
|
141
|
+
end
|
|
142
|
+
else
|
|
143
|
+
product = new
|
|
144
|
+
product.name = row["name"]
|
|
145
|
+
product.sku = row["sku"]
|
|
146
|
+
product.description = row["description"]
|
|
147
|
+
product.short_description = row["short_description"]
|
|
148
|
+
product.weight = row["weight"]
|
|
149
|
+
product.price = row["price"].nil? ? 0 : row["price"]
|
|
150
|
+
product.permalink = row["permalink"]
|
|
151
|
+
|
|
152
|
+
product.product_categories << begin
|
|
153
|
+
if Shoppe::ProductCategory.find_by(name: row["category_name"]).present?
|
|
154
|
+
Shoppe::ProductCategory.find_by(name: row["category_name"])
|
|
155
|
+
else
|
|
156
|
+
Shoppe::ProductCategory.create(name: row["category_name"])
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
product.save!
|
|
161
|
+
|
|
162
|
+
qty = row["qty"].to_i
|
|
163
|
+
if qty > 0
|
|
164
|
+
product.stock_level_adjustments.create!(description: I18n.t('shoppe.import'), adjustment: qty)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def self.open_spreadsheet(file)
|
|
172
|
+
case File.extname(file.original_filename)
|
|
173
|
+
when ".csv" then Roo::CSV.new(file.path)
|
|
174
|
+
when ".xls" then Roo::Excel.new(file.path)
|
|
175
|
+
when ".xlsx" then Roo::Excelx.new(file.path)
|
|
176
|
+
else raise I18n.t('shoppe.imports.errors.unknown_format', filename: File.original_filename)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
private
|
|
181
|
+
|
|
182
|
+
# Validates
|
|
183
|
+
|
|
184
|
+
def has_at_least_one_product_category
|
|
185
|
+
errors.add(:base, 'must add at least one product category') if self.product_categories.blank?
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
end
|
|
189
|
+
end
|