prestashop 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +56 -0
- data/README.md +111 -0
- data/Rakefile +9 -0
- data/lib/prestashop.rb +21 -0
- data/lib/prestashop/api.rb +9 -0
- data/lib/prestashop/api/connection.rb +225 -0
- data/lib/prestashop/api/converter.rb +152 -0
- data/lib/prestashop/api/error.rb +18 -0
- data/lib/prestashop/api/refinement.rb +14 -0
- data/lib/prestashop/client.rb +19 -0
- data/lib/prestashop/client/cache.rb +53 -0
- data/lib/prestashop/client/error.rb +9 -0
- data/lib/prestashop/client/implementation.rb +33 -0
- data/lib/prestashop/mapper.rb +9 -0
- data/lib/prestashop/mapper/extension.rb +161 -0
- data/lib/prestashop/mapper/model.rb +38 -0
- data/lib/prestashop/mapper/models.rb +1 -0
- data/lib/prestashop/mapper/models/address.rb +9 -0
- data/lib/prestashop/mapper/models/carrier.rb +9 -0
- data/lib/prestashop/mapper/models/cart.rb +9 -0
- data/lib/prestashop/mapper/models/cart_rule.rb +9 -0
- data/lib/prestashop/mapper/models/category.rb +114 -0
- data/lib/prestashop/mapper/models/combination.rb +81 -0
- data/lib/prestashop/mapper/models/contact.rb +9 -0
- data/lib/prestashop/mapper/models/content_management_system.rb +9 -0
- data/lib/prestashop/mapper/models/country.rb +15 -0
- data/lib/prestashop/mapper/models/currency.rb +9 -0
- data/lib/prestashop/mapper/models/customer.rb +9 -0
- data/lib/prestashop/mapper/models/customer_message.rb +9 -0
- data/lib/prestashop/mapper/models/customer_thread.rb +9 -0
- data/lib/prestashop/mapper/models/delivery.rb +9 -0
- data/lib/prestashop/mapper/models/employee.rb +9 -0
- data/lib/prestashop/mapper/models/group.rb +9 -0
- data/lib/prestashop/mapper/models/guest.rb +9 -0
- data/lib/prestashop/mapper/models/image.rb +52 -0
- data/lib/prestashop/mapper/models/image_type.rb +9 -0
- data/lib/prestashop/mapper/models/language.rb +15 -0
- data/lib/prestashop/mapper/models/manufacturer.rb +79 -0
- data/lib/prestashop/mapper/models/order.rb +9 -0
- data/lib/prestashop/mapper/models/order_carrier.rb +9 -0
- data/lib/prestashop/mapper/models/order_detail.rb +9 -0
- data/lib/prestashop/mapper/models/order_discount.rb +9 -0
- data/lib/prestashop/mapper/models/order_history.rb +9 -0
- data/lib/prestashop/mapper/models/order_invoice.rb +9 -0
- data/lib/prestashop/mapper/models/order_payment.rb +9 -0
- data/lib/prestashop/mapper/models/order_state.rb +9 -0
- data/lib/prestashop/mapper/models/price_range.rb +9 -0
- data/lib/prestashop/mapper/models/product.rb +198 -0
- data/lib/prestashop/mapper/models/product_feature.rb +69 -0
- data/lib/prestashop/mapper/models/product_feature_value.rb +60 -0
- data/lib/prestashop/mapper/models/product_option.rb +72 -0
- data/lib/prestashop/mapper/models/product_option_value.rb +56 -0
- data/lib/prestashop/mapper/models/product_supplier.rb +31 -0
- data/lib/prestashop/mapper/models/search.rb +9 -0
- data/lib/prestashop/mapper/models/shop.rb +9 -0
- data/lib/prestashop/mapper/models/shop_group.rb +9 -0
- data/lib/prestashop/mapper/models/specific_price.rb +9 -0
- data/lib/prestashop/mapper/models/specific_price_rule.rb +9 -0
- data/lib/prestashop/mapper/models/state.rb +9 -0
- data/lib/prestashop/mapper/models/stock.rb +9 -0
- data/lib/prestashop/mapper/models/stock_available.rb +35 -0
- data/lib/prestashop/mapper/models/stock_movement.rb +9 -0
- data/lib/prestashop/mapper/models/stock_movement_reason.rb +9 -0
- data/lib/prestashop/mapper/models/store.rb +9 -0
- data/lib/prestashop/mapper/models/supplier.rb +45 -0
- data/lib/prestashop/mapper/models/supply_order.rb +9 -0
- data/lib/prestashop/mapper/models/supply_order_detail.rb +9 -0
- data/lib/prestashop/mapper/models/supply_order_history.rb +9 -0
- data/lib/prestashop/mapper/models/supply_order_receipt_history.rb +9 -0
- data/lib/prestashop/mapper/models/supply_order_state.rb +9 -0
- data/lib/prestashop/mapper/models/tag.rb +9 -0
- data/lib/prestashop/mapper/models/tax.rb +24 -0
- data/lib/prestashop/mapper/models/tax_rule.rb +15 -0
- data/lib/prestashop/mapper/models/tax_rule_group.rb +9 -0
- data/lib/prestashop/mapper/models/translated_configuration.rb +9 -0
- data/lib/prestashop/mapper/models/warehouse.rb +9 -0
- data/lib/prestashop/mapper/models/warehouse_product_location.rb +9 -0
- data/lib/prestashop/mapper/models/weight_range.rb +9 -0
- data/lib/prestashop/mapper/models/zone.rb +9 -0
- data/lib/prestashop/mapper/refinement.rb +43 -0
- data/lib/prestashop/version.rb +3 -0
- metadata +279 -0
@@ -0,0 +1,18 @@
|
|
1
|
+
module Prestashop
|
2
|
+
module Api
|
3
|
+
class InvalidCredentials < RuntimeError
|
4
|
+
def initialize
|
5
|
+
super "Your credentials are invalid"
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class RequestFailed < RuntimeError
|
10
|
+
attr_reader :response
|
11
|
+
def initialize response
|
12
|
+
@response = response
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class ParserError < RuntimeError; end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'prestashop/client/error'
|
2
|
+
require 'prestashop/client/implementation'
|
3
|
+
require 'prestashop/client/cache'
|
4
|
+
|
5
|
+
module Prestashop
|
6
|
+
module Client
|
7
|
+
extend SingleForwardable
|
8
|
+
def_delegators :current, :connection, :cache
|
9
|
+
def_delegators :connection, :create, :read, :update, :delete, :check, :upload
|
10
|
+
def_delegators :cache, :manufacturers_cache, :clear_manufacturers_cache, :categories_cache, :clear_categories_cache, :features_cache, :clear_features_cache,
|
11
|
+
:feature_values_cache, :clear_feature_values_cache, :options_cache, :clear_options_cache, :option_values_cache, :clear_option_values_cache
|
12
|
+
|
13
|
+
# Delegate to current user implementation
|
14
|
+
#
|
15
|
+
def self.current
|
16
|
+
Implementation.current
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Prestashop
|
2
|
+
module Client
|
3
|
+
class Cache
|
4
|
+
def manufacturers_cache
|
5
|
+
@manufacturers_cache ||= Mapper::Manufacturer.cache
|
6
|
+
end
|
7
|
+
|
8
|
+
def clear_manufacturers_cache
|
9
|
+
@manufacturers_cache = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def categories_cache
|
13
|
+
@categories_cache ||= Mapper::Category.cache
|
14
|
+
end
|
15
|
+
|
16
|
+
def clear_categories_cache
|
17
|
+
@categories_cache = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def features_cache
|
21
|
+
@features_cache ||= Mapper::ProductFeature.cache
|
22
|
+
end
|
23
|
+
|
24
|
+
def clear_features_cache
|
25
|
+
@features_cache = nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def feature_values_cache
|
29
|
+
@feature_values_cache ||= Mapper::ProductFeatureValue.cache
|
30
|
+
end
|
31
|
+
|
32
|
+
def clear_feature_values_cache
|
33
|
+
@feature_values_cache = nil
|
34
|
+
end
|
35
|
+
|
36
|
+
def options_cache
|
37
|
+
@options_cache ||= Mapper::ProductOption.cache
|
38
|
+
end
|
39
|
+
|
40
|
+
def clear_options_cache
|
41
|
+
@options_cache = nil
|
42
|
+
end
|
43
|
+
|
44
|
+
def option_values_cache
|
45
|
+
@option_values_cache ||= Mapper::ProductOptionValue.cache
|
46
|
+
end
|
47
|
+
|
48
|
+
def clear_option_values_cache
|
49
|
+
@option_values_cache = nil
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
module Prestashop
|
4
|
+
module Client
|
5
|
+
class Implementation
|
6
|
+
include Singleton
|
7
|
+
attr_reader :connection, :cache
|
8
|
+
|
9
|
+
# Initialize new client see +Api::Connection#new+
|
10
|
+
#
|
11
|
+
def initialize api_key, api_url
|
12
|
+
@connection = Api::Connection.new api_key, api_url
|
13
|
+
@cache = Cache.new
|
14
|
+
end
|
15
|
+
|
16
|
+
class << self
|
17
|
+
|
18
|
+
# Create new user implementation, keep it in current thread to allow multithearding, see +#new+
|
19
|
+
#
|
20
|
+
def create api_key, api_url
|
21
|
+
Thread.current[:prestashop_client] = new api_key, api_url
|
22
|
+
current
|
23
|
+
end
|
24
|
+
|
25
|
+
# Get current client or raise exception, when client isn't initialized
|
26
|
+
#
|
27
|
+
def current
|
28
|
+
Thread.current[:prestashop_client] ? Thread.current[:prestashop_client] : raise(UnitializedClient)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
module Prestashop
|
2
|
+
module Mapper
|
3
|
+
module Extension
|
4
|
+
module ClassMethods
|
5
|
+
|
6
|
+
# Determinate if model with class resource exists with given id
|
7
|
+
#
|
8
|
+
# Car.exists?(1) # => true # if given car exist
|
9
|
+
# Car.exists?(2) # => false # if given car don't exist
|
10
|
+
#
|
11
|
+
def exists? id
|
12
|
+
Client.check self.resource, id
|
13
|
+
end
|
14
|
+
|
15
|
+
# Find model by class resource and given id, returns hash
|
16
|
+
# with all nodes, based on node name as key, node value as value
|
17
|
+
#
|
18
|
+
# Car.find(1) # => { id: 1, name: 'BMW' }
|
19
|
+
# Car.find(2) # => nil
|
20
|
+
#
|
21
|
+
def find id
|
22
|
+
result = Client.read self.resource, id
|
23
|
+
result ? result[self.model] : nil
|
24
|
+
end
|
25
|
+
|
26
|
+
# Find model by class resource and params in hash
|
27
|
+
# Returns first result, see #where for more informations
|
28
|
+
#
|
29
|
+
# Car.find_by(name: 'BMW') # => 1
|
30
|
+
#
|
31
|
+
def find_by options = {}
|
32
|
+
results = where(options)
|
33
|
+
results ? results.first : nil
|
34
|
+
end
|
35
|
+
|
36
|
+
# Get models all results by class resource, you can specifi what
|
37
|
+
# you should to see as result by specifiyng +:display+
|
38
|
+
#
|
39
|
+
# Car.all # => [1,2,3]
|
40
|
+
# Car.all(display: ['name']) # => [{ name: { language: { attr: { id: 2, href: 'http://localhost.com/api/languages/2'}, val: 'BMW 7'} }]
|
41
|
+
#
|
42
|
+
def all options = {}
|
43
|
+
result = if options[:display]
|
44
|
+
Client.read self.resource, nil, display: options[:display]
|
45
|
+
else
|
46
|
+
Client.read self.resource
|
47
|
+
end
|
48
|
+
handle_result result, options
|
49
|
+
end
|
50
|
+
|
51
|
+
# Get results by class resource and given conditionals
|
52
|
+
#
|
53
|
+
# Car.where('filter[id_supplier' => 1) # => [1, 2]
|
54
|
+
#
|
55
|
+
def where options = {}
|
56
|
+
result = Client.read self.resource, nil, options
|
57
|
+
handle_result result, options
|
58
|
+
end
|
59
|
+
|
60
|
+
# Destroy model by class resource and given id
|
61
|
+
#
|
62
|
+
# Car.destroy(1) # => true
|
63
|
+
#
|
64
|
+
def destroy id
|
65
|
+
Client.delete self.resource, id
|
66
|
+
end
|
67
|
+
|
68
|
+
# Create hash suitable for update, contains #fixed_hash as hash with deleted
|
69
|
+
# keys, which shouldn't be in payload, if exist
|
70
|
+
#
|
71
|
+
# Car.update_hash(1, name: 'BMW7') # => {name: 'BMW7', manufacturer: 'BMW'}
|
72
|
+
#
|
73
|
+
def update_hash id, options = {}
|
74
|
+
original = defined?(fixed_hash(nil)) ? fixed_hash(id) : find(id)
|
75
|
+
original.merge(options)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Create payload for update, converts hash to XML
|
79
|
+
#
|
80
|
+
# Car.update_payload(1, name: 'BMW 7') # => <prestashop xmlns:xlink="http://www.w3.org/1999/xlink"><car><name><![CDATA[BMW 7]]></name></car></prestashop>
|
81
|
+
#
|
82
|
+
def update_payload id, options = {}
|
83
|
+
Api::Converter.build(self.resource, self.model, update_hash(id, options))
|
84
|
+
end
|
85
|
+
|
86
|
+
# Update model, with class resource by +id+ and given updates
|
87
|
+
#
|
88
|
+
# Car.update(1, name: 'BMW 7') # => {id: 1, name: 'BMW 7'}
|
89
|
+
#
|
90
|
+
def update id, options = {}
|
91
|
+
result = Client.update self.resource, id, update_payload(id, options)
|
92
|
+
result ? result[self.model] : nil
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
# Handle result to return +id+ or array with +ids+ of requested objects
|
97
|
+
#
|
98
|
+
# handle_result({ customers: { customer: [ 1,2 ] } }) # => [1, 2]
|
99
|
+
# handle_result({ customers: { customer: { attr: { id: 1 }} } }) # => [1]
|
100
|
+
#
|
101
|
+
def handle_result result, options = {}
|
102
|
+
if options[:display]
|
103
|
+
if result[self.resource].kind_of?(Hash) and result[self.resource][self.model]
|
104
|
+
objects = result[self.resource][self.model]
|
105
|
+
objects.kind_of?(Array) ? objects : [objects]
|
106
|
+
end
|
107
|
+
else
|
108
|
+
if result[self.resource].kind_of?(Hash) and result[self.resource][self.model]
|
109
|
+
[objects = result[self.resource][self.model]]
|
110
|
+
objects.kind_of?(Array) ? objects.map{ |o| o[:attr][:id] } : [ objects[:attr][:id] ]
|
111
|
+
else
|
112
|
+
nil
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
module InstanceMethods
|
119
|
+
|
120
|
+
# Generate hash with ID
|
121
|
+
#
|
122
|
+
# car.hash_id(1) # => {id: 1}
|
123
|
+
#
|
124
|
+
def hash_id id
|
125
|
+
{ id: id } if id
|
126
|
+
end
|
127
|
+
|
128
|
+
# Make array of unique IDs in hash
|
129
|
+
#
|
130
|
+
# car.hash_ids(1,2,3) # => [{id: 1},{id: 2},{id: 3}]
|
131
|
+
#
|
132
|
+
def hash_ids ids
|
133
|
+
ids.flatten.uniq.map{|id| hash_id(id)} if ids
|
134
|
+
end
|
135
|
+
|
136
|
+
# Create payload for create new object, coverts hash to XML
|
137
|
+
#
|
138
|
+
# car.payload # => '<prestashop xmlns:xlink="http://www.w3.org/1999/xlink"><car><name><![CDATA[BMW 7]]></name><manufacturer><![CDATA[BMW]]></manufacturer></car></prestashop>'
|
139
|
+
#
|
140
|
+
def payload
|
141
|
+
Api::Converter.build(self.class.resource, self.class.model, hash)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Create new model from instance, based on class resource a payload generated from
|
145
|
+
# hash method
|
146
|
+
#
|
147
|
+
# Car.new(name: 'BMW 7', manufacturer: 'BMW').create # => { id: 1, name: 'BMW 7', manufacturer: 'BMW' }
|
148
|
+
#
|
149
|
+
def create
|
150
|
+
result = Client.create self.class.resource, payload
|
151
|
+
result ? result[self.class.model] : nil
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def self.included(receiver)
|
156
|
+
receiver.extend ClassMethods
|
157
|
+
receiver.send :include, InstanceMethods
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
using Prestashop::Mapper::Refinement
|
2
|
+
module Prestashop
|
3
|
+
module Mapper
|
4
|
+
class Model
|
5
|
+
include Extension
|
6
|
+
extend Extension
|
7
|
+
|
8
|
+
# Meta title is same as name, when is not given
|
9
|
+
def meta_title
|
10
|
+
@meta_title ? @meta_title.plain.truncate(61) : name
|
11
|
+
end
|
12
|
+
|
13
|
+
# Meta description is same as description, when is not given
|
14
|
+
def meta_description
|
15
|
+
@meta_description ? @meta_description.restricted.truncate(252) : ( description_short.plain if description_short )
|
16
|
+
end
|
17
|
+
|
18
|
+
# Meta keywords are generated from name, when are not given
|
19
|
+
def meta_keywords
|
20
|
+
@meta_keywords ? @meta_keywords.plain.truncate(61) : name.split(' ').join(', ')
|
21
|
+
end
|
22
|
+
|
23
|
+
def hash_lang name, id_lang
|
24
|
+
{ language: { val: name, attr: { id: id_lang }}} if name
|
25
|
+
end
|
26
|
+
|
27
|
+
class << self
|
28
|
+
def resource value = nil
|
29
|
+
value.nil? ? @resource : @resource = value
|
30
|
+
end
|
31
|
+
|
32
|
+
def model value = nil
|
33
|
+
value.nil? ? @model : @model = value
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
Dir[File.dirname(__FILE__) + '/models/*.rb'].each {|file| require file }
|
@@ -0,0 +1,114 @@
|
|
1
|
+
using Prestashop::Mapper::Refinement
|
2
|
+
module Prestashop
|
3
|
+
module Mapper
|
4
|
+
class Category < Model
|
5
|
+
resource :categories
|
6
|
+
model :category
|
7
|
+
|
8
|
+
attr_accessor :id_lang
|
9
|
+
attr_accessor :id, :id_parent, :level_depth, :active, :id_shop_default, :is_root_category, :position
|
10
|
+
attr_writer :name, :description, :link_rewrite
|
11
|
+
|
12
|
+
def initialize args = {}
|
13
|
+
@id = args[:id]
|
14
|
+
@id_parent = args.fetch(:id_parent, 2)
|
15
|
+
@level_depth = args[:level_depth]
|
16
|
+
# nb_products_recursive
|
17
|
+
@active = args.fetch(:active, 1)
|
18
|
+
@id_shop_default = args.fetch(:id_shop_default, 1)
|
19
|
+
@is_root_category = 0
|
20
|
+
@position = args[:position]
|
21
|
+
# date_add
|
22
|
+
# date_upd
|
23
|
+
@name = args.fetch(:name)
|
24
|
+
@link_rewrite = args[:link_rewrite]
|
25
|
+
@description = args[:description]
|
26
|
+
@meta_title = args[:meta_title]
|
27
|
+
@meta_description = args[:meta_description]
|
28
|
+
@meta_keywords = args[:meta_keywords]
|
29
|
+
|
30
|
+
@id_lang = args.fetch(:id_lang)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Category name can't have some symbols and can't be longer than 63
|
34
|
+
def name
|
35
|
+
@name.plain.truncate(61)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Description can have additional symbols and can't be longer than 255
|
39
|
+
def description
|
40
|
+
@description.restricted.truncate(252) if @description
|
41
|
+
end
|
42
|
+
|
43
|
+
# Link rewrite must be usable in uri
|
44
|
+
def link_rewrite
|
45
|
+
@link_rewrite ? @link_rewrite.parameterize : name.parameterize
|
46
|
+
end
|
47
|
+
|
48
|
+
# Category hash structure, which will be converted to XML
|
49
|
+
def hash
|
50
|
+
{ id_parent: id_parent,
|
51
|
+
active: active ,
|
52
|
+
id_shop_default: id_shop_default,
|
53
|
+
is_root_category: is_root_category,
|
54
|
+
name: hash_lang(name, id_lang),
|
55
|
+
link_rewrite: hash_lang(link_rewrite, id_lang),
|
56
|
+
description: hash_lang(description, id_lang),
|
57
|
+
meta_title: hash_lang(name, id_lang),
|
58
|
+
meta_description: hash_lang(description, id_lang),
|
59
|
+
meta_keywords: hash_lang(meta_keywords, id_lang) }
|
60
|
+
end
|
61
|
+
|
62
|
+
# Find category by name and id parent, create new one from hash, when doesn't exist
|
63
|
+
def find_or_create
|
64
|
+
category = self.class.find_in_cache id_parent, name, id_lang
|
65
|
+
unless category
|
66
|
+
category = create
|
67
|
+
Client.clear_categories_cache
|
68
|
+
end
|
69
|
+
category[:id]
|
70
|
+
end
|
71
|
+
|
72
|
+
class << self
|
73
|
+
|
74
|
+
# Search for category based on args on cached categories, see #cache and #Client::Settings.categories_cache
|
75
|
+
# Returns founded category or nil
|
76
|
+
#
|
77
|
+
def find_in_cache id_parent, name, id_lang
|
78
|
+
Client.categories_cache.find{ |c| c[:id_parent] == id_parent and c[:name].find_lang(name, id_lang) }
|
79
|
+
end
|
80
|
+
|
81
|
+
# Requesting all on Prestashop API, displaying id, id_parent, name
|
82
|
+
def cache
|
83
|
+
all display: '[id, id_parent, name]'
|
84
|
+
end
|
85
|
+
|
86
|
+
# Create new category based on given param, delimited by delimiter in settings
|
87
|
+
#
|
88
|
+
# ==== Example:
|
89
|
+
# Category.create_from_name('Apple||iPhone', 2) # => [1, 2]
|
90
|
+
#
|
91
|
+
def create_from_name category_name, id_lang
|
92
|
+
if category_name and !category_name.empty?
|
93
|
+
names = [category_name.split('||')].flatten!
|
94
|
+
categories = []
|
95
|
+
id_parent = 2
|
96
|
+
names.each do |name|
|
97
|
+
id_parent = new(name: name, id_parent: id_parent, id_lang: id_lang).find_or_create
|
98
|
+
categories << id_parent
|
99
|
+
end
|
100
|
+
categories
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def create_from_names category_names, id_lang
|
105
|
+
categories = []
|
106
|
+
category_names.each do |category_name|
|
107
|
+
categories << create_from_name(category_name, id_lang)
|
108
|
+
end
|
109
|
+
categories.flatten.uniq
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|