cats_core 1.0.48 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e0cb918631b3c8651aca92abc3209f968ce85fbb0e6408332ae5683b95130333
4
- data.tar.gz: 3dca9a7897114e6bf7830951190ac504eaceb03e2f9bf1e3f7de1c5bd1158a5a
3
+ metadata.gz: e1410fa75e3f9e4eb96e4c9df8cbd6bb358bb56e4cce6ef42b095aff3b6331b1
4
+ data.tar.gz: c1529759ffa5ba650bd31df10ae302943afc14e876fff0710a35144cf0841981
5
5
  SHA512:
6
- metadata.gz: a8d658d04da98b1e5607be0843fdfde3663d7ea089b56b9fd7f073420ffa00715613426233c066ca1a92f503bfd8bbc8b9a91663f633ddb93b3e779d1546496a
7
- data.tar.gz: 4aa0772c1ca60ec644476785dc96b3fe7639fcb77e2a97d76e6d757f0a44c6a8f8beb6738edd15361e4edeb41c6559f2ae9f6186bd20e601358fcf1cf1092ab9
6
+ metadata.gz: 9b31f50e3f08e9a65c1da38520f3afef988613654e5250607e48761274951d3be4975e6b47c704c65ad6211d03081ab1c60b3f6ae1eea6fae191d498bd021110
7
+ data.tar.gz: 4046501c4c360769d9bc23628f4e067b8c7b55d95f898d57fda4f202f75ddc73fa5d5a60e5f163cf37ab340024b12a28065bb80f8be9eb5f5fb64d81151e8863
@@ -0,0 +1,13 @@
1
+ module Cats
2
+ module Core
3
+ class DispatchTransactionsController < ApplicationController
4
+ include Common
5
+
6
+ private
7
+
8
+ def model_params
9
+ params.require(:payload).permit(:source_id, :destination_id, :transaction_date, :quantity)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,75 @@
1
+ module Cats
2
+ module Core
3
+ class DispatchesController < ApplicationController
4
+ include Common
5
+
6
+ before_action :set_service, only: %i[create_receipt_authorization approve start search confirm]
7
+ before_action :set_dispatch, only: %i[approve start confirm]
8
+
9
+ def index
10
+ dispatches = Cats::Core::Dispatch.where(allocation_item_id: params[:id])
11
+ render json: { success: true, data: serialize(dispatches) }
12
+ end
13
+
14
+ def create
15
+ dispatch = Cats::Core::Dispatch.new(model_params)
16
+ dispatch.dispatch_status = Cats::Core::Dispatch::DRAFT
17
+ dispatch.prepared_by = current_user
18
+ if dispatch.save
19
+ render json: { success: true, data: serialize(dispatch) }, status: :created
20
+ else
21
+ render json: { success: false, error: dispatch.errors.full_messages[0] }, status: :unprocessable_entity
22
+ end
23
+ end
24
+
25
+ def create_receipt_authorization
26
+ allocation_item = Cats::Core::AllocationItem.find(params[:id])
27
+ dispatch = @service.create_receipt_authorization(
28
+ params[:reference_no], allocation_item, params[:quantity], params[:remark], current_user
29
+ )
30
+ render json: { success: true, data: serialize(dispatch) }
31
+ end
32
+
33
+ def approve
34
+ dispatch = @service.approve(@dispatch)
35
+ render json: { success: true, data: serialize(dispatch) }
36
+ rescue StandardError => e
37
+ render json: { success: false, error: e.message }
38
+ end
39
+
40
+ def start
41
+ dispatch = @service.start(@dispatch)
42
+ render json: { success: true, data: serialize(dispatch) }
43
+ rescue StandardError => e
44
+ render json: { success: false, error: e.message }
45
+ end
46
+
47
+ def confirm
48
+ dispatch = @service.confirm(@dispatch)
49
+ render json: { success: true, data: serialize(dispatch) }
50
+ rescue StandardError => e
51
+ render json: { success: false, error: e.message }
52
+ end
53
+
54
+ def search
55
+ data = @service.search(current_user)
56
+ render json: { success: true, data: serialize(data) }
57
+ end
58
+
59
+ private
60
+
61
+ def set_service
62
+ @service = DispatchService.new
63
+ end
64
+
65
+ def set_dispatch
66
+ @dispatch = Cats::Core::Dispatch.find(params[:id])
67
+ end
68
+
69
+ def model_params
70
+ params.require(:payload).permit(:reference_no, :allocation_item_id, :transporter_id, :plate_no, :driver_name,
71
+ :driver_phone, :quantity, :remark)
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,18 @@
1
+ module Cats
2
+ module Core
3
+ class ReceiptsController < ApplicationController
4
+ include Common
5
+
6
+ def index
7
+ receipts = Cats::Core::Receipt.where(dispatch_id: params[:id])
8
+ render json: { success: true, data: serialize(receipts) }
9
+ end
10
+
11
+ private
12
+
13
+ def model_params
14
+ params.require(:payload).permit(:dispatch_id, :quantity, :commodity_status, :status, :remark, :prepared_by_id)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,55 @@
1
+ module Cats
2
+ module Core
3
+ module Common
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ before_action :set_clazz
8
+ before_action :set_object, only: %i[show update]
9
+ end
10
+
11
+ def index
12
+ data = serialize(@clazz.all)
13
+ render json: { success: true, data: data }
14
+ end
15
+
16
+ def show
17
+ render json: { success: true, data: serialize(@obj) }
18
+ end
19
+
20
+ def create
21
+ obj = @clazz.new(model_params)
22
+ if obj.save
23
+ render json: { success: true, data: serialize(obj) }, status: :created
24
+ else
25
+ render json: { success: false, error: obj.errors.full_messages[0] }, status: :unprocessable_entity
26
+ end
27
+ end
28
+
29
+ def update
30
+ if @obj.update(model_params)
31
+ render json: { success: true, data: serialize(@obj) }
32
+ else
33
+ render json: { success: false, error: @obj.errors.full_messages[0] }, status: :unprocessable_entity
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def serialize(data)
40
+ ActiveModelSerializers::SerializableResource.new(data)
41
+ end
42
+
43
+ def set_clazz
44
+ @clazz = "Cats::Core::#{controller_name.classify}".constantize
45
+ end
46
+
47
+ def set_object
48
+ @obj = @clazz.find(params[:id])
49
+ end
50
+
51
+ # This class should be overridden by respective child controllers
52
+ def model_params; end
53
+ end
54
+ end
55
+ end
@@ -13,6 +13,7 @@ module Cats
13
13
  belongs_to :prepared_by, class_name: 'Cats::Core::User'
