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

#convert_quantity, #fc_group_order_price, #fc_price, #get_unit_ratio_quantity, #gross_group_order_price, #gross_price, #group_order_price, #price_unit_price

Methods included from LocalizeInput

parse

Instance Attribute Details

#article_categoryArticleCategory

Returns Category this article is in.

Returns:



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

belongs_to :supplier

#article_versionsArray<ArticleVersion>

Returns Price history (current price first).

Returns:



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

has_many :article_versions, -> { order('created_at DESC') }

#availabilityBoolean

Returns Whether this article is available within the Foodcoop.

Returns:

  • (Boolean)

    Whether this article is available within the Foodcoop.



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

belongs_to :supplier

#depositNumber

Returns Deposit.

Returns:

  • (Number)

    Deposit

See Also:



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

belongs_to :supplier

#manufacturerString

Returns Original manufacturer.

Returns:

  • (String)

    Original manufacturer.



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

belongs_to :supplier

#nameString

Returns Article name.

Returns:

  • (String)

    Article name



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

belongs_to :supplier

#noteString

Returns Short line with optional extra article information.

Returns:

  • (String)

    Short line with optional extra article information.



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

belongs_to :supplier

#orderArray<Order>

Returns Orders this article appears in.

Returns:

  • (Array<Order>)

    Orders this article appears in.



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

has_many :orders, through: :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.


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

belongs_to :supplier

#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.



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

belongs_to :supplier

#priceNumber

Returns Net price.

Returns:

  • (Number)

    Net price

See Also:



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

belongs_to :supplier

#supplierSupplier

Returns Supplier this article belongs to.

Returns:

  • (Supplier)

    Supplier this article belongs to.



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

belongs_to :supplier

#taxNumber

Returns VAT percentage (10 is 10%).

Returns:

  • (Number)

    VAT percentage (10 is 10%).

See Also:



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

belongs_to :supplier

#unitString

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

Returns:

  • (String)

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



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

belongs_to :supplier

#unit_quantityNumber

Returns Number of units in wholesale package (box).

Returns:

  • (Number)

    Number of units in wholesale package (box).

See Also:



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

belongs_to :supplier

Class Method Details

.ransackable_associations(_auth_object = nil) ⇒ Object



99
100
101
102
# File 'app/models/article.rb', line 99

def self.ransackable_associations(_auth_object = nil)
  # TODO: - see https://github.com/foodcoopsat/foodsoft_hackathon/issues/92
  %w[article_category supplier order_articles orders]
end

.ransackable_attributes(_auth_object = nil) ⇒ Object



94
95
96
97
# File 'app/models/article.rb', line 94

def self.ransackable_attributes(_auth_object = nil)
  # TODO: - see https://github.com/foodcoopsat/foodsoft_hackathon/issues/92
  %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



258
259
260
# File 'app/models/article.rb', line 258

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_quantity] returns false if units aren’t foodsoft-compatible returns nil if units are eqal



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

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

#current_article_unitsObject



242
243
244
245
246
247
# File 'app/models/article.rb', line 242

def current_article_units
  [supplier_order_unit, group_order_unit, billing_unit, price_unit, article_unit_ratios.map(&:unit)]
    .flatten
    .uniq
    .compact
end

#deleted?Boolean

Returns:

  • (Boolean)


233
234
235
# File 'app/models/article.rb', line 233

def deleted?
  deleted_at.present?
end

#duplicate_including_latest_version_and_ratiosObject



249
250
251
252
253
# File 'app/models/article.rb', line 249

def duplicate_including_latest_version_and_ratios
  article = dup
  article.latest_article_version = latest_article_version.duplicate_including_article_unit_ratios
  article
end

#in_open_orderObject

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



110
111
112
113
114
115
116
# File 'app/models/article.rb', line 110

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_version.article_id == id }
    order_article ? order_article.order : nil
  end
end

#mark_as_deletedObject



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

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)


