lucadeal 0.2.9 → 0.2.13

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a4958ee224643a3be3408a9b2c4043ac0ca37a1a4c7f3a537d626756a410e117
4
- data.tar.gz: 74d8ead724f3cf89b0d5163565316c9421d1f8a81675e1fa28411b6248674b43
3
+ metadata.gz: 9a2f79f88f7a7047f07f4c2137888f43e29cec70129e0b8d79544d4c8137e2f4
4
+ data.tar.gz: 8d3d2dbf4908f7e5851f64003eaf39fd455f34c1e8f51c07dff7685fa913e036
5
5
  SHA512:
6
- metadata.gz: 8f91f2b22e69bfa5bbde799ad4664a22b19513e75bcc5bf4a0459e5f0717a945d62abd73850821b320fcd0850c0a77537872f156769e3d3693c8e6c4de24c7c5
7
- data.tar.gz: 4f333744805790545bc8ee27eb771b441a6be18dc2b635c15121a385ba7713880c22295c43e5524d1f593a55018ab506c1f56c79775db08583922594dfcab6c7
6
+ metadata.gz: feb4db73cf4f5d59a1caafb5e18c7e00460e9903244ba9a43ac1781b8a2ffd7b6465d95a309211e98e487b5b77daa5e1463c4647313f001b80f779c2e23354ad
7
+ data.tar.gz: 6204831c6bb54534a3527c7280a5c761bfb422a52831d1dd4d26b484fc221ddc78a979d5b66e42263c1a6d76ebeb140fefcb54c63a203d8b292e6ce40e5e2e05
data/README.md CHANGED
@@ -1,15 +1,13 @@
1
- # Luca::Deal
1
+ # LucaDeal
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/luca/deal`. To experiment with that code, run `bin/console` for an interactive prompt.
4
-
5
- TODO: Delete this and the text above, and describe your gem
3
+ LucaDeal is Sales contract management application.
6
4
 
7
5
  ## Installation
8
6
 
9
7
  Add this line to your application's Gemfile:
10
8
 
11
9
  ```ruby
