Class: PurchaseOrder

Inherits:
ApplicationRecord show all
Includes:
Models::Auditable, Models::LiquidMethods
Defined in:
app/models/purchase_order.rb

Overview

== Schema Information

Table name: purchase_orders
Database name: primary

id :integer not null, primary key
cancel_date :date
currency :string(255)
description :string(255)
drop_ship :boolean
exchange_rate :float
order_date :date
po_type :string(255)
reference_number :string(20) not null
request_date :date
state :string(255)
terms :string(255)
total_cost :decimal(8, 2)
total_weight :float
uploads_count :integer
created_at :datetime
updated_at :datetime
carrier_id :integer
company_id :integer
creator_id :integer
drop_ship_delivery_id :integer
drop_ship_order_id :integer
rma_id :integer
store_id :integer
supplier_id :integer
updater_id :integer

Indexes

index_purchase_orders_on_drop_ship_delivery_id (drop_ship_delivery_id)
index_purchase_orders_on_reference_number (reference_number) UNIQUE
index_purchase_orders_on_state (state)
store_id_state (store_id,state)
supplier_id_state (supplier_id,state)

Defined Under Namespace

Classes: PdfGenerator

Constant Summary collapse

REFERENCE_NUMBER_PATTERN =

Regex pattern matching reference number.

/^PO\d+/i
CARRIERS =

Carriers.

{
  'US' => { 'UPS' => 181_590, 'FedEx' => 181_593, 'UPSFreight' => 181_595, 'Freightquote' => 7_269_908, 'override' => 295_414 },
  'CA' => { 'UPS' => 294_788, 'UPSFreight' => 181_595, 'FedEx' => 1_860_541, 'Purolator' => 294_759, 'Canadapost' => 294_677,
            'PurolatorFreight' => 294_760, 'Freightquote' => 7_269_908, 'override' => 295_414 }
}.freeze

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 one collapse

Has many collapse

Class Method Summary 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

#carrier_idObject (readonly)

Required fields.

Validations:



98
# File 'app/models/purchase_order.rb', line 98

validates :company_id, :store_id, :supplier_id, :carrier_id, :order_date, :request_date, :currency, :po_type, presence: true

#company_idObject (readonly)

Required fields.

Validations:



98
# File 'app/models/purchase_order.rb', line 98

validates :company_id, :store_id, :supplier_id, :carrier_id, :order_date, :request_date, :currency, :po_type, presence: true

#currencyObject (readonly)

Required fields.

Validations:



98
# File 'app/models/purchase_order.rb', line 98

validates :company_id, :store_id, :supplier_id, :carrier_id, :order_date, :request_date, :currency, :po_type, presence: true

#drop_ship_delivery_idInteger?

ID of the linked drop-ship delivery.

Returns:

  • (Integer, nil)


106
# File 'app/models/purchase_order.rb', line 106

validates :drop_ship_delivery_id, presence: { if: :drop_ship? }

#exchange_rateObject (readonly)

Exchange rate is required when company and supplier currencies differ.

Validations:



102
# File 'app/models/purchase_order.rb', line 102

validates :exchange_rate, presence: { if: :currencies_not_matching? }

#order_dateObject (readonly)

Required fields.

Validations:



98
# File 'app/models/purchase_order.rb', line 98

validates :company_id, :store_id, :supplier_id, :carrier_id, :order_date, :request_date, :currency, :po_type, presence: true

#po_typeObject (readonly)

Required fields.

Validations:



98
# File 'app/models/purchase_order.rb', line 98

validates :company_id, :store_id, :supplier_id, :carrier_id, :order_date, :request_date, :currency, :po_type, presence: true

#request_dateObject (readonly)

Required fields.

Validations:



98
# File 'app/models/purchase_order.rb', line 98

validates :company_id, :store_id, :supplier_id, :carrier_id, :order_date, :request_date, :currency, :po_type, presence: true

