stockor 0.1.5

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.
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,29 @@
1
+ module Skr
2
+
3
+ module BusinessEntity
4
+
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ has_random_hash_code
9
+ has_code_identifier from: 'name'
10
+
11
+ belongs_to :billing_address, class_name: 'Skr::Address', export: { writable: true }
12
+ belongs_to :shipping_address, class_name: 'Skr::Address', export: { writable: true }
13
+ belongs_to :terms, class_name: 'Skr::PaymentTerm', export: { writable: true }
14
+
15
+ delegate_and_export :terms_code, :terms_description
16
+
17
+ validates :name, presence: true
18
+ validates :terms, :billing_address, :shipping_address, set: true
19
+
20
+ before_validation :set_defaults, :on=>:create
21
+ end
22
+
23
+ def set_defaults
24
+ self.terms ||= PaymentTerm.find_by_code(Skr.config.customer_terms_code)
25
+ end
26
+
27
+ end
28
+
29
+ end
@@ -0,0 +1,35 @@
1
+ module Skr
2
+
3
+ # Customers are Companies (or individuals) that purchase {Sku}s.
4
+ # They have both a billing and shipping address,
5
+ # a gl account that payments should be applied against, and a payment term.
6
+ #
7
+ class Customer < Skr::Model
8
+
9
+ # Common code shared with {Vendor}
10
+ include BusinessEntity
11
+
12
+ belongs_to :gl_receivables_account, class_name: 'Skr::GlAccount', export: true
13
+
14
+ has_many :sales_orders, inverse_of: :customer
15
+ has_many :invoices, inverse_of: :customer, listen: { save: :update_balance! }
16
+
17
+ delegate_and_export :gl_receivables_account_number
18
+
19
+ validates :gl_receivables_account, set: true
20
+
21
+ # Updates the amount the customer owes, which is the sum of the amount unpaid on open invoices
22
+ def update_balance!(*)
23
+ update_attributes open_balance: invoices.open_for_customer(self)
24
+ .with_details.sum('details.total')
25
+ end
26
+
27
+ private
28
+
29
+ def set_defaults
30
+ super
31
+ self.gl_receivables_account ||= GlAccount.default_for( :ar )
32
+ end
33
+ end
34
+
35
+ end # Skr module
@@ -0,0 +1,56 @@
1
+ module Skr
2
+
3
+ # A GlAccount *(short for General Ledger Account)* is used
4
+ # to define each class of items for
5
+ # which money or the equivalent is spent or received.
6
+ class GlAccount < Skr::Model
7
+
8
+ # @private
9
+ DEFAULT_ACCOUNTS = {}
10
+
11
+ is_immutable except: [:name, :is_active]
12
+
13
+ validates :name, :description, :presence => true
14
+
15
+ # @!attribute description
16
+ # A short description of the GL Account
17
+
18
+ # A future improvement would be to allow arbitrary account masks. For now, keep it simple
19
+ # with mandatory 4 characters + 2 char branch code
20
+ validates :number, :presence => true, :numericality=>true, :length=>{ :is=>4 }
21
+
22
+ # @return [GlAccount] the default account for the key from {Skr::Core::Configuration.default_gl_accounts}
23
+ def self.default_for( lookup )
24
+ number = Skr.config.default_gl_accounts[ lookup ]
25
+ raise RuntimeError.new("Unkown GL default account lookup code: {lookup}") unless number
26
+ DEFAULT_ACCOUNTS[ lookup ] ||= GlAccount.find_by_number( number )
27
+ end
28
+
29
+ # @return [String] the account number combined with location branch code
30
+ def number_for_location( location )
31
+ self.number + location.gl_branch_code
32
+ end
33
+
34
+ # @return [String] the account number combined with the default branch code
35
+ def default_number
36
+ self.number + Skr.config.default_branch_code
37
+ end
38
+
39
+ # @return [BigDecimal] The balance for the current period
40
+ def trial_balance
41
+ balance_for( GlPeriod.current )
42
+ end
43
+
44
+ # @return [String] the account number suitable for querying all branches
45
+ def account_mask
46
+ number + '%'
47
+ end
48
+
49
+ # @return [BigDecimal] the balance for a given period
50
+ def balance_for( period, mask = self.account_mask )
51
+ GlPosting.matching( period, account_mask ).sum(:amount)
52
+ end
53
+
54
+ end
55
+
56
+ end
@@ -0,0 +1,31 @@
1
+ module Skr
2
+
3
+ # Records a manual entry into the General Leger.
4
+ # Most GL entries are triggered by system events such as an Inventory Receipt, or Invoice payment.
5
+ # A manual entry differs in that it's performed, well *Manually*.
6
+ # It's usually used to balance ledger accounts or as part of closing an accounting period.
7
+ class GlManualEntry < Skr::Model
8
+
9
+ has_visible_id
10
+
11
+ is_immutable
12
+
13
+ has_one :gl_transaction, :as=>:source,
14
+ inverse_of: :source, export: { writable: true }
15
+
16
+ validates :gl_transaction, presence: true
17
+
18
+ before_create :copy_notes_to_transaction
19
+
20
+ def gl_transaction=( transaction )
21
+ super
22
+ copy_notes_to_transaction
23
+ end
24
+ private
25
+
26
+ def copy_notes_to_transaction
27
+ self.gl_transaction.description = self.notes[0..100]
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,13 @@
1
+ module Skr
2
+
3
+ class GlPeriod < Skr::Model
4
+
5
+ is_immutable
6
+
7
+ def self.current
8
+ attr = { year: Time.now.year, period: Time.now.month }
9
+ GlPeriod.where( attr ).first || GlPeriod.create( attr )
10
+ end
11
+ end
12
+
13
+ end
@@ -0,0 +1,54 @@
1
+ module Skr
2
+ class GlPosting < Skr::Model
3
+
4
+ is_immutable
5
+
6
+ belongs_to :gl_transaction
7
+
8
+ before_validation :cache_related_attributes
9
+
10
+ validates :gl_transaction, set: true
11
+ validates :account_number, numericality: true, length: { :is=>6 }
12
+ validates :amount, numericality: true, presence: true
13
+ validate :ensure_accounting_validity, on: :create
14
+
15
+ scope :applying_to_period, ->(period){ where( '(period <= :period and year = :year) or (year < :year)',
16
+ { period: period.period, year: period.year } ) }
17
+ scope :matching, ->(period, account_mask){
18
+ applying_to_period( period ).where('account_number like ?', account_mask )
19
+ }
20
+
21
+ def account=(acct)
22
+ @account = acct
23
+ assign_account_number
24
+ end
25
+
26
+ def location=(location)
27
+ @location = location
28
+ assign_account_number
29
+ end
30
+
31
+ private
32
+
33
+ def assign_account_number
34
+ self.account_number = @account.number_for_location(@location) if @account && @location
35
+ end
36
+
37
+ def ensure_accounting_validity
38
+ unless self.gl_transaction.new_record? #postings_create_ok?
39
+ self.errors.add( :gl_transaction, "does not accept new postings" )
40
+ end
41
+ if @account && ! @account.is_active?
42
+ self.errors.add(:account, "is not active")
43
+ end
44
+
45
+ end
46
+
47
+ def cache_related_attributes
48
+ assign_account_number
49
+ self.year = gl_transaction.period.year
50
+ self.period = gl_transaction.period.period
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,175 @@
1
+
2
+ module Skr
3
+
4
+ # A transaction is a record of a business event that has financial consequences.
5
+ # It consists of an at least one credit and at least one debit
6
+ # Transactions can be nested, with each level compacting all the entries that were made on it
7
+ #
8
+ # require 'skr/core'
9
+ # customer = Customer.find_by_code "MONEYBAGS"
10
+ # GlTransaction.record( source: invoice, description: "Invoice Example" ) do | transaction |
11
+ # transaction.location = Location.default # <- could also specify in record's options
12
+ # Sku.where( code: ['HAT','STRING'] ).each do | sku |
13
+ # transaction.add_posting( amount: sku.default_price,
14
+ # debit: sku.gl_asset_account,
15
+ # credit: customer.gl_receivables_account )
16
+ # end
17
+ # end
18
+ #
19
+ class GlTransaction < Skr::Model
20
+
21
+ is_immutable
22
+
23
+ # A Transaction must refer to another record, such as Invoice, Inventory Adjustment, or a Manual Posting.
24
+ belongs_to :source, :polymorphic=>true
25
+
26
+ # Each transaction belongs to an accounting period
27
+ belongs_to :period, :class_name=>'Skr::GlPeriod', export: true
28
+
29
+ has_many :credits, ->{ where({ is_debit: false }) }, class_name: 'Skr::GlPosting',
30
+ extend: Concerns::GlTran::Postings,
31
+ inverse_of: :gl_transaction, export: { writable: true }
32
+
33
+ # Must equal credits, checked by the {#ensure_postings_correct} validation
34
+ has_many :debits, ->{ where({ is_debit: true }) }, class_name: 'Skr::GlPosting',
35
+ extend: Concerns::GlTran::Postings,
36
+ inverse_of: :gl_transaction, export: { writable: true }
37
+
38
+ before_validation :set_defaults
39
+ validate :ensure_postings_correct
40
+ validates :source, :period, :set=>true
41
+ validates :description, :presence=>true
42
+
43
+
44
+ # Add a debit/credit pair to the transaction with amount
45
+ # @param amount [BigDecimal] the amount to apply to each posting
46
+ # @param debit [GlAccount]
47
+ # @param credit [GlAccount]
48
+ def add_posting( amount: nil, debit: nil, credit: nil )
49
+ Skr::Core.logger.debug "GlTransaction add_posting #{debit} : #{credit}"
50
+ self.credits.build( location: @location, is_debit: false,
51
+ account: credit, amount: amount )
52
+ self.debits.build( location: @location, is_debit: true,
53
+ account: debit, amount: amount * -1 )
54
+ end
55
+
56
+ # Passes the location onto the postings.
57
+ # @param location [Location]
58
+ def location=(location)
59
+ @location = location
60
+ each_posting do | posting |
61
+ posting.location = location
62
+ end
63
+ end
64
+
65
+ # @yield [GlPosting] each posting associated with the Transaction
66
+ def each_posting
67
+ self.credits.each{ |posting| yield posting }
68
+ self.debits.each{ |posting| yield posting }
69
+ end
70
+
71
+ # @return [GlTransaction] the current transaction that's in progress
72
+ def self.current
73
+ glt = Thread.current[:gl_transaction]
74
+ glt ? glt.last : nil
75
+ end
76
+
77
+ # Start a new nested GlTransaction
78
+ # When a transaction is created, it can have
79
+ # @return [GlTransaction] new transaction
80
+ # @yield [GlTransaction] new transaction
81
+ def self.record( attributes={} )
82
+ Thread.current[:gl_transaction] ||= []
83
+ glt = GlTransaction.new( attributes )
84
+ Thread.current[:gl_transaction].push( glt )
85
+ Skr::Core.logger.debug "B4 GlTransaction"
86
+ results = yield glt
87
+ Thread.current[:gl_transaction].pop
88
+ if results
89
+ if results.is_a?(Hash) && results.has_key?(:attributes)
90
+ glt.assign_attributes( results[:attributes] )
91
+ end
92
+ glt._save_recorded
93
+ Skr::Core.logger.debug "AF GlTransaction new=#{glt.new_record?} #{glt.errors.full_messages}"
94
+ end
95
+ return glt
96
+ end
97
+
98
+ # @param owner [Skr::Model]
99
+ # @param amount [BigDecimal]
100
+ # @param debit [GlAccount]
101
+ # @param credit [GlAccount]
102
+ # @param options [Hash] options to pass to the [GlTransaction] if one is created
103
+ def self.push_or_save( owner: nil, amount: nil, debit:nil, credit:nil, options:{} )
104
+ if glt = self.current # we push
105
+ glt.add_posting( amount: amount, debit: debit, credit: credit )
106
+ else
107
+ options.merge!({
108
+ source: owner,
109
+ location: options[:location] || owner.location
110
+ })
111
+ glt = GlTransaction.new( options )
112
+ glt.add_posting( amount: amount, debit: debit, credit: credit )
113
+ glt.save
114
+ end
115
+ glt
116
+ end
117
+
118
+ # @private
119
+ def _save_recorded
120
+ compact( 'debits' )
121
+ compact( 'credits' )
122
+ self.save if self.credits.any? || self.debits.any?
123
+ self
124
+ end
125
+
126
+ private
127
+
128
+ def compact( assoc_name )
129
+ accounts = self.send( assoc_name ).to_a
130
+ self.send( assoc_name + "=", [] )
131
+ account_numbers = accounts.group_by{ |posting| posting.account_number }
132
+ account_numbers.each do | number, matching |
133
+ amount = matching.sum(&:amount)
134
+ self.send( assoc_name ).build({
135
+ account_number: number,
136
+ is_debit: ( assoc_name == "debits" ),
137
+ amount: amount,
138
+ })
139
+ end
140
+ end
141
+
142
+ def ensure_postings_correct
143
+ if debits.total != ( -1 * credits.total )
144
+ self.errors.add(:credits, "must equal debits")
145
+ self.errors.add(:debits, "must equal credits")
146
+ return false
147
+ end
148
+ true
149
+ end
150
+
151
+ def set_defaults
152
+ self.period ||= GlPeriod.current
153
+ end
154
+
155
+ # def ensure_postings_exist
156
+ # if self.new_record? and self.postings.empty?
157
+ # 2.times { self.postings.build }
158
+ # end
159
+ # end
160
+
161
+ end
162
+ end
163
+
164
+ __END__
165
+
166
+ # export_join_tables :details
167
+ # export_scope :with_details_for, lambda { | acct |
168
+ # acct = acct.gsub(/\D/,'') + '%'
169
+ # cs = "case when details.debit_account_number like '#{acct}' then details.debit_amount " +
170
+ # "when details.credit_account_number like '#{acct}' then details.credit_amount else 0 end"
171
+ # window = "#{cs} as amount, sum(#{cs}) over (order by created_at) as balance"
172
+ # joins('join gl_transaction_details as details on details.gl_transaction_id = gl_transactions.id')
173
+ # .select("gl_transactions.*, details.*, #{window}")
174
+ # .where("debit_account_number like :acct or credit_account_number like :acct", acct: acct)
175
+ # }
@@ -0,0 +1,129 @@
1
+ module Skr
2
+
3
+ # An Inventory Adjustment Line.
4
+ # Each model contains the {SkuLoc}, cost, UOM, and qty for a {Sku} to adjust
5
+ #
6
+ # An adjustment starts out in the "pending" state, then when it moves to the "applied"
7
+ # state, each line creates an {SkuTran} record that adjusts the inventory in or out.
8
+ class IaLine < Skr::Model
9
+
10
+ acts_as_uom
11
+
12
+ belongs_to :inventory_adjustment
13
+ belongs_to :sku_loc, export: true
14
+ has_one :sku, :through => :sku_loc, export: true
15
+
16
+ has_one :sku_tran, :as=>:origin
17
+ scope :unapplied, lambda { includes(:sku_tran).references(:sku_tran).where( SkuTran.table_name+'.id is null' ) }
18
+
19
+ export_methods :sku_loc_qty, :sku_loc_mac, :optional=>false
20
+
21
+ validates :sku_loc, set: true
22
+ validates :qty, numericality: true
23
+ validates :uom_code, :uom_size, presence: true
24
+ validate :ensure_cost_set_properly
25
+
26
+ before_save :set_cost_from_sku_loc
27
+ before_destroy :ensure_adjustment_isnt_applied
28
+ before_update :ensure_adjustment_isnt_applied
29
+ delegate_and_export :sku_code, :sku_description
30
+
31
+ has_locks :adjusting
32
+
33
+ # @return [Fixnum] The qty available on the location, expressed in terms of the UOM
34
+ def sku_loc_qty
35
+ ( self.uom_size && self.sku_loc ) ? BigDecimal.new( self.sku_loc.qty ) / self.uom_size : 0
36
+ end
37
+
38
+ # @return [BigDecimal] the current moving average cost (mac) on the location, expressed in terms of the UOM
39
+ def sku_loc_mac
40
+ self.sku_loc ? ( self.sku_loc.mac * self.uom_size ) : 0
41
+ end
42
+
43
+ # @return [Boolean] has the line been applied
44
+ def is_applied?
45
+ sku_tran.present?
46
+ end
47
+
48
+ # @return [Boolean] is the qty negative?
49
+ def is_removing_qty?
50
+ qty && qty <=0
51
+ end
52
+
53
+ # The qty for the line expressed in terms of the single UOM
54
+ def ea_qty
55
+ self.uom_size * self.qty
56
+ end
57
+
58
+ # @return [BigDecimal] the total value of the line
59
+ def total
60
+ self.ledger_cost * self.qty
61
+ end
62
+ #alias_method :ledger_value, :total
63
+
64
+ # @return [BigDecimal] either the current MAC for the sku's location or the cost that was manually set
65
+ def ledger_cost
66
+ if cost_was_set? || is_applied?
67
+ self.cost
68
+ else
69
+ self.sku_loc_mac
70
+ end
71
+ end
72
+
73
+ # copies the cost from the sku_loc to the {#cost} field
74
+ def set_cost_from_sku_loc
75
+ if ! cost_was_set?
76
+ self.cost = self.sku_loc_mac
77
+ end
78
+ true
79
+ end
80
+
81
+ # Perform the adjustment. Requires adjusting to be unlocked and {#is_applied?} must be false
82
+ #
83
+ # It creates a {SkuTran} to adjust the inventory, and allocates available qty to the {SoLine}
84
+ def adjust_qty!
85
+ if ! is_adjusting_unlocked? || is_applied?
86
+ raise "Unable to apply line, either not approved or previously applied"
87
+ end
88
+ set_cost_from_sku_loc
89
+ Core.logger.debug( "Adjusting #{self.qty} #{combined_uom} of #{sku_code} into stock")
90
+ self.build_sku_tran({
91
+ :origin=>self, :qty => self.qty, :sku_loc=>self.sku_loc,
92
+ origin_description: "IA #{self.inventory_adjustment.visible_id}:#{self.sku.code}",
93
+ cost: total, uom: self.uom,
94
+ allocate_after_save: true,
95
+ debit_gl_account: self.inventory_adjustment.reason.gl_account,
96
+ credit_gl_account: self.sku.gl_asset_account
97
+ })
98
+ self.sku_tran.save unless self.new_record?
99
+ true
100
+ end
101
+
102
+ private
103
+
104
+
105
+ # Cost cannot be set if the qty is negative
106
+ def ensure_cost_set_properly
107
+ if cost_was_set? && is_removing_qty?
108
+ errors.add(:cost,'cannot be set if removing qty')
109
+ return false
110
+ end
111
+ true
112
+ end
113
+
114
+ def ensure_adjustment_isnt_applied
115
+ if inventory_adjustment.applied?
116
+ errors.add(:base,'cannot be modified after adjustment is approved and applied')
117
+ return false
118
+ else
119
+ return true
120
+ end
121
+ end
122
+
123
+
124
+
125
+
126
+ end
127
+
128
+
129
+ end # Skr module
@@ -0,0 +1,16 @@
1
+ module Skr
2
+
3
+ class IaReason < Skr::Model
4
+
5
+ has_code_identifier
6
+
7
+ belongs_to :gl_account, export: true
8
+
9
+ delegate_and_export :gl_account_number, :gl_account_name
10
+
11
+ validates :gl_account, set: true
12
+
13
+ end
14
+
15
+
16
+ end # Skr module
@@ -0,0 +1,90 @@
1
+ module Skr
2
+
3
+ class InvLine < Skr::Model
4
+
5
+ acts_as_uom
6
+ is_immutable
7
+ is_sku_loc_line parent: "invoice"
8
+
9
+ locked_fields :qty, :sku_loc_id, :so_line
10
+ belongs_to :invoice
11
+
12
+ belongs_to :so_line, export: true
13
+ belongs_to :pt_line, :inverse_of=>:inv_line, export: true
14
+ belongs_to :sku_loc, export: true
15
+
16
+ has_one :sku, :through => :sku_loc, export: true
17
+ has_one :location, :through => :sku_loc
18
+ has_one :sku_tran, :as=>:origin
19
+
20
+ validates :sku_loc, set: true
21
+
22
+ validates :price, :qty, :numericality=>true
23
+ validates :uom_code, :sku_code, :description, :uom_size, :presence=>true
24
+
25
+ before_validation :set_defaults
26
+ before_save :adjust_inventory
27
+
28
+ scope :with_details, lambda { |should_use=true |
29
+ compose_query_using_detail_view( view: 'inv_details', join_to: 'inv_line_id' ) if should_use
30
+ }, export: true
31
+
32
+ scope :summarize_by, lambda { | values |
33
+ select( "sum(inv_lines.qty) as qty, count(*) as num_sales, sum(inv_lines.price) as price, avg(inv_lines.price) as avg_price, s.code as sku_code, s.description, s.id as sku_id, 1 as uom_size, 'EA' as uom_code")
34
+ .joins("join sku_locs sl on sl.id = inv_lines.sku_loc_id join skus s on s.id = sl.sku_id")
35
+ .where(["inv_lines.created_at between ? and ?",values['from'], values['to'] ])
36
+ .group('s.code,s.description, s.id')
37
+ }, export: true
38
+
39
+
40
+ private
41
+
42
+ def set_defaults
43
+ # puts "set_defaults #{self}"
44
+ line = [ self.pt_line, self.so_line ].detect{ |l| ! l.blank? }
45
+ if line
46
+ self.uom = line.uom if self.uom.blank?
47
+ self.sku_code ||= line.sku_code
48
+ self.description ||= line.description
49
+ self.sku_loc ||= line.sku_loc
50
+ self.qty ||= line.qty
51
+ self.so_line ||= line.so_line
52
+ elsif sku
53
+ self.uom = sku.uoms.default if self.uom_code.blank?
54
+ self.sku_code ||= sku.code
55
+ self.description ||= sku.description
56
+ self.sku_loc_id ||= sku.sku_locs.for_location( self.invoice.location ).id if self.invoice && self.invoice.location
57
+ end
58
+ if !price && invoice && invoice.customer && sku_loc && uom.present?
59
+ self.price = Skr.config.pricing_provider.price(
60
+ sku_loc:sku_loc, customer:invoice.customer,
61
+ uom:uom, qty: qty )
62
+ end
63
+ true
64
+ end
65
+
66
+ def adjust_inventory
67
+ debit = self.sku.gl_asset_account
68
+ credit = self.invoice.customer.gl_receivables_account
69
+ if self.sku.does_track_inventory?
70
+ self.build_sku_tran({
71
+ origin: self, qty: self.qty*-1, sku_loc: self.sku_loc, uom: self.uom,
72
+ mac: 0, cost: self.extended_price,
73
+ origin_description: "INV #{self.invoice.visible_id}:#{self.sku.code}",
74
+ debit_gl_account: debit, credit_gl_account: credit
75
+ })
76
+ else
77
+ GlTransaction.push_or_save(
78
+ owner: self, amount: self.total,
79
+ debit: debit, credit: credit
80
+ )
81
+ end
82
+ true
83
+ end
84
+
85
+
86
+
87
+ end
88
+
89
+
90
+ end # Skr module
@@ -0,0 +1,60 @@
1
+ module Skr
2
+
3
+ class InventoryAdjustment < Skr::Model
4
+
5
+ has_visible_id
6
+ has_gl_transaction if: :should_apply_gl?
7
+
8
+ has_one :gl_transaction, :as=>:source, :inverse_of=>:source
9
+
10
+ belongs_to :location, export: true
11
+ belongs_to :reason, :class_name=>'Skr::IaReason', export: true
12
+
13
+ has_many :lines, :class_name=>'Skr::IaLine', :inverse_of=>:inventory_adjustment, export: { writable:true }
14
+
15
+ validates :reason, :location, :set=>true
16
+ validate :ensure_state_is_savable
17
+ validates_associated :lines
18
+
19
+ delegate_and_export :location_code
20
+ delegate_and_export :reason_code
21
+
22
+ state_machine do
23
+ state :pending , :initial=>true
24
+ state :applied
25
+ event :mark_applied do
26
+ transitions from: :pending, to: :applied
27
+ before :apply_adjustment
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def should_apply_gl?
34
+ state_event == :mark_applied
35
+ end
36
+
37
+ def attributes_for_gl_transaction
38
+ { source: self, location: location,
39
+ description: "IA #{self.visible_id}" }
40
+ end
41
+
42
+ def ensure_state_is_savable
43
+ if applied? && state_was == 'applied'
44
+ errors.add('base' , "Cannot update record once it's approved and applied")
45
+ return false
46
+ end
47
+ end
48
+
49
+ def apply_adjustment
50
+ self.lines.each do | line |
51
+ next if line.is_applied?
52
+ line.unlock_adjusting{ line.adjust_qty! }
53
+ end
54
+ true
55
+ end
56
+
57
+ end
58
+
59
+
60
+ end # Skr module