14
14
  belongs_to :transporter
15
15
  belongs_to :allocation_item
16
+ has_many :receipts
16
17
 
17
18
  validates :reference_no, :plate_no, :driver_name, :driver_phone, :quantity, :commodity_status, presence: true
18
19
  validates :dispatch_status, presence: true, inclusion: { in: DISPATCH_STATUSES }
@@ -3,9 +3,8 @@ module Cats
3
3
  class Receipt < ApplicationRecord
4
4
  # Receipt status
5
5
  DRAFT = 'Draft'.freeze
6
- ACCEPTED = 'Accepted'.freeze
7
- DECLINED = 'Declined'.freeze
8
- RECEIPT_STATUSES = [DRAFT, ACCEPTED, DECLINED].freeze
6
+ CONFIRMED = 'Confirmed'.freeze
7
+ RECEIPT_STATUSES = [DRAFT, CONFIRMED].freeze
9
8
 
10
9
  belongs_to :dispatch
11
10
  belongs_to :prepared_by, class_name: 'Cats::Core::User'
@@ -14,6 +13,28 @@ module Cats
14
13
  validates :quantity, numericality: { greater_than: 0 }
15
14
  validates :status, inclusion: { in: RECEIPT_STATUSES }
16
15
  validates :commodity_status, inclusion: { in: Cats::Core::Commodity::COMMODITY_STATUSES }