#store_idObject (readonly)

Required fields.

Validations:



98
# File 'app/models/purchase_order.rb', line 98

validates :company_id, :store_id, :supplier_id, :carrier_id, :order_date, :request_date, :currency, :po_type, presence: true

#supplier_idObject (readonly)

Required fields.

Validations:



98
# File 'app/models/purchase_order.rb', line 98

validates :company_id, :store_id, :supplier_id, :carrier_id, :order_date, :request_date, :currency, :po_type, presence: true

Class Method Details

.carrier_options_for_selectArray<Array(String, Integer)>

Carrier choices as [label, party_id] pairs for select boxes.

Returns:

  • (Array<Array(String, Integer)>)


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

def self.carrier_options_for_select
  CARRIERS.map { |c, cv| cv.map { |k, v| ["#{c} - #{k}", v] } }.flatten
end

.get_next_reference_numberString

Next PO reference number from the sequence.

Returns:

  • (String)


537
538
539
540
# File 'app/models/purchase_order.rb', line 537

def self.get_next_reference_number
  seq = PurchaseOrder.find_by_sql("SELECT nextval('purchase_order_reference_numbers_seq') AS reference_number")
  "PO#{seq[0].reference_number}"
end

.new_or_update_st_po_from_delivery(delivery) ⇒ PurchaseOrder

Builds or rebuilds the store-transfer PO for a delivery's order.

Parameters:

  • delivery (Delivery)

    the delivery driving the transfer

Returns:



229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'app/models/purchase_order.rb', line 229

def self.new_or_update_st_po_from_delivery(delivery)
  order = delivery.order
  order.from_store.company
  to_company = order.to_store.company
  shipment = delivery.shipments.completed.first
  supplier = order.from_store.supplier
  store = order.from_store
  new_store = order.to_store
  carrier_id = if shipment
                 CARRIERS.dig(store.country.iso, shipment.carrier) || CARRIERS.dig(store.country.iso, 'override')
               else
                 supplier.id
               end
  # carrier_id ||= 7269908 # set it to Freightquote if we couldn't find a match
  po = order.purchase_order || PurchaseOrder.new(po_type: 'store_transfer',
                                                 company: to_company,
                                                 store: new_store,
                                                 supplier:,
                                                 terms: supplier.terms,
                                                 state: 'pending_fulfillment',
                                                 carrier_id:,
                                                 order_date: Date.current,
                                                 request_date: delivery.order.created_at,
                                                 currency: store.currency,
                                                 exchange_rate: store.currency == new_store.currency ? nil : ExchangeRate.get_exchange_rate(store.currency, new_store.currency, Date.current)) # if the currencies are different, need to record the exchange rate
  po.purchase_order_items.removable.destroy_all
  # Create the PO only with the kit component items or simple items
  delivery.line_items.without_children.non_shipping.each do |li|
    unit_cost = li.parent.present? ? li.store_item.unit_cogs : li.discounted_price
    total_cost = unit_cost * li.quantity
    po.purchase_order_items << PurchaseOrderItem.new(item: li.item,
                                                     sku: li.item.sku,
                                                     description: li.item.name,
                                                     quantity: li.quantity,
                                                     unit_weight: li.item.base_weight,
                                                     total_weight: li.item.base_weight * li.quantity,
                                                     unit_cost:,
                                                     total_cost:,
                                                     uom: 'EA',
                                                     unit_quantity: li.quantity,
                                                     line_item: li)
  end
  po.save!
  order.update(purchase_order: po)
  po
end

.open_poActiveRecord::Relation<PurchaseOrder>

A relation of PurchaseOrders that are open po. Active Record Scope

Returns:

See Also:



126
# File 'app/models/purchase_order.rb', line 126

scope :open_po, -> { where(state: %w[awaiting_transmission pending_fulfillment shipped partially_receipted]) }

.pending_service_fulfillmentActiveRecord::Relation<PurchaseOrder>

