Class: Article

Inherits:
ApplicationRecord show all
Includes:
LocalizeInput, 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

Methods included from LocalizeInput

parse

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

#orderArray<Order>

Returns Orders this article appears in.

Returns:

  • (Array<Order>)

    Orders this article appears in.



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

has_many :orders, through: :order_articles

#order_articlesArray<OrderArticle>

Returns Order articles for this article.

Returns:

  • (Array<OrderArticle>)

    Order articles for this article.



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

has_many :order_articles

#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



168
169
170
171
172
173
# File 'app/models/article.rb', line 168

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

.ransackable_associations(_auth_object = nil) ⇒ Object



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

def self.ransackable_associations(_auth_object = nil)
  %w[article_category supplier order_articles orders]
end

.ransackable_attributes(_auth_object = nil) ⇒ Object



81
82
83
# File 'app/models/article.rb', line 81

def self.ransackable_attributes(_auth_object = nil)
  %w[id name supplier_id article_category_id unit note manufacturer origin unit_quantity order_number]
end

Instance Method Details

#check_article_in_useObject (protected)

Checks if the article is in use before it will deleted



238
239
240
# File 'app/models/article.rb', line 238

def check_article_in_use
  raise I18n.t('articles.model.error_in_use', article: name.to_s) if 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



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

def convert_units(new_article = shared_article)
  return unless unit != new_article.unit
  return false if new_article.unit.include?(',')

  # 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 = begin
      ::Unit.new(unit)
    rescue StandardError
      nil
    end
    supplier_unit = begin
      ::Unit.new(new_article.unit)
    rescue StandardError
      nil
    end
    if fc_unit != 0 && supplier_unit != 0 && 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
end

#deleted?Boolean

Returns:

  • (Boolean)


226
227
228
# File 'app/models/article.rb', line 226

def deleted?
  deleted_at.present?
end

#in_open_orderObject

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



95
96
97
98
99
100
101
# File 'app/models/article.rb', line 95

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
  end
end

#mark_as_deletedObject



230
231
232
233
# File 'app/models/article.rb', line 230

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)


104
105
106
# File 'app/models/article.rb', line 104

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

#price_changed?Boolean (protected)

Returns:

  • (Boolean)


254
255
256
# File 'app/models/article.rb', line 254

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



90
91
92
# File 'app/models/article.rb', line 90

def recently_updated
  updated_at > 2.days.ago
end

#shared_article(supplier = self.supplier) ⇒ Object

to get the correspondent shared article



176
177
178
179
180
181
182
183
# File 'app/models/article.rb', line 176

def shared_article(supplier = self.supplier)
  order_number.blank? and return nil
  @shared_article ||= begin
    supplier.shared_supplier.find_article_by_number(order_number)
  rescue StandardError
    nil
  end
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)


112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'app/models/article.rb', line 112

def shared_article_changed?(supplier = self.supplier)
  # skip early if the timestamp hasn't changed
  shared_article = self.shared_article(supplier)
  return if shared_article.nil? || shared_updated_on == shared_article.updated_on

  attrs = unequal_attributes(shared_article)
  if attrs.empty?
    # when attributes not changed, update timestamp of article
    update_attribute(:shared_updated_on, shared_article.updated_on)
    false
  else
    attrs
  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



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

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

  Article.compare_attributes(
    {
      name: [name, new_article.name],
      manufacturer: [manufacturer, new_article.manufacturer.to_s],
      origin: [origin, new_article.origin],
      unit: [unit, new_unit],
      price: [price.to_f.round(2), new_price.to_f.round(2)],
      tax: [tax, new_article.tax],
      deposit: [deposit.to_f.round(2), new_article.deposit.to_f.round(2)],
      # take care of different num-objects.
      unit_quantity: [unit_quantity.to_s.to_f, new_unit_quantity.to_s.to_f],
      note: [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.



261
262
263
264
265
266
267
268
269
270
# File 'app/models/article.rb', line 261

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?
  elsif matches.where(unit: unit, unit_quantity: unit_quantity).any?
    errors.add :name, :taken_with_unit
  end
end

#update_price_historyObject (protected)

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



243
244
245
246
247
248
249
250
251
252
# File 'app/models/article.rb', line 243

def update_price_history
  return unless price_changed?

  article_prices.build(
    price: price,
    tax: tax,
    deposit: deposit,
    unit_quantity: unit_quantity
  )
end