stockor 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (259) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/Gemfile +11 -0
  4. data/Gemfile.lock +220 -0
  5. data/Guardfile +13 -0
  6. data/README.md +7 -0
  7. data/Rakefile +7 -0
  8. data/client/skr/Extension.coffee +12 -0
  9. data/client/skr/components/.gitkeep +0 -0
  10. data/client/skr/components/address/Address.coffee +21 -0
  11. data/client/skr/components/address/address.html +20 -0
  12. data/client/skr/index.js +21 -0
  13. data/client/skr/models/Address.coffee +17 -0
  14. data/client/skr/models/Base.coffee +8 -0
  15. data/client/skr/models/Customer.coffee +26 -0
  16. data/client/skr/models/GlAccount.coffee +10 -0
  17. data/client/skr/models/GlManualEntry.coffee +11 -0
  18. data/client/skr/models/GlPeriod.coffee +10 -0
  19. data/client/skr/models/GlPosting.coffee +15 -0
  20. data/client/skr/models/GlTransaction.coffee +16 -0
  21. data/client/skr/models/IaLine.coffee +19 -0
  22. data/client/skr/models/IaReason.coffee +12 -0
  23. data/client/skr/models/InvLine.coffee +27 -0
  24. data/client/skr/models/InventoryAdjustment.coffee +17 -0
  25. data/client/skr/models/Invoice.coffee +31 -0
  26. data/client/skr/models/Location.coffee +15 -0
  27. data/client/skr/models/PaymentTerm.coffee +11 -0
  28. data/client/skr/models/PickTicket.coffee +19 -0
  29. data/client/skr/models/PoLine.coffee +27 -0
  30. data/client/skr/models/PoReceipt.coffee +20 -0
  31. data/client/skr/models/PorLine.coffee +26 -0
  32. data/client/skr/models/PtLine.coffee +27 -0
  33. data/client/skr/models/PurchaseOrder.coffee +23 -0
  34. data/client/skr/models/SalesOrder.coffee +32 -0
  35. data/client/skr/models/Sku.coffee +21 -0
  36. data/client/skr/models/SkuLoc.coffee +21 -0
  37. data/client/skr/models/SkuTran.coffee +23 -0
  38. data/client/skr/models/SkuVendor.coffee +19 -0
  39. data/client/skr/models/SoLine.coffee +27 -0
  40. data/client/skr/models/Uom.coffee +17 -0
  41. data/client/skr/models/Vendor.coffee +28 -0
  42. data/client/skr/models/VoLine.coffee +23 -0
  43. data/client/skr/models/Voucher.coffee +22 -0
  44. data/client/skr/models/mixins/CodeField.coffee +5 -0
  45. data/client/skr/screens/.gitkeep +0 -0
  46. data/client/skr/screens/Base.coffee +3 -0
  47. data/client/skr/screens/base/index.js +5 -0
  48. data/client/skr/screens/base/index.scss +9 -0
  49. data/client/skr/screens/base/layout.html +3 -0
  50. data/client/skr/screens/customer-maint/CustomerMaint.coffee +49 -0
  51. data/client/skr/screens/customer-maint/index.js +5 -0
  52. data/client/skr/screens/customer-maint/index.scss +9 -0
  53. data/client/skr/screens/customer-maint/layout.html +32 -0
  54. data/client/skr/styles.scss +1 -0
  55. data/client/skr/views/.gitkeep +0 -0
  56. data/client/skr/views/Base.coffee +5 -0
  57. data/config/database.yml +9 -0
  58. data/config/lanes.rb +7 -0
  59. data/config/routes.rb +39 -0
  60. data/config/screens.rb +17 -0
  61. data/config.ru +5 -0
  62. data/db/.gitkeep +0 -0
  63. data/db/migrate/20120110142845_create_skr_sequential_ids.rb +35 -0
  64. data/db/migrate/20140202185309_create_skr_gl_accounts.rb +15 -0
  65. data/db/migrate/20140202193316_create_skr_gl_periods.rb +16 -0
  66. data/db/migrate/20140202193318_create_skr_gl_transactions.rb +14 -0
  67. data/db/migrate/20140202193319_create_skr_gl_postings.rb +16 -0
  68. data/db/migrate/20140202193700_create_skr_gl_manual_entries.rb +13 -0
  69. data/db/migrate/20140213040608_create_skr_payment_terms.rb +16 -0
  70. data/db/migrate/20140220031700_create_skr_addresses.rb +19 -0
  71. data/db/migrate/20140220031800_create_skr_locations.rb +19 -0
  72. data/db/migrate/20140220190836_create_skr_vendors.rb +22 -0
  73. data/db/migrate/20140220203029_create_skr_customers.rb +22 -0
  74. data/db/migrate/20140224034759_create_skr_skus.rb +22 -0
  75. data/db/migrate/20140225032853_create_skr_sku_locs.rb +21 -0
  76. data/db/migrate/20140320030501_create_skr_uoms.rb +19 -0
  77. data/db/migrate/20140321031604_create_skr_sku_vendors.rb +18 -0
  78. data/db/migrate/20140322012143_create_skr_ia_reasons.rb +14 -0
  79. data/db/migrate/20140322014401_create_skr_inventory_adjustments.rb +16 -0
  80. data/db/migrate/20140322023453_create_skr_ia_lines.rb +18 -0
  81. data/db/migrate/20140322035024_create_skr_sku_trans.rb +21 -0
  82. data/db/migrate/20140322223912_create_skr_sales_orders.rb +27 -0
  83. data/db/migrate/20140322223920_create_skr_so_lines.rb +25 -0
  84. data/db/migrate/20140323001446_create_so_details_view.rb +81 -0
  85. data/db/migrate/20140327202102_create_skr_purchase_orders.rb +20 -0
  86. data/db/migrate/20140327202107_create_skr_po_lines.rb +25 -0
  87. data/db/migrate/20140327202207_create_skr_pick_tickets.rb +16 -0
  88. data/db/migrate/20140327202209_create_skr_pt_lines.rb +23 -0
  89. data/db/migrate/20140327224000_create_skr_invoices.rb +25 -0
  90. data/db/migrate/20140327224002_create_skr_inv_lines.rb +23 -0
  91. data/db/migrate/20140330232808_create_skr_sku_loc_details_view.rb +31 -0
  92. data/db/migrate/20140330232810_create_skr_sku_qty_details_view.rb +48 -0
  93. data/db/migrate/20140400164729_create_skr_vouchers.rb +22 -0
  94. data/db/migrate/20140400164733_create_skr_vo_lines.rb +21 -0
  95. data/db/migrate/20140401164729_create_skr_po_receipt.rb +16 -0
  96. data/db/migrate/20140401164740_create_skr_por_line.rb +21 -0
  97. data/db/migrate/20140422024010_create_skr_inv_details_view.rb +42 -0
  98. data/db/schema.sql +2662 -0
  99. data/db/seed/chart_of_accounts.yml +168 -0
  100. data/db/seed/payment_terms.yml +60 -0
  101. data/db/seed.rb +29 -0
  102. data/lib/skr/access_roles.rb +28 -0
  103. data/lib/skr/concerns/acts_as_uom.rb +47 -0
  104. data/lib/skr/concerns/code_identifier.rb +43 -0
  105. data/lib/skr/concerns/gl_tran_extensions.rb +18 -0
  106. data/lib/skr/concerns/has_gl_transaction.rb +67 -0
  107. data/lib/skr/concerns/has_sku_loc_lines.rb +47 -0
  108. data/lib/skr/concerns/immutable_model.rb +32 -0
  109. data/lib/skr/concerns/inv_extensions.rb +24 -0
  110. data/lib/skr/concerns/is_order_like.rb +47 -0
  111. data/lib/skr/concerns/is_sku_loc_line.rb +65 -0
  112. data/lib/skr/concerns/locked_fields.rb +84 -0
  113. data/lib/skr/concerns/pt_extensions.rb +22 -0
  114. data/lib/skr/concerns/random_hash_code.rb +40 -0
  115. data/lib/skr/concerns/so_extensions.rb +30 -0
  116. data/lib/skr/concerns/state_machine.rb +61 -0
  117. data/lib/skr/concerns/visible_id_identifier.rb +53 -0
  118. data/lib/skr/configuration.rb +68 -0
  119. data/lib/skr/db/migration_helpers.rb +178 -0
  120. data/lib/skr/extension.rb +23 -0
  121. data/lib/skr/model.rb +19 -0
  122. data/lib/skr/models/address.rb +97 -0
  123. data/lib/skr/models/business_entity.rb +29 -0
  124. data/lib/skr/models/customer.rb +35 -0
  125. data/lib/skr/models/gl_account.rb +56 -0
  126. data/lib/skr/models/gl_manual_entry.rb +31 -0
  127. data/lib/skr/models/gl_period.rb +13 -0
  128. data/lib/skr/models/gl_posting.rb +54 -0
  129. data/lib/skr/models/gl_transaction.rb +175 -0
  130. data/lib/skr/models/ia_line.rb +129 -0
  131. data/lib/skr/models/ia_reason.rb +16 -0
  132. data/lib/skr/models/inv_line.rb +90 -0
  133. data/lib/skr/models/inventory_adjustment.rb +60 -0
  134. data/lib/skr/models/invoice.rb +159 -0
  135. data/lib/skr/models/location.rb +31 -0
  136. data/lib/skr/models/payment_term.rb +30 -0
  137. data/lib/skr/models/pick_ticket.rb +71 -0
  138. data/lib/skr/models/po_line.rb +69 -0
  139. data/lib/skr/models/po_receipt.rb +51 -0
  140. data/lib/skr/models/por_line.rb +80 -0
  141. data/lib/skr/models/pt_line.rb +74 -0
  142. data/lib/skr/models/purchase_order.rb +112 -0
  143. data/lib/skr/models/sales_order.rb +159 -0
  144. data/lib/skr/models/sequential_id.rb +23 -0
  145. data/lib/skr/models/sku.rb +99 -0
  146. data/lib/skr/models/sku_loc.rb +94 -0
  147. data/lib/skr/models/sku_tran.rb +111 -0
  148. data/lib/skr/models/sku_vendor.rb +26 -0
  149. data/lib/skr/models/so_line.rb +159 -0
  150. data/lib/skr/models/uom.rb +63 -0
  151. data/lib/skr/models/user_proxy.rb +60 -0
  152. data/lib/skr/models/vendor.rb +33 -0
  153. data/lib/skr/models/vo_line.rb +35 -0
  154. data/lib/skr/models/voucher.rb +119 -0
  155. data/lib/skr/standard_pricing_provider.rb +14 -0
  156. data/lib/skr/version.rb +3 -0
  157. data/lib/skr.rb +18 -0
  158. data/lib/stockor.rb +4 -0
  159. data/lib/tasks/debug-activity.rake +58 -0
  160. data/log/test.log +0 -0
  161. data/spec/fixtures/skr/address.yml +2 -0
  162. data/spec/fixtures/skr/customer.yml +2 -0
  163. data/spec/fixtures/skr/gl_account.yml +2 -0
  164. data/spec/fixtures/skr/gl_manual_entry.yml +2 -0
  165. data/spec/fixtures/skr/gl_period.yml +2 -0
  166. data/spec/fixtures/skr/gl_posting.yml +2 -0
  167. data/spec/fixtures/skr/gl_transaction.yml +2 -0
  168. data/spec/fixtures/skr/ia_line.yml +2 -0
  169. data/spec/fixtures/skr/ia_reason.yml +2 -0
  170. data/spec/fixtures/skr/inv_line.yml +2 -0
  171. data/spec/fixtures/skr/inventory_adjustment.yml +2 -0
  172. data/spec/fixtures/skr/invoice.yml +2 -0
  173. data/spec/fixtures/skr/location.yml +2 -0
  174. data/spec/fixtures/skr/payment_term.yml +2 -0
  175. data/spec/fixtures/skr/pick_ticket.yml +2 -0
  176. data/spec/fixtures/skr/po_line.yml +2 -0
  177. data/spec/fixtures/skr/po_receipt.yml +2 -0
  178. data/spec/fixtures/skr/por_line.yml +2 -0
  179. data/spec/fixtures/skr/pt_line.yml +2 -0
  180. data/spec/fixtures/skr/purchase_order.yml +2 -0
  181. data/spec/fixtures/skr/sales_order.yml +2 -0
  182. data/spec/fixtures/skr/sku.yml +2 -0
  183. data/spec/fixtures/skr/sku_loc.yml +2 -0
  184. data/spec/fixtures/skr/sku_tran.yml +2 -0
  185. data/spec/fixtures/skr/sku_vendor.yml +2 -0
  186. data/spec/fixtures/skr/so_line.yml +2 -0
  187. data/spec/fixtures/skr/uom.yml +2 -0
  188. data/spec/fixtures/skr/vendor.yml +2 -0
  189. data/spec/fixtures/skr/vo_line.yml +2 -0
  190. data/spec/fixtures/skr/voucher.yml +2 -0
  191. data/spec/skr/address.rb +10 -0
  192. data/spec/skr/concerns/code_identifier_spec.rb +45 -0
  193. data/spec/skr/customer.rb +10 -0
  194. data/spec/skr/gl_account.rb +10 -0
  195. data/spec/skr/gl_manual_entry.rb +10 -0
  196. data/spec/skr/gl_period.rb +10 -0
  197. data/spec/skr/gl_posting.rb +10 -0
  198. data/spec/skr/gl_transaction.rb +10 -0
  199. data/spec/skr/ia_line.rb +10 -0
  200. data/spec/skr/ia_reason.rb +10 -0
  201. data/spec/skr/inv_line.rb +10 -0
  202. data/spec/skr/inventory_adjustment.rb +10 -0
  203. data/spec/skr/invoice.rb +10 -0
  204. data/spec/skr/location.rb +10 -0
  205. data/spec/skr/models/AddressSpec.coffee +5 -0
  206. data/spec/skr/models/CustomerSpec.coffee +5 -0
  207. data/spec/skr/models/GlAccountSpec.coffee +5 -0
  208. data/spec/skr/models/GlManualEntrySpec.coffee +5 -0
  209. data/spec/skr/models/GlPeriodSpec.coffee +5 -0
  210. data/spec/skr/models/GlPostingSpec.coffee +5 -0
  211. data/spec/skr/models/GlTransactionSpec.coffee +5 -0
  212. data/spec/skr/models/IaLineSpec.coffee +5 -0
  213. data/spec/skr/models/IaReasonSpec.coffee +5 -0
  214. data/spec/skr/models/InvLineSpec.coffee +5 -0
  215. data/spec/skr/models/InventoryAdjustmentSpec.coffee +5 -0
  216. data/spec/skr/models/InvoiceSpec.coffee +5 -0
  217. data/spec/skr/models/LocationSpec.coffee +5 -0
  218. data/spec/skr/models/PaymentTermSpec.coffee +5 -0
  219. data/spec/skr/models/PickTicketSpec.coffee +5 -0
  220. data/spec/skr/models/PoLineSpec.coffee +5 -0
  221. data/spec/skr/models/PoReceiptSpec.coffee +5 -0
  222. data/spec/skr/models/PorLineSpec.coffee +5 -0
  223. data/spec/skr/models/PtLineSpec.coffee +5 -0
  224. data/spec/skr/models/PurchaseOrderSpec.coffee +5 -0
  225. data/spec/skr/models/SalesOrderSpec.coffee +5 -0
  226. data/spec/skr/models/SkuLocSpec.coffee +5 -0
  227. data/spec/skr/models/SkuSpec.coffee +5 -0
  228. data/spec/skr/models/SkuTranSpec.coffee +5 -0
  229. data/spec/skr/models/SkuVendorSpec.coffee +5 -0
  230. data/spec/skr/models/SoLineSpec.coffee +5 -0
  231. data/spec/skr/models/UomSpec.coffee +5 -0
  232. data/spec/skr/models/VendorSpec.coffee +5 -0
  233. data/spec/skr/models/VoLineSpec.coffee +5 -0
  234. data/spec/skr/models/VoucherSpec.coffee +5 -0
  235. data/spec/skr/payment_term.rb +10 -0
  236. data/spec/skr/pick_ticket.rb +10 -0
  237. data/spec/skr/po_line.rb +10 -0
  238. data/spec/skr/po_receipt.rb +10 -0
  239. data/spec/skr/por_line.rb +10 -0
  240. data/spec/skr/pt_line.rb +10 -0
  241. data/spec/skr/purchase_order.rb +10 -0
  242. data/spec/skr/sales_order.rb +10 -0
  243. data/spec/skr/screens/Base.coffee +7 -0
  244. data/spec/skr/screens/CustomerMaint.coffee +7 -0
  245. data/spec/skr/screens/vendor-maint/VendorMaintSpec.coffee +5 -0
  246. data/spec/skr/sku.rb +10 -0
  247. data/spec/skr/sku_loc.rb +10 -0
  248. data/spec/skr/sku_tran.rb +10 -0
  249. data/spec/skr/sku_vendor.rb +10 -0
  250. data/spec/skr/so_line.rb +10 -0
  251. data/spec/skr/spec_helper.rb +26 -0
  252. data/spec/skr/uom.rb +10 -0
  253. data/spec/skr/vendor.rb +10 -0
  254. data/spec/skr/views/AddressSpec.coffee +5 -0
  255. data/spec/skr/vo_line.rb +10 -0
  256. data/spec/skr/voucher.rb +10 -0
  257. data/stockor.gemspec +38 -0
  258. data/tmp/.gitkeep +0 -0
  259. 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