cirandas.net

ref: master

plugins/products/models/products_plugin/product.rb


  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
require 'products_plugin'

module ProductsPlugin
  class Product < ApplicationRecord

    ##
    # Keep compatibility with previous core name
    #
    def self.sti_name
      'Product'
    end

    self.table_name = :products

    SEARCHABLE_FIELDS = {
      name: {label: _('Name'), weight: 10},
      description: {label: _('Description'), weight: 1},
    }

    SEARCH_FILTERS = {
      order: %w[more_recent],
      display: %w[full map]
    }

    attr_accessible :name, :product_category, :product_category_id, :profile, :profile_id, :enterprise,
      :highlighted, :price, :image_builder, :description, :available, :qualifiers,
      :unit_id, :unit, :discount, :inputs, :qualifiers_list,
      :price_details


    def self.default_search_display
      'full'
    end

    belongs_to :profile
    # backwards compatibility
    belongs_to :enterprise, foreign_key: :profile_id, class_name: 'Enterprise'
    alias_method :enterprise=, :profile=
    alias_method :enterprise, :profile

    has_one :region, through: :profile
    validates_presence_of :profile

    # `class_name` is necessary for OrdersCyclePlugin::Cycle#product_categories
    belongs_to :product_category, class_name: 'ProductsPlugin::ProductCategory'

    has_many :inputs, -> { order 'position' }, dependent: :destroy
    has_many :price_details, dependent: :destroy
    has_many :production_costs, through: :price_details

    has_many :product_qualifiers, dependent: :destroy
    has_many :qualifiers, through: :product_qualifiers
    has_many :certifiers, through: :product_qualifiers

    validates_uniqueness_of :name, scope: :profile_id, allow_nil: true, if: :validate_uniqueness_of_column_name?

    validates_presence_of :product_category
    validates_associated :product_category

    extend CurrencyFields::ClassMethods
    has_currency :price
    has_currency :discount

    scope :alphabetically, -> { order 'products.name ASC' }

    scope :available, -> { where available: true }
    scope :unavailable, -> { where 'products.available <> true' }
    scope :archived, -> { where archived: true }
    scope :unarchived, -> { where 'products.archived <> true' }

    scope :with_available, -> (available) { where available: available }
    scope :with_price, -> { where 'products.price > 0' }

    extend ActsAsHavingSettings::ClassMethods
    acts_as_having_settings field: :data

    track_actions :create_product, :after_create, keep_params: [:name, :url ], if: -> a { a.notifiable? }, custom_user: :action_tracker_user
    track_actions :update_product, :before_update, keep_params: [:name, :url], if: -> a { a.notifiable? }, custom_user: :action_tracker_user
    track_actions :remove_product, :before_destroy, keep_params: [:name], if: -> a { a.notifiable? }, custom_user: :action_tracker_user

    # FIXME: transliterate input and name column
    scope :name_like, -> (name) { where "products.name ILIKE ?", "%#{name}%" }
    scope :with_product_category_id, -> (id) { where product_category_id: id }

    scope :by_profile, -> (profile) { where profile_id: profile.id }
    scope :by_profile_id, -> (profile_id) { where profile_id: profile_id }

    attr_accessible :external_id
    settings_items :external_id, type: String, default: nil

    validates_numericality_of :price, allow_nil: true
    validates_numericality_of :discount, allow_nil: true
    validate :valid_discount

    scope :enabled, -> { where 'profiles.enabled = ?', true }
    scope :visible, -> { where 'profiles.visible = ?', true }
    scope :is_public, -> { where 'profiles.visible = ? AND profiles.public_profile = ?', true, true }

    scope :more_recent, -> { order 'created_at DESC' }

    scope :from_category, -> category {
      joins(:product_category).where('categories.path LIKE ?', "%#{category.slug}%") if category
    }

    scope :visible_for_person, lambda { |person|
      joins('INNER JOIN "profiles" enterprises ON enterprises."id" = "products"."profile_id"')
        .joins('LEFT JOIN "role_assignments" ON ("role_assignments"."resource_id" = enterprises."id"
          AND "role_assignments"."resource_type" = \'Profile\') OR (
          "role_assignments"."resource_id" = enterprises."environment_id" AND
          "role_assignments"."resource_type" = \'Environment\' )')
        .joins('LEFT JOIN "roles" ON "role_assignments"."role_id" = "roles"."id"')
        .where(
      ['( (roles.key = ? OR roles.key = ?) AND role_assignments.accessor_type = \'Profile\' AND role_assignments.accessor_id = ? )
        OR
        ( ( ( role_assignments.accessor_type = \'Profile\' AND
              role_assignments.accessor_id = ? ) OR
            ( enterprises.public_profile = ? AND enterprises.enabled = ? ) ) AND
          ( enterprises.visible = ? ) )',
          'profile_admin', 'environment_administrator', person.id, person.id,
          true, true, true]
      ).uniq
    }

    scope :recent, -> limit=nil { order('id DESC').limit(limit) }

    after_update :save_image

    def self.product_categories_of products
      ProductCategory.find products.collect(&:product_category_id).compact.select{ |id| not id.zero? }
    end

    def lat
      self.profile.lat
    end
    def lng
      self.profile.lng
    end

    xss_terminate only: [ :name ], on: 'validation'
    xss_terminate only: [ :description ], with: 'white_list', on: 'validation'

    belongs_to :unit

    include WhiteListFilter
    filter_iframes :description

    def iframe_whitelist
      self.profile && self.profile.environment && self.profile.environment.trusted_sites_for_iframe
    end

    def name
      self[:name].blank? ? category_name : self[:name]
    end

    def name=(value)
      if (value == category_name)
        self[:name] = nil
      else
        self[:name] = value
      end
    end

    def name_is_blank?
      self[:name].blank?
    end

    extend ActsAsHavingImage::ClassMethods
    acts_as_having_image

    def default_image(size='thumb')
      image ? image.public_filename(size) : '/images/icons-app/product-default-pic-%s.png' % (size || 'big')
    end

    acts_as_having_image

    def save_image
      image.save if image
    end

    def category_name
      product_category ? product_category.name : _('Uncategorized product')
    end

    def url
      self.profile.public_profile_url.merge(controller: 'products_plugin/page', action: 'show', id: id)
    end

    def public?
      self.profile.public?
    end

    def formatted_value(method)
      value = self[method] || self.send(method)
      ("%.2f" % value).to_s.gsub('.', self.profile.environment.currency_separator) if value
    end

    def price_with_discount
      discount ? (price - discount) : price
    end

    def inputs_prices?
      return false if self.inputs.count <= 0
      self.inputs.each do |input|
        return false if input.has_price_details? == false
      end
      true
    end

    def any_inputs_details?
      return false if self.inputs.count <= 0
      self.inputs.each do |input|
        return true if input.has_all_price_details? == true
      end
      false
    end

    def has_basic_info?
      %w[unit price discount].each do |field|
        return true if !self.send(field).blank?
      end
      false
    end

    def qualifiers_list=(qualifiers)
      self.product_qualifiers.destroy_all
      qualifiers.each do |qualifier_id, certifier_id|
        next if qualifier_id == 'nil'
        product_qualifier = self.product_qualifiers.build
        product_qualifier.product = self
        product_qualifier.qualifier_id = qualifier_id
        product_qualifier.certifier_id = certifier_id
        product_qualifier.save!
      end
    end

    def order_inputs!(order = [])
      order.each_with_index do |input_id, array_index|
        input = self.inputs.find(input_id)
        input.position = array_index + 1
        input.save!
      end
    end

    def name_with_unit
      unit.blank? ? name : "#{name} - #{unit.name.downcase}"
    end

    def display_supplier_on_search?
      true
    end

    def inputs_cost
      return 0 if inputs.empty?
      inputs.relevant_to_price.map(&:cost).inject(0) { |sum,price| sum + price }
    end

    def total_production_cost
      return inputs_cost || 0 if price_details.empty?
      inputs_cost + price_details.map(&:price).inject(0){ |sum,price| sum + price }
    end

    def price_described?
      return false if price.blank? or price == 0
      (price - total_production_cost.to_f).zero?
    end

    def update_price_details(new_price_details)
      price_details.destroy_all
      new_price_details.each do |detail|
        price_details.create(detail)
      end
      reload # to remove temporary duplicated price_details
      price_details
    end

    def price_description_percentage
      return 0 if price.blank? || price.zero?
      total_production_cost * 100 / price
    end

    def available_production_costs
      self.profile.environment.production_costs + self.profile.production_costs
    end

    include Rails.application.routes.url_helpers
    def price_composition_bar_display_url
      url_for({host: self.profile.default_hostname, controller: 'products_plugin/page', action: 'display_price_composition_bar', profile: self.profile.identifier, id: self.id }.merge(Noosfero.url_options))
    end

    def inputs_cost_update_url
      url_for({host: self.profile.default_hostname, controller: 'products_plugin/page', action: 'display_inputs_cost', profile: self.profile.identifier, id: self.id }.merge(Noosfero.url_options))
    end

    def percentage_from_solidarity_economy
      se_i = t_i = 0
      self.inputs.each{ |i| t_i += 1; se_i += 1 if i.is_from_solidarity_economy }
      t_i = 1 if t_i == 0 # avoid division by 0
      p = case (se_i.to_f/t_i)*100
          when 0 then [0, '']
          when 0..24.999 then [0, _("0%")];
          when 25..49.999 then [25, _("25%")];
          when 50..74.999 then [50, _("50%")];
          when 75..99.999 then [75, _("75%")];
          when 100 then [100, _("100%")];
          end
      p
    end

    def self.products_by_supplier products
      products_by_supplier = {}
      products.each do |product|
        supplier = product.supplier.abbreviation_or_name
        products_by_supplier[supplier] ||= Set.new
        products_by_supplier[supplier] << product
      end

      products_by_supplier
    end

    delegate :enabled, :region, :region_id, :environment, :environment_id, to: :profile, allow_nil: true

    protected

    def validate_uniqueness_of_column_name?
      false
    end

    def notifiable?
      # shopping_cart create products without profile
      self.profile.present?
    end

    def is_trackable?
      # shopping_cart create products without profile
      self.profile.present?
    end

    def action_tracker_user
      self.profile
    end

    def valid_discount
      if discount && (price.blank? || discount.to_f > price.to_f)
        self.errors.add(:discount, _("should not be bigger than the price"))
      end
    end

  end
end