16
+ validate :validate_dispatch_status, :validate_total_quantity
17
+
18
+ delegate(:reference_no, to: :dispatch, prefix: true)
19
+ delegate(:email, to: :prepared_by, prefix: true)
20
+
21
+ def validate_dispatch_status
22
+ return unless dispatch
23
+
24
+ return if dispatch.dispatch_status == Cats::Core::Dispatch::STARTED
25
+
26
+ errors.add(:dispatch, 'Dispatch should be in "Started" state.')
27
+ end
28
+
29
+ def validate_total_quantity
30
+ return unless dispatch && quantity
31
+
32
+ received = Cats::Core::Receipt.where(dispatch: dispatch).sum(:quantity)
33
+ remaining = dispatch.quantity - received
34
+ return if quantity <= remaining
35
+
36
+ errors.add(:quantity, "Total receipt quantity is higher than dispatch quantity (Max = #{remaining}).")
37
+ end
17
38
  end
18
39
  end
19
40
  end
@@ -2,6 +2,7 @@ module Cats
2
2
  module Core
3
3
  class Transaction < ApplicationRecord
4
4
  self.abstract_class = true
5
+ after_initialize :set_status
5
6
 
6
7
  # Transaction statuses
7
8
  DRAFT = 'Draft'.freeze
@@ -25,8 +26,16 @@ module Cats
25
26
  destination.quantity += quantity
26
27
  source.save
27
28
  destination.save
29
+ self.status = COMMITTED
30
+ save
28
31
  end
29
32
  end
33
+
34
+ def set_status
35
+ return unless new_record?
36
+
37
+ self.status = DRAFT
38
+ end
30
39
  end
31
40
  end
32
41
  end
