tienda 1.0.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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/Rakefile +35 -0
- data/app/assets/images/tienda/chosen-sprite.png +0 -0
- data/app/assets/images/tienda/chosen-sprite@2x.png +0 -0
- data/app/assets/images/tienda/document.svg +1 -0
- data/app/assets/images/tienda/icons/bag.svg +1 -0
- data/app/assets/images/tienda/icons/balance.svg +1 -0
- data/app/assets/images/tienda/icons/box.svg +1 -0
- data/app/assets/images/tienda/icons/building.svg +1 -0
- data/app/assets/images/tienda/icons/chart.svg +1 -0
- data/app/assets/images/tienda/icons/chat.svg +1 -0
- data/app/assets/images/tienda/icons/checkbox.svg +1 -0
- data/app/assets/images/tienda/icons/checkbox2.svg +1 -0
- data/app/assets/images/tienda/icons/cloud.svg +1 -0
- data/app/assets/images/tienda/icons/cone.svg +1 -0
- data/app/assets/images/tienda/icons/credit_card.svg +1 -0
- data/app/assets/images/tienda/icons/currency.svg +1 -0
- data/app/assets/images/tienda/icons/edit.svg +14 -0
- data/app/assets/images/tienda/icons/flowchart.svg +1 -0
- data/app/assets/images/tienda/icons/gift.svg +1 -0
- data/app/assets/images/tienda/icons/globe.svg +1 -0
- data/app/assets/images/tienda/icons/id.svg +1 -0
- data/app/assets/images/tienda/icons/id2.svg +1 -0
- data/app/assets/images/tienda/icons/locked.svg +1 -0
- data/app/assets/images/tienda/icons/report.svg +1 -0
- data/app/assets/images/tienda/icons/search.svg +1 -0
- data/app/assets/images/tienda/icons/support.svg +1 -0
- data/app/assets/images/tienda/icons/tags.svg +1 -0
- data/app/assets/images/tienda/icons/toolbox.svg +1 -0
- data/app/assets/images/tienda/icons/unlocked.svg +1 -0
- data/app/assets/images/tienda/icons/wallet.svg +1 -0
- data/app/assets/images/tienda/logo.svg +47 -0
- data/app/assets/images/tienda/move.svg +1 -0
- data/app/assets/images/tienda/shoppe.svg +25 -0
- data/app/assets/images/tienda/square.svg +9 -0
- data/app/assets/images/tienda/statuses/accepted.svg +14 -0
- data/app/assets/images/tienda/statuses/paid.svg +16 -0
- data/app/assets/images/tienda/statuses/received.svg +15 -0
- data/app/assets/images/tienda/statuses/rejected.svg +14 -0
- data/app/assets/images/tienda/statuses/shipped.svg +14 -0
- data/app/assets/images/tienda/table-tear-off.png +0 -0
- data/app/assets/javascripts/tienda/application.coffee +119 -0
- data/app/assets/javascripts/tienda/chosen.jquery.js +1166 -0
- data/app/assets/javascripts/tienda/jquery_ui.js +6 -0
- data/app/assets/javascripts/tienda/mousetrap.js +9 -0
- data/app/assets/javascripts/tienda/order_form.coffee +47 -0
- data/app/assets/stylesheets/tienda/application.scss +585 -0
- data/app/assets/stylesheets/tienda/chosen.scss +424 -0
- data/app/assets/stylesheets/tienda/dialog.scss +25 -0
- data/app/assets/stylesheets/tienda/elements.scss +79 -0
- data/app/assets/stylesheets/tienda/printable.scss +67 -0
- data/app/assets/stylesheets/tienda/reset.scss +93 -0
- data/app/assets/stylesheets/tienda/sub.scss +106 -0
- data/app/assets/stylesheets/tienda/variables.scss +1 -0
- data/app/controllers/tienda/application_controller.rb +46 -0
- data/app/controllers/tienda/attachments_controller.rb +14 -0
- data/app/controllers/tienda/countries_controller.rb +47 -0
- data/app/controllers/tienda/dashboard_controller.rb +9 -0
- data/app/controllers/tienda/delivery_service_prices_controller.rb +45 -0
- data/app/controllers/tienda/delivery_services_controller.rb +47 -0
- data/app/controllers/tienda/orders_controller.rb +90 -0
- data/app/controllers/tienda/payments_controller.rb +33 -0
- data/app/controllers/tienda/product_categories_controller.rb +47 -0
- data/app/controllers/tienda/products_controller.rb +58 -0
- data/app/controllers/tienda/sessions_controller.rb +34 -0
- data/app/controllers/tienda/settings_controller.rb +16 -0
- data/app/controllers/tienda/stock_level_adjustments_controller.rb +40 -0
- data/app/controllers/tienda/tax_rates_controller.rb +49 -0
- data/app/controllers/tienda/users_controller.rb +53 -0
- data/app/controllers/tienda/variants_controller.rb +50 -0
- data/app/helpers/tienda/application_helper.rb +60 -0
- data/app/mailers/tienda/order_mailer.rb +25 -0
- data/app/mailers/tienda/user_mailer.rb +10 -0
- data/app/models/tienda/country.rb +27 -0
- data/app/models/tienda/delivery_service.rb +33 -0
- data/app/models/tienda/delivery_service_price.rb +31 -0
- data/app/models/tienda/order/actions.rb +98 -0
- data/app/models/tienda/order/billing.rb +105 -0
- data/app/models/tienda/order/delivery.rb +229 -0
- data/app/models/tienda/order/states.rb +69 -0
- data/app/models/tienda/order.rb +93 -0
- data/app/models/tienda/order_item.rb +239 -0
- data/app/models/tienda/payment.rb +80 -0
- data/app/models/tienda/product/product_attributes.rb +20 -0
- data/app/models/tienda/product/variants.rb +51 -0
- data/app/models/tienda/product.rb +169 -0
- data/app/models/tienda/product_attribute.rb +66 -0
- data/app/models/tienda/product_category.rb +23 -0
- data/app/models/tienda/setting.rb +68 -0
- data/app/models/tienda/stock_level_adjustment.rb +19 -0
- data/app/models/tienda/tax_rate.rb +46 -0
- data/app/models/tienda/user.rb +49 -0
- data/app/validators/permalink_validator.rb +7 -0
- data/app/views/layouts/tienda/application.html.haml +34 -0
- data/app/views/layouts/tienda/printable.html.haml +11 -0
- data/app/views/layouts/tienda/sub.html.haml +10 -0
- data/app/views/tienda/countries/_form.html.haml +35 -0
- data/app/views/tienda/countries/edit.html.haml +6 -0
- data/app/views/tienda/countries/index.html.haml +25 -0
- data/app/views/tienda/countries/new.html.haml +7 -0
- data/app/views/tienda/delivery_service_prices/_form.html.haml +44 -0
- data/app/views/tienda/delivery_service_prices/edit.html.haml +6 -0
- data/app/views/tienda/delivery_service_prices/index.html.haml +23 -0
- data/app/views/tienda/delivery_service_prices/new.html.haml +6 -0
- data/app/views/tienda/delivery_services/_form.html.haml +38 -0
- data/app/views/tienda/delivery_services/edit.html.haml +9 -0
- data/app/views/tienda/delivery_services/index.html.haml +27 -0
- data/app/views/tienda/delivery_services/new.html.haml +6 -0
- data/app/views/tienda/order_mailer/accepted.text.erb +12 -0
- data/app/views/tienda/order_mailer/received.text.erb +12 -0
- data/app/views/tienda/order_mailer/rejected.text.erb +10 -0
- data/app/views/tienda/order_mailer/shipped.text.erb +16 -0
- data/app/views/tienda/orders/_form.html.haml +55 -0
- data/app/views/tienda/orders/_order_details.html.haml +57 -0
- data/app/views/tienda/orders/_order_items.html.haml +38 -0
- data/app/views/tienda/orders/_order_items_form.html.haml +61 -0
- data/app/views/tienda/orders/_payments_form.html.haml +15 -0
- data/app/views/tienda/orders/_payments_table.html.haml +37 -0
- data/app/views/tienda/orders/_search_form.html.haml +24 -0
- data/app/views/tienda/orders/_status_bar.html.haml +35 -0
- data/app/views/tienda/orders/despatch_note.html.haml +45 -0
- data/app/views/tienda/orders/edit.html.haml +21 -0
- data/app/views/tienda/orders/index.html.haml +39 -0
- data/app/views/tienda/orders/new.html.haml +14 -0
- data/app/views/tienda/orders/show.html.haml +25 -0
- data/app/views/tienda/payments/refund.html.haml +13 -0
- data/app/views/tienda/product_categories/_form.html.haml +26 -0
- data/app/views/tienda/product_categories/edit.html.haml +6 -0
- data/app/views/tienda/product_categories/index.html.haml +19 -0
- data/app/views/tienda/product_categories/new.html.haml +6 -0
- data/app/views/tienda/products/_form.html.haml +118 -0
- data/app/views/tienda/products/_table.html.haml +42 -0
- data/app/views/tienda/products/edit.html.haml +8 -0
- data/app/views/tienda/products/import.html.haml +63 -0
- data/app/views/tienda/products/index.html.haml +9 -0
- data/app/views/tienda/products/new.html.haml +7 -0
- data/app/views/tienda/sessions/new.html.haml +12 -0
- data/app/views/tienda/sessions/reset.html.haml +12 -0
- data/app/views/tienda/settings/edit.html.haml +19 -0
- data/app/views/tienda/shared/error.html.haml +6 -0
- data/app/views/tienda/stock_level_adjustments/index.html.haml +40 -0
- data/app/views/tienda/tax_rates/form.html.haml +28 -0
- data/app/views/tienda/tax_rates/index.html.haml +17 -0
- data/app/views/tienda/user_mailer/new_password.text.erb +9 -0
- data/app/views/tienda/users/_form.html.haml +27 -0
- data/app/views/tienda/users/edit.html.haml +5 -0
- data/app/views/tienda/users/index.html.haml +17 -0
- data/app/views/tienda/users/new.html.haml +7 -0
- data/app/views/tienda/variants/form.html.haml +66 -0
- data/app/views/tienda/variants/index.html.haml +33 -0
- data/config/locales/en.yml +650 -0
- data/config/locales/pl.yml +650 -0
- data/config/locales/pt-BR.yml +643 -0
- data/config/routes.rb +42 -0
- data/db/countries.txt +252 -0
- data/db/migrate/20150124094549_create_tienda_initial_schema.rb +184 -0
- data/db/seeds.rb +128 -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/tasks/tienda.rake +29 -0
- data/lib/tienda/associated_countries.rb +20 -0
- data/lib/tienda/country_importer.rb +14 -0
- data/lib/tienda/default_navigation.rb +20 -0
- data/lib/tienda/engine.rb +48 -0
- data/lib/tienda/error.rb +21 -0
- data/lib/tienda/errors/inappropriate_delivery_service.rb +6 -0
- data/lib/tienda/errors/insufficient_stock_to_fulfil.rb +15 -0
- data/lib/tienda/errors/invalid_configuration.rb +6 -0
- data/lib/tienda/errors/not_enough_stock.rb +15 -0
- data/lib/tienda/errors/payment_declined.rb +6 -0
- data/lib/tienda/errors/refund_failed.rb +6 -0
- data/lib/tienda/errors/unorderable_item.rb +6 -0
- data/lib/tienda/navigation_manager.rb +81 -0
- data/lib/tienda/orderable_item.rb +39 -0
- data/lib/tienda/settings.rb +26 -0
- data/lib/tienda/settings_loader.rb +16 -0
- data/lib/tienda/setup_generator.rb +10 -0
- data/lib/tienda/version.rb +3 -0
- data/lib/tienda/view_helpers.rb +16 -0
- data/lib/tienda.rb +59 -0
- metadata +604 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Tienda
|
|
2
|
+
class UsersController < Tienda::ApplicationController
|
|
3
|
+
|
|
4
|
+
before_filter { @active_nav = :users }
|
|
5
|
+
before_filter { params[:id] && @user = Tienda::User.find(params[:id]) }
|
|
6
|
+
before_filter(only: [:create, :update, :destroy]) do
|
|
7
|
+
if Tienda.settings.demo_mode?
|
|
8
|
+
raise Tienda::Error, t('tienda.users.demo_mode_error')
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def index
|
|
13
|
+
@users = Tienda::User.all
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def new
|
|
17
|
+
@user = Tienda::User.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def create
|
|
21
|
+
@user = Tienda::User.new(safe_params)
|
|
22
|
+
if @user.save
|
|
23
|
+
redirect_to :users, flash: { notice: t('tienda.users.create_notice') }
|
|
24
|
+
else
|
|
25
|
+
render action: "new"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def edit
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def update
|
|
33
|
+
if @user.update(safe_params)
|
|
34
|
+
redirect_to [:edit, @user], flash: { notice t('tienda.users.update_notice') }
|
|
35
|
+
else
|
|
36
|
+
render action: "edit"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def destroy
|
|
41
|
+
raise Tienda::Error, t('tienda.users.self_remove_error') if @user == current_user
|
|
42
|
+
@user.destroy
|
|
43
|
+
redirect_to :users, flash: { notice: t('tienda.users.destroy_notice') }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def safe_params
|
|
49
|
+
params[:user].permit(:first_name, :last_name, :email_address, :password, :password_confirmation)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module Tienda
|
|
2
|
+
class VariantsController < ApplicationController
|
|
3
|
+
|
|
4
|
+
before_filter { @active_nav = :products }
|
|
5
|
+
before_filter { @product = Tienda::Product.find(params[:product_id]) }
|
|
6
|
+
before_filter { params[:id] && @variant = @product.variants.find(params[:id]) }
|
|
7
|
+
|
|
8
|
+
def index
|
|
9
|
+
@variants = @product.variants.ordered
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def new
|
|
13
|
+
@variant = @product.variants.build
|
|
14
|
+
render action: "form"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def create
|
|
18
|
+
@variant = @product.variants.build(safe_params)
|
|
19
|
+
if @variant.save
|
|
20
|
+
redirect_to [@product, :variants], notice: t('tienda.variants.create_notice')
|
|
21
|
+
else
|
|
22
|
+
render action: "form"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def edit
|
|
27
|
+
render action: "form"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def update
|
|
31
|
+
if @variant.update(safe_params)
|
|
32
|
+
redirect_to edit_product_variant_path(@product, @variant), notice: t('tienda.variants.update_notice')
|
|
33
|
+
else
|
|
34
|
+
render action: "form"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def destroy
|
|
39
|
+
@variant.destroy
|
|
40
|
+
redirect_to [@product, :variants], notice: t('tienda.variants.destroy_notice')
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def safe_params
|
|
46
|
+
params[:product].permit(:name, :permalink, :sku, :default_image_file, :price, :cost_price, :tax_rate_id, :weight, :stock_control, :active, :default)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module Tienda
|
|
2
|
+
module ApplicationHelper
|
|
3
|
+
|
|
4
|
+
def navigation_manager_link(item)
|
|
5
|
+
link_to item.description, item.url(self), item.link_options.merge(:class => item.active?(self) ? 'active' : 'inactive')
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def status_tag(status)
|
|
9
|
+
content_tag :span, t("tienda.orders.statuses.#{status}"), :class => "status-tag #{status}"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def attachment_preview(attachment, options = {})
|
|
13
|
+
if attachment
|
|
14
|
+
String.new.tap do |s|
|
|
15
|
+
if attachment.image?
|
|
16
|
+
style = "style='background-image:url(#{attachment.path})'"
|
|
17
|
+
else
|
|
18
|
+
style = ''
|
|
19
|
+
end
|
|
20
|
+
s << "<div class='attachmentPreview #{attachment.image? ? 'image' : 'doc'}'>"
|
|
21
|
+
s << "<div class='imgContainer'><div class='img' #{style}></div></div>"
|
|
22
|
+
s << "<div class='desc'>"
|
|
23
|
+
s << "<span class='filename'><a href='#{attachment_path(attachment)}'>#{attachment.file_name}</a></span>"
|
|
24
|
+
s << "<span class='delete'>"
|
|
25
|
+
s << link_to(t('helpers.attachment_preview.delete', :default => 'Delete this file?'), attachment_path(attachment), :method => :delete, :data => {:confirm => t('helpers.attachment_preview.delete_confirm', :default => "Are you sure you wish to remove this attachment?")})
|
|
26
|
+
s << "</span>"
|
|
27
|
+
s << "</div>"
|
|
28
|
+
s << "</div>"
|
|
29
|
+
end.html_safe
|
|
30
|
+
elsif !options[:hide_if_blank]
|
|
31
|
+
"<div class='attachmentPreview'><div class='imgContainer'><div class='img none'></div></div><div class='desc none'>#{t('helpers.attachment_preview.no_attachment')},</div></div>".html_safe
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def settings_label(field)
|
|
36
|
+
"<label for='settings_#{field}'>#{t("tienda.settings.labels.#{field}")}</label>".html_safe
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def settings_field(field, options = {})
|
|
40
|
+
default = I18n.t("tienda.settings.defaults")[field.to_sym]
|
|
41
|
+
value = (params[:settings] && params[:settings][field]) || Tienda.settings[field.to_s]
|
|
42
|
+
type = I18n.t("tienda.settings.types")[field.to_sym] || 'string'
|
|
43
|
+
case type
|
|
44
|
+
when 'boolean'
|
|
45
|
+
String.new.tap do |s|
|
|
46
|
+
value = default if value.blank?
|
|
47
|
+
s << "<div class='radios'>"
|
|
48
|
+
s << radio_button_tag("settings[#{field}]", 'true', value == true, :id => "settings_#{field}_true")
|
|
49
|
+
s << label_tag("settings_#{field}_true", t("tienda.settings.options.#{field}.affirmative", :default => 'Yes'))
|
|
50
|
+
s << radio_button_tag("settings[#{field}]", 'false', value == false, :id => "settings_#{field}_false")
|
|
51
|
+
s << label_tag("settings_#{field}_false", t("tienda.settings.options.#{field}.negative", :default => 'No'))
|
|
52
|
+
s << "</div>"
|
|
53
|
+
end.html_safe
|
|
54
|
+
else
|
|
55
|
+
text_field_tag "settings[#{field}]", value, options.merge(:placeholder => default, :class => 'text')
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Tienda
|
|
2
|
+
class OrderMailer < ActionMailer::Base
|
|
3
|
+
|
|
4
|
+
def received(order)
|
|
5
|
+
@order = order
|
|
6
|
+
mail :from => Tienda.settings.outbound_email_address, :to => order.email_address, :subject => I18n.t('tienda.order_mailer.received.subject', :default => "Order Confirmation")
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def accepted(order)
|
|
10
|
+
@order = order
|
|
11
|
+
mail :from => Tienda.settings.outbound_email_address, :to => order.email_address, :subject => I18n.t('tienda.order_mailer.received.accepted', :default => "Order Accepted")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def rejected(order)
|
|
15
|
+
@order = order
|
|
16
|
+
mail :from => Tienda.settings.outbound_email_address, :to => order.email_address, :subject => I18n.t('tienda.order_mailer.received.rejected', :default => "Order Rejected")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def shipped(order)
|
|
20
|
+
@order = order
|
|
21
|
+
mail :from => Tienda.settings.outbound_email_address, :to => order.email_address, :subject => I18n.t('tienda.order_mailer.received.shipped', :default => "Order Shipped")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Tienda
|
|
2
|
+
|
|
3
|
+
# The Tienda::Country model stores countries which can be used for delivery & billing
|
|
4
|
+
# addresses for orders.
|
|
5
|
+
#
|
|
6
|
+
# You can use the Tienda::CountryImporter to import a pre-defined list of countries
|
|
7
|
+
# into your database. This automatically happens when you run the 'tienda:setup'
|
|
8
|
+
# rake task.
|
|
9
|
+
|
|
10
|
+
class Country < ActiveRecord::Base
|
|
11
|
+
|
|
12
|
+
self.table_name = 'tienda_countries'
|
|
13
|
+
|
|
14
|
+
# All orders which have this country set as their billing country
|
|
15
|
+
has_many :billed_orders, :dependent => :restrict_with_exception, :class_name => 'Tienda::Order', :foreign_key => 'billing_country_id'
|
|
16
|
+
|
|
17
|
+
# All orders which have this country set as their delivery country
|
|
18
|
+
has_many :delivered_orders, :dependent => :restrict_with_exception, :class_name => 'Tienda::Order', :foreign_key => 'delivery_country_id'
|
|
19
|
+
|
|
20
|
+
# All countries ordered by their name asending
|
|
21
|
+
scope :ordered, -> { order(:name => :asc) }
|
|
22
|
+
|
|
23
|
+
# Validations
|
|
24
|
+
validates :name, :presence => true
|
|
25
|
+
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Tienda
|
|
2
|
+
class DeliveryService < ActiveRecord::Base
|
|
3
|
+
|
|
4
|
+
self.table_name = 'tienda_delivery_services'
|
|
5
|
+
|
|
6
|
+
# Validations
|
|
7
|
+
validates :name, :presence => true
|
|
8
|
+
validates :courier, :presence => true
|
|
9
|
+
|
|
10
|
+
# Orders which are assigned to this delivery service
|
|
11
|
+
has_many :orders, :dependent => :restrict_with_exception, :class_name => 'Tienda::Order'
|
|
12
|
+
|
|
13
|
+
# Prices for the different levels of service within this delivery service
|
|
14
|
+
has_many :delivery_service_prices, :dependent => :destroy, :class_name => 'Tienda::DeliveryServicePrice'
|
|
15
|
+
|
|
16
|
+
# All active delivery services
|
|
17
|
+
scope :active, -> { where(:active => true)}
|
|
18
|
+
|
|
19
|
+
# Returns a tracking URL for the passed order
|
|
20
|
+
#
|
|
21
|
+
# @param order [Tienda::Order]
|
|
22
|
+
# @return [String] the full URL for the order.
|
|
23
|
+
def tracking_url_for(order)
|
|
24
|
+
return nil if self.tracking_url.blank?
|
|
25
|
+
tracking_url = self.tracking_url.dup
|
|
26
|
+
tracking_url.gsub!("{{consignment_number}}", CGI.escape(order.consignment_number.to_s))
|
|
27
|
+
tracking_url.gsub!("{{delivery_postcode}}", CGI.escape(order.delivery_postcode.to_s))
|
|
28
|
+
tracking_url.gsub!("{{billing_postcode}}", CGI.escape(order.billing_postcode.to_s))
|
|
29
|
+
tracking_url
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Tienda
|
|
2
|
+
class DeliveryServicePrice < ActiveRecord::Base
|
|
3
|
+
|
|
4
|
+
# Set the table name
|
|
5
|
+
self.table_name = 'tienda_delivery_service_prices'
|
|
6
|
+
|
|
7
|
+
include Tienda::AssociatedCountries
|
|
8
|
+
|
|
9
|
+
# The delivery service which this price belongs to
|
|
10
|
+
belongs_to :delivery_service, :class_name => 'Tienda::DeliveryService'
|
|
11
|
+
|
|
12
|
+
# The tax rate which should be applied
|
|
13
|
+
belongs_to :tax_rate, :class_name => "Tienda::TaxRate"
|
|
14
|
+
|
|
15
|
+
# Validations
|
|
16
|
+
validates :code, :presence => true
|
|
17
|
+
validates :price, :numericality => true
|
|
18
|
+
validates :cost_price, :numericality => true, :allow_blank => true
|
|
19
|
+
validates :min_weight, :numericality => true
|
|
20
|
+
validates :max_weight, :numericality => true
|
|
21
|
+
|
|
22
|
+
# All prices ordered by their price ascending
|
|
23
|
+
scope :ordered, -> { order(:price => :asc) }
|
|
24
|
+
|
|
25
|
+
# All prices which are suitable for the weight passed.
|
|
26
|
+
#
|
|
27
|
+
# @param weight [BigDecimal] the weight of the order
|
|
28
|
+
scope :for_weight, -> weight { where("min_weight <= ? AND max_weight >= ?", weight, weight) }
|
|
29
|
+
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
module Tienda
|
|
2
|
+
class Order < ActiveRecord::Base
|
|
3
|
+
|
|
4
|
+
extend ActiveModel::Callbacks
|
|
5
|
+
|
|
6
|
+
# These additional callbacks allow for applications to hook into other
|
|
7
|
+
# parts of the order lifecycle.
|
|
8
|
+
define_model_callbacks :confirmation, :acceptance, :rejection
|
|
9
|
+
|
|
10
|
+
# This method should be called by the base application when the user has completed their
|
|
11
|
+
# first round of entering details. This will mark the order as "confirming" which means
|
|
12
|
+
# the customer now just must confirm.
|
|
13
|
+
#
|
|
14
|
+
# @param params [Hash] a hash of order attributes
|
|
15
|
+
# @return [Boolean]
|
|
16
|
+
def proceed_to_confirm(params = {})
|
|
17
|
+
self.status = 'confirming'
|
|
18
|
+
if self.update(params)
|
|
19
|
+
true
|
|
20
|
+
else
|
|
21
|
+
false
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# This method should be executed by the application when the order should be completed
|
|
26
|
+
# by the customer. It will raise exceptions if anything goes wrong or return true if
|
|
27
|
+
# the order has been confirmed successfully
|
|
28
|
+
#
|
|
29
|
+
# @return [Boolean]
|
|
30
|
+
def confirm!
|
|
31
|
+
no_stock_of = self.order_items.select(&:validate_stock_levels)
|
|
32
|
+
unless no_stock_of.empty?
|
|
33
|
+
raise Tienda::Errors::InsufficientStockToFulfil, :order => self, :out_of_stock_items => no_stock_of
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
run_callbacks :confirmation do
|
|
37
|
+
# If we have successfully charged the card (i.e. no exception) we can go ahead and mark this
|
|
38
|
+
# order as 'received' which means it can be accepted by staff.
|
|
39
|
+
self.status = 'received'
|
|
40
|
+
self.received_at = Time.now
|
|
41
|
+
self.save!
|
|
42
|
+
|
|
43
|
+
self.order_items.each(&:confirm!)
|
|
44
|
+
|
|
45
|
+
# Send an email to the customer
|
|
46
|
+
deliver_received_order_email
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# We're all good.
|
|
50
|
+
true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Mark order as accepted
|
|
54
|
+
#
|
|
55
|
+
# @param user [Tienda::User] the user who carried out this action
|
|
56
|
+
def accept!(user = nil)
|
|
57
|
+
transaction do
|
|
58
|
+
run_callbacks :acceptance do
|
|
59
|
+
self.accepted_at = Time.now
|
|
60
|
+
self.accepter = user if user
|
|
61
|
+
self.status = 'accepted'
|
|
62
|
+
self.save!
|
|
63
|
+
self.order_items.each(&:accept!)
|
|
64
|
+
deliver_accepted_order_email
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Mark order as rejected
|
|
70
|
+
#
|
|
71
|
+
# @param user [Tienda::User] the user who carried out the action
|
|
72
|
+
def reject!(user = nil)
|
|
73
|
+
transaction do
|
|
74
|
+
run_callbacks :rejection do
|
|
75
|
+
self.rejected_at = Time.now
|
|
76
|
+
self.rejecter = user if user
|
|
77
|
+
self.status = 'rejected'
|
|
78
|
+
self.save!
|
|
79
|
+
self.order_items.each(&:reject!)
|
|
80
|
+
deliver_rejected_order_email
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def deliver_accepted_order_email
|
|
86
|
+
Tienda::OrderMailer.accepted(self).deliver
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def deliver_rejected_order_email
|
|
90
|
+
Tienda::OrderMailer.rejected(self).deliver
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def deliver_received_order_email
|
|
94
|
+
Tienda::OrderMailer.received(self).deliver
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
module Tienda
|
|
2
|
+
class Order < ActiveRecord::Base
|
|
3
|
+
|
|
4
|
+
# The country which this order should be billed to
|
|
5
|
+
#
|
|
6
|
+
# @return [Tienda::Country]
|
|
7
|
+
belongs_to :billing_country, :class_name => 'Tienda::Country', :foreign_key => 'billing_country_id'
|
|
8
|
+
|
|
9
|
+
# Payments which have been stored for the order
|
|
10
|
+
has_many :payments, :dependent => :destroy, :class_name => 'Tienda::Payment'
|
|
11
|
+
|
|
12
|
+
# Validations
|
|
13
|
+
with_options :if => Proc.new { |o| !o.building? } do |order|
|
|
14
|
+
order.validates :first_name, :presence => true
|
|
15
|
+
order.validates :last_name, :presence => true
|
|
16
|
+
order.validates :billing_address1, :presence => true
|
|
17
|
+
order.validates :billing_address3, :presence => true
|
|
18
|
+
order.validates :billing_address4, :presence => true
|
|
19
|
+
order.validates :billing_postcode, :presence => true
|
|
20
|
+
order.validates :billing_country, :presence => true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The name for billing purposes
|
|
24
|
+
#
|
|
25
|
+
# @return [String]
|
|
26
|
+
def billing_name
|
|
27
|
+
company.blank? ? full_name : "#{full_name} (#{company})"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The total cost of the order
|
|
31
|
+
#
|
|
32
|
+
# @return [BigDecimal]
|
|
33
|
+
def total_cost
|
|
34
|
+
self.delivery_cost_price +
|
|
35
|
+
order_items.inject(BigDecimal(0)) { |t, i| t + i.total_cost }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Return the price for the order
|
|
39
|
+
#
|
|
40
|
+
# @return [BigDecimal]
|
|
41
|
+
def profit
|
|
42
|
+
total_before_tax - total_cost
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The total price of all items in the basket excluding delivery
|
|
46
|
+
#
|
|
47
|
+
# @return [BigDecimal]
|
|
48
|
+
def items_sub_total
|
|
49
|
+
order_items.inject(BigDecimal(0)) { |t, i| t + i.sub_total }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The total price of the order before tax
|
|
53
|
+
#
|
|
54
|
+
# @return [BigDecimal]
|
|
55
|
+
def total_before_tax
|
|
56
|
+
self.delivery_price + self.items_sub_total
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The total amount of tax due on this order
|
|
60
|
+
#
|
|
61
|
+
# @return [BigDecimal]
|
|
62
|
+
def tax
|
|
63
|
+
self.delivery_tax_amount +
|
|
64
|
+
order_items.inject(BigDecimal(0)) { |t, i| t + i.tax_amount }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The total of the order including tax
|
|
68
|
+
#
|
|
69
|
+
# @return [BigDecimal]
|
|
70
|
+
def total
|
|
71
|
+
self.delivery_price +
|
|
72
|
+
self.delivery_tax_amount +
|
|
73
|
+
order_items.inject(BigDecimal(0)) { |t, i| t + i.total }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The total amount due on the order
|
|
77
|
+
#
|
|
78
|
+
# @return [BigDecimal]
|
|
79
|
+
def balance
|
|
80
|
+
@balance ||= total - amount_paid
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Is there a payment still outstanding on this order?
|
|
84
|
+
#
|
|
85
|
+
# @return [Boolean]
|
|
86
|
+
def payment_outstanding?
|
|
87
|
+
balance > BigDecimal(0)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Has this order been paid in full?
|
|
91
|
+
#
|
|
92
|
+
# @return [Boolean]
|
|
93
|
+
def paid_in_full?
|
|
94
|
+
!payment_outstanding?
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Is the order invoiced?
|
|
98
|
+
#
|
|
99
|
+
# @return [Boolean]
|
|
100
|
+
def invoiced?
|
|
101
|
+
!invoice_number.blank?
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
end
|
|
105
|
+
end
|