Class: Article

Inherits:
ActiveRecord::Base
  • Object
show all
Includes:
PriceCalculation
Defined in:
app/models/article.rb

Direct Known Subclasses

StockArticle

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from PriceCalculation

#fc_price, #gross_price

Instance Attribute Details

#article_categoryArticleCategory

Returns Category this article is in.

Returns:


39
# File 'app/models/article.rb', line 39

belongs_to :article_category

#article_pricesArray<ArticlePrice>

Returns Price history (current price first).

Returns:

  • (Array<ArticlePrice>)

    Price history (current price first).


45
# File 'app/models/article.rb', line 45

has_many :article_prices, -> { order("created_at DESC") }

#availabilityBoolean

Returns Whether this article is available within the Foodcoop.

Returns:

  • (Boolean)

    Whether this article is available within the Foodcoop.


39
# File 'app/models/article.rb', line 39

belongs_to :article_category

#depositNumber

Returns Deposit

Returns:

  • (Number)

    Deposit

See Also:


39
# File 'app/models/article.rb', line 39

belongs_to :article_category

#manufacturerString

Returns Original manufacturer.

Returns:

  • (String)

    Original manufacturer.


39
# File 'app/models/article.rb', line 39

belongs_to :article_category

#nameString

Returns Article name

Returns:

  • (String)

    Article name


39
# File 'app/models/article.rb', line 39

belongs_to :article_category

#noteString

Returns Short line with optional extra article information.

Returns:

  • (String)

    Short line with optional extra article information.


39
# File 'app/models/article.rb', line 39

belongs_to :article_category

#order_numberObject

Order number, this can be used by the supplier to identify articles. This is required when using the shared database functionality.

@return [String] Order number.

39
# File 'app/models/article.rb', line 39

belongs_to :article_category

#originString

Where the article was produced. ISO 3166-1 2-letter country code, optionally prefixed with region. E.g. NL or Sicily, IT or Berlin, DE.


39
# File 'app/models/article.rb', line 39

belongs_to :article_category

#priceNumber

Returns Net price

Returns:

  • (Number)

    Net price

See Also:


39
# File 'app/models/article.rb', line 39

belongs_to :article_category

#supplierSupplier

Returns Supplier this article belongs to.

Returns:

  • (Supplier)

    Supplier this article belongs to.


42
# File 'app/models/article.rb', line 42

belongs_to :supplier

#taxNumber

Returns VAT percentage (10 is 10%).

Returns:

  • (Number)

    VAT percentage (10 is 10%).

See Also:


39
# File 'app/models/article.rb', line 39

belongs_to :article_category

#unitString

Returns Unit, e.g. kg, 2 L or 5 pieces.

Returns:

  • (String)

    Unit, e.g. kg, 2 L or 5 pieces.


39
# File 'app/models/article.rb', line 39

belongs_to :article_category

#unit_quantityNumber

Returns Number of units in wholesale package (box).

Returns:

  • (Number)

    Number of units in wholesale package (box).

See Also:


39
# File 'app/models/article.rb', line 39

belongs_to :article_category

Class Method Details

.compare_attributes(attributes) ⇒ Hash<Symbol, Object>

Compare attributes from two different articles.

This is used for auto-synchronization

Parameters:

  • attributes (Hash<Symbol, Array>)

    Attributes with old and new values

Returns:

  • (Hash<Symbol, Object>)

    Changed attributes with new values


149
150
151
152
# File 'app/models/article.rb', line 149

def self.compare_attributes(attributes)
  unequal_attributes = attributes.select { |name, values| values[0] != values[1] && !(values[0].blank? && values[1].blank?) }
  Hash[unequal_attributes.to_a.map {|a| [a[0], a[1].last]}]
end

Instance Method Details

#check_article_in_useObject (protected)

Checks if the article is in use before it will deleted


206
207
208
# File 'app/models/article.rb', line 206

def check_article_in_use
  raise I18n.t('articles.model.error_in_use', :article => self.name.to_s) if self.in_open_order
end

#convert_units(new_article = shared_article) ⇒ Object

convert units in foodcoop-size uses unit factors in app_config.yml to calc the price/unit_quantity returns new price and unit_quantity in array, when calc is possible => [price, unit_quanity] returns false if units aren't foodsoft-compatible returns nil if units are eqal


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
# File 'app/models/article.rb', line 165

def convert_units(new_article = shared_article)
  if unit != new_article.unit
    # legacy, used by foodcoops in Germany
    if new_article.unit == "KI" && unit == "ST" # 'KI' means a box, with a different amount of items in it
      # try to match the size out of its name, e.g. "banana 10-12 St" => 10
      new_unit_quantity = /[0-9\-\s]+(St)/.match(new_article.name).to_s.to_i
      if new_unit_quantity && new_unit_quantity > 0
        new_price = (new_article.price/new_unit_quantity.to_f).round(2)
        [new_price, new_unit_quantity]
      else
        false
      end
    else # use ruby-units to convert
      fc_unit = (::Unit.new(unit) rescue nil)
      supplier_unit = (::Unit.new(new_article.unit) rescue nil)
      if fc_unit && supplier_unit && fc_unit =~ supplier_unit
        conversion_factor = (supplier_unit / fc_unit).to_base.to_r
        new_price = new_article.price / conversion_factor
        new_unit_quantity = new_article.unit_quantity * conversion_factor
        [new_price, new_unit_quantity]
      else
        false
      end
    end
  else
    nil
  end
end

#deleted?Boolean

