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,20 @@
|
|
|
1
|
+
module Tienda
|
|
2
|
+
class Product < ActiveRecord::Base
|
|
3
|
+
|
|
4
|
+
# Product attributes for this product
|
|
5
|
+
has_many :product_attributes, -> { order(:position) }, :class_name => 'Tienda::ProductAttribute'
|
|
6
|
+
|
|
7
|
+
# Used for setting an array of product attributes which will be updated. Usually
|
|
8
|
+
# received from a web browser.
|
|
9
|
+
attr_accessor :product_attributes_array
|
|
10
|
+
|
|
11
|
+
# After saving automatically try to update the product attributes based on the
|
|
12
|
+
# the contents of the product_attributes_array array.
|
|
13
|
+
after_save do
|
|
14
|
+
if product_attributes_array.is_a?(Array)
|
|
15
|
+
self.product_attributes.update_from_array(product_attributes_array)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module Tienda
|
|
2
|
+
class Product < ActiveRecord::Base
|
|
3
|
+
|
|
4
|
+
# Validations
|
|
5
|
+
validate { errors.add :base, :can_belong_to_root if self.parent && self.parent.parent }
|
|
6
|
+
|
|
7
|
+
# Variants of the product
|
|
8
|
+
has_many :variants, -> { order(:default => :desc, :name => :asc) }, :class_name => 'Tienda::Product', :foreign_key => 'parent_id', :dependent => :destroy
|
|
9
|
+
|
|
10
|
+
# The parent product (only applies to variants)
|
|
11
|
+
belongs_to :parent, :class_name => 'Tienda::Product', :foreign_key => 'parent_id'
|
|
12
|
+
|
|
13
|
+
# All products which are not variants
|
|
14
|
+
scope :root, -> { where(:parent_id => nil) }
|
|
15
|
+
|
|
16
|
+
# If a variant is created, the base product should be updated so that it doesn't have stock control enabled
|
|
17
|
+
after_save do
|
|
18
|
+
if self.parent
|
|
19
|
+
self.parent.price = 0
|
|
20
|
+
self.parent.cost_price = 0
|
|
21
|
+
self.parent.tax_rate = nil
|
|
22
|
+
self.parent.weight = 0
|
|
23
|
+
self.parent.stock_control = false
|
|
24
|
+
self.parent.save if self.parent.changed?
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Does this product have any variants?
|
|
29
|
+
#
|
|
30
|
+
# @return [Boolean]
|
|
31
|
+
def has_variants?
|
|
32
|
+
!variants.empty?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Returns the default variant for the product or nil if none exists.
|
|
36
|
+
#
|
|
37
|
+
# @return [Tienda::Product]
|
|
38
|
+
def default_variant
|
|
39
|
+
return nil if self.parent
|
|
40
|
+
@default_variant ||= self.variants.select { |v| v.default? }.first
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Is this product a variant of another?
|
|
44
|
+
#
|
|
45
|
+
# @return [Boolean]
|
|
46
|
+
def variant?
|
|
47
|
+
!self.parent_id.blank?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
require 'roo'
|
|
2
|
+
|
|
3
|
+
module Tienda
|
|
4
|
+
class Product < ActiveRecord::Base
|
|
5
|
+
|
|
6
|
+
self.table_name = 'tienda_products'
|
|
7
|
+
|
|
8
|
+
# Add dependencies for products
|
|
9
|
+
require_dependency 'tienda/product/product_attributes'
|
|
10
|
+
require_dependency 'tienda/product/variants'
|
|
11
|
+
|
|
12
|
+
# Products have a default_image and a data_sheet
|
|
13
|
+
attachment :default_image
|
|
14
|
+
attachment :data_sheet
|
|
15
|
+
|
|
16
|
+
# The product's category
|
|
17
|
+
#
|
|
18
|
+
# @return [Tienda::ProductCategory]
|
|
19
|
+
belongs_to :product_category, :class_name => 'Tienda::ProductCategory'
|
|
20
|
+
|
|
21
|
+
# The product's tax rate
|
|
22
|
+
#
|
|
23
|
+
# @return [Tienda::TaxRate]
|
|
24
|
+
belongs_to :tax_rate, :class_name => "Tienda::TaxRate"
|
|
25
|
+
|
|
26
|
+
# Ordered items which are associated with this product
|
|
27
|
+
has_many :order_items, :dependent => :restrict_with_exception, :class_name => 'Tienda::OrderItem', :as => :ordered_item
|
|
28
|
+
|
|
29
|
+
# Orders which have ordered this product
|
|
30
|
+
has_many :orders, :through => :order_items, :class_name => 'Tienda::Order'
|
|
31
|
+
|
|
32
|
+
# Stock level adjustments for this product
|
|
33
|
+
has_many :stock_level_adjustments, :dependent => :destroy, :class_name => 'Tienda::StockLevelAdjustment', :as => :item
|
|
34
|
+
|
|
35
|
+
# Validations
|
|
36
|
+
with_options :if => Proc.new { |p| p.parent.nil? } do |product|
|
|
37
|
+
product.validates :product_category_id, :presence => true
|
|
38
|
+
product.validates :description, :presence => true
|
|
39
|
+
product.validates :short_description, :presence => true
|
|
40
|
+
end
|
|
41
|
+
validates :name, :presence => true
|
|
42
|
+
validates :permalink, :presence => true, :uniqueness => true, :permalink => true
|
|
43
|
+
validates :sku, :presence => true
|
|
44
|
+
validates :weight, :numericality => true
|
|
45
|
+
validates :price, :numericality => true
|
|
46
|
+
validates :cost_price, :numericality => true, :allow_blank => true
|
|
47
|
+
|
|
48
|
+
# Before validation, set the permalink if we don't already have one
|
|
49
|
+
before_validation { self.permalink = self.name.parameterize if self.permalink.blank? && self.name.is_a?(String) }
|
|
50
|
+
|
|
51
|
+
# All active products
|
|
52
|
+
scope :active, -> { where(:active => true) }
|
|
53
|
+
|
|
54
|
+
# All featured products
|
|
55
|
+
scope :featured, -> {where(:featured => true)}
|
|
56
|
+
|
|
57
|
+
# All products ordered with default items first followed by name ascending
|
|
58
|
+
scope :ordered, -> {order(:default => :desc, :name => :asc)}
|
|
59
|
+
|
|
60
|
+
# Return the name of the product
|
|
61
|
+
#
|
|
62
|
+
# @return [String]
|
|
63
|
+
def full_name
|
|
64
|
+
self.parent ? "#{self.parent.name} (#{name})" : name
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Is this product orderable?
|
|
68
|
+
#
|
|
69
|
+
# @return [Boolean]
|
|
70
|
+
def orderable?
|
|
71
|
+
return false unless self.active?
|
|
72
|
+
return false if self.has_variants?
|
|
73
|
+
true
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The price for the product
|
|
77
|
+
#
|
|
78
|
+
# @return [BigDecimal]
|
|
79
|
+
def price
|
|
80
|
+
self.default_variant ? self.default_variant.price : read_attribute(:price)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Is this product currently in stock?
|
|
84
|
+
#
|
|
85
|
+
# @return [Boolean]
|
|
86
|
+
def in_stock?
|
|
87
|
+
self.default_variant ? self.default_variant.in_stock? : (stock_control? ? stock > 0 : true)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Return the total number of items currently in stock
|
|
91
|
+
#
|
|
92
|
+
# @return [Fixnum]
|
|
93
|
+
def stock
|
|
94
|
+
self.stock_level_adjustments.sum(:adjustment)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Search for products which include the given attributes and return an active record
|
|
98
|
+
# scope of these products. Chainable with other scopes and with_attributes methods.
|
|
99
|
+
# For example:
|
|
100
|
+
#
|
|
101
|
+
# Tienda::Product.active.with_attribute('Manufacturer', 'Apple').with_attribute('Model', ['Macbook', 'iPhone'])
|
|
102
|
+
#
|
|
103
|
+
# @return [Enumerable]
|
|
104
|
+
def self.with_attributes(key, values)
|
|
105
|
+
product_ids = Tienda::ProductAttribute.searchable.where(:key => key, :value => values).pluck(:product_id).uniq
|
|
106
|
+
where(:id => product_ids)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Imports products from a spreadsheet file
|
|
110
|
+
# Example:
|
|
111
|
+
#
|
|
112
|
+
# Tienda:Product.import("path/to/file.csv")
|
|
113
|
+
def self.import(file)
|
|
114
|
+
spreadsheet = open_spreadsheet(file)
|
|
115
|
+
spreadsheet.default_sheet = spreadsheet.sheets.first
|
|
116
|
+
header = spreadsheet.row(1)
|
|
117
|
+
(2..spreadsheet.last_row).each do |i|
|
|
118
|
+
row = Hash[[header, spreadsheet.row(i)].transpose]
|
|
119
|
+
|
|
120
|
+
# Don't import products where the name is blank
|
|
121
|
+
unless row["name"].nil?
|
|
122
|
+
if product = find_by(name: row["name"])
|
|
123
|
+
# Dont import products with the same name but update quantities if they're not the same
|
|
124
|
+
qty = row["qty"].to_i
|
|
125
|
+
if qty > 0 && qty != product.stock
|
|
126
|
+
product.stock_level_adjustments.create!(description: I18n.t('tienda.import'), adjustment: qty)
|
|
127
|
+
end
|
|
128
|
+
else
|
|
129
|
+
product = new
|
|
130
|
+
product.name = row["name"]
|
|
131
|
+
product.sku = row["sku"]
|
|
132
|
+
product.description = row["description"]
|
|
133
|
+
product.short_description = row["short_description"]
|
|
134
|
+
product.weight = row["weight"]
|
|
135
|
+
product.price = row["price"].nil? ? 0 : row["price"]
|
|
136
|
+
|
|
137
|
+
product.product_category_id = begin
|
|
138
|
+
if Tienda::ProductCategory.find_by(name: row["category_name"]).present?
|
|
139
|
+
# Find and set the category
|
|
140
|
+
Tienda::ProductCategory.find_by(name: row["category_name"]).id
|
|
141
|
+
else
|
|
142
|
+
# Create the category
|
|
143
|
+
Tienda::ProductCategory.create(name: row["category_name"]).id
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
product.save!
|
|
148
|
+
|
|
149
|
+
# Create quantities
|
|
150
|
+
qty = row["qty"].to_i
|
|
151
|
+
if qty > 0
|
|
152
|
+
product.stock_level_adjustments.create!(description: I18n.t('tienda.import'), adjustment: qty)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def self.open_spreadsheet(file)
|
|
160
|
+
case File.extname(file.original_filename)
|
|
161
|
+
when ".csv" then Roo::CSV.new(file.path)
|
|
162
|
+
when ".xls" then Roo::Excel.new(file.path)
|
|
163
|
+
when ".xlsx" then Roo::Excelx.new(file.path)
|
|
164
|
+
else raise I18n.t('tienda.imports.errors.unknown_format', filename: File.original_filename)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module Tienda
|
|
2
|
+
class ProductAttribute < ActiveRecord::Base
|
|
3
|
+
|
|
4
|
+
self.table_name = 'tienda_product_attributes'
|
|
5
|
+
|
|
6
|
+
# Validations
|
|
7
|
+
validates :key, :presence => true
|
|
8
|
+
|
|
9
|
+
# The associated product
|
|
10
|
+
#
|
|
11
|
+
# @return [Tienda::Product]
|
|
12
|
+
belongs_to :product, :class_name => 'Tienda::Product'
|
|
13
|
+
|
|
14
|
+
# All attributes which are searchable
|
|
15
|
+
scope :searchable, -> { where(:searchable => true) }
|
|
16
|
+
|
|
17
|
+
# All attributes which are public
|
|
18
|
+
scope :publicly_accessible, -> { where(:public => true) }
|
|
19
|
+
|
|
20
|
+
# Return the the available options as a hash
|
|
21
|
+
#
|
|
22
|
+
# @return [Hash]
|
|
23
|
+
def self.grouped_hash
|
|
24
|
+
all.group_by(&:key).inject(Hash.new) do |h, (key, attributes)|
|
|
25
|
+
h[key] = attributes.map(&:value).uniq
|
|
26
|
+
h
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Create/update attributes for a product based on the provided hash of
|
|
31
|
+
# keys & values.
|
|
32
|
+
#
|
|
33
|
+
# @param array [Array]
|
|
34
|
+
def self.update_from_array(array)
|
|
35
|
+
existing_keys = self.pluck(:key)
|
|
36
|
+
index = 0
|
|
37
|
+
array.each do |hash|
|
|
38
|
+
next if hash['key'].blank?
|
|
39
|
+
index += 1
|
|
40
|
+
params = hash.merge({
|
|
41
|
+
:searchable => hash['searchable'].to_s == '1',
|
|
42
|
+
:public => hash['public'].to_s == '1',
|
|
43
|
+
:position => index
|
|
44
|
+
})
|
|
45
|
+
if existing_attr = self.where(:key => hash['key']).first
|
|
46
|
+
if hash['value'].blank?
|
|
47
|
+
existing_attr.destroy
|
|
48
|
+
index -= 1
|
|
49
|
+
else
|
|
50
|
+
existing_attr.update_attributes(params)
|
|
51
|
+
end
|
|
52
|
+
else
|
|
53
|
+
attribute = self.create(params)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
self.where(:key => existing_keys - array.map { |h| h['key']}).delete_all
|
|
57
|
+
true
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.public
|
|
61
|
+
ActiveSupport::Deprecation.warn("The use of Tienda::ProductAttribute.public is deprecated. use Tienda::ProductAttribute.publicly_accessible.")
|
|
62
|
+
self.publicly_accessible
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Tienda
|
|
2
|
+
class ProductCategory < ActiveRecord::Base
|
|
3
|
+
|
|
4
|
+
self.table_name = 'tienda_product_categories'
|
|
5
|
+
|
|
6
|
+
# Categories have an image attachment
|
|
7
|
+
attachment :image
|
|
8
|
+
|
|
9
|
+
# All products within this category
|
|
10
|
+
has_many :products, :dependent => :restrict_with_exception, :class_name => 'Tienda::Product'
|
|
11
|
+
|
|
12
|
+
# Validations
|
|
13
|
+
validates :name, :presence => true
|
|
14
|
+
validates :permalink, :presence => true, :uniqueness => true, :permalink => true
|
|
15
|
+
|
|
16
|
+
# All categories ordered by their name ascending
|
|
17
|
+
scope :ordered, -> { order(:name) }
|
|
18
|
+
|
|
19
|
+
# Set the permalink on callback
|
|
20
|
+
before_validation { self.permalink = self.name.parameterize if self.permalink.blank? && self.name.is_a?(String) }
|
|
21
|
+
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
require 'ostruct'
|
|
2
|
+
|
|
3
|
+
module Tienda
|
|
4
|
+
class Setting < ActiveRecord::Base
|
|
5
|
+
|
|
6
|
+
# Validations
|
|
7
|
+
validates :key, :presence => true, :uniqueness => true
|
|
8
|
+
validates :value, :presence => true
|
|
9
|
+
validates :value_type, :presence => true
|
|
10
|
+
|
|
11
|
+
before_validation do
|
|
12
|
+
self.value_type = I18n.t("tienda.settings.types")[self.key.to_sym].try(:capitalize) || self.value.class.to_s
|
|
13
|
+
self.value = encoded_value
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# The encoded value for saving in the backend (as a string)
|
|
17
|
+
#
|
|
18
|
+
# @return [String]
|
|
19
|
+
def encoded_value
|
|
20
|
+
case value_type
|
|
21
|
+
when 'Array', 'Hash' then value.to_json
|
|
22
|
+
when 'Boolean' then value.to_s == 'true' ? 'true' : 'false'
|
|
23
|
+
else value.to_s
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The decoded value for the setting attribute (in it's native type)
|
|
28
|
+
#
|
|
29
|
+
# @return [Object]
|
|
30
|
+
def decoded_value
|
|
31
|
+
case value_type
|
|
32
|
+
when 'Fixnum' then value.to_i
|
|
33
|
+
when 'Float' then value.to_f
|
|
34
|
+
when 'Array', 'Hash' then JSON.parse(value)
|
|
35
|
+
when 'Boolean' then value == 'true' ? true : false
|
|
36
|
+
else value.to_s
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# A full hash of all settings available in the current scope
|
|
41
|
+
#
|
|
42
|
+
# @return [Hash]
|
|
43
|
+
def self.to_hash
|
|
44
|
+
all.inject({}) do |h, setting|
|
|
45
|
+
h[setting.key.to_s] = setting.decoded_value
|
|
46
|
+
h
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Update settings from a given hash and persist them. Accepts a
|
|
51
|
+
# hash of keys (which should be strings).
|
|
52
|
+
#
|
|
53
|
+
# @return [Hash]
|
|
54
|
+
def self.update_from_hash(hash)
|
|
55
|
+
existing_settings = self.all.to_a
|
|
56
|
+
hash.each do |key, value|
|
|
57
|
+
existing = existing_settings.select { |s| s.key.to_s == key.to_s }.first
|
|
58
|
+
if existing
|
|
59
|
+
value.blank? ? existing.destroy! : existing.update!(:value => value)
|
|
60
|
+
else
|
|
61
|
+
value.blank? ? nil : self.create!(:key => key, :value => value)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
hash
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Tienda
|
|
2
|
+
class StockLevelAdjustment < ActiveRecord::Base
|
|
3
|
+
|
|
4
|
+
# The orderable item which the stock level adjustment belongs to
|
|
5
|
+
belongs_to :item, :polymorphic => true
|
|
6
|
+
|
|
7
|
+
# The parent (OrderItem) which the stock level adjustment belongs to
|
|
8
|
+
belongs_to :parent, :polymorphic => true
|
|
9
|
+
|
|
10
|
+
# Validations
|
|
11
|
+
validates :description, :presence => true
|
|
12
|
+
validates :adjustment, :numericality => true
|
|
13
|
+
validate { errors.add(:adjustment, I18n.t('tienda.activerecord.attributes.stock_level_adjustment.must_be_greater_or_equal_zero')) if adjustment == 0 }
|
|
14
|
+
|
|
15
|
+
# All stock level adjustments ordered by their created date desending
|
|
16
|
+
scope :ordered, -> { order(:id => :desc) }
|
|
17
|
+
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Tienda
|
|
2
|
+
class TaxRate < ActiveRecord::Base
|
|
3
|
+
|
|
4
|
+
self.table_name = 'tienda_tax_rates'
|
|
5
|
+
|
|
6
|
+
include Tienda::AssociatedCountries
|
|
7
|
+
|
|
8
|
+
# The order address types which may be used when choosing how to apply the tax rate
|
|
9
|
+
ADDRESS_TYPES = ['billing', 'delivery']
|
|
10
|
+
|
|
11
|
+
# Validations
|
|
12
|
+
validates :name, :presence => true
|
|
13
|
+
validates :address_type, :inclusion => {:in => ADDRESS_TYPES}
|
|
14
|
+
validates :rate, :numericality => true
|
|
15
|
+
|
|
16
|
+
# All products which are assigned to this tax rate
|
|
17
|
+
has_many :products, :dependent => :restrict_with_exception, :class_name => 'Tienda::Product'
|
|
18
|
+
|
|
19
|
+
# All delivery service prices which are assigned to this tax rate
|
|
20
|
+
has_many :delivery_service_prices, :dependent => :restrict_with_exception, :class_name => 'Tienda::DeliveryServicePrice'
|
|
21
|
+
|
|
22
|
+
# All tax rates ordered by their ID
|
|
23
|
+
scope :ordered, -> { order(:id)}
|
|
24
|
+
|
|
25
|
+
# Set the address type if appropriate
|
|
26
|
+
before_validation { self.address_type = ADDRESS_TYPES.first if self.address_type.blank? }
|
|
27
|
+
|
|
28
|
+
# A description of the tax rate including its name & percentage
|
|
29
|
+
#
|
|
30
|
+
# @return [String]
|
|
31
|
+
def description
|
|
32
|
+
"#{name} (#{rate}%)"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The rate for a given order based on the rules on the tax rate
|
|
36
|
+
#
|
|
37
|
+
# @return [BigDecimal]
|
|
38
|
+
def rate_for(order)
|
|
39
|
+
return rate if countries.empty?
|
|
40
|
+
return rate if address_type == 'billing' && (order.billing_country.nil? || country?(order.billing_country))
|
|
41
|
+
return rate if address_type == 'delivery' && (order.delivery_country.nil? || country?(order.delivery_country))
|
|
42
|
+
BigDecimal(0)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module Tienda
|
|
2
|
+
class User < ActiveRecord::Base
|
|
3
|
+
|
|
4
|
+
self.table_name = 'tienda_users'
|
|
5
|
+
|
|
6
|
+
has_secure_password
|
|
7
|
+
|
|
8
|
+
# Validations
|
|
9
|
+
validates :first_name, :presence => true
|
|
10
|
+
validates :last_name, :presence => true
|
|
11
|
+
validates :email_address, :presence => true
|
|
12
|
+
|
|
13
|
+
# The user's first name & last name concatenated
|
|
14
|
+
#
|
|
15
|
+
# @return [String]
|
|
16
|
+
def full_name
|
|
17
|
+
"#{first_name} #{last_name}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# The user's first name & initial of last name concatenated
|
|
21
|
+
#
|
|
22
|
+
# @return [String]
|
|
23
|
+
def short_name
|
|
24
|
+
"#{first_name} #{last_name[0,1]}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Reset the user's password to something random and e-mail it to them
|
|
28
|
+
def reset_password!
|
|
29
|
+
self.password = SecureRandom.hex(8)
|
|
30
|
+
self.password_confirmation = self.password
|
|
31
|
+
self.save!
|
|
32
|
+
Tienda::UserMailer.new_password(self).deliver
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Attempt to authenticate a user based on email & password. Returns the
|
|
36
|
+
# user if successful otherwise returns false.
|
|
37
|
+
#
|
|
38
|
+
# @param email_address [String]
|
|
39
|
+
# @param paassword [String]
|
|
40
|
+
# @return [Tienda::User]
|
|
41
|
+
def self.authenticate(email_address, password)
|
|
42
|
+
user = self.where(:email_address => email_address).first
|
|
43
|
+
return false if user.nil?
|
|
44
|
+
return false unless user.authenticate(password)
|
|
45
|
+
user
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
!!!
|
|
2
|
+
%html
|
|
3
|
+
%head
|
|
4
|
+
%title #{@page_title} - Tienda
|
|
5
|
+
= stylesheet_link_tag 'tienda/application'
|
|
6
|
+
= javascript_include_tag 'tienda/application'
|
|
7
|
+
= csrf_meta_tags
|
|
8
|
+
:javascript
|
|
9
|
+
window.Tienda = {}
|
|
10
|
+
window.Tienda.currencyUnit = "#{h Tienda.settings.currency_unit}";
|
|
11
|
+
%body
|
|
12
|
+
%nav.sidebar
|
|
13
|
+
%header.logo
|
|
14
|
+
%h1= link_to "Tienda", root_path
|
|
15
|
+
%p v#{Tienda::VERSION}
|
|
16
|
+
%p.logged= t('.logged_in_as', user_name: current_user.full_name)
|
|
17
|
+
|
|
18
|
+
%ul
|
|
19
|
+
- for item in Tienda::NavigationManager.find(:admin_primary).items
|
|
20
|
+
%li= navigation_manager_link item
|
|
21
|
+
|
|
22
|
+
%li= link_to t('.logout'), [:logout], :method => :delete
|
|
23
|
+
|
|
24
|
+
%header.main
|
|
25
|
+
= yield :header
|
|
26
|
+
|
|
27
|
+
#content
|
|
28
|
+
= display_flash
|
|
29
|
+
%section.main
|
|
30
|
+
= yield
|
|
31
|
+
|
|
32
|
+
%footer
|
|
33
|
+
%p.store= link_to "← #{t('.goto')} #{Tienda.settings.store_name}".html_safe, '/'
|
|
34
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
= form_for @country do |f|
|
|
2
|
+
= f.error_messages
|
|
3
|
+
= field_set_tag t('tienda.countries.country_details') do
|
|
4
|
+
|
|
5
|
+
.splitContainer
|
|
6
|
+
%dl.third
|
|
7
|
+
%dt= f.label :name, t('tienda.countries.name')
|
|
8
|
+
%dd= f.text_field :name, :class => 'focus text'
|
|
9
|
+
%dl.third
|
|
10
|
+
%dt= f.label :code2, t('tienda.countries.iso_alpha_2')
|
|
11
|
+
%dd= f.text_field :code2, :class => 'text'
|
|
12
|
+
%dl.third
|
|
13
|
+
%dt= f.label :code3, t('tienda.countries.iso_alpha_3')
|
|
14
|
+
%dd= f.text_field :code3, :class => 'text'
|
|
15
|
+
|
|
16
|
+
.splitContainer
|
|
17
|
+
%dl.third
|
|
18
|
+
%dt= f.label :continent, t('tienda.countries.continent')
|
|
19
|
+
%dd= f.text_field :continent, :class => 'text'
|
|
20
|
+
%dl.third
|
|
21
|
+
%dt= f.label :tld, t('tienda.countries.tld')
|
|
22
|
+
%dd= f.text_field :tld, :class => 'text'
|
|
23
|
+
%dl.third
|
|
24
|
+
%dt= f.label :eu_member, t('tienda.countries.eu_member')
|
|
25
|
+
%dd.checkbox
|
|
26
|
+
= f.check_box :eu_member
|
|
27
|
+
= f.label :eu_member, t('tienda.countries.is_eu_member')
|
|
28
|
+
|
|
29
|
+
%p.submit
|
|
30
|
+
- unless @country.new_record?
|
|
31
|
+
%span.right
|
|
32
|
+
= link_to t('tienda.delete'), @country, :class => 'button purple',
|
|
33
|
+
:method => :delete, :data => {:confirm => t('tienda.countries.delete_confirmation')}
|
|
34
|
+
= f.submit t('tienda.submit'), :class => 'button green'
|
|
35
|
+
= link_to t('tienda.cancel'), :countries, :class => 'button'
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
- @page_title = t('tienda.countries.countries')
|
|
2
|
+
|
|
3
|
+
= content_for :header do
|
|
4
|
+
%p.buttons=link_to t('tienda.countries.new_country') , :new_country, :class => 'button green'
|
|
5
|
+
%h2.countries= t('tienda.countries.countries')
|
|
6
|
+
|
|
7
|
+
.table
|
|
8
|
+
%table.data
|
|
9
|
+
%thead
|
|
10
|
+
%tr
|
|
11
|
+
%th= t('tienda.countries.name')
|
|
12
|
+
%th= t('tienda.countries.iso_alpha_2')
|
|
13
|
+
%th= t('tienda.countries.iso_alpha_3')
|
|
14
|
+
%th= t('tienda.countries.continent')
|
|
15
|
+
%th= t('tienda.countries.tld')
|
|
16
|
+
%th= t('tienda.countries.eu?')
|
|
17
|
+
%tbody
|
|
18
|
+
- for country in @countries
|
|
19
|
+
%tr
|
|
20
|
+
%td= link_to country.name, [:edit, country]
|
|
21
|
+
%td= country.code2
|
|
22
|
+
%td= country.code3
|
|
23
|
+
%td= country.continent
|
|
24
|
+
%td= country.tld
|
|
25
|
+
%td= boolean_tag country.eu_member?
|