Class: ShipmentReceipt

Inherits:
ApplicationRecord show all
Includes:
Models::Auditable
Defined in:
app/models/shipment_receipt.rb

Overview

== Schema Information

Table name: shipment_receipts
Database name: primary

id :integer not null, primary key
currency :string(255)
effective_date :date
estimated_landed_cost :decimal(8, 2)
landed_cost :decimal(8, 2)
location :string(255)
serial_number_state :string
state :string(255)
created_at :datetime
updated_at :datetime
creator_id :integer
purchase_order_shipment_id :integer
updater_id :integer

Indexes

index_on_pos_id (purchase_order_shipment_id)

Constant Summary

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Constants included from Models::Schedulable

Models::Schedulable::SIMPLE_FORM_OPTIONS

Instance Attribute Summary collapse

Belongs to collapse

Methods included from Models::Auditable

#creator, #updater

Has many collapse

Instance Method Summary collapse

Methods included from Models::Auditable

#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Models::Schedulable

config

Methods included from Models::AfterCommittable

#after_commit

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#effective_dateObject (readonly)

Validates presence of the PO shipment, receipt date, and receiving location.

Validations:



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

validates :purchase_order_shipment_id, :effective_date, :location, presence: true

#locationObject (readonly)

Validates presence of the PO shipment, receipt date, and receiving location.

Validations:



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

validates :purchase_order_shipment_id, :effective_date, :location, presence: true

#purchase_order_shipment_idObject (readonly)

Validates presence of the PO shipment, receipt date, and receiving location.

Validations:



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

validates :purchase_order_shipment_id, :effective_date, :location, presence: true

Instance Method Details

#all_items_in_stock?Boolean

Whether every received item is in stock at its location.

Returns:

  • (Boolean)


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

def all_items_in_stock?
  shipment_receipt_items.all?(&:has_stock?)
end

#all_serial_numbers_provided?Boolean

Whether every serialized item has serial numbers covering its full quantity.

Returns:

  • (Boolean)


101
102
103
104
105
106
107
108
109
# File 'app/models/shipment_receipt.rb', line 101

def all_serial_numbers_provided?
  # using .to_a so it inspects the unsaved serial numbers
  sri_with_reservations = shipment_receipt_items.select do |sri|
    sri.purchase_order_item.item.require_reservation?
  end
  sri_with_reservations.all? do |sri|
    sri.serial_numbers.to_a.filter_map(&:presence).sum(&:qty) == sri.quantity
  end
end

#apply_estimated_landed_costsObject

Backfill a carrier on the parent shipment if missing, then push the
estimated landed cost onto each LandedCost row. No-op when there
is no estimate or no carrier could be inferred.



326
327
328
329
330
331
332
# File 'app/models/shipment_receipt.rb', line 326

def apply_estimated_landed_costs
  purchase_order_shipment.update(carrier_id: purchase_order_shipment.purchase_orders.first.carrier_id) if purchase_order_shipment.carrier_id.nil?
  ShipmentReceipt.transaction do
    # now apply the estimated landed costs if we have one
    enter_landed_costs(estimated_landed_cost.to_s, purchase_order_shipment.carrier_id, true) unless estimated_landed_cost.nil? || purchase_order_shipment.carrier_id.nil?
  end
end

#currency_symbolString

Currency symbol for display, falling back to the first item's
currency when the receipt itself has none yet.

Returns:

  • (String)


247
248
249
250
# File 'app/models/shipment_receipt.rb', line 247

def currency_symbol
  cur_index = currency || shipment_receipt_items.pick(:currency)
  Money::Currency.new(cur_index).symbol
end

#enter_landed_costs(landed_cost = '', carrier_id = nil, estimated = false) ⇒ void

This method returns an undefined value.

Allocate a freight/duty landed_cost across each
ShipmentReceiptItem pro-rata by line value (not weight, despite the
in-line comments) and create the matching LandedCost rows. When
estimated is false, also flips the receipted POs through
landed_costs_applied so they advance to the AP-ready state.

Parameters:

  • landed_cost (String, Numeric) (defaults to: '')

    coerced to BigDecimal

  • carrier_id (Integer, nil) (defaults to: nil)

    no-op when nil

  • estimated (Boolean) (defaults to: false)

    true to write to estimated_landed_cost



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