A relation of PurchaseOrders that are pending service fulfillment. Active Record Scope

Returns:

See Also:



124
# File 'app/models/purchase_order.rb', line 124

scope :pending_service_fulfillment, -> { where(state: 'pending_service_fulfillment') }

.ship_to_attributes(order) ⇒ Hash

Ship-to contact attributes for an order, with fallbacks to the customer,
their sales rep, and the country shipper configuration.

Parameters:

  • order (Order)

    the order being shipped

Returns:

  • (Hash)

    address_type/phone/email attributes



280
281
282
283
284
285
286
287
288
289
290
291
292
# File 'app/models/purchase_order.rb', line 280

def self.ship_to_attributes(order)
  phone = order.shipping_phone || order.customer.phone || order.customer.cell_phone || (begin
    order.customer.sales_rep.direct_phone
  rescue StandardError
    nil
  end) || SHIPPING_SHIPPER_CONFIGURATION[order.country.iso3.to_sym][:shipper_phone]
  email = order.first_tracking_email || (begin
    order.customer.sales_rep.email
  rescue StandardError
    nil
  end) || SHIPPING_SHIPPER_CONFIGURATION[order.country.iso3.to_sym][:shipper_email]
  { address_type: true, phone:, email: }
end

.states_for_selectArray<Array(String, String)>

PO states as [human name, value] pairs for select boxes.

Returns:

  • (Array<Array(String, String)>)


216
217
218
# File 'app/models/purchase_order.rb', line 216

def self.states_for_select
  state_machines[:state].states.sort_by(&:human_name).map { |s| [s.human_name, s.value] }
end

Instance Method Details

#activitiesActiveRecord::Relation<Activity>

Activity log entries for the PO.

Returns:

See Also:



79
# File 'app/models/purchase_order.rb', line 79

has_many :activities, as: :resource, dependent: :nullify

#all_items_cancelled?Boolean

Whether every line item is cancelled.

Returns:

  • (Boolean)


449
450
451
# File 'app/models/purchase_order.rb', line 449

def all_items_cancelled?
  purchase_order_items.all?(&:cancelled?)
end

#all_items_receipted?Boolean

Whether every line item is fully receipted.

Returns:

  • (Boolean)


431
432
433
# File 'app/models/purchase_order.rb', line 431

def all_items_receipted?
  purchase_order_items.all?(&:fully_receipted?)
end

#all_landed_costs_applied?Boolean

Whether all line items have landed costs fully applied.

Returns:

  • (Boolean)


443
444
445
# File 'app/models/purchase_order.rb', line 443

def all_landed_costs_applied?
  purchase_order_items.all?(&:fully_applied?)
end

#build_activityActivity

Builds an unsaved activity for this PO attributed to the supplier.

Returns:



296
297
298
# File 'app/models/purchase_order.rb', line 296

def build_activity
  activities.new(resource: self, party: supplier)
end

#can_be_cancelled?Boolean

Whether the PO can be cancelled in its current state.

Returns:

  • (Boolean)


406
407
408
# File 'app/models/purchase_order.rb', line 406

def can_be_cancelled?
  %w[awaiting_transmission pending_fulfillment pending_service_fulfillment pending_acknowledgement shipped].include?(state)
end

#can_be_edited?Boolean

Whether the PO can still be edited in its current state.

Returns:

  • (Boolean)


400
401
402
# File 'app/models/purchase_order.rb', line 400

def can_be_edited?
  %w[fully_receipted landed_costs cancelled].exclude?(state)
end

#cancel_items_and_selfvoid

This method returns an undefined value.

Cancels all line items and the PO itself.



387
388
389
390
# File 'app/models/purchase_order.rb', line 387

def cancel_items_and_self
  purchase_order_items.each(&:cancel)
  cancel
end

#carrierParty

Carrier used to ship the PO.

Returns:

See Also:



63
# File 'app/models/purchase_order.rb', line 63