119
120
121
# File 'app/models/article.rb', line 119

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

#recently_updatedObject

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



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

def recently_updated
  latest_article_version.updated_at > 2.days.ago
end

#reload_article_on_version_changeObject (protected)



282
283
284
285
# File 'app/models/article.rb', line 282

def reload_article_on_version_change
  reload if @version_changed_before_save
  @version_changed_before_save = false
end

#shared_article(supplier = self.supplier) ⇒ Object

to get the correspondent shared article



124
125
126
127
128
129
130
131
# File 'app/models/article.rb', line 124

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

#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



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

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

  ret = ArticleVersion.compare_attributes(
    {
      name: [latest_article_version.name, new_article.name],
      manufacturer: [latest_article_version.manufacturer, new_article.manufacturer.to_s],
      origin: [latest_article_version.origin, new_article.origin],
      unit: [latest_article_version.unit, new_unit],
      supplier_order_unit: [latest_article_version.supplier_order_unit, new_article.supplier_order_unit],
      minimum_order_quantity: [latest_article_version.minimum_order_quantity, new_article.minimum_order_quantity],
      billing_unit: [latest_article_version.billing_unit || latest_article_version.supplier_order_unit,
                     new_article.billing_unit || new_article.supplier_order_unit],
      group_order_granularity: [latest_article_version.group_order_granularity, new_article.group_order_granularity],
      group_order_unit: [latest_article_version.group_order_unit, new_article.group_order_unit],
      price: [latest_article_version.price.to_f.round(2), new_price.to_f.round(2)],
      tax: [latest_article_version.tax, new_article.tax],
      deposit: [latest_article_version.deposit.to_f.round(2), new_article.deposit.to_f.round(2)],
      note: [latest_article_version.note.to_s, new_article.note.to_s]
    }
  )

  ratios_differ = latest_article_version.article_unit_ratios.length != new_article.article_unit_ratios.length ||
                  latest_article_version.article_unit_ratios.each_with_index.any? do |article_unit_ratio, index|
                    new_article.article_unit_ratios[index].unit != article_unit_ratio.unit ||
                      new_article.article_unit_ratios[index].quantity != article_unit_ratio.quantity
                  end

  if ratios_differ
    ratio_attribs = new_article.article_unit_ratios.map(&:attributes)
    ret[:article_unit_ratios_attributes] = ratio_attribs
  end

  if options[:convert_units] && latest_article_version.article_unit_ratios.length < 2 && new_article.article_unit_ratios.length < 2 && !new_unit_quantity.nil?
    ret[:article_unit_ratios_attributes] = [new_article.article_unit_ratios.build(unit: 'XPP', quantity: new_unit_quantity, sort: 1).attributes]
    # TODO: Either remove this aspect of the :convert_units feature or extend it to also work for the new units system (see https://github.com/foodcoopsat/foodsoft_hackathon/issues/90)
  end

  ret
end

#update_or_create_article_versionObject (protected)

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



263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# File 'app/models/article.rb', line 263

def update_or_create_article_version
  @version_changed_before_save = false
  return unless version_dup_required?

  old_version = latest_article_version
  new_version = old_version.duplicate_including_article_unit_ratios
  article_versions << new_version

  OrderArticle.belonging_to_open_order
              .joins(:article_version)
              .where(article_versions: { article_id: id })
              .update_all(article_version_id: new_version.id)

  # reload old version to avoid updating it too (would automatically happen after before_save):
  old_version.reload

  @version_changed_before_save = true
end

#version_dup_required?Boolean (protected)

Returns:

  • (Boolean)


287
288
289
290
291
292
# File 'app/models/article.rb', line 287

def version_dup_required?
  return false if latest_article_version.nil?
  return false unless latest_article_version.self_or_ratios_changed?

  OrderArticle.belonging_to_finished_order.exists?(article_version_id: latest_article_version.id)
end