stockor-core 0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/Gemfile +6 -0
- data/Guardfile +13 -0
- data/LICENSE.txt +674 -0
- data/README.md +88 -0
- data/Rakefile +58 -0
- data/config/database.yml +9 -0
- data/db/migrate/20120110142845_create_skr_sequential_ids.rb +35 -0
- data/db/migrate/20140202185309_create_skr_gl_accounts.rb +15 -0
- data/db/migrate/20140202193316_create_skr_gl_periods.rb +16 -0
- data/db/migrate/20140202193318_create_skr_gl_transactions.rb +14 -0
- data/db/migrate/20140202193319_create_skr_gl_postings.rb +16 -0
- data/db/migrate/20140202193700_create_skr_gl_manual_entries.rb +13 -0
- data/db/migrate/20140213040608_create_skr_payment_terms.rb +16 -0
- data/db/migrate/20140220031700_create_skr_addresses.rb +19 -0
- data/db/migrate/20140220031800_create_skr_locations.rb +19 -0
- data/db/migrate/20140220190836_create_skr_vendors.rb +22 -0
- data/db/migrate/20140220203029_create_skr_customers.rb +22 -0
- data/db/migrate/20140224034759_create_skr_skus.rb +22 -0
- data/db/migrate/20140225032853_create_skr_sku_locs.rb +21 -0
- data/db/migrate/20140320030501_create_skr_uoms.rb +19 -0
- data/db/migrate/20140321031604_create_skr_sku_vendors.rb +18 -0
- data/db/migrate/20140322012143_create_skr_ia_reasons.rb +14 -0
- data/db/migrate/20140322014401_create_skr_inventory_adjustments.rb +16 -0
- data/db/migrate/20140322023453_create_skr_ia_lines.rb +18 -0
- data/db/migrate/20140322035024_create_skr_sku_trans.rb +21 -0
- data/db/migrate/20140322223912_create_skr_sales_orders.rb +27 -0
- data/db/migrate/20140322223920_create_skr_so_lines.rb +25 -0
- data/db/migrate/20140323001446_create_so_details_view.rb +81 -0
- data/db/migrate/20140327202102_create_skr_purchase_orders.rb +20 -0
- data/db/migrate/20140327202107_create_skr_po_lines.rb +25 -0
- data/db/migrate/20140327202207_create_skr_pick_tickets.rb +16 -0
- data/db/migrate/20140327202209_create_skr_pt_lines.rb +23 -0
- data/db/migrate/20140327224000_create_skr_invoices.rb +25 -0
- data/db/migrate/20140327224002_create_skr_inv_lines.rb +23 -0
- data/db/migrate/20140330232808_create_skr_sku_loc_details_view.rb +31 -0
- data/db/migrate/20140330232810_create_skr_sku_qty_details_view.rb +48 -0
- data/db/migrate/20140400164729_create_skr_vouchers.rb +22 -0
- data/db/migrate/20140400164733_create_skr_vo_lines.rb +21 -0
- data/db/migrate/20140401164729_create_skr_po_receipt.rb +16 -0
- data/db/migrate/20140401164740_create_skr_por_line.rb +21 -0
- data/db/migrate/20140422024010_create_skr_inv_details_view.rb +42 -0
- data/lib/generators/stockor/migrations/install_generator.rb +42 -0
- data/lib/skr/address.rb +97 -0
- data/lib/skr/business_entity.rb +25 -0
- data/lib/skr/concerns/acts_as_uom.rb +47 -0
- data/lib/skr/concerns/all.rb +30 -0
- data/lib/skr/concerns/association_extensions.rb +85 -0
- data/lib/skr/concerns/attr_accessor_with_default.rb +54 -0
- data/lib/skr/concerns/code_identifier.rb +43 -0
- data/lib/skr/concerns/export_associations.rb +52 -0
- data/lib/skr/concerns/export_join_tables.rb +39 -0
- data/lib/skr/concerns/export_methods.rb +104 -0
- data/lib/skr/concerns/export_scope.rb +66 -0
- data/lib/skr/concerns/exported_limit_evaluator.rb +17 -0
- data/lib/skr/concerns/gl_tran_extensions.rb +18 -0
- data/lib/skr/concerns/has_gl_transaction.rb +67 -0
- data/lib/skr/concerns/has_sku_loc_lines.rb +47 -0
- data/lib/skr/concerns/immutable_model.rb +32 -0
- data/lib/skr/concerns/inv_extensions.rb +24 -0
- data/lib/skr/concerns/is_order_like.rb +47 -0
- data/lib/skr/concerns/is_sku_loc_line.rb +65 -0
- data/lib/skr/concerns/json_attribute_access.rb +55 -0
- data/lib/skr/concerns/locked_fields.rb +84 -0
- data/lib/skr/concerns/pt_extensions.rb +22 -0
- data/lib/skr/concerns/pub_sub.rb +105 -0
- data/lib/skr/concerns/queries.rb +20 -0
- data/lib/skr/concerns/random_hash_code.rb +40 -0
- data/lib/skr/concerns/sanitize_json.rb +49 -0
- data/lib/skr/concerns/sku_extensions.rb +52 -0
- data/lib/skr/concerns/so_extensions.rb +30 -0
- data/lib/skr/concerns/state_machine.rb +62 -0
- data/lib/skr/concerns/track_modifications.rb +48 -0
- data/lib/skr/concerns/visible_id_identifier.rb +53 -0
- data/lib/skr/core.rb +30 -0
- data/lib/skr/core/configuration.rb +87 -0
- data/lib/skr/core/db.rb +82 -0
- data/lib/skr/core/db/migration_helpers.rb +178 -0
- data/lib/skr/core/db/migrations.rb +15 -0
- data/lib/skr/core/db/seed.rb +46 -0
- data/lib/skr/core/db/seed/chart_of_accounts.yml +168 -0
- data/lib/skr/core/db/seed/payment_terms.yml +60 -0
- data/lib/skr/core/logger.rb +36 -0
- data/lib/skr/core/numbers.rb +71 -0
- data/lib/skr/core/rails_engine.rb +5 -0
- data/lib/skr/core/standard_pricing_provider.rb +15 -0
- data/lib/skr/core/strings.rb +57 -0
- data/lib/skr/core/testing.rb +11 -0
- data/lib/skr/core/testing/assertions.rb +44 -0
- data/lib/skr/core/testing/fixtures.rb +25 -0
- data/lib/skr/core/testing/fixtures/skr/addresses.yml +53 -0
- data/lib/skr/core/testing/fixtures/skr/customers.yml +43 -0
- data/lib/skr/core/testing/fixtures/skr/gl_accounts.yml +86 -0
- data/lib/skr/core/testing/fixtures/skr/gl_manual_entries.yml +4 -0
- data/lib/skr/core/testing/fixtures/skr/gl_periods.yml +26 -0
- data/lib/skr/core/testing/fixtures/skr/gl_postings.yml +88 -0
- data/lib/skr/core/testing/fixtures/skr/gl_transactions.yml +35 -0
- data/lib/skr/core/testing/fixtures/skr/ia_lines.yml +7 -0
- data/lib/skr/core/testing/fixtures/skr/ia_reasons.yml +15 -0
- data/lib/skr/core/testing/fixtures/skr/inv_lines.yml +22 -0
- data/lib/skr/core/testing/fixtures/skr/inventory_adjustments.yml +6 -0
- data/lib/skr/core/testing/fixtures/skr/invoices.yml +10 -0
- data/lib/skr/core/testing/fixtures/skr/locations.yml +24 -0
- data/lib/skr/core/testing/fixtures/skr/payment_terms.yml +42 -0
- data/lib/skr/core/testing/fixtures/skr/pick_tickets.yml +4 -0
- data/lib/skr/core/testing/fixtures/skr/po_lines.yml +44 -0
- data/lib/skr/core/testing/fixtures/skr/po_receipts.yml +5 -0
- data/lib/skr/core/testing/fixtures/skr/por_lines.yml +13 -0
- data/lib/skr/core/testing/fixtures/skr/pt_lines.yml +12 -0
- data/lib/skr/core/testing/fixtures/skr/purchase_orders.yml +16 -0
- data/lib/skr/core/testing/fixtures/skr/sales_orders.yml +32 -0
- data/lib/skr/core/testing/fixtures/skr/sku_locs.yml +78 -0
- data/lib/skr/core/testing/fixtures/skr/sku_trans.yml +9 -0
- data/lib/skr/core/testing/fixtures/skr/sku_vendors.yml +35 -0
- data/lib/skr/core/testing/fixtures/skr/skus.yml +50 -0
- data/lib/skr/core/testing/fixtures/skr/so_lines.yml +71 -0
- data/lib/skr/core/testing/fixtures/skr/uoms.yml +61 -0
- data/lib/skr/core/testing/fixtures/skr/vendors.yml +48 -0
- data/lib/skr/core/testing/fixtures/skr/vo_lines.yml +12 -0
- data/lib/skr/core/testing/fixtures/skr/vouchers.yml +8 -0
- data/lib/skr/core/testing/helper.rb +18 -0
- data/lib/skr/core/testing/test_case.rb +17 -0
- data/lib/skr/core/version.rb +5 -0
- data/lib/skr/customer.rb +34 -0
- data/lib/skr/gl_account.rb +56 -0
- data/lib/skr/gl_manual_entry.rb +31 -0
- data/lib/skr/gl_period.rb +13 -0
- data/lib/skr/gl_posting.rb +54 -0
- data/lib/skr/gl_transaction.rb +174 -0
- data/lib/skr/ia_line.rb +129 -0
- data/lib/skr/ia_reason.rb +16 -0
- data/lib/skr/inv_line.rb +90 -0
- data/lib/skr/inventory_adjustment.rb +60 -0
- data/lib/skr/invoice.rb +159 -0
- data/lib/skr/location.rb +31 -0
- data/lib/skr/model.rb +37 -0
- data/lib/skr/null_user.rb +30 -0
- data/lib/skr/payment_term.rb +30 -0
- data/lib/skr/pick_ticket.rb +71 -0
- data/lib/skr/po_line.rb +69 -0
- data/lib/skr/po_receipt.rb +51 -0
- data/lib/skr/por_line.rb +80 -0
- data/lib/skr/pt_line.rb +74 -0
- data/lib/skr/purchase_order.rb +112 -0
- data/lib/skr/sales_order.rb +159 -0
- data/lib/skr/sequential_id.rb +23 -0
- data/lib/skr/sku.rb +99 -0
- data/lib/skr/sku_loc.rb +94 -0
- data/lib/skr/sku_tran.rb +111 -0
- data/lib/skr/sku_vendor.rb +26 -0
- data/lib/skr/so_line.rb +159 -0
- data/lib/skr/uom.rb +63 -0
- data/lib/skr/user_proxy.rb +60 -0
- data/lib/skr/validators/all.rb +2 -0
- data/lib/skr/validators/email.rb +17 -0
- data/lib/skr/validators/set.rb +18 -0
- data/lib/skr/vendor.rb +33 -0
- data/lib/skr/vo_line.rb +35 -0
- data/lib/skr/voucher.rb +119 -0
- data/lib/stockor/core.rb +9 -0
- data/stockor-core.gemspec +37 -0
- data/tasks/migrations.rake +23 -0
- data/tasks/publish.rake +8 -0
- data/test/address_test.rb +57 -0
- data/test/concerns/attr_with_default_test.rb +45 -0
- data/test/concerns/code_identifier_test.rb +46 -0
- data/test/concerns/export_associations_test.rb +7 -0
- data/test/concerns/export_methods_test.rb +31 -0
- data/test/concerns/export_scope_test.rb +16 -0
- data/test/concerns/exported_limits_test.rb +47 -0
- data/test/concerns/json_attribute_access_test.rb +27 -0
- data/test/concerns/pub_sub_test.rb +83 -0
- data/test/concerns/sanitize_json_test.rb +47 -0
- data/test/core/configuration_test.rb +24 -0
- data/test/core/numbers_test.rb +26 -0
- data/test/core/strings_test.rb +41 -0
- data/test/customer_test.rb +34 -0
- data/test/gl_account_test.rb +23 -0
- data/test/gl_manual_entry_test.rb +17 -0
- data/test/gl_period_test.rb +12 -0
- data/test/gl_posting_test.rb +39 -0
- data/test/gl_transaction_test.rb +58 -0
- data/test/ia_line_test.rb +68 -0
- data/test/ia_reason_test.rb +11 -0
- data/test/inv_line_test.rb +58 -0
- data/test/inventory_adjustment_test.rb +72 -0
- data/test/invoice_test.rb +63 -0
- data/test/location_test.rb +10 -0
- data/test/payment_term_test.rb +25 -0
- data/test/pick_ticket_test.rb +27 -0
- data/test/po_line_test.rb +36 -0
- data/test/po_receipt_test.rb +93 -0
- data/test/por_line_test.rb +79 -0
- data/test/pt_line_test.rb +29 -0
- data/test/purchase_order_test.rb +44 -0
- data/test/sales_order_test.rb +74 -0
- data/test/sku_loc_test.rb +50 -0
- data/test/sku_test.rb +45 -0
- data/test/sku_tran_test.rb +21 -0
- data/test/sku_vendor_test.rb +13 -0
- data/test/so_line_test.rb +89 -0
- data/test/test_helper.rb +1 -0
- data/test/uom_test.rb +14 -0
- data/test/vendor_test.rb +35 -0
- data/test/vo_line_test.rb +20 -0
- data/test/voucher_test.rb +35 -0
- data/yard_ext/all.rb +9 -0
- data/yard_ext/code_identifier_handler.rb +33 -0
- data/yard_ext/concern_meta_methods.rb +60 -0
- data/yard_ext/config_options.rb +27 -0
- data/yard_ext/exported_scope.rb +4 -0
- data/yard_ext/immutable_handler.rb +17 -0
- data/yard_ext/json_attr_accessor.rb +22 -0
- data/yard_ext/locked_fields_handler.rb +21 -0
- data/yard_ext/templates/default/layout/html/layout.erb +20 -0
- data/yard_ext/templates/default/method_details/html/github_link.erb +1 -0
- data/yard_ext/templates/default/method_details/setup.rb +3 -0
- data/yard_ext/validators.rb +1 -0
- data/yard_ext/visible_id_handler.rb +38 -0
- metadata +448 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module Skr
|
|
2
|
+
|
|
3
|
+
# Is a record of an inventory receipt
|
|
4
|
+
# A {PurchaseOrder} can have one or more of them
|
|
5
|
+
|
|
6
|
+
class PoReceipt < Skr::Model
|
|
7
|
+
|
|
8
|
+
has_visible_id
|
|
9
|
+
has_sku_loc_lines
|
|
10
|
+
has_gl_transaction
|
|
11
|
+
|
|
12
|
+
belongs_to :purchase_order, export: true
|
|
13
|
+
belongs_to :vendor, export: true
|
|
14
|
+
belongs_to :location, export: true
|
|
15
|
+
|
|
16
|
+
has_one :gl_transaction, :as=>:source
|
|
17
|
+
|
|
18
|
+
has_many :lines, :class_name=>'Skr::PorLine', export: { writable: true }, inverse_of: :po_receipt
|
|
19
|
+
|
|
20
|
+
validates :freight, numericality: true
|
|
21
|
+
validates :purchase_order, :location, presence: true
|
|
22
|
+
|
|
23
|
+
before_create :record_freight, if: ->{ freight.nonzero? }
|
|
24
|
+
after_create :logit
|
|
25
|
+
|
|
26
|
+
def purchase_order=(po)
|
|
27
|
+
super
|
|
28
|
+
self.location ||= purchase_order.location
|
|
29
|
+
self.vendor = purchase_order.vendor
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def attributes_for_gl_transaction
|
|
35
|
+
{ location: location, source: self,
|
|
36
|
+
description: "PO RECPT #{self.visible_id}" }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def logit
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def record_freight
|
|
43
|
+
GlTransaction.current.add_posting( amount: self.freight,
|
|
44
|
+
debit: GlAccount.default_for( :inventory_receipts_clearing ),
|
|
45
|
+
credit: vendor.gl_freight_account
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
end
|
data/lib/skr/por_line.rb
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
module Skr
|
|
2
|
+
|
|
3
|
+
class PorLine < Skr::Model
|
|
4
|
+
|
|
5
|
+
acts_as_uom
|
|
6
|
+
is_sku_loc_line parent: 'po_receipt'
|
|
7
|
+
|
|
8
|
+
belongs_to :po_receipt
|
|
9
|
+
belongs_to :sku_loc, export: true
|
|
10
|
+
belongs_to :po_line, export: true
|
|
11
|
+
belongs_to :sku_vendor, export: true
|
|
12
|
+
|
|
13
|
+
has_one :sku, :through => :sku_loc, export: true
|
|
14
|
+
|
|
15
|
+
has_many :sku_trans, :as=>:origin, validate: true
|
|
16
|
+
|
|
17
|
+
before_create :adjust_inventory
|
|
18
|
+
# after_update :adjust_gl
|
|
19
|
+
validates :qty, numericality: { greater_than: 0 }
|
|
20
|
+
|
|
21
|
+
json_attr_accessor :auto_allocate
|
|
22
|
+
|
|
23
|
+
def po_line=(pol)
|
|
24
|
+
super
|
|
25
|
+
%w{ sku_code part_code description uom_code uom_size }.each do | attr |
|
|
26
|
+
self[ attr ] = po_line[ attr ]
|
|
27
|
+
end
|
|
28
|
+
self.sku_loc = pol.sku_loc
|
|
29
|
+
self.sku_vendor = pol.sku_vendor
|
|
30
|
+
self.uom = pol.uom
|
|
31
|
+
self.price = pol.price
|
|
32
|
+
self.qty = pol.qty_unreceived
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def adjust_inventory
|
|
38
|
+
Core.logger.debug( "Receiving #{self.ea_qty} into stock" )
|
|
39
|
+
tran = self.sku_trans.build({
|
|
40
|
+
origin: self, qty: self.qty,
|
|
41
|
+
sku_loc: po_line.sku_loc,
|
|
42
|
+
origin_description: "PO #{self.po_line.purchase_order.visible_id}",
|
|
43
|
+
cost: self.extended_price, uom: self.uom,
|
|
44
|
+
credit_gl_account: self.sku.gl_asset_account,
|
|
45
|
+
debit_gl_account: GlAccount.default_for( :inventory_receipts_clearing )
|
|
46
|
+
})
|
|
47
|
+
if self.auto_allocate
|
|
48
|
+
tran.allocate_after_save = true
|
|
49
|
+
end
|
|
50
|
+
true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def ensure_qty_is_less_than_unreceived
|
|
54
|
+
if qty_changed? && po_line && qty > po_line.qty_unreceived
|
|
55
|
+
errors.add(:qty,"#{qty} must be less than unreceived qty of #{po_line.qty_unreceived}")
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def adjust_gl
|
|
60
|
+
diff = price - price_was
|
|
61
|
+
return if diff.zero?
|
|
62
|
+
|
|
63
|
+
tran = self.sku_trans.build({ :origin=>self, :qty => 0, :sku_loc=>po_line.sku_loc,
|
|
64
|
+
:credit_gl_account => self.sku.gl_asset_account,
|
|
65
|
+
:uom_size=>self.uom_size, :uom_code=>self.uom_code })
|
|
66
|
+
|
|
67
|
+
tran.cost = diff * qty
|
|
68
|
+
|
|
69
|
+
tran.debit_gl_account = if voucher.confirmed?
|
|
70
|
+
voucher.vendor.gl_payables_account
|
|
71
|
+
else # otherwise it's in the clearing account
|
|
72
|
+
GlAccount.default_for( :inventory_receipts_clearing )
|
|
73
|
+
end
|
|
74
|
+
tran.save!
|
|
75
|
+
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
end
|
|
80
|
+
end
|
data/lib/skr/pt_line.rb
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module Skr
|
|
2
|
+
|
|
3
|
+
class PtLine < Skr::Model
|
|
4
|
+
|
|
5
|
+
acts_as_uom
|
|
6
|
+
is_sku_loc_line parent: 'pick_ticket'
|
|
7
|
+
|
|
8
|
+
attr_accessor :qty_to_ship
|
|
9
|
+
|
|
10
|
+
belongs_to :pick_ticket, export: true
|
|
11
|
+
belongs_to :sku_loc, export: true
|
|
12
|
+
belongs_to :so_line
|
|
13
|
+
|
|
14
|
+
has_one :inv_line, inverse_of: :pt_line, listen: { save: :update_from_inv_line }
|
|
15
|
+
has_one :sales_order, :through=>:pick_ticket, export: true
|
|
16
|
+
has_one :sku, :through => :sku_loc, export: true
|
|
17
|
+
|
|
18
|
+
scope :picking, ->{ where({ :is_complete=>false }) }
|
|
19
|
+
|
|
20
|
+
delegate_and_export_field :pick_ticket, :visible_id
|
|
21
|
+
delegate_and_export_field :sales_order, :visible_id
|
|
22
|
+
|
|
23
|
+
before_create :set_defaults#, on: :create
|
|
24
|
+
|
|
25
|
+
def cancel!
|
|
26
|
+
self.update_attributes! :is_complete=>true
|
|
27
|
+
self.pick_ticket.maybe_cancel
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def is_invoiceable?
|
|
31
|
+
! self.is_complete? && self.qty_to_ship.to_i > 0
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def total
|
|
35
|
+
self.price * ( self.qty_to_ship || self.qty )
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def update_from_inv_line( inv_line )
|
|
41
|
+
self.update_attributes( qty_invoiced: inv_line.qty )
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def set_defaults
|
|
45
|
+
|
|
46
|
+
if self.so_line.blank?
|
|
47
|
+
self.so_line = self.pick_ticket.sales_order.lines.where({ sku_loc_id: self.sku_loc_id }).first
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
if self.so_line.blank?
|
|
51
|
+
self.so_line = self.pick_ticket.sales_order.lines.create({
|
|
52
|
+
sku_loc: self.sku_loc, qty: self.qty, price: self.price,
|
|
53
|
+
uom: self.uom, description: self.description
|
|
54
|
+
})
|
|
55
|
+
unless self.so_line.valid?
|
|
56
|
+
self.errors.add(:so_line, "cannot be created because #{self.so_line.errors.full_messages.join(', ')}" )
|
|
57
|
+
return false
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
self.sku_loc ||= so_line.sku_loc
|
|
62
|
+
self.bin ||= sku_loc.bin
|
|
63
|
+
self.price ||= so_line.price
|
|
64
|
+
self.sku_code = sku_loc.sku.code if self.sku_code.blank?
|
|
65
|
+
self.description = so_line.description if self.description.blank?
|
|
66
|
+
self.uom = so_line.uom if self.uom.blank?
|
|
67
|
+
true
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
end # Skr module
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
module Skr
|
|
2
|
+
|
|
3
|
+
# A Purchase Order (often abbreviated as PO)
|
|
4
|
+
# is a record of a request to purchase a product from a Vendor.
|
|
5
|
+
# It contains one or more {Sku}, the quantity desired for each and the price offered for them.
|
|
6
|
+
#
|
|
7
|
+
# A Purchase Order progresses through set stages:
|
|
8
|
+
#
|
|
9
|
+
# * It starts of as "saved"
|
|
10
|
+
# This state indicates that the PO has been saved but no further action has been under-taken.
|
|
11
|
+
# * Once it has been sent to the Vendor, it transitions to Sent.
|
|
12
|
+
# * On reciept of a confirmation from the vendor it becomes Confirmed At this point the PO may be
|
|
13
|
+
# considered a binding agreement with the {Vendor}.
|
|
14
|
+
# * When the ordered SKUs are received, the PO will be marked as either Partial or Complete.
|
|
15
|
+
# * A {PoReceipt} is then created from the Purchase Order.
|
|
16
|
+
|
|
17
|
+
class PurchaseOrder < Skr::Model
|
|
18
|
+
|
|
19
|
+
has_visible_id
|
|
20
|
+
|
|
21
|
+
belongs_to :terms, export: true, class_name: 'Skr::PaymentTerm'
|
|
22
|
+
belongs_to :vendor, export: true
|
|
23
|
+
belongs_to :location, export: true
|
|
24
|
+
belongs_to :ship_addr, :class_name=>'Skr::Address', export: { writable: true }
|
|
25
|
+
|
|
26
|
+
has_many :lines, ->{ order(:position) }, :class_name=>'Skr::PoLine', :inverse_of=>:purchase_order, export: {writable:true}
|
|
27
|
+
has_many :receipts, class_name: 'Skr::PoReceipt'
|
|
28
|
+
|
|
29
|
+
before_validation :set_defaults, :on=>:create
|
|
30
|
+
|
|
31
|
+
validates :vendor, :location, :terms, :presence=>true
|
|
32
|
+
|
|
33
|
+
delegate_and_export :vendor_code, :vendor_name
|
|
34
|
+
delegate_and_export :location_code
|
|
35
|
+
delegate_and_export :terms_code, :terms_description
|
|
36
|
+
|
|
37
|
+
export_methods :total
|
|
38
|
+
|
|
39
|
+
blacklist_json_attributes :receiving_completed_at
|
|
40
|
+
|
|
41
|
+
after_save :set_maybe_completed!
|
|
42
|
+
|
|
43
|
+
export_join_tables :details
|
|
44
|
+
export_scope :with_details, lambda { |should_use=true |
|
|
45
|
+
joins('join po_details as details on details.purchase_order_id = purchase_orders.id')
|
|
46
|
+
.select('purchase_orders.*, details.*') if should_use
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export_scope :only_incoming, lambda { |should_use=true|
|
|
50
|
+
with_details.where("state !='received'") if should_use
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
state_machine do
|
|
54
|
+
state :open, initial: true
|
|
55
|
+
state :received
|
|
56
|
+
|
|
57
|
+
event :mark_received do
|
|
58
|
+
transitions from: :open, to: :received
|
|
59
|
+
before do
|
|
60
|
+
self.receiving_completed_at = Time.now
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def after_message_transmitted( msg )
|
|
66
|
+
self.mark_transmitted if self.can_mark_transmitted?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def set_maybe_completed!
|
|
70
|
+
self.mark_received! unless received? or self.lines.incomplete.any?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def to_shipping_address
|
|
74
|
+
Address.copy_from(self,'ship_')
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def set_message_fields( msg )
|
|
78
|
+
msg.recipient = self.vendor.payments_address.email_with_name if msg.recipient.blank?
|
|
79
|
+
msg.subject = "Purchase Order # #{self.visible_id}"
|
|
80
|
+
attach = msg.attachments.build
|
|
81
|
+
attach.set_type_to_pdf
|
|
82
|
+
pdf = self.as_pdf
|
|
83
|
+
attach.file = pdf
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def as_pdf( opts={} )
|
|
87
|
+
defaults = { :purchase_order => self, :include_received=>false }
|
|
88
|
+
Latex::Runner.new( 'purchase_order', defaults.merge( opts ) ).pdf( "PurchaseOrder_#{self.visible_id}.pdf" )
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def total
|
|
92
|
+
if total = self.read_attribute('total')
|
|
93
|
+
BigDecimal.new(total)
|
|
94
|
+
elsif self.association(:lines).loaded?
|
|
95
|
+
self.lines.inject(0){|sum,line| sum + line.total }
|
|
96
|
+
else
|
|
97
|
+
BigDecimal.new( self.lines.sum('price*qty') )
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def set_defaults
|
|
104
|
+
self.location ||= Location.default
|
|
105
|
+
self.order_date ||= Date.today
|
|
106
|
+
self.terms ||= self.vendor.terms if self.vendor
|
|
107
|
+
self.ship_addr = self.location.address if self.ship_addr.blank? && self.location
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
end # Skr module
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
module Skr
|
|
2
|
+
|
|
3
|
+
# A SalesOrder is a record of a {Customer}'s desire to purchase one or more {Sku}s.
|
|
4
|
+
# It can be converted into an {Invoice} when the goods are delivered (or shipped)
|
|
5
|
+
# to the {Customer}
|
|
6
|
+
#
|
|
7
|
+
# customer = Customer.find_by_code "VIP1"
|
|
8
|
+
# so = SalesOrder.new( customer: customer )
|
|
9
|
+
# Sku.where( code: ['HAT','STRING'] ).each do | sku |
|
|
10
|
+
# so.lines.build( sku_loc: sku.sku_locs.default )
|
|
11
|
+
# end
|
|
12
|
+
# so.save
|
|
13
|
+
#
|
|
14
|
+
# invoice = Invoice.new( sales_order: so )
|
|
15
|
+
# invoice.lines.from_sales_order!
|
|
16
|
+
# invoice.save
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SalesOrder < Skr::Model
|
|
20
|
+
|
|
21
|
+
has_visible_id
|
|
22
|
+
has_random_hash_code
|
|
23
|
+
is_order_like
|
|
24
|
+
|
|
25
|
+
belongs_to :customer, export: true
|
|
26
|
+
belongs_to :location, export: true
|
|
27
|
+
belongs_to :terms, class_name: 'Skr::PaymentTerm', export: { writable: true }
|
|
28
|
+
belongs_to :billing_address, class_name: 'Skr::Address', export: { writable: true }
|
|
29
|
+
belongs_to :shipping_address, class_name: 'Skr::Address', export: { writable: true }
|
|
30
|
+
|
|
31
|
+
has_many :lines, ->{ order(:position) }, :class_name=>'Skr::SoLine', :inverse_of=>:sales_order,
|
|
32
|
+
extend: Concerns::SO::Lines, export: { writable: true }
|
|
33
|
+
has_many :skus, through: :lines
|
|
34
|
+
has_many :pick_tickets, inverse_of: :sales_order, before_add: :setup_new_pt
|
|
35
|
+
has_many :invoices, inverse_of: :sales_order, listen: { save: 'on_invoice' }
|
|
36
|
+
|
|
37
|
+
validates :location, :terms, :customer, set: true
|
|
38
|
+
validates :billing_address, :shipping_address, :order_date, presence: true
|
|
39
|
+
validate :ensure_location_changes_are_valid
|
|
40
|
+
|
|
41
|
+
after_save :check_if_location_changed
|
|
42
|
+
before_validation :set_defaults, on: :create
|
|
43
|
+
|
|
44
|
+
delegate_and_export :customer_code, :customer_name
|
|
45
|
+
delegate_and_export :location_code, :location_name
|
|
46
|
+
delegate_and_export :terms_code, :terms_description
|
|
47
|
+
delegate_and_export :billing_address_name
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# joins the so_amount_details view which includes additional fields:
|
|
51
|
+
# customer_code, customer_name, bill_addr_name, total, num_lines, total_other_charge_amount,
|
|
52
|
+
# total_tax_amount, total_shipping_amount,subtotal_amount
|
|
53
|
+
scope :with_amount_details, lambda { | *args |
|
|
54
|
+
compose_query_using_detail_view(view: 'so_amount_details', join_to: 'sales_order_id')
|
|
55
|
+
}, export: true
|
|
56
|
+
|
|
57
|
+
# joins the so_allocation_details which includes the additional fields:
|
|
58
|
+
# number_of_lines, allocated_total, number_of_lines_allocated, number_of_lines_fully_allocated
|
|
59
|
+
scope :with_allocation_details, lambda {
|
|
60
|
+
compose_query_using_detail_view(view: 'so_allocation_details', join_to: 'sales_order_id')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# a open SalesOrder is one who's state is not "complete" or "canceled"
|
|
64
|
+
scope :open, lambda { | *args |
|
|
65
|
+
where( arel_table[:state].not_in ['complete', 'canceled'] )
|
|
66
|
+
}, export: true
|
|
67
|
+
|
|
68
|
+
# a SalesOrder is allocated if it has one or more lines with qty_allocated>0
|
|
69
|
+
scope :allocated, lambda { | *unused |
|
|
70
|
+
with_allocation_details.where('details.number_of_lines_allocated>0')
|
|
71
|
+
}, export: true
|
|
72
|
+
|
|
73
|
+
# a SalesOrder is fully allocated when it has all it's lines allocated
|
|
74
|
+
scope :fully_allocated, -> {
|
|
75
|
+
allocated.where('details.number_of_lines=details.number_of_lines_allocated')
|
|
76
|
+
}, export: true
|
|
77
|
+
|
|
78
|
+
# a SalesOrder is considered pickable if either:
|
|
79
|
+
# ship_partial=true and at least one line is allocated
|
|
80
|
+
# all lines are fully allocated
|
|
81
|
+
scope :pickable, ->(unused=nil){
|
|
82
|
+
allocated.where("( ship_partial='t' and details.number_of_lines_allocated > 0 ) " \
|
|
83
|
+
" or ( details.number_of_lines=details.number_of_lines_fully_allocated)")
|
|
84
|
+
}, export: true
|
|
85
|
+
|
|
86
|
+
# @return [Array of Array[day_ago,date, order_count,line_count,total]]
|
|
87
|
+
def self.sales_history( ndays )
|
|
88
|
+
qry = "select * from #{Skr::Core.config.table_prefix}so_dailly_sales_history where days_ago<#{ndays.to_i}"
|
|
89
|
+
connection.execute(qry).values
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
state_machine do
|
|
93
|
+
state :open, initial: true
|
|
94
|
+
state :complete
|
|
95
|
+
state :canceled
|
|
96
|
+
|
|
97
|
+
event :mark_complete do
|
|
98
|
+
transitions from: :open, to: :complete
|
|
99
|
+
end
|
|
100
|
+
event :mark_canceled do
|
|
101
|
+
transitions from: :open, to: :canceled
|
|
102
|
+
before :cancel_all_lines
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def initialize(attributes = {})
|
|
107
|
+
super
|
|
108
|
+
self.order_date = Date.today
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
# When the location changes, lines need to have their sku_loc modified to point to the new location as well
|
|
114
|
+
def check_if_location_changed
|
|
115
|
+
if location_id_changed?
|
|
116
|
+
self.lines.each{ |l| l.location = self.location }
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# The location can only be updated if all the line's skus are setup in the new location
|
|
121
|
+
def ensure_location_changes_are_valid
|
|
122
|
+
return true unless changes['location_id']
|
|
123
|
+
errors.add(:location, 'cannot be changed unless sales order is open') unless open?
|
|
124
|
+
current = self.sku_ids
|
|
125
|
+
setup = location.sku_locs.where( sku_id: current ).pluck('sku_id')
|
|
126
|
+
missing = current - setup
|
|
127
|
+
if missing.any?
|
|
128
|
+
codes = Sku.where( id: missing ).pluck('code')
|
|
129
|
+
errors.add(:location, "#{location.code} does not have skus #{codes.join(',')}")
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Initialize a new {PickTicket} by copying the pickable lines to it
|
|
134
|
+
def setup_new_pt(pt)
|
|
135
|
+
self.lines.each do | so_line |
|
|
136
|
+
pt.lines << so_line.pt_lines.build if so_line.pickable_qty > 0
|
|
137
|
+
end
|
|
138
|
+
true
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# when the order is canceled, inform the lines
|
|
142
|
+
def cancel_all_lines
|
|
143
|
+
self.pick_tickets.each{ |pt| pt.cancel! }
|
|
144
|
+
self.lines.each{ | soline | soline.cancel! }
|
|
145
|
+
true
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def on_invoice(inv)
|
|
149
|
+
self.mark_complete! if may_mark_complete? and lines.unshipped.none?
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def set_defaults
|
|
153
|
+
if customer
|
|
154
|
+
self.billing_address = customer.billing_address if self.billing_address.blank?
|
|
155
|
+
self.shipping_address = customer.shipping_address if self.shipping_address.blank?
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end # Skr module
|