belongs_to :carrier, class_name: 'Party', optional: true

#communicationsActiveRecord::Relation<Communication>

Communications attached to the PO.

Returns:

See Also:



91
# File 'app/models/purchase_order.rb', line 91

has_many :communications, as: :resource, dependent: :nullify

#companyCompany

Company the PO belongs to.

Returns:

See Also:



59
# File 'app/models/purchase_order.rb', line 59

belongs_to :company, optional: true

#currencies_not_matching?Boolean

Whether the PO currency differs from the company currency.

Returns:

  • (Boolean)


425
426
427
# File 'app/models/purchase_order.rb', line 425

def currencies_not_matching?
  currency and company and currency != company.currency
end

#currency_symbolString

Symbol for the PO currency.

Returns:

  • (String)


461
462
463
# File 'app/models/purchase_order.rb', line 461

def currency_symbol
  Money::Currency.new(currency).symbol
end

#drop_ship_deliveryDelivery

Linked delivery when this PO is a drop-shipment.

Returns:

See Also:



65
# File 'app/models/purchase_order.rb', line 65

belongs_to :drop_ship_delivery, class_name: 'Delivery', optional: true

#drop_ship_order_refString?

Reference number of the order behind the drop-ship delivery.

Returns:

  • (String, nil)


484
485
486
# File 'app/models/purchase_order.rb', line 484

def drop_ship_order_ref
  drop_ship_delivery.order.try(:reference_number) if drop_ship_delivery.present?
end

#drop_ship_order_ref=(ref) ⇒ void

This method returns an undefined value.

Sets the drop-ship delivery from an order reference number.

Parameters:

  • ref (String)

    order reference number



491
492
493
494
495
496
497
498
499
# File 'app/models/purchase_order.rb', line 491

def drop_ship_order_ref=(ref)
  return if ref.blank?

  ord = Order.find_by(reference_number: ref)
  return if ord.nil?

  self.drop_ship_delivery ||= ord.deliveries.where.not(deliveries: { origin_address_id: ord.origin_address.id }).first
  # only set to first likely dropship delivery on order, if not already set
end

#file_nameString

File name for the generated PO PDF.

Returns:

  • (String)


324
325
326
# File 'app/models/purchase_order.rb', line 324

def file_name
  "purchase-order-#{reference_number}.pdf"
end

#generate_pdfUpload

Generates the PO PDF and attaches it as an upload.

Returns:

  • (Upload)

    the created upload



467
468
469
470
471
472
473
474
475
476
477
478
479
480
# File 'app/models/purchase_order.rb', line 467

def generate_pdf
  store
  pdf = PurchaseOrder::PdfGenerator.new(self).generate

  path = Rails.application.config.x.temp_storage_path.join(file_name)
  File.open(path, 'wb') do |file|
    file.write pdf
    file.flush
    file.fsync
  end
  upload = Upload.uploadify(path, 'purchase_order', self, file_name)
  uploads << upload
  upload
end

#get_or_regen_pdfUpload

Latest PO PDF upload, regenerating when none exists.

Returns:



330
331
332
# File 'app/models/purchase_order.rb', line 330

def get_or_regen_pdf
  uploads.where(category: 'purchase_order').order('uploads.id desc').find { |u| u.attachment.present? } || generate_pdf
end

#has_service_items?Boolean

Whether any line item is a service (tax class svc).

Returns:

  • (Boolean)


381
382
383
# File 'app/models/purchase_order.rb', line 381

def has_service_items?
  purchase_order_items.any? { |poi| poi.item.present? and poi.item.tax_class == 'svc' }
end

#landed_costsActiveRecord::Relation<LandedCost>

Landed cost adjustments applied to the PO.

Returns:

See Also:



81
# File 'app/models/purchase_order.rb', line 81

has_many :landed_costs

#mark_st_po_as_shipped_from_delivery(delivery) ⇒ PurchaseOrder?