def enter_landed_costs(landed_cost = '', carrier_id = nil, estimated = false)
  landed_cost = BigDecimal(landed_cost)
  return if carrier_id.nil?

  # do this as a transaction so if any part fails it will roll back
  ShipmentReceipt.transaction do
    shipment_total_cost = shipment_receipt_items.sum { |si| si.shipment_item.total_cost } || BigDecimal('0')
    item_details = []
    shipment_receipt_items.each do |si|
      # find the matching purchase order item

      poi = si.purchase_order_item
      uom_to_ea_weight = poi.total_weight / poi.unit_quantity
      item_detail = {}
      # need to store the weight per item so that we can work out landed cost per pound
      item_detail['sr_item'] = si
      item_detail['po_item'] = poi
      item_detail['total_weight'] = si.quantity * uom_to_ea_weight
      item_details << item_detail
    end

    # sum up all item weights to find total weight
    total_weight = BigDecimal('0')

    item_details.each do |item|
      total_weight += item['total_weight']

    # now we can calculate landed cost per pound
    # landed_cost_per_pound = landed_cost / total_weight

    # now we can create the landed costs
      poi = item['po_item']
      si = item['sr_item']

      uom_to_ea_weight = (poi.total_weight / poi.unit_quantity)

      # unit_landed_cost = (landed_cost_per_pound * uom_to_ea_weight)
      # total_landed_cost = (unit_landed_cost * si.quantity)

      total_landed_cost = shipment_total_cost.zero? ? BigDecimal('0') : ((si.shipment_item.total_cost / shipment_total_cost) * landed_cost)
      unit_landed_cost = total_landed_cost / si.quantity

      LandedCost.create!(purchase_order: poi.purchase_order,
                         purchase_order_item: poi,
                         shipment_receipt_item: si,
                         shipment_receipt: si.shipment_receipt,
                         quantity: si.quantity,
                         currency: poi.currency,
                         unit_weight: uom_to_ea_weight,
                         total_weight: item['total_weight'],
                         unit_landed_cost: unit_landed_cost,
                         total_landed_cost: total_landed_cost,
                         carrier_id: carrier_id,
                         estimated: estimated)
    end

    if estimated.to_bool == true
      update(estimated_landed_cost: landed_cost,
             currency: purchase_order_shipment.currency)
    else
      update(landed_cost: landed_cost,
             currency: purchase_order_shipment.currency)
      purchase_orders.each(&:landed_costs_applied)
    end
  end
end

#enter_receipts(params) ⇒ Array<ShipmentReceiptItem>?

Build ShipmentReceiptItems from a qty_received-keyed params hash
(typically the receiving form). Iterates the corresponding
ShipmentItems, converts UoM-priced lines into per-each costs, and
appends unsaved children for the caller to persist.

Parameters:

  • params (Hash, ActionController::Parameters)

    keyed by
    ShipmentItem#id with :qty_received values

Returns:



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

def enter_receipts(params)
  # do this as a transaction so if any part fails it will roll back
  return if params.blank?

  # Load up all shipment items
  shipment_items_attributes = (params.to_h || {}).select do |_k, attrs|
    attrs[:qty_received].present?
  end.transform_keys(&:to_i)
  shipment_item_ids = shipment_items_attributes.keys
  shipment_items = ShipmentItem.joins(:purchase_order_item).includes(purchase_order_item: :item).where(id: shipment_item_ids)
  # Loop through
  shipment_items.each do |si|
    qty_received = shipment_items_attributes[si.id][:qty_received].to_i
    next unless qty_received.positive?

    poi = si.purchase_order_item
    uom_to_ea_qty = poi.unit_quantity / poi.quantity
    shipment_receipt_items.build(purchase_order: poi.purchase_order,
                                 purchase_order_item: poi,
                                 quantity: qty_received * uom_to_ea_qty,
                                 currency: poi.currency,
                                 unit_cost: poi.unit_cost / uom_to_ea_qty,
                                 total_cost: qty_received * poi.unit_cost,
                                 effective_date: effective_date,
                                 shipment_item: si)
  end
  shipment_receipt_items
end

#has_items_requiring_serial_number?Boolean

Whether any received item requires serial numbers.

Returns:

  • (Boolean)


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

def has_items_requiring_serial_number?
  shipment_receipt_items.any? { |sri| sri.purchase_order_item.item.require_reservation? }
end

#landed_costsActiveRecord::Relation<LandedCost>

Landed costs applied to this receipt.

Returns:

See Also:



36
# File 'app/models/shipment_receipt.rb', line 36

has_many :landed_costs

#ledger_transactionsActiveRecord::Relation<LedgerTransaction>

GL transactions posted for this receipt.

Returns:

See Also:



38
# File 'app/models/shipment_receipt.rb', line 38

has_many :ledger_transactions

#prorate_estimated_landed_costBigDecimal, Integer

Share of the parent shipment's estimated landed cost that belongs to
this receipt, weighted by line value.

Returns:

  • (BigDecimal, Integer)


317
318
319
320
321
# File 'app/models/shipment_receipt.rb', line 317

