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 Schedulable

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 Schedulable

config

Methods included from Models::AfterCommittable

#after_commit

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#effective_dateObject (readonly)



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

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

#locationObject (readonly)



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

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

#purchase_order_shipment_idObject (readonly)



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

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

Instance Method Details

#all_items_in_stock?Boolean

Returns:

  • (Boolean)


242
243
244
# File 'app/models/shipment_receipt.rb', line 242

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

#all_serial_numbers_provided?Boolean

Returns:

  • (Boolean)


91
92
93
94
95
96
97
98
99
# File 'app/models/shipment_receipt.rb', line 91

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.



314
315
316
317
318
319
320
# File 'app/models/shipment_receipt.rb', line 314

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)


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

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



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

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:



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

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

Returns:

  • (Boolean)


87
88
89
# File 'app/models/shipment_receipt.rb', line 87

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

#landed_costsActiveRecord::Relation<LandedCost>

Returns:

See Also:



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

has_many :landed_costs

#ledger_transactionsActiveRecord::Relation<LedgerTransaction>

Returns:

See Also:



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

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)


305
306
307
308
309
# File 'app/models/shipment_receipt.rb', line 305

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)


295
296
297
298
299
# File 'app/models/shipment_receipt.rb', line 295

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



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

belongs_to :purchase_order_shipment, optional: true

#purchase_ordersActiveRecord::Relation<PurchaseOrder>

Returns:

See Also:



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

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)


83
84
85
# File 'app/models/shipment_receipt.rb', line 83

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

#serial_numbersActiveRecord::Relation<SerialNumber>

Returns:

See Also:



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

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.



75
76
77
# File 'app/models/shipment_receipt.rb', line 75

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

#shipment_receipt_itemsActiveRecord::Relation<ShipmentReceiptItem>

Returns:

See Also:



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

has_many :shipment_receipt_items, dependent: :destroy

#void_estimated_landed_costsObject

Reverse estimated LandedCost rows and clear estimated_landed_cost
before applying actuals.



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

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.



217
218
219
220
221
222
# File 'app/models/shipment_receipt.rb', line 217

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" => … }



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

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