12
- gem 'luca-deal'
10
+ gem 'lucadeal'
13
11
  ```
14
12
 
15
13
  And then execute:
@@ -18,12 +16,138 @@ And then execute:
18
16
 
19
17
  Or install it yourself as:
20
18
 
21
- $ gem install luca-deal
19
+ $ gem install lucadeal
22
20
 
23
21
  ## Usage
24
22
 
25
23
  TODO: Write usage instructions here
26
24
 
25
+
26
+ ## Data Structure
27
+
28
+ Records are stored in YAML format. On historical records, see [LucaRecord](../lucarecord/README.md#historical-field).
29
+
30
+ ### Customer
31
+
32
+ Customer consists of label information.
33
+
34
+ | Top level | Second level | | historical | Description |
35
+ |-----------|--------------|------|------------|--------------------------------|
36
+ | id | | auto | | uuid |
37
+ | name | | must | x | customer's name |
38
+ | address | | | x | |
39
+ | address2 | | | x | |
40
+ | contacts | | | | Array of contact information |
41
+ | | mail | | | mail address receiving invoice |
42
+
43
+
44
+ ### Product
45
+
46
+ Product is items template referred by Contract.
47
+
48
+ | Top level | Second level | | historical | Description |
49
+ |-----------|--------------|----------|------------|------------------------------------------------------------------------------------------------------|
50
+ | id | | auto | | uuid |
51
+ | name | | | x | Product name. |
52
+ | items | | | | Array of items. |
53
+ | | name | | x | Item name. |
54
+ | | price | | x | Item price. |
55
+ | | qty | optional | x | quantity. Default: 1. |
56
+ | | type | optional | | If 'initial', this item is treated as initial cost, applied only on the first month of the contract. |
57
+
58
+
59
+ ### Contract
60
+
61
+ Contract is core object for calculation. Common fields are as follows:
62
+
63
+ | Top level | Second level | | historical | Description |
64
+ |-------------|---------------|----------|------------|------------------------------------------------------------------------------------------------------|
65
+ | id | | auto | | uuid |
66
+ | customer_id | | must | x | customer's uuid |
67
+ | terms | | | | |
68
+ | | effective | must | | Start date of the contract. |
69
+ | | defunct | | | End date of the contract. |
70
+
71
+ Fields for subscription customers are as bellows:
72
+
73
+ | Top level | Second level | | historical | Description |
74
+ |-----------|---------------|----------|------------|------------------------------------------------------------------------------------------------------|
75
+ | terms | | | | |
76
+ | | billing_cycle | optional | | If 'monthly', invoices are generated on each month. |
77
+ | | category | optional | | Default: 'subscription' |
78
+ | items | | | | Array of items. |
79
+ | | name | | x | Item name. |
80
+ | | price | | x | Item price. |
81
+ | | qty | optional | x | quantity. Default: 1. |
82
+ | | type | optional | | If 'initial', this item is treated as initial cost, applied only on the first month of the contract. |
83
+ | sales_fee | | optional | | |
84
+ | | id | | | contract id of fee with sales partner. |
85
+
86
+
87
+ Fields for sales fee are as bellows:
88
+
89
+ | Top level | Second level | | historical | Description |
90
+ |-----------|--------------|----------|------------|-------------------------------------------------------------------------------------|
91
+ | terms | | | | |
92
+ | | category | | | If 'sales_fee', contract is treated as selling commission. |
93
+ | rate | | optional | | |
94
+ | | default | | | sales fee rate. |
95
+ | | initial | | | sales fee rate for items of type=initial. |
96
+
97
+
98
+ ### Invoice
99
+
100
+ Invoice is basically auto generated from Customer and Contract objects.
101
+
102
+ | Top level | Second level | Description |
103
+ |------------|--------------|------------------------------------------|
104
+ | id | | uuid |
105
+ | issue_date | | |
106
+ | due_date | | |
107
+ | customer | | |
108
+ | | id | customer's uuid |
109
+ | | name | customer name |
110
+ | | address | |
111
+ | | address2 | |
112
+ | | to | Array of mail addresses |
113
+ | items | | Array of items. |
114
+ | | name | Item name. |
115
+ | | price | Item price. |
116
+ | | qty | quantity. Default: 1. |
117
+ | | type | |
118
+ | subtotal | | Array of subtotal by tax category. |
119
+ | | items | amount of items |
120
+ | | tax | amount of tax |
121
+ | | rate | applied tax category. Default: 'default' |
122
+ | sales_fee | | |
123
+ | | id | contract id of fee with sales partner. |
124
+ | status | | Array of status with timestamp. |
125
+
126
+
127
+ ### Fee
128
+
129
+ Fee is basically auto generated from Contract and Invoice objects.
130
+
131
+ | Top level | Second level | Description |
132
+ |-----------|--------------|----------------------------------------------|
133
+ | id | | uuid |
134
+ | sales_fee | | |
135
+ | | id | contract id with sales partner. |
136
+ | | default.fee | Amount of fee on dafault rate. |
137
+ | | default.tax | Amount of tax for default.fee. |
138
+ | | initial.fee | Amount of fee on initial cost. |
139
+ | | initial.tax | Amount of tax for initial.fee. |
140
+ | invoice | | Carbon copy of Invoice attributes. |
141
+ | | id | |
142
+ | | contract_id | |
143
+ | | issue_date | |
144
+ | | due_date | |
145
+ | customer | | Carbon copy of Invoice customer except 'to'. |
146
+ | items | | Carbon copy of Invoice items. |
147
+ | subtotal | | Carbon copy of Invoice subtotal. |
148
+ | status | | Array of status with timestamp. |
149
+
150
+
27
151
  ## Development
28
152
 
29
153
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -32,4 +156,4 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
32
156
 
33
157
  ## Contributing
34
158
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/luca-deal.
159
+ Bug reports and pull requests are welcome on GitHub at https://github.com/chumaltd/luca .
@@ -11,7 +11,7 @@ def customer(args = nil, params = {})
11
11
  LucaDeal::Customer.new.list_name
12
12
  when 'create'
13
13
  if params['name']
14
- id = LucaDeal::Customer.new.generate!(params['name'])
14
+ id = LucaDeal::Customer.create(name: params['name'])
15
15
  puts "Successfully generated Customer #{id}" if id
16
16
  puts 'Edit customer detail.' if id
17
17
  else
@@ -6,6 +6,8 @@ require 'luca_deal/version'
6
6
  module LucaDeal
7
7
  autoload :Customer, 'luca_deal/customer'
8
8
  autoload :Contract, 'luca_deal/contract'
9
+ autoload :Fee, 'luca_deal/fee'
9
10
  autoload :Invoice, 'luca_deal/invoice'
11
+ autoload :Product, 'luca_deal/product'
10
12
  autoload :Setup, 'luca_deal/setup'
11
13
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'luca_deal/version'
2
4
 
3
5
  require 'yaml'
@@ -13,35 +15,55 @@ module LucaDeal
13
15
  @pjdir = Pathname(Dir.pwd)
14
16
  end
15
17
 
18
+ # returns active contracts on specified date.
19
+ #
20
+ def self.asof(year, month, day)
21
+ return enum_for(:asof, year, month, day) unless block_given?
22
+
23
+ new("#{year}-#{month}-#{day}").active do |contract|
24
+ yield contract
25
+ end
26
+ end
27
+
16
28
  #
17
29
  # collect active contracts
18
30
  #
19
31
  def active
32
+ return enum_for(:active) unless block_given?
33
+
20
34
  self.class.all do |data|
21
- contract = parse_current(data)
22
- contract['items'] = contract['items'].map { |item| parse_current(item) }
23
- next if !self.class.active_period?(contract)
35
+ next if !active_period?(data.dig('terms'))
24
36
 
25
- yield contract
37
+ contract = parse_current(data)
38
+ contract['items'] = contract['items']&.map { |item| parse_current(item) }
39
+ # TODO: handle sales_fee rate change
40
+ contract['rate'] = contract['rate']
41
+ yield contract.compact
26
42
  end
27
43
  end
28
44
 
29
- def generate!(customer_id, mode = nil)
45
+ def generate!(customer_id, mode = 'subscription')
30
46
  LucaDeal::Customer.find(customer_id) do |customer|
31
47
  current_customer = parse_current(customer)
32
- obj = { 'customer_id' => current_customer['id'], 'customer_name' => current_customer['name'] }
33
- obj['terms'] = { 'effective' => @date }
34
48
  if mode == 'sales_fee'
35
- obj.merge! salesfee_template
49
+ obj = salesfee_template
36
50
  else
37
- obj.merge! monthly_template
51
+ obj = monthly_template
38
52
  end
53
+ obj.merge!({ 'customer_id' => current_customer['id'], 'customer_name' => current_customer['name'] })
54
+ obj['terms'] ||= {}
55
+ obj['terms']['effective'] = @date
39
56
  self.class.create(obj)
40
57
  end
41
58
  end
42
59
 
43
- def self.active_period?(dat)
44
- !dat.dig('terms').nil?
60
+ def active_period?(dat)
61
+ unless dat.dig('defunct').nil?
62
+ defunct = dat.dig('defunct').respond_to?(:year) ? dat.dig('defunct') : Date.parse(dat.dig('defunct'))
63
+ return false if @date > defunct
64
+ end
65
+ effective = dat.dig('effective').respond_to?(:year) ? dat.dig('effective') : Date.parse(dat.dig('effective'))
66
+ @date >= effective
45
67
  end
46
68
 
47
69
  private
@@ -21,17 +21,20 @@ module LucaDeal
21
21
  YAML.dump(list).tap { |l| puts l }
22
22
  end
23
23
 
24
- def generate!(name)
25
- contact = {
24
+ def self.create(obj)
25
+ raise ':name is required' if obj[:name].nil?
26
+
27
+ contacts = obj[:contact]&.map { |c| { 'mail' => c[:mail] } }&.compact
28
+ contacts ||= [{
26
29
  'mail' => '_MAIL_ADDRESS_FOR_CONTACT_'
30
+ }]
31
+ h = {
32
+ 'name' => obj[:name],
33
+ 'address' => obj[:address] || '_CUSTOMER_ADDRESS_FOR_INVOICE_',
34
+ 'address2' => obj[:address2] || '_CUSTOMER_ADDRESS_FOR_INVOICE_',
35
+ 'contacts' => contacts
27
36
  }
28
- obj = {
29
- 'name' => name,
30
- 'address' => '_CUSTOMER_ADDRESS_FOR_INVOICE_',
31
- 'address2' => '_CUSTOMER_ADDRESS_FOR_INVOICE_',
32
- 'contacts' => [contact]
33
- }
34
- self.class.create(obj)
37
+ super(h)
35
38
  end
36
39
  end
37
40
  end
@@ -0,0 +1,118 @@
1
+ require 'luca_deal/version'
2
+
3
+ require 'mail'
4
+ require 'yaml'
5
+ require 'pathname'
6
+ require 'bigdecimal'
7
+ require 'luca_support/config'
8
+ require 'luca_support/mail'
9
+ require 'luca_deal'
10
+
11
+ module LucaDeal
12
+ class Fee < LucaRecord::Base
13
+ @dirname = 'fees'
14
+
15
+ def initialize(date = nil)
16
+ @date = issue_date(date)
17
+ @config = load_config('config.yml')
18
+ end
19
+
20
+ # calculate fee, based on invoices
21
+ #
22
+ def monthly_fee
23
+ LucaDeal::Contract.asof(@date.year, @date.month, @date.day) do |contract|
24
+ next if contract.dig('terms', 'category') != 'sales_fee'
25
+
26
+ @rate = { 'default' => BigDecimal(contract.dig('rate', 'default')) }
27
+ @rate['initial'] = contract.dig('rate', 'initial') ? BigDecimal(contract.dig('rate', 'initial')) : @rate['default']
28
+
29
+ LucaDeal::Invoice.asof(@date.year, @date.month) do |invoice|
30
+ next if invoice.dig('sales_fee', 'id') != contract['id']
31
+ next if duplicated_contract? invoice['contract_id']
32
+
33
+ fee = invoice.dup
34
+ fee['invoice'] = {}.tap do |f_invoice|
35
+ %w[id contract_id issue_date due_date].each do |i|
36
+ f_invoice[i] = invoice[i]
37
+ fee.delete i
38
+ end
39
+ end
40
+ fee['id'] = issue_random_id
41
+ fee['customer'].delete('to')
42
+ fee['sales_fee'].merge! subtotal(fee['items'])
43
+ gen_fee!(fee)
44
+ end
45
+ end
46
+ end
47
+
48
+ def get_customer(id)
49
+ {}.tap do |res|
50
+ LucaDeal::Customer.find(id) do |dat|
51
+ customer = parse_current(dat)
52
+ res['id'] = customer['id']
53
+ res['name'] = customer.dig('name')
54
+ res['address'] = customer.dig('address')
55
+ res['address2'] = customer.dig('address2')
56
+ res['to'] = customer.dig('contacts').map { |h| take_current(h, 'mail') }.compact
57
+ end
58
+ end
59
+ end
60
+
61
+ def gen_fee!(fee)
62
+ id = fee.dig('invoice', 'contract_id')
63
+ self.class.create_record!(fee, @date, Array(id))
64
+ end
65
+
66
+ private
67
+
68
+ def lib_path
69
+ __dir__
70
+ end
71
+
72
+ # load user company profile from config.
73
+ #
74
+ def set_company
75
+ {}.tap do |h|
76
+ h['name'] = @config.dig('company', 'name')
77
+ h['address'] = @config.dig('company', 'address')
78
+ h['address2'] = @config.dig('company', 'address2')
79
+ end
80
+ end
81
+
82
+ # calc fee & tax amount by tax category
83
+ #
84
+ def subtotal(items)
85
+ {}.tap do |subtotal|
86
+ items.each do |i|
87
+ rate = i.dig('type') || 'default'
88
+ subtotal[rate] = { 'fee' => 0, 'tax' => 0 } if subtotal.dig(rate).nil?
89
+ subtotal[rate]['fee'] += i['qty'] * i['price'] * @rate[rate]
90
+ end
91
+ subtotal.each do |rate, amount|
92
+ amount['tax'] = (amount['fee'] * load_tax_rate(rate)).to_i
93
+ amount['fee'] = amount['fee'].to_i
94
+ end
95
+ end
96
+ end
97
+
98
+ def issue_date(date)
99
+ base = date.nil? ? Date.today : Date.parse(date)
100
+ Date.new(base.year, base.month, -1)
101
+ end
102
+
103
+ # load Tax Rate from config.
104
+ #
105
+ def load_tax_rate(name)
106
+ return 0 if @config.dig('tax_rate', name).nil?
107
+
108
+ BigDecimal(take_current(@config['tax_rate'], name).to_s)
109
+ end
110
+
111
+ def duplicated_contract?(id)
112
+ self.class.asof(@date.year, @date.month, @date.day) do |_f, path|
113
+ return true if path.include?(id)
114
+ end
115
+ false
116
+ end
117
+ end
118
+ end
@@ -58,7 +58,6 @@ module LucaDeal
58
58
  mail
59
59
  end
60
60
 
61
- #
62
61
  # Output seriarized invoice data to stdout.
63
62
  # Returns previous N months on multiple count
64
63
  #
@@ -113,20 +112,23 @@ module LucaDeal
113
112
  invoice['customer'] = get_customer(contract.dig('customer_id'))
114
113
  invoice['due_date'] = due_date(@date)
115
114
  invoice['issue_date'] = @date
115
+ invoice['sales_fee'] = contract.dig('sales_fee')
116
116
  invoice['items'] = contract.dig('items').map do |item|
117
+ next if item.dig('type') == 'initial' && subsequent_month?(contract.dig('terms', 'effective'))
118
+
117
119
  {}.tap do |h|
118
120
  h['name'] = item.dig('name')
119
121
  h['price'] = item.dig('price')
120
- h['qty'] = item.dig('qty')
122
+ h['qty'] = item.dig('qty') || 1
123
+ h['type'] = item.dig('type') if item.dig('type')
121
124
  end
122
- end
125
+ end.compact
123
126
  invoice['subtotal'] = subtotal(invoice['items'])
124
127
  .map { |k, v| v.tap { |dat| dat['rate'] = k } }
125
128
  gen_invoice!(invoice)
126
129
  end
127
130
  end
128
131
 
129
- #
130
132
  # set variables for ERB template
131
133
  #
132
134
  def invoice_vars(invoice_dat)
@@ -172,7 +174,6 @@ module LucaDeal
172
174
  __dir__
173
175
  end
174
176
 
175
- #
176
177
  # load user company profile from config.
177
178
  #
178
179
  def set_company
@@ -183,7 +184,6 @@ module LucaDeal
183
184
  end
184
185
  end
185
186
 
186
- #
187
187
  # calc items & tax amount by tax category
188
188
  #
189
189
  def subtotal(items)
@@ -199,7 +199,6 @@ module LucaDeal
199
199
  end
200
200
  end
201
201
 
202
- #
203
202
  # load Tax Rate from config.
204
203
  #
205
204
  def load_tax_rate(name)
@@ -218,5 +217,10 @@ module LucaDeal
218
217
  end
219
218
  false
220
219
  end
220
+
221
+ def subsequent_month?(effective_date)
222
+ effective_date = Date.parse(effective_date) unless effective_date.respond_to? :year
223
+ effective_date.year != @date.year || effective_date.month != @date.month
224
+ end
221
225
  end
222
226
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'luca_deal/version'
4
+
5
+ require 'date'
6
+ require 'yaml'
7
+ require 'pathname'
8
+ require 'luca_record'
9
+
10
+ module LucaDeal
11
+ class Product < LucaRecord::Base
12
+ @dirname = 'products'
13
+
14
+ def list_name
15
+ list = self.class.all.map { |dat| parse_current(dat) }
16
+ YAML.dump(list).tap { |l| puts l }
17
+ end
18
+
19
+ def self.create(obj)
20
+ raise ':name is required' if obj[:name].nil?
21
+
22
+ items = [{
23
+ 'name' => obj[:name],
24
+ 'price' => obj[:price] || 0,
25
+ 'qty' => obj[:qty] || 1
26
+ }]
27
+ if obj[:initial]
28
+ items << {
29
+ 'name' => obj.dig(:initial, :name),
30
+ 'price' => obj.dig(:initial, :price) || 0,
31
+ 'qty' => obj.dig(:initial, :qty) || 1,
32
+ 'type' => 'initial'
33
+ }
34
+ end
35
+ h = {
36
+ 'name' => obj[:name],
37
+ 'items' => items
38
+ }
39
+ super(h)
40
+ end
41
+ end
42
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LucaDeal
4
- VERSION = "0.2.9"
4
+ VERSION = '0.2.13'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lucadeal
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.9
4
+ version: 0.2.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuma Takahiro
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-11-04 00:00:00.000000000 Z
11
+ date: 2020-11-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lucarecord
@@ -83,7 +83,9 @@ files:
83
83
  - lib/luca_deal.rb
84
84
  - lib/luca_deal/contract.rb
85
85
  - lib/luca_deal/customer.rb
86
+ - lib/luca_deal/fee.rb
86
87
  - lib/luca_deal/invoice.rb
88
+ - lib/luca_deal/product.rb
87
89
  - lib/luca_deal/setup.rb
88
90
  - lib/luca_deal/templates/.keep
89
91
  - lib/luca_deal/templates/config.yml