Marks a store-transfer PO as shipped based on a completed delivery.

Parameters:

  • delivery (Delivery)

    the completed delivery

Returns:



504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
# File 'app/models/purchase_order.rb', line 504

def mark_st_po_as_shipped_from_delivery(delivery)
  return unless po_type == 'store_transfer'

  shipment = delivery.shipments.completed.first
  carrier_id = CARRIERS.dig(store.country.iso, shipment.carrier) || CARRIERS.dig(store.country.iso, 'override') if shipment
  save
  pos = PurchaseOrderShipment.new(date_shipped: delivery.shipped_date,
                                  promised_delivery_date: delivery.shipped_date + 5.working.days,
                                  currency: delivery.order.purchase_order.currency,
                                  carrier_id:,
                                  tracking_number: shipment.try(:tracking_number),
                                  weight: shipment.try(:weight),
                                  length: shipment.try(:length),
                                  width: shipment.try(:width),
                                  height: shipment.try(:height),
                                  actual_shipping_cost: shipment.try(:actual_total_charges))
  purchase_order_items.each do |poi|
    pos.shipment_items.build(purchase_order: self,
                             purchase_order_item: poi,
                             quantity: poi.quantity,
                             uom: 'EA',
                             ea_quantity: poi.quantity)
  end
  pos.save!
  pos.set_estimated_landed_cost if pos.estimated_landed_cost.nil?
  pos.ship!
  self
end

#move_service_gl_fundsvoid

This method returns an undefined value.

Moves GL funds for receipted service items from Receipts-Not-Vouchered
into Services-Pending-Fulfillment-Credit.



337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
# File 'app/models/purchase_order.rb', line 337

def move_service_gl_funds
  transaction do
     = LedgerCompanyAccount.(company_id, RECEIPTS_NOT_VOUCHERED_ACCOUNT)
     = LedgerCompanyAccount.(company_id, SERVICES_PENDING_FULFILLMENT_CREDIT_ACCOUNT)

    raise 'Unable to find Services-Pending Fulfillment Credit company account' if .nil?
    raise 'Unable to find Receipts Not Vouchered company account' if .nil?

    grouped_items = shipment_receipt_items.group_by(&:purchase_order)

    grouped_items.each do |po, sr_items|
      next if po != self # don't process the ones which are not for this po

      service_items = sr_items.select { |sri| sri.purchase_order_item.item.present? and sri.purchase_order_item.item.tax_class == 'svc' }

      # collect the total value of the service receipted items
      total_service_value = service_items.sum(&:total_cost)

      # need to take money out of 4122 and put into 4120
      # + Services-Pending Fulfillment Credit (4122)
      # - Receipts Not Vouchered (4120)
      transaction = LedgerTransaction.new(transaction_type: 'PO_SERVICE_FULFILLMENT',
                                          transaction_date: Date.current,
                                          description:,
                                          currency:,
                                          company:,
                                          shipment_receipt: sr_items.first.shipment_receipt)
      transaction.ledger_entries << LedgerEntry.new(ledger_company_account_id: .id, currency:,
                                                    amount: total_service_value, description:)
      transaction.ledger_entries << LedgerEntry.new(ledger_company_account_id: .id, currency:,
                                                    amount: -total_service_value, description:)
      transaction.save!
    end
  end
end

#nameString

Display name (the reference number).

Returns:

  • (String)


455
456
457
# File 'app/models/purchase_order.rb', line 455

def name
  reference_number.to_s
end

#orderOrder

Sales order fulfilled by this PO.

Returns:

See Also:



72
# File 'app/models/purchase_order.rb', line 72

has_one :order

#post_communication_queued_hook(_params = {}) ⇒ void

This method returns an undefined value.

Hook fired after a communication for this PO is queued.

Parameters:

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

    unused hook params



303
304
305
306
# File 'app/models/purchase_order.rb', line 303

