stockor 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +220 -0
- data/Guardfile +13 -0
- data/README.md +7 -0
- data/Rakefile +7 -0
- data/client/skr/Extension.coffee +12 -0
- data/client/skr/components/.gitkeep +0 -0
- data/client/skr/components/address/Address.coffee +21 -0
- data/client/skr/components/address/address.html +20 -0
- data/client/skr/index.js +21 -0
- data/client/skr/models/Address.coffee +17 -0
- data/client/skr/models/Base.coffee +8 -0
- data/client/skr/models/Customer.coffee +26 -0
- data/client/skr/models/GlAccount.coffee +10 -0
- data/client/skr/models/GlManualEntry.coffee +11 -0
- data/client/skr/models/GlPeriod.coffee +10 -0
- data/client/skr/models/GlPosting.coffee +15 -0
- data/client/skr/models/GlTransaction.coffee +16 -0
- data/client/skr/models/IaLine.coffee +19 -0
- data/client/skr/models/IaReason.coffee +12 -0
- data/client/skr/models/InvLine.coffee +27 -0
- data/client/skr/models/InventoryAdjustment.coffee +17 -0
- data/client/skr/models/Invoice.coffee +31 -0
- data/client/skr/models/Location.coffee +15 -0
- data/client/skr/models/PaymentTerm.coffee +11 -0
- data/client/skr/models/PickTicket.coffee +19 -0
- data/client/skr/models/PoLine.coffee +27 -0
- data/client/skr/models/PoReceipt.coffee +20 -0
- data/client/skr/models/PorLine.coffee +26 -0
- data/client/skr/models/PtLine.coffee +27 -0
- data/client/skr/models/PurchaseOrder.coffee +23 -0
- data/client/skr/models/SalesOrder.coffee +32 -0
- data/client/skr/models/Sku.coffee +21 -0
- data/client/skr/models/SkuLoc.coffee +21 -0
- data/client/skr/models/SkuTran.coffee +23 -0
- data/client/skr/models/SkuVendor.coffee +19 -0
- data/client/skr/models/SoLine.coffee +27 -0
- data/client/skr/models/Uom.coffee +17 -0
- data/client/skr/models/Vendor.coffee +28 -0
- data/client/skr/models/VoLine.coffee +23 -0
- data/client/skr/models/Voucher.coffee +22 -0
- data/client/skr/models/mixins/CodeField.coffee +5 -0
- data/client/skr/screens/.gitkeep +0 -0
- data/client/skr/screens/Base.coffee +3 -0
- data/client/skr/screens/base/index.js +5 -0
- data/client/skr/screens/base/index.scss +9 -0
- data/client/skr/screens/base/layout.html +3 -0
- data/client/skr/screens/customer-maint/CustomerMaint.coffee +49 -0
- data/client/skr/screens/customer-maint/index.js +5 -0
- data/client/skr/screens/customer-maint/index.scss +9 -0
- data/client/skr/screens/customer-maint/layout.html +32 -0
- data/client/skr/styles.scss +1 -0
- data/client/skr/views/.gitkeep +0 -0
- data/client/skr/views/Base.coffee +5 -0
- data/config/database.yml +9 -0
- data/config/lanes.rb +7 -0
- data/config/routes.rb +39 -0
- data/config/screens.rb +17 -0
- data/config.ru +5 -0
- data/db/.gitkeep +0 -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/db/schema.sql +2662 -0
- data/db/seed/chart_of_accounts.yml +168 -0
- data/db/seed/payment_terms.yml +60 -0
- data/db/seed.rb +29 -0
- data/lib/skr/access_roles.rb +28 -0
- data/lib/skr/concerns/acts_as_uom.rb +47 -0
- data/lib/skr/concerns/code_identifier.rb +43 -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/locked_fields.rb +84 -0
- data/lib/skr/concerns/pt_extensions.rb +22 -0
- data/lib/skr/concerns/random_hash_code.rb +40 -0
- data/lib/skr/concerns/so_extensions.rb +30 -0
- data/lib/skr/concerns/state_machine.rb +61 -0
- data/lib/skr/concerns/visible_id_identifier.rb +53 -0
- data/lib/skr/configuration.rb +68 -0
- data/lib/skr/db/migration_helpers.rb +178 -0
- data/lib/skr/extension.rb +23 -0
- data/lib/skr/model.rb +19 -0
- data/lib/skr/models/address.rb +97 -0
- data/lib/skr/models/business_entity.rb +29 -0
- data/lib/skr/models/customer.rb +35 -0
- data/lib/skr/models/gl_account.rb +56 -0
- data/lib/skr/models/gl_manual_entry.rb +31 -0
- data/lib/skr/models/gl_period.rb +13 -0
- data/lib/skr/models/gl_posting.rb +54 -0
- data/lib/skr/models/gl_transaction.rb +175 -0
- data/lib/skr/models/ia_line.rb +129 -0
- data/lib/skr/models/ia_reason.rb +16 -0
- data/lib/skr/models/inv_line.rb +90 -0
- data/lib/skr/models/inventory_adjustment.rb +60 -0
- data/lib/skr/models/invoice.rb +159 -0
- data/lib/skr/models/location.rb +31 -0
- data/lib/skr/models/payment_term.rb +30 -0
- data/lib/skr/models/pick_ticket.rb +71 -0
- data/lib/skr/models/po_line.rb +69 -0
- data/lib/skr/models/po_receipt.rb +51 -0
- data/lib/skr/models/por_line.rb +80 -0
- data/lib/skr/models/pt_line.rb +74 -0
- data/lib/skr/models/purchase_order.rb +112 -0
- data/lib/skr/models/sales_order.rb +159 -0
- data/lib/skr/models/sequential_id.rb +23 -0
- data/lib/skr/models/sku.rb +99 -0
- data/lib/skr/models/sku_loc.rb +94 -0
- data/lib/skr/models/sku_tran.rb +111 -0
- data/lib/skr/models/sku_vendor.rb +26 -0
- data/lib/skr/models/so_line.rb +159 -0
- data/lib/skr/models/uom.rb +63 -0
- data/lib/skr/models/user_proxy.rb +60 -0
- data/lib/skr/models/vendor.rb +33 -0
- data/lib/skr/models/vo_line.rb +35 -0
- data/lib/skr/models/voucher.rb +119 -0
- data/lib/skr/standard_pricing_provider.rb +14 -0
- data/lib/skr/version.rb +3 -0
- data/lib/skr.rb +18 -0
- data/lib/stockor.rb +4 -0
- data/lib/tasks/debug-activity.rake +58 -0
- data/log/test.log +0 -0
- data/spec/fixtures/skr/address.yml +2 -0
- data/spec/fixtures/skr/customer.yml +2 -0
- data/spec/fixtures/skr/gl_account.yml +2 -0
- data/spec/fixtures/skr/gl_manual_entry.yml +2 -0
- data/spec/fixtures/skr/gl_period.yml +2 -0
- data/spec/fixtures/skr/gl_posting.yml +2 -0
- data/spec/fixtures/skr/gl_transaction.yml +2 -0
- data/spec/fixtures/skr/ia_line.yml +2 -0
- data/spec/fixtures/skr/ia_reason.yml +2 -0
- data/spec/fixtures/skr/inv_line.yml +2 -0
- data/spec/fixtures/skr/inventory_adjustment.yml +2 -0
- data/spec/fixtures/skr/invoice.yml +2 -0
- data/spec/fixtures/skr/location.yml +2 -0
- data/spec/fixtures/skr/payment_term.yml +2 -0
- data/spec/fixtures/skr/pick_ticket.yml +2 -0
- data/spec/fixtures/skr/po_line.yml +2 -0
- data/spec/fixtures/skr/po_receipt.yml +2 -0
- data/spec/fixtures/skr/por_line.yml +2 -0
- data/spec/fixtures/skr/pt_line.yml +2 -0
- data/spec/fixtures/skr/purchase_order.yml +2 -0
- data/spec/fixtures/skr/sales_order.yml +2 -0
- data/spec/fixtures/skr/sku.yml +2 -0
- data/spec/fixtures/skr/sku_loc.yml +2 -0
- data/spec/fixtures/skr/sku_tran.yml +2 -0
- data/spec/fixtures/skr/sku_vendor.yml +2 -0
- data/spec/fixtures/skr/so_line.yml +2 -0
- data/spec/fixtures/skr/uom.yml +2 -0
- data/spec/fixtures/skr/vendor.yml +2 -0
- data/spec/fixtures/skr/vo_line.yml +2 -0
- data/spec/fixtures/skr/voucher.yml +2 -0
- data/spec/skr/address.rb +10 -0
- data/spec/skr/concerns/code_identifier_spec.rb +45 -0
- data/spec/skr/customer.rb +10 -0
- data/spec/skr/gl_account.rb +10 -0
- data/spec/skr/gl_manual_entry.rb +10 -0
- data/spec/skr/gl_period.rb +10 -0
- data/spec/skr/gl_posting.rb +10 -0
- data/spec/skr/gl_transaction.rb +10 -0
- data/spec/skr/ia_line.rb +10 -0
- data/spec/skr/ia_reason.rb +10 -0
- data/spec/skr/inv_line.rb +10 -0
- data/spec/skr/inventory_adjustment.rb +10 -0
- data/spec/skr/invoice.rb +10 -0
- data/spec/skr/location.rb +10 -0
- data/spec/skr/models/AddressSpec.coffee +5 -0
- data/spec/skr/models/CustomerSpec.coffee +5 -0
- data/spec/skr/models/GlAccountSpec.coffee +5 -0
- data/spec/skr/models/GlManualEntrySpec.coffee +5 -0
- data/spec/skr/models/GlPeriodSpec.coffee +5 -0
- data/spec/skr/models/GlPostingSpec.coffee +5 -0
- data/spec/skr/models/GlTransactionSpec.coffee +5 -0
- data/spec/skr/models/IaLineSpec.coffee +5 -0
- data/spec/skr/models/IaReasonSpec.coffee +5 -0
- data/spec/skr/models/InvLineSpec.coffee +5 -0
- data/spec/skr/models/InventoryAdjustmentSpec.coffee +5 -0
- data/spec/skr/models/InvoiceSpec.coffee +5 -0
- data/spec/skr/models/LocationSpec.coffee +5 -0
- data/spec/skr/models/PaymentTermSpec.coffee +5 -0
- data/spec/skr/models/PickTicketSpec.coffee +5 -0
- data/spec/skr/models/PoLineSpec.coffee +5 -0
- data/spec/skr/models/PoReceiptSpec.coffee +5 -0
- data/spec/skr/models/PorLineSpec.coffee +5 -0
- data/spec/skr/models/PtLineSpec.coffee +5 -0
- data/spec/skr/models/PurchaseOrderSpec.coffee +5 -0
- data/spec/skr/models/SalesOrderSpec.coffee +5 -0
- data/spec/skr/models/SkuLocSpec.coffee +5 -0
- data/spec/skr/models/SkuSpec.coffee +5 -0
- data/spec/skr/models/SkuTranSpec.coffee +5 -0
- data/spec/skr/models/SkuVendorSpec.coffee +5 -0
- data/spec/skr/models/SoLineSpec.coffee +5 -0
- data/spec/skr/models/UomSpec.coffee +5 -0
- data/spec/skr/models/VendorSpec.coffee +5 -0
- data/spec/skr/models/VoLineSpec.coffee +5 -0
- data/spec/skr/models/VoucherSpec.coffee +5 -0
- data/spec/skr/payment_term.rb +10 -0
- data/spec/skr/pick_ticket.rb +10 -0
- data/spec/skr/po_line.rb +10 -0
- data/spec/skr/po_receipt.rb +10 -0
- data/spec/skr/por_line.rb +10 -0
- data/spec/skr/pt_line.rb +10 -0
- data/spec/skr/purchase_order.rb +10 -0
- data/spec/skr/sales_order.rb +10 -0
- data/spec/skr/screens/Base.coffee +7 -0
- data/spec/skr/screens/CustomerMaint.coffee +7 -0
- data/spec/skr/screens/vendor-maint/VendorMaintSpec.coffee +5 -0
- data/spec/skr/sku.rb +10 -0
- data/spec/skr/sku_loc.rb +10 -0
- data/spec/skr/sku_tran.rb +10 -0
- data/spec/skr/sku_vendor.rb +10 -0
- data/spec/skr/so_line.rb +10 -0
- data/spec/skr/spec_helper.rb +26 -0
- data/spec/skr/uom.rb +10 -0
- data/spec/skr/vendor.rb +10 -0
- data/spec/skr/views/AddressSpec.coffee +5 -0
- data/spec/skr/vo_line.rb +10 -0
- data/spec/skr/voucher.rb +10 -0
- data/stockor.gemspec +38 -0
- data/tmp/.gitkeep +0 -0
- metadata +414 -0
@@ -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.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
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Skr
|
2
|
+
class SequentialId < Skr::Model
|
3
|
+
FUNCTION_NAME="#{Skr.config.table_prefix}next_sequential_id"
|
4
|
+
|
5
|
+
self.primary_key = 'name'
|
6
|
+
|
7
|
+
locked_fields :name, :current_value
|
8
|
+
|
9
|
+
def self.next_for( klass )
|
10
|
+
begin
|
11
|
+
res=ActiveRecord::Base.connection.raw_connection.exec( "select #{FUNCTION_NAME}( $1 )", [ klass.to_s ] )
|
12
|
+
res.getvalue(0,0).to_i
|
13
|
+
ensure
|
14
|
+
res.clear if res
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.set_next( klass, value )
|
19
|
+
self.connection.raw_connection.exec( "update #{table_name} set current_value = $1 where name = $2", [ value, klass.to_s ] )
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Skr
|
2
|
+
|
3
|
+
#### A (S)tock (K)eeping (U)nit (SKU) is the cornerstone of Stockor
|
4
|
+
#
|
5
|
+
# At it's simplest form a SKU tracks a *resource* that the company controlls.
|
6
|
+
# It can be manufactured (by combining other SKUs), purchased, stored, and sold.
|
7
|
+
#
|
8
|
+
# Although SKUs usually refer to physical item, it may also track intangibles
|
9
|
+
# such as "Labor", "Handling", or "Freight"
|
10
|
+
#
|
11
|
+
class Sku < Skr::Model
|
12
|
+
|
13
|
+
has_code_identifier :from=>'description'
|
14
|
+
|
15
|
+
belongs_to :default_vendor, class_name: 'Skr::Vendor', export: true
|
16
|
+
belongs_to :gl_asset_account, class_name: 'Skr::GlAccount', export: true
|
17
|
+
belongs_to :default_vendor, class_name: 'Skr::Vendor', export: true
|
18
|
+
|
19
|
+
has_many :sku_locs, ->{extending Concerns::Sku::Locations },
|
20
|
+
inverse_of: :sku, dependent: :destroy,
|
21
|
+
export: { writable:true }
|
22
|
+
|
23
|
+
has_many :sku_vendors, ->{ extending Concerns::Sku::Vendors },
|
24
|
+
dependent: :destroy, inverse_of: :sku,
|
25
|
+
export: { writable: true, allow_destroy: true }
|
26
|
+
|
27
|
+
has_many :uoms, ->{ order(:size); extending(Concerns::Sku::Uoms) },
|
28
|
+
dependent: :destroy, inverse_of: :sku,
|
29
|
+
export: { writable: true, allow_destroy: true }
|
30
|
+
|
31
|
+
validates :uoms, presence: true, :on => :update
|
32
|
+
validates :description, presence: true
|
33
|
+
validates :gl_asset_account, set: true
|
34
|
+
validates :default_vendor, set: true
|
35
|
+
validates :default_uom_code, presence: true, :on => :update
|
36
|
+
validate :ensure_default_uom_exists
|
37
|
+
|
38
|
+
before_validation :set_defaults, :on=>:create
|
39
|
+
after_create :create_associated_records
|
40
|
+
|
41
|
+
scope :with_vendor_part_code, lambda { | vendor_sku |
|
42
|
+
joins(:sku_vendors).where( SkuVendor.arel_table[:part_code].matches( vendor_sku ) )
|
43
|
+
}, :export=>true
|
44
|
+
|
45
|
+
scope :in_location, lambda { | location_id |
|
46
|
+
joins(:sku_locs).where( SkuLoc.table_name => { location_id: location_id } )
|
47
|
+
}, :export=>true
|
48
|
+
|
49
|
+
scope :with_qty_details, lambda { | *args |
|
50
|
+
compose_query_using_detail_view( view: 'sku_qty_details', join_to: 'sku_id' )
|
51
|
+
}, export: { :join_table=>:details }
|
52
|
+
|
53
|
+
scope :only_back_ordered, lambda{ | *args |
|
54
|
+
with_qty_details.where("details.qty_on_order > details.qty_on_hand")
|
55
|
+
}, export: true
|
56
|
+
|
57
|
+
|
58
|
+
# Rebuilding is sometimes needed for cases where the location's
|
59
|
+
# allocation/on order/reserved counts get out of sync with the
|
60
|
+
# SalesOrder counts. This forces recalculation of the cached values
|
61
|
+
def rebuild!
|
62
|
+
sku_locs.each(&:rebuild!)
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
# If the default uom code was changed, make sure the UOM
|
68
|
+
# is actually present on the uoms list
|
69
|
+
def ensure_default_uom_exists
|
70
|
+
if default_uom_code_changed? && uoms.default.nil?
|
71
|
+
errors.add( :default_uom_code, "does not exist on UOMs" )
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Setup the associations after create
|
76
|
+
def create_associated_records
|
77
|
+
if sku_locs.empty?
|
78
|
+
self.sku_locs.create({ sku: self, location: Location.default })
|
79
|
+
end
|
80
|
+
true # don't cancel save op
|
81
|
+
end
|
82
|
+
|
83
|
+
# Set the default values for the Sku if they are not present
|
84
|
+
def set_defaults
|
85
|
+
if self.default_vendor.blank? && self.sku_vendors.any?
|
86
|
+
self.default_vendor = self.sku_vendors.at(0).vendor
|
87
|
+
end
|
88
|
+
self.uoms << Uom.ea if self.uoms.empty?
|
89
|
+
|
90
|
+
self.can_backorder = Skr.config.skus_backorder_default if self.can_backorder.nil?
|
91
|
+
self.gl_asset_account ||= GlAccount.default_for(:asset)
|
92
|
+
self.default_uom_code ||= self.uoms.first.code
|
93
|
+
|
94
|
+
true # don't cancel save op
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
end # Skr module
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Skr
|
2
|
+
|
3
|
+
# Next to the {Sku} class, SkuLoc is the second most integral model in Stockor. It tracks
|
4
|
+
# which Skus are setup in each location and the related quantity information about them
|
5
|
+
#
|
6
|
+
# It is also the model that is linked to by other models that need to refer to a sku's location
|
7
|
+
# such as the lines on {PurchaseOrder}, Quotes, {SalesOrder}, PickTickets, and Invoices
|
8
|
+
class SkuLoc < Skr::Model
|
9
|
+
|
10
|
+
belongs_to :sku, export: true
|
11
|
+
belongs_to :location, export: true
|
12
|
+
|
13
|
+
has_many :so_lines, inverse_of: :sku_loc, extend: Concerns::SO::Lines, listen: { qty_change: :update_so_qty }
|
14
|
+
has_many :pt_lines, :inverse_of=>:sku_loc, extend: Concerns::PT::Lines, listen: { save: :update_qty_picking }
|
15
|
+
|
16
|
+
has_many :sku_vendors, :primary_key=>:sku_id, :foreign_key=>:sku_id
|
17
|
+
|
18
|
+
delegate_and_export :location_name, :location_code
|
19
|
+
delegate_and_export :sku_code, :sku_description
|
20
|
+
|
21
|
+
validates :mac, numericality: true
|
22
|
+
validates :sku, :location, presence: true
|
23
|
+
validates :sku, uniqueness: { scope: :location_id, message: "SKU may not be in the same location twice" }
|
24
|
+
|
25
|
+
export_methods :qty_available
|
26
|
+
|
27
|
+
locked_fields :qty, :mac
|
28
|
+
|
29
|
+
has_additional_events :qty_change
|
30
|
+
|
31
|
+
# @return [BigDecimal] the value of inventory for {Sku} in this {Location}
|
32
|
+
def onhand_mac_value
|
33
|
+
qty*mac
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [Fixnum] the qty that is not allocated, picking or reserved
|
37
|
+
def qty_available
|
38
|
+
qty - qty_allocated - qty_picking - qty_reserved
|
39
|
+
end
|
40
|
+
|
41
|
+
# Adjust the on hand qty. Can only be called while qty is unlocked
|
42
|
+
# @example
|
43
|
+
# sl = SkuLoc.first
|
44
|
+
# sl.unlock_fields( :qty ) do
|
45
|
+
# sl.adjust_qty( 10 )
|
46
|
+
# sl.save!
|
47
|
+
# end
|
48
|
+
# @param [Fixnum] qty the amount to adjust the onhand qty by
|
49
|
+
# @return [Fixnum] new qty on hand
|
50
|
+
def adjust_qty( qty )
|
51
|
+
self.qty += qty
|
52
|
+
end
|
53
|
+
|
54
|
+
# Rebuilding is sometimes needed for cases where the location's
|
55
|
+
# allocation/on order/reserved counts get out of sync with the
|
56
|
+
# SalesOrder counts. This forces recalculation of the cached values
|
57
|
+
def rebuild!
|
58
|
+
self.update_attributes({
|
59
|
+
qty_picking: pt_lines.pt_lines.picking_qty,
|
60
|
+
qty_allocated: self.so_lines.open.allocated.eq_qty_allocated
|
61
|
+
})
|
62
|
+
end
|
63
|
+
|
64
|
+
# Allocate the maximum available quantity to {SalesOrder}
|
65
|
+
# that are not currrently allocated
|
66
|
+
def allocate_available_qty!
|
67
|
+
update_so_qty
|
68
|
+
so_lines.unallocated.order(:created_at).each do | sol |
|
69
|
+
sol.sku_loc = self
|
70
|
+
sol.allocate_max_available
|
71
|
+
sol.save
|
72
|
+
break if qty_allocated <= 0
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def fire_after_save_events
|
79
|
+
fire_event(:qty_change) if qty_changed?
|
80
|
+
end
|
81
|
+
|
82
|
+
# Caches the qty of skus that are allocated to sales orders in the {#qty_allocated} field
|
83
|
+
def update_so_qty( so_line=nil )
|
84
|
+
self.update_attributes({ qty_allocated: self.so_lines.open.allocated.eq_qty_allocated })
|
85
|
+
end
|
86
|
+
|
87
|
+
def update_qty_picking( pt=nil )
|
88
|
+
update_attributes( :qty_picking=> self.pt_lines.picking.ea_picking_qty )
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
end # Skr module
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module Skr
|
2
|
+
|
3
|
+
# In Stockor, inventory related transactions are not performed directly on the model(s)
|
4
|
+
#
|
5
|
+
# Instead a SkuTran is created, and it is responsible for adjusting
|
6
|
+
# either the cost or qty of the inventory. By doing so, all inventory
|
7
|
+
# changes is logged and can be referred to in order to audit
|
8
|
+
# changes.
|
9
|
+
class SkuTran < Skr::Model
|
10
|
+
|
11
|
+
acts_as_uom
|
12
|
+
|
13
|
+
is_immutable
|
14
|
+
|
15
|
+
belongs_to :sku_loc
|
16
|
+
has_one :location, :through => :sku_loc
|
17
|
+
|
18
|
+
belongs_to :origin, :polymorphic=>true
|
19
|
+
|
20
|
+
has_one :gl_transaction, :as=>:source, :inverse_of=>:source
|
21
|
+
|
22
|
+
validates :sku_loc, :set=>true
|
23
|
+
validates :origin_description, :presence=>true
|
24
|
+
validates :prior_mac, :numericality=>true
|
25
|
+
|
26
|
+
validate :ensure_cost_and_qty_present
|
27
|
+
|
28
|
+
attr_accessor :credit_gl_account
|
29
|
+
attr_accessor :debit_gl_account
|
30
|
+
attr_accessor :gl_tran_description_text
|
31
|
+
|
32
|
+
after_save :adjust_sku_loc_values
|
33
|
+
after_save :create_needed_gl_transaction
|
34
|
+
before_save :calculate_mac
|
35
|
+
|
36
|
+
attr_accessor :allocate_after_save
|
37
|
+
|
38
|
+
# @param sl [SkuLoc] set's the sku loc and also sets {#prior_qty} and {#prior_mac}
|
39
|
+
def sku_loc=(sl)
|
40
|
+
super
|
41
|
+
self.prior_qty = sl.qty
|
42
|
+
self.prior_mac = sl.mac
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return [Fixnum] {#qty} expressed in terms of single UOM
|
46
|
+
def ea_qty
|
47
|
+
self.qty * ( self.uom_size || 1 )
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [String] a description intended for use by the #{GlTransaction}
|
51
|
+
# def description_for_gl_transaction(gl)
|
52
|
+
# self.origin_description
|
53
|
+
# end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
# sets {#mac} to the correct amount for the {SkuLoc}.
|
58
|
+
# To calculate the MAC, the {SkuLoc#onhand_mac_value} is added to {#cost}
|
59
|
+
# and then divided by #{SkuLoc#qty} + {#ea_qty}
|
60
|
+
def calculate_mac
|
61
|
+
new_qty = sku_loc.qty + self.ea_qty
|
62
|
+
return true if self.mac.present?
|
63
|
+
if new_qty.zero?
|
64
|
+
self.mac = BigDecimal.new(0)
|
65
|
+
elsif cost
|
66
|
+
self.mac = ( sku_loc.onhand_mac_value + cost ) / new_qty
|
67
|
+
else
|
68
|
+
self.mac = sku_loc.onhand_mac_value
|
69
|
+
end
|
70
|
+
true
|
71
|
+
end
|
72
|
+
|
73
|
+
# If {#cost} is non-zero, then create a {GlTransaction}
|
74
|
+
def create_needed_gl_transaction
|
75
|
+
Skr::Core.logger.debug "Recording SkuTran in GL, mac is: #{self.mac}, cost = #{cost}"
|
76
|
+
return if self.cost.nil? || self.cost.zero?
|
77
|
+
GlTransaction.push_or_save(
|
78
|
+
owner: self, amount: cost,
|
79
|
+
debit: debit_gl_account, credit: credit_gl_account
|
80
|
+
)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Adjusts {SkuLoc#qty} by {#ea_qty}
|
84
|
+
def adjust_sku_loc_values
|
85
|
+
sl = self.sku_loc
|
86
|
+
Skr::Core.logger.debug "Adj +#{ea_qty} Sku #{sl.sku.code} location #{location.code} " +
|
87
|
+
"from MAC: #{sl.mac} to #{self.mac}, qty: #{sl.qty} += #{ea_qty} #{combined_uom}"
|
88
|
+
sl.unlock_fields( :qty, :mac ) do
|
89
|
+
sl.mac = self.mac unless self.mac.nan? or self.mac.zero?
|
90
|
+
sl.adjust_qty( ea_qty )
|
91
|
+
sl.save!
|
92
|
+
end
|
93
|
+
sl.reload
|
94
|
+
sl.allocate_available_qty! if self.allocate_after_save
|
95
|
+
|
96
|
+
Skr::Core.logger.debug "After Adj Qty #{sl.qty}"
|
97
|
+
end
|
98
|
+
|
99
|
+
def ensure_cost_and_qty_present
|
100
|
+
if ea_qty.zero?
|
101
|
+
errors.add( :base, "Transaction has no effect, must change inventory onhand value")
|
102
|
+
end
|
103
|
+
if cost.present? && cost.nonzero? && debit_gl_account.nil?
|
104
|
+
errors.add( :debit_gl_account, "was not specified even though we need to adjust the GL by #{cost}")
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
end # Skr module
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Skr
|
2
|
+
|
3
|
+
class SkuVendor < Skr::Model
|
4
|
+
|
5
|
+
acts_as_uom
|
6
|
+
|
7
|
+
belongs_to :sku, inverse_of: :sku_vendors, export: true
|
8
|
+
belongs_to :vendor, inverse_of: :sku_vendors, export: true
|
9
|
+
has_many :sku_locs, primary_key: :sku_id, export: true
|
10
|
+
|
11
|
+
delegate_and_export :vendor_code, :vendor_name
|
12
|
+
delegate_and_export :sku_code, :sku_description
|
13
|
+
|
14
|
+
validates :list_price, :cost, :uom_size, :numericality=>true, :presence=>true
|
15
|
+
validates :uom_code, :part_code, :presence=>true
|
16
|
+
validates :sku, :uniqueness=>{ scope: :part_code }
|
17
|
+
|
18
|
+
scope :in_location, lambda { | location |
|
19
|
+
location_id = location.is_a?(Numeric) ? location : location.id
|
20
|
+
includes(:sku_locs).references(:sku_locs).where(['sku_locs.location_id=?',location_id])
|
21
|
+
}, :export=>true
|
22
|
+
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end # Skr module
|
@@ -0,0 +1,159 @@
|
|
1
|
+
module Skr
|
2
|
+
|
3
|
+
class SoLine < Skr::Model
|
4
|
+
|
5
|
+
acts_as_uom
|
6
|
+
is_sku_loc_line parent: 'sales_order'
|
7
|
+
|
8
|
+
belongs_to :sales_order
|
9
|
+
belongs_to :sku_loc, export: true
|
10
|
+
has_one :sku, :through => :sku_loc, export: true
|
11
|
+
has_one :location, :through => :sales_order
|
12
|
+
has_many :pt_lines, :before_add=>:setup_new_pt_line, :inverse_of=>:so_line,
|
13
|
+
extend: Concerns::PT::Lines, :listen=>{save:'update_qty_picking'}
|
14
|
+
|
15
|
+
# has_many :inv_lines, :before_add=>:setup_new_inv_line, :inverse_of=>:so_line
|
16
|
+
|
17
|
+
validates :sales_order, :sku_loc, set: true
|
18
|
+
|
19
|
+
validates :price, :qty, :numericality=>true
|
20
|
+
validates :qty_allocated, :numericality=>{ :greater_than_or_equal_to=>0 }
|
21
|
+
validate :ensure_allocation_is_correct
|
22
|
+
validate :ensure_so_is_open, on: :create
|
23
|
+
|
24
|
+
has_additional_events :qty_change
|
25
|
+
|
26
|
+
before_validation :set_defaults_from_associations
|
27
|
+
before_create :allocate_max_available
|
28
|
+
before_destroy :ensure_deleteable
|
29
|
+
|
30
|
+
scope :open, ->{
|
31
|
+
joins(:sales_order).merge(SalesOrder.open).where( arel_table[:qty].gt( arel_table[:qty_invoiced] ) )
|
32
|
+
}
|
33
|
+
scope :allocated, ->{ where( arel_table[:qty_allocated].gt( 0 ) ) }
|
34
|
+
scope :unallocated, ->{
|
35
|
+
t = table_name; where( "#{t}.qty_allocated < #{t}.qty - #{t}.qty_invoiced - #{t}.qty_canceled" )
|
36
|
+
}
|
37
|
+
scope :unshipped, lambda {|unused=nil|
|
38
|
+
t = table_name; where( "#{t}.qty > #{t}.qty_invoiced + #{t}.qty_canceled" )
|
39
|
+
}
|
40
|
+
scope :pickable, ->{ where( arel_table[:qty_allocated].gt( arel_table[:qty_picking] ) ) }
|
41
|
+
|
42
|
+
def location=(location)
|
43
|
+
self.cancel!
|
44
|
+
self.sku_loc = self.sku.sku_locs.find_or_create_for( location )
|
45
|
+
self.allocate_max_available
|
46
|
+
self.save!
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
# allocate the maximum available qty to the line
|
51
|
+
def allocate_max_available
|
52
|
+
self.qty_allocated = [ 0, [ sku_loc.qty_available+qty_allocated, qty ].min ].max if self.sku.does_track_inventory?
|
53
|
+
self
|
54
|
+
end
|
55
|
+
|
56
|
+
# A line is fully allocated if the qty_allocated is less than the qty ordered - the qty invoiced - the qty canceled
|
57
|
+
# @return [Boolean]
|
58
|
+
def is_fully_allocated?
|
59
|
+
self.qty_allocated >= qty - qty_invoiced - qty_canceled
|
60
|
+
end
|
61
|
+
|
62
|
+
# The pickable qty is the qty allocated - the qty already on pick tickets
|
63
|
+
# @return [Fixnum]
|
64
|
+
def pickable_qty
|
65
|
+
qty_allocated - qty_picking
|
66
|
+
end
|
67
|
+
|
68
|
+
def cancel!
|
69
|
+
self.update_attributes :qty_allocated => 0, :qty_picking=> 0
|
70
|
+
pt_lines.picking.each{ |ptl| ptl.cancel! }
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def update_qty_shipped
|
76
|
+
inv_qty = self.inv_lines.sum(:qty)
|
77
|
+
update_attributes( :qty_invoiced=> inv_qty, :qty_allocated => [ qty_allocated - inv_qty, 0 ].max )
|
78
|
+
end
|
79
|
+
def update_qty_picking( pt=nil )
|
80
|
+
update_attributes( :qty_picking=> pt_lines.ea_picking_qty/uom_size )
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
def fire_after_save_events
|
85
|
+
%w{ allocated picking invoiced canceled }.each do | event |
|
86
|
+
if changes[ "qty_#{event}" ]
|
87
|
+
fire_event( :qty_change )
|
88
|
+
break
|
89
|
+
end
|
90
|
+
end
|
91
|
+
super
|
92
|
+
end
|
93
|
+
|
94
|
+
def set_defaults_from_associations
|
95
|
+
self.uom = sku.uoms.default if self.uom_code.blank?
|
96
|
+
self.description = sku.description if self.description.blank?
|
97
|
+
self.sku_code = sku.code if self.sku_code.blank?
|
98
|
+
if !price && sales_order && sales_order.customer && sku_loc && uom.present?
|
99
|
+
self.price = Skr.config.pricing_provider.price(sku_loc:sku_loc, customer:sales_order.customer, uom:uom, qty:qty)
|
100
|
+
end
|
101
|
+
true
|
102
|
+
end
|
103
|
+
|
104
|
+
def setup_new_inv_line( line )
|
105
|
+
line.qty = self.sku.is_other_charge? ? self.qty : self.qty_allocated
|
106
|
+
setup_new_line(line)
|
107
|
+
end
|
108
|
+
def setup_new_pt_line( line )
|
109
|
+
line.qty = self.sku.is_other_charge? ? self.qty : self.pickable_qty
|
110
|
+
setup_new_line(line)
|
111
|
+
end
|
112
|
+
|
113
|
+
def setup_new_line(line)
|
114
|
+
line.price = self.price
|
115
|
+
line.sku_loc = self.sku_loc
|
116
|
+
line.uom = self.uom
|
117
|
+
true
|
118
|
+
end
|
119
|
+
|
120
|
+
def ensure_allocation_is_correct
|
121
|
+
return true unless qty_allocated_changed?
|
122
|
+
diff = qty_allocated - qty_allocated_was
|
123
|
+
if qty_allocated > qty
|
124
|
+
errors.add(:qty_allocated, "must be less than qty ordered (#{qty})")
|
125
|
+
end
|
126
|
+
if diff > 0 && sku_loc && diff > sku_loc.qty_available
|
127
|
+
errors.add(:qty_allocated, "new allocation (#{qty_allocated}) - old allocation (#{qty_allocated_was}) can't be more than qty available (#{ sku_loc.qty_available })")
|
128
|
+
return false
|
129
|
+
end
|
130
|
+
true
|
131
|
+
end
|
132
|
+
|
133
|
+
|
134
|
+
def ensure_so_is_open
|
135
|
+
unless self.sales_order.open?
|
136
|
+
errors.add(:base,"Cannot add item #{self.sku_code} to non-open Sales Order")
|
137
|
+
return false
|
138
|
+
end
|
139
|
+
true
|
140
|
+
end
|
141
|
+
|
142
|
+
def ensure_deleteable
|
143
|
+
if qty_allocated > 0
|
144
|
+
errors.add(:base,'Cannot delete line when allocated')
|
145
|
+
return false
|
146
|
+
end
|
147
|
+
if qty_invoiced > 0
|
148
|
+
errors.add(:base,"Cannot delete line after it's shipped")
|
149
|
+
return false
|
150
|
+
end
|
151
|
+
true
|
152
|
+
end
|
153
|
+
|
154
|
+
|
155
|
+
|
156
|
+
end
|
157
|
+
|
158
|
+
|
159
|
+
end # Skr module
|