lucadeal 0.2.9 → 0.2.18

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: d8159099446dcdae1b4c928d4eac5861cd35fe13f8d37155cdf28dafb30fc21e
4
+ data.tar.gz: 3c086a2ea6c4c9ec6bc9014fa9500ab9fde0a07c1778a5817fbb64e7409b5670
5
5
  SHA512:
6
- metadata.gz: 8f91f2b22e69bfa5bbde799ad4664a22b19513e75bcc5bf4a0459e5f0717a945d62abd73850821b320fcd0850c0a77537872f156769e3d3693c8e6c4de24c7c5
7
- data.tar.gz: 4f333744805790545bc8ee27eb771b441a6be18dc2b635c15121a385ba7713880c22295c43e5524d1f593a55018ab506c1f56c79775db08583922594dfcab6c7
6
+ metadata.gz: 0a26cec96f1a0805044470f8710b36b60aacf344bad53527c2a9fd4ff64557a06836e4eefec21f083e0b51bc48a9854dbe2fdd6fed4d4754aba9fe51fa5890ce
7
+ data.tar.gz: 8a611e016833673fb1e7fcab747a24d4bfdb4152a60086791a1377d8576feb181255c8b2a0dfeca2f6807a4d11d4d2f579be6353ca62a7996a5f960d8b4f1311
@@ -0,0 +1,20 @@
1
+ ## LucaDeal 0.2.18
2
+
3
+ * Breaking change: restructure CLI in sub-sub command format.
4
+ * Add 'x-customer', 'x-editor' on export to LucaBook
5
+
6
+ ## LucaDeal 0.2.17
7
+
8
+ * `luca-deal export` export JSON for LucaBook
9
+
10
+ ## LucaDeal 0.2.14
11
+
12
+ * Introduce Product for selling items template.
13
+
14
+ ## LucaDeal 0.2.12
15
+
16
+ * Introduce Sales fee calculation.
17
+
18
+ ## LucaDeal 0.2.10
19
+
20
+ * items can have one time cost at initial month of contract.
data/README.md CHANGED
@@ -1,15 +1,15 @@
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.
3
+ [![Gem Version](https://badge.fury.io/rb/lucadeal.svg)](https://badge.fury.io/rb/lucadeal)
4
4
 
5
- TODO: Delete this and the text above, and describe your gem
5
+ LucaDeal is Sales contract management application.
6
6
 
7
7
  ## Installation
8
8
 
9
9
  Add this line to your application's Gemfile:
10
10
 
11
11
  ```ruby
12
- gem 'luca-deal'
12
+ gem 'lucadeal'
13
13
  ```
14
14
 
15
15
  And then execute:
@@ -18,11 +18,179 @@ And then execute:
18
18
 
19
19
  Or install it yourself as:
20
20
 
21
- $ gem install luca-deal
21
+ $ gem install lucadeal
22
22
 
23
23
  ## Usage
24
24
 
25
- TODO: Write usage instructions here
25
+ You can create project skelton with `new` sub command.
26
+
27
+ ```
28
+ $ luca-deal new Dir
29
+ ```
30
+
31
+ Example assumes setup with bundler shim. `bundle exec` prefix may be needed in most cases.
32
+
33
+
34
+ ### Manage Contract
35
+
36
+ Customer object can be created by `customer create` subcommand with name.
37
+
38
+ ```
39
+ $ luca-deal customer create CustomerName
40
+ Successfully generated Customer 5976652cc2d9c0ebf4a8646f7a28aa8d6bd2d606
41
+ Edit customer detail.
42
+ ```
43
+
44
+ Customer is filed under `data/customers` as YAML. Detail need to be editted.
45
+ Then, Contract object is created by `contract create` sub command with customer id.
46
+
47
+ ```
48
+ $ luca-deal contract create 5976652cc2d9c0ebf4a8646f7a28aa8d6bd2d606
49
+ uccessfully generated Contract 814c6fc9fffe5566fe8e7ef683b439b355d612dc
50
+ Conditions are tentative. Edit contract detail.
51
+ ```
52
+
53
+ Contract is filed under `data/contracts` as YAML. Detail need to be editted.
54
+
55
+
56
+ ### Issue invoice
57
+
58
+ Monthly invoices are generated with `invoice create --monthly` sub command. Target month is optional. Without month, this month including today is the target.
59
+
60
+ ```
61
+ $ luca-deal invoice create --monthly [yyyy m]
62
+ ```
63
+
64
+ Invoice conditions are defined by contracts.
65
+
66
+
67
+ ## Data Structure
68
+
69
+ Records are stored in YAML format. On historical records, see [LucaRecord](../lucarecord/README.md#historical-field).
70
+
71
+ ### Customer
72
+
73
+ Customer consists of label information.
74
+
75
+ | Top level | Second level | | historical | Description |
76
+ |-----------|--------------|------|------------|--------------------------------|
77
+ | id | | auto | | uuid |
78
+ | name | | must | x | customer's name |
79
+ | address | | | x | |
80
+ | address2 | | | x | |
81
+ | contacts | | | | Array of contact information |
82
+ | | mail | | | mail address receiving invoice |
83
+
84
+
85
+ ### Product
86
+
87
+ Product is items template referred by Contract.
88
+
89
+ | Top level | Second level | | historical | Description |
90
+ |-----------|--------------|----------|------------|------------------------------------------------------------------------------------------------------|
91
+ | id | | auto | | uuid |
92
+ | name | | | x | Product name. |
93
+ | items | | | | Array of items. |
94
+ | | name | | x | Item name. |
95
+ | | price | | x | Item price. |
96
+ | | qty | optional | x | quantity. Default: 1. |
97
+ | | type | optional | | If 'initial', this item is treated as initial cost, applied only on the first month of the contract. |
98
+
99
+
100
+ ### Contract
101
+
102
+ Contract is core object for calculation. Common fields are as follows:
103
+
104
+ | Top level | Second level | | historical | Description |
105
+ |-------------|---------------|----------|------------|------------------------------------------------------------------------------------------------------|
106
+ | id | | auto | | uuid |
107
+ | customer_id | | must | x | customer's uuid |
108
+ | terms | | | | |
109
+ | | effective | must | | Start date of the contract. |
110
+ | | defunct | | | End date of the contract. |
111
+
112
+ Fields for subscription customers are as bellows:
113
+
114
+ | Top level | Second level | | historical | Description |
115
+ |-----------|---------------|----------|------------|------------------------------------------------------------------------------------------------------|
116
+ | terms | | | | |
117
+ | | billing_cycle | optional | | If 'monthly', invoices are generated on each month. |
118
+ | | category | optional | | Default: 'subscription' |
119
+ | products | | | | Array of products. |
120
+ | | id | | | reference for Product |
121
+ | items | | | | Array of items. |
122
+ | | name | | x | Item name. |
123
+ | | price | | x | Item price. |
124
+ | | qty | optional | x | quantity. Default: 1. |
125
+ | | type | optional | | If 'initial', this item is treated as initial cost, applied only on the first month of the contract. |
126
+ | sales_fee | | optional | | |
127
+ | | id | | | contract id of fee with sales partner. |
128
+
129
+
130
+ Fields for sales fee are as bellows:
131
+
132
+ | Top level | Second level | | historical | Description |
133
+ |-----------|--------------|----------|------------|-------------------------------------------------------------------------------------|
134
+ | terms | | | | |
135
+ | | category | | | If 'sales_fee', contract is treated as selling commission. |
136
+ | rate | | optional | | |
137
+ | | default | | | sales fee rate. |
138
+ | | initial | | | sales fee rate for items of type=initial. |
139
+
140
+
141
+ ### Invoice
142
+
143
+ Invoice is basically auto generated from Customer and Contract objects.
144
+
145
+ | Top level | Second level | Description |
146
+ |------------|--------------|------------------------------------------|
147
+ | id | | uuid |
148
+ | issue_date | | |
149
+ | due_date | | |
150
+ | customer | | |
151
+ | | id | customer's uuid |
152
+ | | name | customer name |
153
+ | | address | |
154
+ | | address2 | |
155
+ | | to | Array of mail addresses |
156
+ | items | | Array of items. |
157
+ | | name | Item name. |
158
+ | | price | Item price. |
159
+ | | qty | quantity. Default: 1. |
160
+ | | type | |
161
+ | | product_id | refrence for Product |
162
+ | subtotal | | Array of subtotal by tax category. |
163
+ | | items | amount of items |
164
+ | | tax | amount of tax |
165
+ | | rate | applied tax category. Default: 'default' |
166
+ | sales_fee | | |
167
+ | | id | contract id of fee with sales partner. |
168
+ | status | | Array of status with timestamp. |
169
+
170
+
171
+ ### Fee
172
+
173
+ Fee is basically auto generated from Contract and Invoice objects.
174
+
175
+ | Top level | Second level | Description |
176
+ |-----------|--------------|----------------------------------------------|
177
+ | id | | uuid |
178
+ | sales_fee | | |
179
+ | | id | contract id with sales partner. |
180
+ | | default.fee | Amount of fee on dafault rate. |
181
+ | | default.tax | Amount of tax for default.fee. |
182
+ | | initial.fee | Amount of fee on initial cost. |
183
+ | | initial.tax | Amount of tax for initial.fee. |
184
+ | invoice | | Carbon copy of Invoice attributes. |
185
+ | | id | |
186
+ | | contract_id | |
187
+ | | issue_date | |
188
+ | | due_date | |
189
+ | customer | | Carbon copy of Invoice customer except 'to'. |
190
+ | items | | Carbon copy of Invoice items. |
191
+ | subtotal | | Carbon copy of Invoice subtotal. |
192
+ | status | | Array of status with timestamp. |
193
+
26
194
 
27
195
  ## Development
28
196
 
@@ -32,4 +200,4 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
32
200
 
33
201
  ## Contributing
34
202
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/luca-deal.
203
+ Bug reports and pull requests are welcome on GitHub at https://github.com/chumaltd/luca .
@@ -5,57 +5,102 @@ require 'date'
5
5
  require 'optparse'
6
6
  require 'luca_deal'
7
7
 
8
- def customer(args = nil, params = {})
9
- case params['mode']
10
- when 'list'
11
- LucaDeal::Customer.new.list_name
12
- when 'create'
13
- if params['name']
14
- id = LucaDeal::Customer.new.generate!(params['name'])
15
- puts "Successfully generated Customer #{id}" if id
16
- puts 'Edit customer detail.' if id
17
- else
18
- puts 'requires customer\'s name. exit'
19
- exit 1
8
+ module LucaCmd
9
+ class Customer
10
+ def self.create(args = nil, params = {})
11
+ if args
12
+ id = LucaDeal::Customer.create(name: args[0])
13
+ puts "Successfully generated Customer #{id}" if id
14
+ puts 'Edit customer detail.' if id
15
+ else
16
+ puts 'requires customer\'s name. exit'
17
+ exit 1
18
+ end
19
+ end
20
+
21
+ def self.delete(args = nil, params = {})
22
+ if args
23
+ id = LucaDeal::Customer.delete(args[0])
24
+ else
25
+ puts 'requires customer\'s id. exit'
26
+ exit 1
27
+ end
28
+ end
29
+
30
+ def self.list(args = nil, params = {})
31
+ LucaDeal::Customer.new.list_name
20
32
  end
21
- else
22
- puts 'invalid option. --help for usage'
23
- exit 1
24
33
  end
25
- end
26
34
 
27
- def contract(args = nil, params = {})
28
- case params['mode']
29
- when 'create'
30
- if params['customer_id']
31
- id = LucaDeal::Contract.new.generate!(params['customer_id'], params['category'])
32
- puts "Successfully generated Contract #{id}" if id
33
- puts 'Conditions are tentative. Edit contract detail.' if id
34
- else
35
- puts 'requires customer\'s id. exit'
36
- exit 1
35
+ class Contract
36
+ def self.create(args = nil, params = {})
37
+ if args
38
+ id = LucaDeal::Contract.new.generate!(args[0], params['category'])
39
+ puts "Successfully generated Contract #{id}" if id
40
+ puts 'Conditions are tentative. Edit contract detail.' if id
41
+ else
42
+ puts 'requires customer\'s id. exit'
43
+ exit 1
44
+ end
45
+ end
46
+
47
+ def self.delete(args = nil, params = {})
48
+ if args
49
+ id = LucaDeal::Contract.delete(args[0])
50
+ else
51
+ puts 'requires contract id. exit'
52
+ exit 1
53
+ end
37
54
  end
38
- else
39
55
  end
40
- end
41
56
 
42
- def invoice(args = nil, params = {})
43
- date = "#{args[0]}-#{args[1]}-#{args[2] || '1'}" if !args.empty?
44
- case params['mode']
45
- when 'monthly'
46
- LucaDeal::Invoice.new(date).monthly_invoice
47
- when 'mail'
48
- LucaDeal::Invoice.new(date).deliver_mail
49
- when 'preview'
50
- LucaDeal::Invoice.new(date).preview_mail
51
- when 'stats'
52
- if args.empty?
53
- date = "#{Date.today.year}-#{Date.today.month}-1"
54
- count = 3
55
- end
56
- LucaDeal::Invoice.new(date).stats(count || 1)
57
- else
58
- puts 'not implemented mode'
57
+ class Invoice
58
+ def self.create(args = nil, params = {})
59
+ date = "#{args[0]}-#{args[1]}-#{args[2] || '1'}" if !args.empty?
60
+ case params['mode']
61
+ when 'monthly'
62
+ LucaDeal::Invoice.new(date).monthly_invoice
63
+ else
64
+ puts 'not implemented mode'
65
+ end
66
+ end
67
+
68
+ def self.delete(args = nil, params = {})
69
+ if args
70
+ id = LucaDeal::Invoice.delete(args[0])
71
+ else
72
+ puts 'requires contract id. exit'
73
+ exit 1
74
+ end
75
+ end
76
+
77
+ def self.export(args = nil, _params = nil)
78
+ if args
79
+ args << 28 if args.length == 2 # specify safe last day
80
+ LucaDeal::Invoice.new(args.join('-')).export_json
81
+ else
82
+ LucaDeal::Invoice.new.export_json
83
+ end
84
+ end
85
+
86
+ def self.list(args = nil, params = {})
87
+ date = "#{args[0]}-#{args[1]}-#{args[2] || '1'}" if !args.empty?
88
+ if args.empty?
89
+ date = "#{Date.today.year}-#{Date.today.month}-1"
90
+ count = 3
91
+ end
92
+ LucaDeal::Invoice.new(date).stats(count || 1)
93
+ end
94
+
95
+ def self.mail(args = nil, params = {})
96
+ date = "#{args[0]}-#{args[1]}-#{args[2] || '1'}" if !args.empty?
97
+ case params['mode']
98
+ when 'preview'
99
+ LucaDeal::Invoice.new(date).preview_mail
100
+ else
101
+ LucaDeal::Invoice.new(date).deliver_mail
102
+ end
103
+ end
59
104
  end
60
105
  end
61
106
 
@@ -65,44 +110,75 @@ end
65
110
 
66
111
  LucaRecord::Base.valid_project?
67
112
  cmd = ARGV.shift
113
+ params = {}
68
114
 
69
115
  case cmd
70
- when 'customer'
71
- params = {}
72
- OptionParser.new do |opt|
73
- opt.banner = 'Usage: luca-deal customer [options]'
74
- opt.on('--list', 'list all customers') { |v| params['mode'] = 'list' }
75
- opt.on('--create CustomerName', 'register new customer') do |v|
76
- params['mode'] = 'create'
77
- params['name'] = v
116
+ when /customers?/
117
+ subcmd = ARGV.shift
118
+ case subcmd
119
+ when 'list'
120
+ OptionParser.new do |opt|
121
+ opt.banner = 'Usage: luca-deal customers list [options]'
122
+ args = opt.parse(ARGV)
123
+ LucaCmd::Customer.list(args, params)
78
124
  end
79
- args = opt.parse(ARGV)
80
- customer(args, params)
81
- end
82
- when 'contract'
83
- params = {}
84
- OptionParser.new do |opt|
85
- opt.banner = 'Usage: luca-deal contract [options]'
86
- opt.on('--create CustomerId', 'register new contract') do |v|
87
- params['mode'] = 'create'
88
- params['customer_id'] = v
125
+ when 'create'
126
+ OptionParser.new do |opt|
127
+ opt.banner = 'Usage: luca-deal customers create CustomerName'
128
+ args = opt.parse(ARGV)
129
+ LucaCmd::Customer.create(args, params)
89
130
  end
90
- opt.on('--salesfee', 'create contract as sales fee definition') do |_v|
91
- params['category'] = 'sales_fee'
131
+ when 'delete'
132
+ LucaCmd::Customer.delete(ARGV)
133
+ else
134
+ puts 'Usage: luca-deal customers sub-commands'
135
+ end
136
+ when /contracts?/
137
+ subcmd = ARGV.shift
138
+ case subcmd
139
+ when 'create'
140
+ OptionParser.new do |opt|
141
+ opt.banner = 'Usage: luca-deal contracts create [options] CustomerId'
142
+ opt.on('--salesfee', 'create contract as sales fee definition') do |_v|
143
+ params['category'] = 'sales_fee'
144
+ end
145
+ args = opt.parse(ARGV)
146
+ LucaCmd::Contract.create(args, params)
92
147
  end
93
- args = opt.parse(ARGV)
94
- contract(args, params)
148
+ when 'delete'
149
+ LucaCmd::Contract.delete(ARGV)
150
+ else
151
+ puts 'Usage: luca-deal contracts Subcommand'
95
152
  end
96
- when 'invoice'
97
- params = {}
98
- OptionParser.new do |opt|
99
- opt.banner = 'Usage: luca-deal invoice [options] year month [date]'
100
- opt.on('--monthly', 'generate monthly data') { |v| params['mode'] = 'monthly' }
101
- opt.on('--mail', 'send to customers') { |v| params['mode'] = 'mail' }
102
- opt.on('--preview', 'send to preview user') { |v| params['mode'] = 'preview' }
103
- opt.on('--stats', 'list invoices') { |v| params['mode'] = 'stats' }
104
- args = opt.parse(ARGV)
105
- invoice(args, params)
153
+ when 'export'
154
+ LucaCmd::Invoice.export(ARGV)
155
+ when /invoices?/, 'i'
156
+ subcmd = ARGV.shift
157
+ case subcmd
158
+ when 'create'
159
+ OptionParser.new do |opt|
160
+ opt.banner = 'Usage: luca-deal invoices create [options] year month [date]'
161
+ opt.on('--monthly', 'generate monthly data') { |v| params['mode'] = 'monthly' }
162
+ args = opt.parse(ARGV)
163
+ LucaCmd::Invoice.create(args, params)
164
+ end
165
+ when 'delete'
166
+ LucaCmd::Invoice.delete(ARGV)
167
+ when 'list'
168
+ OptionParser.new do |opt|
169
+ opt.banner = 'Usage: luca-deal invoices list [options] year month [date]'
170
+ args = opt.parse(ARGV)
171
+ LucaCmd::Invoice.list(args, params)
172
+ end
173
+ when 'mail'
174
+ OptionParser.new do |opt|
175
+ opt.banner = 'Usage: luca-deal invoices mail [options] year month [date]'
176
+ opt.on('--preview', 'send to preview user') { |v| params['mode'] = 'preview' }
177
+ args = opt.parse(ARGV)
178
+ LucaCmd::Invoice.mail(args, params)
179
+ end
180
+ else
181
+ puts 'Usage: luca-deal invoices SubCommand'
106
182
  end
107
183
  when 'new'
108
184
  params = {}
@@ -112,7 +188,8 @@ when 'new'
112
188
  end
113
189
  else
114
190
  puts 'Usage: luca-deal sub-command [--help|options]'
115
- puts ' customer'
116
- puts ' contract'
117
- puts ' invoice'
191
+ puts ' customers'
192
+ puts ' contracts'
193
+ puts ' invoices'
194
+ puts ' export'
118
195
  end
@@ -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'
@@ -7,41 +9,62 @@ require 'luca_record'
7
9
  module LucaDeal
8
10
  class Contract < LucaRecord::Base
9
11
  @dirname = 'contracts'
12
+ @required = ['customer_id', 'terms']
10
13
 
11
14
  def initialize(date = nil)
12
15
  @date = date ? Date.parse(date) : Date.today
13
16
  @pjdir = Pathname(Dir.pwd)
14
17
  end
15
18
 
19
+ # returns active contracts on specified date.
20
+ #
21
+ def self.asof(year, month, day)
22
+ return enum_for(:asof, year, month, day) unless block_given?
23
+
24
+ new("#{year}-#{month}-#{day}").active do |contract|
25
+ yield contract
26
+ end
27
+ end
28
+
16
29
  #
17
30
  # collect active contracts
18
31
  #
19
32
  def active
33
+ return enum_for(:active) unless block_given?
34
+
20
35
  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)
36
+ next if !active_period?(data.dig('terms'))
24
37
 
25
- yield contract
38
+ contract = parse_current(data)
39
+ contract['items'] = contract['items']&.map { |item| parse_current(item) }
40
+ # TODO: handle sales_fee rate change
41
+ contract['rate'] = contract['rate']
42
+ yield contract.compact
26
43
  end
27
44
  end
28
45
 
29
- def generate!(customer_id, mode = nil)
46
+ def generate!(customer_id, mode = 'subscription')
30
47
  LucaDeal::Customer.find(customer_id) do |customer|
31
48
  current_customer = parse_current(customer)
32
- obj = { 'customer_id' => current_customer['id'], 'customer_name' => current_customer['name'] }
33
- obj['terms'] = { 'effective' => @date }
34
49
  if mode == 'sales_fee'
35
- obj.merge! salesfee_template
50
+ obj = salesfee_template
36
51
  else
37
- obj.merge! monthly_template
52
+ obj = monthly_template
38
53
  end
54
+ obj.merge!({ 'customer_id' => current_customer['id'], 'customer_name' => current_customer['name'] })
55
+ obj['terms'] ||= {}
56
+ obj['terms']['effective'] = @date
39
57
  self.class.create(obj)
40
58
  end
41
59
  end
42
60
 
43
- def self.active_period?(dat)
44
- !dat.dig('terms').nil?
61
+ def active_period?(dat)
62
+ unless dat.dig('defunct').nil?
63
+ defunct = dat.dig('defunct').respond_to?(:year) ? dat.dig('defunct') : Date.parse(dat.dig('defunct'))
64
+ return false if @date > defunct
65
+ end
66
+ effective = dat.dig('effective').respond_to?(:year) ? dat.dig('effective') : Date.parse(dat.dig('effective'))
67
+ @date >= effective
45
68
  end
46
69
 
47
70
  private
@@ -10,6 +10,7 @@ require 'luca_record'
10
10
  module LucaDeal
11
11
  class Customer < LucaRecord::Base
12
12
  @dirname = 'customers'
13
+ @required = ['name']
13
14
 
14
15
  def initialize(pjdir = nil)
15
16
  @date = Date.today
@@ -21,17 +22,20 @@ module LucaDeal
21
22
  YAML.dump(list).tap { |l| puts l }
22
23
  end
23
24
 
24
- def generate!(name)
25
- contact = {
25
+ def self.create(obj)
26
+ raise ':name is required' if obj[:name].nil?
27
+
28
+ contacts = obj[:contact]&.map { |c| { 'mail' => c[:mail] } }&.compact
29
+ contacts ||= [{
26
30
  'mail' => '_MAIL_ADDRESS_FOR_CONTACT_'
31
+ }]
32
+ h = {
33
+ 'name' => obj[:name],
34
+ 'address' => obj[:address] || '_CUSTOMER_ADDRESS_FOR_INVOICE_',
35
+ 'address2' => obj[:address2] || '_CUSTOMER_ADDRESS_FOR_INVOICE_',
36
+ 'contacts' => contacts
27
37
  }
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)
38
+ super(h)
35
39
  end
36
40
  end
37
41
  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(fee, date: @date, codes: 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
@@ -1,6 +1,7 @@
1
1
  require 'luca_deal/version'
2
2
 
3
3
  require 'mail'
4
+ require 'json'
4
5
  require 'yaml'
5
6
  require 'pathname'
6
7
  require 'bigdecimal'
@@ -12,6 +13,7 @@ require 'luca_record'
12
13
  module LucaDeal
13
14
  class Invoice < LucaRecord::Base
14
15
  @dirname = 'invoices'
16
+ @required = ['issue_date', 'customer', 'items', 'subtotal']
15
17
 
16
18
  def initialize(date = nil)
17
19
  @date = issue_date(date)
@@ -58,7 +60,6 @@ module LucaDeal
58
60
  mail
59
61
  end
60
62
 
61
- #
62
63
  # Output seriarized invoice data to stdout.
63
64
  # Returns previous N months on multiple count
64
65
  #
@@ -97,7 +98,28 @@ module LucaDeal
97
98
  collection << stat
98
99
  end
99
100
  end
100
- puts YAML.dump(collection)
101
+ puts YAML.dump(LucaSupport::Code.readable(collection))
102
+ end
103
+ end
104
+
105
+ def export_json
106
+ [].tap do |res|
107
+ self.class.asof(@date.year, @date.month) do |dat|
108
+ item = {}
109
+ item['date'] = dat['issue_date']
110
+ item['debit'] = []
111
+ item['credit'] = []
112
+ dat['subtotal'].map do |sub|
113
+ item['debit'] << { 'label' => '売掛金', 'value' => LucaSupport::Code.readable(sub['items']) }
114
+ item['debit'] << { 'label' => '売掛金', 'value' => LucaSupport::Code.readable(sub['tax']) }
115
+ item['credit'] << { 'label' => '売上高', 'value' => LucaSupport::Code.readable(sub['items']) }
116
+ item['credit'] << { 'label' => '売上高', 'value' => LucaSupport::Code.readable(sub['tax']) }
117
+ end
118
+ item['x-customer'] = dat['customer']['name'] if dat.dig('customer', 'name')
119
+ item['x-editor'] = 'LucaDeal'
120
+ res << item
121
+ end
122
+ puts JSON.dump(res)
101
123
  end
102
124
  end
103
125
 
@@ -113,12 +135,12 @@ module LucaDeal
113
135
  invoice['customer'] = get_customer(contract.dig('customer_id'))
114
136
  invoice['due_date'] = due_date(@date)
115
137
  invoice['issue_date'] = @date
116
- invoice['items'] = contract.dig('items').map do |item|
117
- {}.tap do |h|
118
- h['name'] = item.dig('name')
119
- h['price'] = item.dig('price')
120
- h['qty'] = item.dig('qty')
121
- end
138
+ invoice['sales_fee'] = contract['sales_fee'] if contract.dig('sales_fee')
139
+ invoice['items'] = get_products(contract['products'])
140
+ .concat(contract['items']&.map { |i| i['qty'] ||= 1; i } || [])
141
+ .compact
142
+ invoice['items'].reject! do |item|
143
+ item.dig('type') == 'initial' && subsequent_month?(contract.dig('terms', 'effective'))
122
144
  end
123
145
  invoice['subtotal'] = subtotal(invoice['items'])
124
146
  .map { |k, v| v.tap { |dat| dat['rate'] = k } }
@@ -126,7 +148,6 @@ module LucaDeal
126
148
  end
127
149
  end
128
150
 
129
- #
130
151
  # set variables for ERB template
131
152
  #
132
153
  def invoice_vars(invoice_dat)
@@ -151,9 +172,23 @@ module LucaDeal
151
172
  end
152
173
  end
153
174
 
175
+ def get_products(products)
176
+ return [] if products.nil?
177
+
178
+ [].tap do |res|
179
+ products.each do |product|
180
+ LucaDeal::Product.find(product['id'])['items'].each do |item|
181
+ item['product_id'] = product['id']
182
+ item['qty'] ||= 1
183
+ res << item
184
+ end
185
+ end
186
+ end
187
+ end
188
+
154
189
  def gen_invoice!(invoice)
155
190
  id = invoice.dig('contract_id')
156
- self.class.create_record!(invoice, @date, Array(id))
191
+ self.class.create(invoice, date: @date, codes: Array(id))
157
192
  end
158
193
 
159
194
  def issue_date(date)
@@ -172,7 +207,6 @@ module LucaDeal
172
207
  __dir__
173
208
  end
174
209
 
175
- #
176
210
  # load user company profile from config.
177
211
  #
178
212
  def set_company
@@ -183,23 +217,22 @@ module LucaDeal
183
217
  end
184
218
  end
185
219
 
186
- #
187
220
  # calc items & tax amount by tax category
188
221
  #
189
222
  def subtotal(items)
190
223
  {}.tap do |subtotal|
191
224
  items.each do |i|
192
225
  rate = i.dig('tax') || 'default'
226
+ qty = i['qty'] || BigDecimal('1')
193
227
  subtotal[rate] = { 'items' => 0, 'tax' => 0 } if subtotal.dig(rate).nil?
194
- subtotal[rate]['items'] += i['qty'] * i['price']
228
+ subtotal[rate]['items'] += i['price'] * qty
195
229
  end
196
230
  subtotal.each do |rate, amount|
197
- amount['tax'] = (amount['items'] * load_tax_rate(rate)).to_i
231
+ amount['tax'] = (amount['items'] * load_tax_rate(rate))
198
232
  end
199
233
  end
200
234
  end
201
235
 
202
- #
203
236
  # load Tax Rate from config.
204
237
  #
205
238
  def load_tax_rate(name)
@@ -218,5 +251,10 @@ module LucaDeal
218
251
  end
219
252
  false
220
253
  end
254
+
255
+ def subsequent_month?(effective_date)
256
+ effective_date = Date.parse(effective_date) unless effective_date.respond_to? :year
257
+ effective_date.year != @date.year || effective_date.month != @date.month
258
+ end
221
259
  end
222
260
  end
@@ -0,0 +1,50 @@
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
+ @required = ['items']
14
+
15
+ def list_name
16
+ list = self.class.all.map { |dat| parse_current(dat) }
17
+ YAML.dump(list).tap { |l| puts l }
18
+ end
19
+
20
+ # Save data with hash in Product format. Simple format is also available as bellows:
21
+ # {
22
+ # name: 'item_name(required)', price: 'item_price', qty: 'item_qty',
23
+ # initial: { name: 'item_name', price: 'item_price', qty: 'item_qty' }
24
+ # }
25
+ def self.create(obj)
26
+ if obj[:name].nil?
27
+ h = obj
28
+ else
29
+ items = [{
30
+ 'name' => obj[:name],
31
+ 'price' => obj[:price] || 0,
32
+ 'qty' => obj[:qty] || 1
33
+ }]
34
+ if obj[:initial]
35
+ items << {
36
+ 'name' => obj.dig(:initial, :name),
37
+ 'price' => obj.dig(:initial, :price) || 0,
38
+ 'qty' => obj.dig(:initial, :qty) || 1,
39
+ 'type' => 'initial'
40
+ }
41
+ end
42
+ h = {
43
+ 'name' => obj[:name],
44
+ 'items' => items
45
+ }
46
+ end
47
+ super(h)
48
+ end
49
+ end
50
+ 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.18'
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.18
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-10 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