Returns:

  • (Boolean)

194
195
196
# File 'app/models/article.rb', line 194

def deleted?
  deleted_at.present?
end

#in_open_orderObject

If the article is used in an open Order, the Order will be returned.


77
78
79
80
81
82
83
# File 'app/models/article.rb', line 77

def in_open_order
  @in_open_order ||= begin
    order_articles = OrderArticle.where(order_id: Order.open.collect(&:id))
    order_article = order_articles.detect {|oa| oa.article_id == id }
    order_article ? order_article.order : nil
  end
end

#mark_as_deletedObject


198
199
200
201
# File 'app/models/article.rb', line 198

def mark_as_deleted
  check_article_in_use
  update_column :deleted_at, Time.now
end

#ordered_in_order?(order) ⇒ Boolean

Returns true if the article has been ordered in the given order at least once

Returns:

  • (Boolean)

86
87
88
# File 'app/models/article.rb', line 86

def ordered_in_order?(order)
  order.order_articles.where(article_id: id).where('quantity > 0').one?
end

#price_changed?Boolean (protected)

Returns:

  • (Boolean)

222
223
224
# File 'app/models/article.rb', line 222

def price_changed?
  changed.detect { |attr| attr == 'price' || 'tax' || 'deposit' || 'unit_quantity' } ? true : false
end

#recently_updatedObject

Returns true if article has been updated at least 2 days ago


72
73
74
# File 'app/models/article.rb', line 72

def recently_updated
  updated_at > 2.days.ago
end

#shared_article(supplier = self.supplier) ⇒ Object

to get the correspondent shared article


155
156
157
158
# File 'app/models/article.rb', line 155

def shared_article(supplier = self.supplier)
  self.order_number.blank? and return nil
  @shared_article ||= supplier.shared_supplier.shared_articles.find_by_number(self.order_number) rescue nil
end

#shared_article_changed?(supplier = self.supplier) ⇒ Boolean

this method checks, if the shared_article has been changed unequal attributes will returned in array if only the timestamps differ and the attributes are equal, false will returned and self.shared_updated_on will be updated

Returns:

  • (Boolean)

94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'app/models/article.rb', line 94

def shared_article_changed?(supplier = self.supplier)
  # skip early if the timestamp hasn't changed
  shared_article = self.shared_article(supplier)
  unless shared_article.nil? || self.shared_updated_on == shared_article.updated_on
    attrs = unequal_attributes(shared_article)
    if attrs.empty?
      # when attributes not changed, update timestamp of article
      self.update_attribute(:shared_updated_on, shared_article.updated_on)
      false
    else
      attrs
    end
  end
end

#unequal_attributes(new_article, options = {}) ⇒ Hash<Symbol, Object>

Return article attributes that were changed (incl. unit conversion)

Parameters:

  • new_article (Article)

    New article to update self

  • options (Hash) (defaults to: {})

    a customizable set of options

Options Hash (options):

  • :convert_units (Boolean)

    Omit or set to true to keep current unit and recompute unit quantity and price.

Returns:

  • (Hash<Symbol, Object>)

    Attributes with new values


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
# File 'app/models/article.rb', line 113

def unequal_attributes(new_article, options={})
  # try to convert different units when desired
  if options[:convert_units] == false
    new_price, new_unit_quantity = nil, nil
  else
    new_price, new_unit_quantity = convert_units(new_article)
  end
  if new_price && new_unit_quantity
    new_unit = self.unit
  else
    new_price = new_article.price
    new_unit_quantity = new_article.unit_quantity
    new_unit = new_article.unit
  end

  return Article.compare_attributes(
    {
      :name => [self.name, new_article.name],
      :manufacturer => [self.manufacturer, new_article.manufacturer.to_s],
      :origin => [self.origin, new_article.origin],
      :unit => [self.unit, new_unit],
      :price => [self.price.to_f.round(2), new_price.to_f.round(2)],
      :tax => [self.tax, new_article.tax],
      :deposit => [self.deposit.to_f.round(2), new_article.deposit.to_f.round(2)],
      # take care of different num-objects.
      :unit_quantity => [self.unit_quantity.to_s.to_f, new_unit_quantity.to_s.to_f],
      :note => [self.note.to_s, new_article.note.to_s]
    }
  )
end

#uniqueness_of_nameObject (protected)

We used have the name unique per supplier+deleted_at+type. With the addition of shared_sync_method all, this came in the way, and we now allow duplicate names for the 'all' methods - expecting foodcoops to make their own choice among products with different units by making articles available/unavailable.


229
230
231
232
233
234
235
236
237
238
# File 'app/models/article.rb', line 229

def uniqueness_of_name
  matches = Article.where(name: name, supplier_id: supplier_id, deleted_at: deleted_at, type: type)
  matches = matches.where.not(id: id) unless new_record?
  # supplier should always be there - except, perhaps, on initialization (on seeding)
  if supplier && (supplier.shared_sync_method.blank? || supplier.shared_sync_method == 'import')
    errors.add :name, :taken if matches.any?
  else
    errors.add :name, :taken_with_unit if matches.where(unit: unit, unit_quantity: unit_quantity).any?
  end
end

#update_price_historyObject (protected)

Create an ArticlePrice, when the price-attr are changed.


211
212
213
214
215
216
217
218
219
220
# File 'app/models/article.rb', line 211

def update_price_history
  if price_changed?
    article_prices.build(
      :price => price,
      :tax => tax,
      :deposit => deposit,
      :unit_quantity => unit_quantity
    )
  end
end