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 +4 -4
- data/CHANGELOG.md +20 -0
- data/README.md +175 -7
- data/exe/luca-deal +157 -80
- data/lib/luca_deal.rb +2 -0
- data/lib/luca_deal/contract.rb +34 -11
- data/lib/luca_deal/customer.rb +13 -9
- data/lib/luca_deal/fee.rb +118 -0
- data/lib/luca_deal/invoice.rb +53 -15
- data/lib/luca_deal/product.rb +50 -0
- data/lib/luca_deal/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d8159099446dcdae1b4c928d4eac5861cd35fe13f8d37155cdf28dafb30fc21e
|
4
|
+
data.tar.gz: 3c086a2ea6c4c9ec6bc9014fa9500ab9fde0a07c1778a5817fbb64e7409b5670
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0a26cec96f1a0805044470f8710b36b60aacf344bad53527c2a9fd4ff64557a06836e4eefec21f083e0b51bc48a9854dbe2fdd6fed4d4754aba9fe51fa5890ce
|
7
|
+
data.tar.gz: 8a611e016833673fb1e7fcab747a24d4bfdb4152a60086791a1377d8576feb181255c8b2a0dfeca2f6807a4d11d4d2f579be6353ca62a7996a5f960d8b4f1311
|
data/CHANGELOG.md
CHANGED
@@ -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
|
-
#
|
1
|
+
# LucaDeal
|
2
2
|
|
3
|
-
|
3
|
+
[](https://badge.fury.io/rb/lucadeal)
|
4
4
|
|
5
|
-
|
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 '
|
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
|
21
|
+
$ gem install lucadeal
|
22
22
|
|
23
23
|
## Usage
|
24
24
|
|
25
|
-
|
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/
|
203
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/chumaltd/luca .
|
data/exe/luca-deal
CHANGED
@@ -5,57 +5,102 @@ require 'date'
|
|
5
5
|
require 'optparse'
|
6
6
|
require 'luca_deal'
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
params
|
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
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
-
|
91
|
-
|
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
|
-
|
94
|
-
|
148
|
+
when 'delete'
|
149
|
+
LucaCmd::Contract.delete(ARGV)
|
150
|
+
else
|
151
|
+
puts 'Usage: luca-deal contracts Subcommand'
|
95
152
|
end
|
96
|
-
when '
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
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 '
|
116
|
-
puts '
|
117
|
-
puts '
|
191
|
+
puts ' customers'
|
192
|
+
puts ' contracts'
|
193
|
+
puts ' invoices'
|
194
|
+
puts ' export'
|
118
195
|
end
|
data/lib/luca_deal.rb
CHANGED
@@ -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
|
data/lib/luca_deal/contract.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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 =
|
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
|
50
|
+
obj = salesfee_template
|
36
51
|
else
|
37
|
-
obj
|
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
|
44
|
-
|
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
|
data/lib/luca_deal/customer.rb
CHANGED
@@ -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
|
25
|
-
|
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
|
-
|
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
|
data/lib/luca_deal/invoice.rb
CHANGED
@@ -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['
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
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.
|
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['
|
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))
|
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
|
data/lib/luca_deal/version.rb
CHANGED
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.
|
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-
|
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
|