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,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