@@ -0,0 +1,8 @@
1
+ class DispatchSerializer < ActiveModel::Serializer
2
+ attributes :id, :reference_no, :allocation_item_id, :transporter_id, :transporter_name, :plate_no, :driver_name,
3
+ :driver_phone, :quantity, :remark, :prepared_by_id, :prepared_by_email, :dispatch_status, :destination
4
+
5
+ def destination
6
+ object.allocation_item.destination.name
7
+ end
8
+ end
@@ -0,0 +1,4 @@
1
+ class DispatchTransactionSerializer < ActiveModel::Serializer
2
+ attributes :id, :source_id, :source_code, :destination_id, :destination_reference_no, :quantity, :transaction_date,
3
+ :status
4
+ end
@@ -0,0 +1,4 @@
1
+ class ReceiptSerializer < ActiveModel::Serializer
2
+ attributes :id, :dispatch_id, :dispatch_reference_no, :commodity_status, :quantity, :status, :remark,
3
+ :prepared_by_id, :prepared_by_email
4
+ end
@@ -0,0 +1,136 @@
1
+ module Cats
2
+ module Core
3
+ class DispatchService
4
+ def create_receipt_authorization(reference_no, allocation_item, quantity, remark, user)
5
+ status = allocation_item.allocation.allocation_status
6
+ raise(StandardError, 'Allocation not approved.') unless status == Cats::Core::Allocation::APPROVED
7
+
8
+ transporter = Cats::Core::Transporter.find_by(code: 'SUP-TRANS')
9
+ raise(StandardError, 'Supplier transporter does not exist.') unless transporter
10
+
11
+ store = Cats::Core::Store.find_by(code: 'SUP-STORE')
12
+ raise(StandardError, 'Supplier store does not exist.') unless store
13
+
14
+ commodity = allocation_item.allocation.commodity
15
+ stack = store.stacks.find_by(commodity: commodity)
16
+
17
+ raise(StandardError, 'Commodity not found in supplier store.') unless stack
18
+
19
+ dispatch = Cats::Core::Dispatch.create!(
20
+ reference_no: reference_no,
21
+ allocation_item: allocation_item,
22
+ transporter: transporter,
23
+ plate_no: 'Supplier plate no',
24
+ driver_name: 'Supplier driver',
25
+ driver_phone: 'Supplier driver phone',
26
+ quantity: quantity,
27
+ remark: remark,
28
+ prepared_by: user,
29
+ dispatch_status: Cats::Core::Dispatch::DRAFT
30
+ )
31
+
32
+ # Create a dispatch transaction
33
+ Cats::Core::DispatchTransaction.create!(
34
+ source: stack,
35
+ destination: dispatch,
36
+ transaction_date: Date.today,
37
+ quantity: quantity,
38
+ status: Cats::Core::Transaction::DRAFT
39
+ )
40
+ dispatch
41
+ end
42
+
43
+ def search(user)
44
+ details = user.details
45
+
46
+ unless details['stores'] || details['warehouse'] || details['hub']
47
+ raise(StandardError, 'User does not have associated location.')
48
+ end
49
+
50
+ if details['stores']
51
+ warehouse = Cats::Core::Store.find(details['stores'][0]).warehouse
52
+ hub = warehouse.parent
53
+
54
+ dispatches = Cats::Core::Dispatch.joins(:allocation_item)
55
+ .where(
56
+ allocation_item: { destination: hub },
57
+ dispatch_status: Cats::Core::Dispatch::STARTED
58
+ ).or(
59
+ Cats::Core::Dispatch.joins(:allocation_item)
60
+ .where(
61
+ allocation_item: { destination: warehouse },
62
+ dispatch_status: Cats::Core::Dispatch::STARTED
63
+ )
64
+ )
65
+ elsif details['warehouse']
66
+ warehouse = Cats::Core::Location.find(details['warehouse'])
67
+ hub = warehouse.parent
68
+ dispatches = Cats::Core::Dispatch.joins(:allocation_item)
69
+ .where(
70
+ allocation_item: { destination: hub },
71
+ dispatch_status: Cats::Core::Dispatch::STARTED
72
+ ).or(
73
+ Cats::Core::Dispatch.joins(:allocation_item)
74
+ .where(
75
+ allocation_item: { destination: warehouse },
76
+ dispatch_status: Cats::Core::Dispatch::STARTED
77
+ )
78
+ )
79
+ elsif details['hub']
80
+ hub = Cats::Core::Location.find(details['hub'])
81
+ dispatches = Cats::Core::Dispatch.joins(:allocation_item)
82
+ .where(
83
+ allocation_item: { destination: hub },
84
+ dispatch_status: Cats::Core::Dispatch::STARTED
85
+ )
86
+ end
87
+ dispatches
88
+ end
89
+
90
+ def approve(dispatch)
91
+ unless dispatch.dispatch_status == Cats::Core::Dispatch::DRAFT
92
+ raise(StandardError, 'Dispatch has to be in draft state.')
93
+ end
94
+
95
+ transactions = Cats::Core::DispatchTransaction.where(destination: dispatch).count
96
+ raise(StandardError, 'Dispatch has no transactions.') unless transactions.positive?
97
+
98
+ dispatch.dispatch_status = Cats::Core::Dispatch::APPROVED
99
+ dispatch.save
100
+ dispatch
101
+ end
102
+
103
+ def start(dispatch)
104
+ unless dispatch.dispatch_status == Cats::Core::Dispatch::APPROVED
105
+ raise(StandardError, 'Dispatch has to be approved first.')
106
+ end
107
+
108
+ dispatch.dispatch_status = Cats::Core::Dispatch::STARTED
109
+ dispatch.save
110
+ transactions = Cats::Core::DispatchTransaction.where(destination: dispatch)
111
+ transactions.each(&:commit)
112
+ dispatch
113
+ end
114
+
115
+ def confirm(dispatch)
116
+ total = dispatch.receipts.sum(:quantity)
117
+
118
+ unless total == dispatch.quantity
119
+ raise(
120
+ StandardError,
121
+ "There is an amount of #{dispatch.quantity - total} in the dispatch which is unaccounted for."
122
+ )
123
+ end
124
+
125
+ Cats::Core::Dispatch.transaction do
126
+ dispatch.dispatch_status = Cats::Core::Dispatch::RECEIVED
127
+ dispatch.receipts.each { |r| r.status = Cats::Core::Receipt::CONFIRMED }
128
+ dispatch.save
129
+ dispatch.receipts.each(&:save)
130
+ end
131
+
132
+ dispatch
133
+ end
134
+ end
135
+ end
136
+ end
@@ -8,7 +8,9 @@ module Cats
8
8
  def available_space(id, level)