def prorate_estimated_landed_cost
  poi_total_cost = purchase_order_shipment.purchase_order_items.sum(&:total_cost)
  sri_total_cost = shipment_receipt_items.sum(&:total_cost)
  poi_total_cost.zero? ? 0 : (purchase_order_shipment.estimated_landed_cost * (sri_total_cost / poi_total_cost))
end

#prorate_landed_costBigDecimal, Integer

Share of the parent shipment's actual landed cost that belongs to
this receipt, weighted by line value.

Returns:

  • (BigDecimal, Integer)


307
308
309
310
311
# File 'app/models/shipment_receipt.rb', line 307

def prorate_landed_cost
  poi_total_cost = purchase_order_shipment.purchase_order_items.sum(&:total_cost)
  sri_total_cost = shipment_receipt_items.sum(&:total_cost)
  poi_total_cost.zero? ? 0 : (purchase_order_shipment.landed_cost * (sri_total_cost / poi_total_cost))
end

#purchase_order_shipmentPurchaseOrderShipment

PO shipment this receipt receives against.



29
# File 'app/models/shipment_receipt.rb', line 29

belongs_to :purchase_order_shipment, optional: true

#purchase_ordersActiveRecord::Relation<PurchaseOrder>

Purchase orders receiving goods through this receipt.

Returns:

See Also:



34
# File 'app/models/shipment_receipt.rb', line 34

has_many :purchase_orders, -> { distinct }, through: :shipment_receipt_items

#serial_number_numbersString

Comma-separated list of serial numbers received on this shipment, in
number order. Used in PO receipt confirmations and audit views.

Returns:

  • (String)


89
90
91
# File 'app/models/shipment_receipt.rb', line 89

def serial_number_numbers
  serial_numbers.order(:number).map(&:number).join(', ')
end

#serial_numbersActiveRecord::Relation<SerialNumber>

Serial numbers registered by this receipt.

Returns:

See Also:



40
# File 'app/models/shipment_receipt.rb', line 40

has_many :serial_numbers, through: :shipment_receipt_items

#set_supplier_lead_timeObject

Recalculate every receipted PO's supplier lead time using this
arrival as a fresh data point.



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

def set_supplier_lead_time
  purchase_orders.map(&:supplier).uniq.each(&:calculate_lead_time)
end

#shipment_receipt_itemsActiveRecord::Relation<ShipmentReceiptItem>

Received item lines.

Returns:

See Also:



32
# File 'app/models/shipment_receipt.rb', line 32

has_many :shipment_receipt_items, dependent: :destroy

#void_estimated_landed_costsObject

Reverse estimated LandedCost rows and clear estimated_landed_cost
before applying actuals.



236
237
238
239
240
241
# File 'app/models/shipment_receipt.rb', line 236

def void_estimated_landed_costs
  ShipmentReceipt.transaction do
    landed_costs.estimated.each(&:void!)
    update(estimated_landed_cost: nil)
  end
end

#void_landed_costsObject

Reverse actual (non-estimated) LandedCost rows and clear the
landed_cost attribute, e.g. when the carrier voucher is corrected.



227
228
229
230
231
232
# File 'app/models/shipment_receipt.rb', line 227

def void_landed_costs
  ShipmentReceipt.transaction do
    landed_costs.actual.each(&:void!)
    update(landed_cost: nil)
  end
end

#void_shipment_receiptHash{String=>Boolean,String}

Void this receipt: reverse the GL transaction, reverse the per-item
ledger entries, discontinue any auto-created serial numbers, void
estimated landed costs, then run the :void state event. Refuses
when the receipt is already voided or any received unit has left
stock.

Returns:

  • (Hash{String=>Boolean,String})

    { "success" => …, "message" => … }



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

def void_shipment_receipt
  res = { 'success' => nil, 'message' => nil }
  if voided?
    # check if it's already been voided
    res['success'] = false
    res['message'] = 'Shipment receipt has already been voided'
    res
  elsif !all_items_in_stock?
    # check all the items are in stock
    res['success'] = false
    res['message'] = 'All items must be in stock before you can void the shipment receipt'
    res
  else
    lt = ledger_transactions.first
    raise "Unable to find ledger transaction for shipment receipt #{id}" if lt.nil?

    # do this as a transaction so if any part fails it will roll back
    ShipmentReceipt.transaction do
      # reverse the ledger transaction
      rev = lt.reverse(Date.current)
      # loop through each shipment receipt item and reverse the item ledger entry
      shipment_receipt_items.each do |sri|
        sri.reverse_item_ledger(rev)
      end
      # Discontinue any serial numbers created
      serial_numbers.each(&:discontinue)
      # now void the estimated landed cost
      void_estimated_landed_costs
      # now void the shipment receipt
      void!
    end

    res['success'] = true
    res['message'] = 'Shipment receipt has been voided'
    res
  end
end