def post_communication_queued_hook(_params = {})
  # Handles post transmission
  transmit if can_transmit?
end

#post_communication_sent_hook(_params = {}) ⇒ void

This method returns an undefined value.

Hook fired after a communication for this PO is sent.

Parameters:

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

    unused hook params



311
312
313
314
# File 'app/models/purchase_order.rb', line 311

def post_communication_sent_hook(_params = {})
  # Handles post transmission
  transmit if can_transmit?
end

#promised_delivery_dateDate?

Earliest promised delivery date among open shipments.

Returns:

  • (Date, nil)


318
319
320
# File 'app/models/purchase_order.rb', line 318

def promised_delivery_date
  purchase_order_shipments.open_shipments.minimum(:promised_delivery_date)
end

#purchase_order_itemsActiveRecord::Relation<PurchaseOrderItem>

Line items on the PO.

Returns:

See Also:



75
# File 'app/models/purchase_order.rb', line 75

has_many :purchase_order_items, dependent: :destroy, inverse_of: :purchase_order

#purchase_order_shipmentsActiveRecord::Relation<PurchaseOrderShipment>

Distinct shipments linked via shipment items.

Returns:

See Also:



85
# File 'app/models/purchase_order.rb', line 85

has_many :purchase_order_shipments, -> { distinct }, through: :shipment_items

#rmaRma

RMA that spawned this PO, when applicable.

Returns:

See Also:



69
# File 'app/models/purchase_order.rb', line 69

belongs_to :rma, optional: true

#serial_numbersActiveRecord::Relation<SerialNumber>

Serial numbers recorded through receipt line items.

Returns:

See Also:



93
# File 'app/models/purchase_order.rb', line 93

has_many :serial_numbers, through: :shipment_receipt_items

#shipment_itemsActiveRecord::Relation<ShipmentItem>

Shipment items linked to the PO.

Returns:

See Also:



83
# File 'app/models/purchase_order.rb', line 83

has_many :shipment_items, dependent: :destroy

#shipment_receipt_itemsActiveRecord::Relation<ShipmentReceiptItem>

Receipt line items for the PO.

Returns:

See Also:



87
# File 'app/models/purchase_order.rb', line 87

has_many :shipment_receipt_items

#shipment_receiptsActiveRecord::Relation<ShipmentReceipt>

Distinct shipment receipts via receipt line items.

Returns:

See Also:



89
# File 'app/models/purchase_order.rb', line 89

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

#should_lock_linked_order?Boolean

Whether the linked sales order should be locked from edits.

Returns:

  • (Boolean)


394
395
396
# File 'app/models/purchase_order.rb', line 394

def should_lock_linked_order?
  %w[shipped partially_receipted fully_receipted landed_costs].include?(state)
end

#some_items_receipted?Boolean

Whether any line item is partially or fully receipted.

Returns:

  • (Boolean)


437
438
439
# File 'app/models/purchase_order.rb', line 437

def some_items_receipted?
  purchase_order_items.any? { |poi| poi.partially_receipted? or poi.fully_receipted? }
end

#storeStore

Store the PO belongs to.

Returns:

See Also:



67
# File 'app/models/purchase_order.rb', line 67

belongs_to :store, optional: true

#supplierParty

Supplier the PO is placed with.

Returns:

See Also:



61
# File 'app/models/purchase_order.rb', line 61

belongs_to :supplier, class_name: 'Party', inverse_of: :purchase_orders, optional: true

#to_sString

String representation of the PO.

Returns:

  • (String)


375
376
377
# File 'app/models/purchase_order.rb', line 375

def to_s
  "PO # #{reference_number}"
end

#uploadsActiveRecord::Relation<Upload>

File uploads attached to the PO.

Returns:

  • (ActiveRecord::Relation<Upload>)

See Also:



77
# File 'app/models/purchase_order.rb', line 77

has_many :uploads, as: :resource, dependent: :destroy