9
9
  case level
10
10
  when HUB
11
- warehouse_ids = Cats::Core::Location.find(id).children.map(&:id)
11
+ hub = Cats::Core::Location.find(id)
12
+ warehouses = hub.children
13
+ warehouse_ids = warehouses.map(&:id)
12
14
  stores = Cats::Core::Store.joins(:warehouse)
13
15
  .where(
14
16
  cats_core_locations: {
@@ -19,19 +21,48 @@ module Cats
19
21
  wh_details = []
20
22
  stores.each do |key, value|
21
23
  total_space = value.inject(0) { |sum, val| sum + val.available_space }
22
- details = value.map { |val| { store_id: val.id, total_space: val.available_space } }
23
- wh_details << { warehouse_id: key, total_space: total_space, details: details }
24
+ details = value.map do |val|
25
+ { store_id: val.id, code: val.code, name: val.name, total_space: val.available_space }
26
+ end
27
+ warehouse = warehouses.find(key)
28
+ wh_details << {
29
+ warehouse_id: key,
30
+ code: warehouse.code,
31
+ name: warehouse.name,
32
+ total_space: total_space,
33
+ details: details
34
+ }
24
35
  end
25
36
  hub_space = wh_details.inject(0) { |sum, val| sum + val[:total_space] }
26
- space = { hub_id: id, total_space: hub_space, details: wh_details }
37
+ space = {
38
+ hub_id: id,
39
+ code: hub.code,
40
+ name: hub.name,
41
+ total_space: hub_space,
42
+ details: wh_details
43
+ }
27
44
  when WAREHOUSE
45
+ warehouse = Cats::Core::Location.find(id)
28
46
  stores = Cats::Core::Store.where(warehouse_id: id)
29
- details = stores.map { |store| { store_id: store.id, total_space: store.available_space } }
47
+ details = stores.map do |store|
48
+ { store_id: store.id, code: store.code, name: store.name, total_space: store.available_space }
49
+ end
30
50
  total_space = stores.inject(0) { |sum, val| sum + val.available_space }
31
- space = { warehouse_id: id, total_space: total_space, details: details }
51
+ space = {
52
+ warehouse_id: id,
53
+ code: warehouse.code,
54
+ name: warehouse.name,
55
+ total_space: total_space,
56
+ details: details
57
+ }
32
58
  when STORE
33
59
  store = Cats::Core::Store.find(id)
34
- space = { store_id: store.id, total_space: store.available_space }
60
+ space = {
61
+ store_id: store.id,
62
+ code: store.code,
63
+ name: store.name,
64
+ total_space: store.available_space
65
+ }
35
66
  end
36
67
  space
37
68
  end
data/config/routes.rb CHANGED
@@ -33,11 +33,11 @@ Cats::Core::Engine.routes.draw do
33
33
  to: 'spaces#available_space',
34
34
  as: :hub_space,
35
35
  defaults: { level: Cats::Core::SpaceService::HUB }
36
- get '/warehousess/:id/space',
36
+ get '/warehouses/:id/space',
37
37
  to: 'spaces#available_space',
38
38
  as: :warehouse_space,
39
39
  defaults: { level: Cats::Core::SpaceService::WAREHOUSE }
40
- get '/store/:id/space',
40
+ get '/stores/:id/space',
41
41
  to: 'spaces#available_space',
42
42
  as: :store_space,
43
43
  defaults: { level: Cats::Core::SpaceService::STORE }
@@ -49,4 +49,22 @@ Cats::Core::Engine.routes.draw do
49
49
  end
50
50
  resources :currencies, except: %i[destroy]
51
51
  resources :unit_of_measures, except: %i[destroy]
52
+
53
+ get '/allocation_items/:id/dispatches', controller: :dispatches, action: :index, as: :dispatches_allocation_item
54
+ post '/allocation_items/:id/receipt_authorizaton',
55
+ controller: :dispatches,
56
+ action: :create_receipt_authorization,
57
+ as: :receipt_authorization_allocation_item
58
+
59
+ get '/dispatches/search', controller: :dispatches, action: :search, as: :search_dispatches
60
+ resources :dispatches, except: %i[index destroy] do
61
+ member do
62
+ get 'receipts', controller: :receipts, action: :index
63
+ post 'approve'
64
+ post 'start'
65
+ post 'confirm'
66
+ end
67
+ end
68
+ resources :dispatch_transactions, except: %i[new edit destroy]
69
+ resources :receipts, except: %i[index new edit destroy]
52
70
  end
@@ -1,5 +1,5 @@
1
1
  module Cats
2
2
  module Core
3
- VERSION = '1.0.48'.freeze
3
+ VERSION = '1.1.1'.freeze
4
4
  end
5
5
  end
@@ -1,6 +1,6 @@
1
1
  FactoryBot.define do
2
2
  factory :receipt, class: 'Cats::Core::Receipt' do
3
- dispatch
3
+ dispatch factory: :dispatch, dispatch_status: Cats::Core::Dispatch::STARTED
4
4
  quantity { 1.5 }
5
5
  commodity_status { Cats::Core::Commodity::GOOD }
6
6
  status { Cats::Core::Receipt::DRAFT }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cats_core
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.48
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Henock L.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-09-22 00:00:00.000000000 Z
11
+ date: 2021-09-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: active_model_serializers
@@ -220,13 +220,17 @@ files:
220
220
  - app/controllers/cats/core/application_controller.rb
221
221
  - app/controllers/cats/core/commodity_categories_controller.rb
222
222
  - app/controllers/cats/core/currencies_controller.rb
223
+ - app/controllers/cats/core/dispatch_transactions_controller.rb
224
+ - app/controllers/cats/core/dispatches_controller.rb
223
225
  - app/controllers/cats/core/locations_controller.rb
224
226
  - app/controllers/cats/core/menus_controller.rb
225
227
  - app/controllers/cats/core/notifications_controller.rb
228
+ - app/controllers/cats/core/receipts_controller.rb
226
229
  - app/controllers/cats/core/roles_controller.rb
227
230
  - app/controllers/cats/core/spaces_controller.rb
228
231
  - app/controllers/cats/core/unit_of_measures_controller.rb
229
232
  - app/controllers/cats/core/users_controller.rb
233
+ - app/controllers/concerns/cats/core/common.rb
230
234
  - app/jobs/cats/core/application_job.rb
231
235
  - app/models/cats/core/allocation.rb
232
236
  - app/models/cats/core/allocation_item.rb
@@ -269,9 +273,13 @@ files:
269
273
  - app/notifications/cats/core/simple_notification.rb
270
274
  - app/serializers/cats/core/commodity_category_serializer.rb
271
275
  - app/serializers/cats/core/currency_serializer.rb
276
+ - app/serializers/cats/core/dispatch_serializer.rb
277
+ - app/serializers/cats/core/dispatch_transaction_serializer.rb
272
278
  - app/serializers/cats/core/location_serializer.rb
279
+ - app/serializers/cats/core/receipt_serializer.rb
273
280
  - app/serializers/cats/core/role_menu_serializer.rb
274
281
  - app/serializers/cats/core/unit_of_measure_serializer.rb
282
+ - app/services/cats/core/dispatch_service.rb
275
283
  - app/services/cats/core/menu_service.rb
276
284
  - app/services/cats/core/notification_service.rb
277
285
  - app/services/cats/core/space_service.rb