lucadeal 0.2.9 → 0.2.13

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