lucadeal 0.2.14 → 0.2.21
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 +35 -0
- data/README.md +48 -1
- data/exe/luca-deal +384 -80
- data/lib/luca_deal/contract.rb +11 -1
- data/lib/luca_deal/customer.rb +17 -4
- data/lib/luca_deal/fee.rb +100 -34
- data/lib/luca_deal/invoice.rb +99 -51
- data/lib/luca_deal/templates/fee-report-mail.txt.erb +6 -0
- data/lib/luca_deal/templates/fee-report.html.erb +54 -0
- data/lib/luca_deal/templates/invoice-mail.txt.erb +6 -0
- data/lib/luca_deal/templates/invoice.html.erb +52 -0
- data/lib/luca_deal/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f73de0d5baa72b53fcaeb3ec5ef85c0d3d80a21b7d7531f914892ff476d8eee0
|
4
|
+
data.tar.gz: 3ecfc52d4a1e2257e926773f3e7db455dccce71e8fe89822fccb91402d0a979e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1f8c7a3301a69e5891736379fb9cf4fac3d1cda121e295d77903bb36f9f87ab6485be8d69e4bb398b23cf18af80f47f3d1b4a2f67d2dc3cfba5cc1f9c6fcf5cf
|
7
|
+
data.tar.gz: 1515a346552be7a7ec33a37bd3af941b57f8be9c890d67ccfde40bf0f0ece6f678a405d0723ad1d04649ae70e745e4dfa5d3a869400314b366fddf576c31c21a
|
data/CHANGELOG.md
CHANGED
@@ -0,0 +1,35 @@
|
|
1
|
+
## LucaDeal 0.2.21
|
2
|
+
|
3
|
+
* Implement `luca-deal fee` subcommands.
|
4
|
+
* single invoice creation with contract id fragment.
|
5
|
+
|
6
|
+
## LucaDeal 0.2.20
|
7
|
+
|
8
|
+
* CLI provides `--nu` option. Add JSON output.
|
9
|
+
* `luca-deal invoices list --html`, rendering HTML to stdout.
|
10
|
+
|
11
|
+
## LucaDeal 0.2.19
|
12
|
+
|
13
|
+
* CLI id completion on Customer delete, Contract create/delete
|
14
|
+
* add `describe` to Customer / Contract
|
15
|
+
|
16
|
+
## LucaDeal 0.2.18
|
17
|
+
|
18
|
+
* Breaking change: restructure CLI in sub-sub command format.
|
19
|
+
* Add 'x-customer', 'x-editor' on export to LucaBook
|
20
|
+
|
21
|
+
## LucaDeal 0.2.17
|
22
|
+
|
23
|
+
* `luca-deal export` export JSON for LucaBook
|
24
|
+
|
25
|
+
## LucaDeal 0.2.14
|
26
|
+
|
27
|
+
* Introduce Product for selling items template.
|
28
|
+
|
29
|
+
## LucaDeal 0.2.12
|
30
|
+
|
31
|
+
* Introduce Sales fee calculation.
|
32
|
+
|
33
|
+
## LucaDeal 0.2.10
|
34
|
+
|
35
|
+
* items can have one time cost at initial month of contract.
|
data/README.md
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# LucaDeal
|
2
2
|
|
3
|
+
[](https://badge.fury.io/rb/lucadeal)
|
4
|
+
|
3
5
|
LucaDeal is Sales contract management application.
|
4
6
|
|
5
7
|
## Installation
|
@@ -20,7 +22,52 @@ Or install it yourself as:
|
|
20
22
|
|
21
23
|
## Usage
|
22
24
|
|
23
|
-
|
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
|
+
Or, any invoice can be created with contract ID. Contract ID is just unique fragment of uuid, listed with `luca-deal invoice create` with no args.
|
65
|
+
|
66
|
+
```
|
67
|
+
$ luca-deal invoice create 1d3 yyyy m
|
68
|
+
```
|
69
|
+
|
70
|
+
Invoice conditions are defined by contracts.
|
24
71
|
|
25
72
|
|
26
73
|
## Data Structure
|
data/exe/luca-deal
CHANGED
@@ -5,57 +5,245 @@ 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
|
+
class LucaCmd
|
9
|
+
class Customer < LucaCmd
|
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.describe(args = nil, params = {})
|
22
|
+
if args.empty?
|
23
|
+
list = LucaDeal::Customer.id_completion('')
|
24
|
+
puts 'requires customer\'s id. exit'
|
25
|
+
list.each { |item| puts " #{item[:id]} #{item[:label]}" }
|
26
|
+
exit 1
|
27
|
+
else
|
28
|
+
list = LucaDeal::Customer.id_completion(args[0])
|
29
|
+
case list.length
|
30
|
+
when 1
|
31
|
+
render(LucaDeal::Customer.new.describe(list.first), params)
|
32
|
+
else
|
33
|
+
puts 'found multiple contract id. exit'
|
34
|
+
list.each { |item| puts " #{item[:id]} #{item[:label]}" }
|
35
|
+
exit 1
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.delete(args = nil, params = {})
|
41
|
+
if args.empty?
|
42
|
+
list = LucaDeal::Customer.id_completion('')
|
43
|
+
puts 'requires customer\'s id. exit'
|
44
|
+
list.each { |item| puts " #{item[:id]} #{item[:label]}" }
|
45
|
+
exit 1
|
46
|
+
else
|
47
|
+
list = LucaDeal::Customer.id_completion(args[0])
|
48
|
+
case list.length
|
49
|
+
when 1
|
50
|
+
id = LucaDeal::Customer.delete(list.first)
|
51
|
+
else
|
52
|
+
puts 'found multiple customer\'s id. exit'
|
53
|
+
list.each { |item| puts " #{item[:id]} #{item[:label]}" }
|
54
|
+
exit 1
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.list(args = nil, params = {})
|
60
|
+
render(LucaDeal::Customer.new.list_name, params)
|
20
61
|
end
|
21
|
-
else
|
22
|
-
puts 'invalid option. --help for usage'
|
23
|
-
exit 1
|
24
62
|
end
|
25
|
-
end
|
26
63
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
64
|
+
class Contract < LucaCmd
|
65
|
+
def self.create(args = nil, params = {})
|
66
|
+
if args.empty?
|
67
|
+
list = LucaDeal::Customer.id_completion('')
|
68
|
+
puts 'requires customer\'s id. exit'
|
69
|
+
list.each { |item| puts " #{item[:id]} #{item[:label]}" }
|
70
|
+
exit 1
|
71
|
+
else
|
72
|
+
list = LucaDeal::Customer.id_completion(args[0])
|
73
|
+
case list.length
|
74
|
+
when 1
|
75
|
+
id = LucaDeal::Contract.new.generate!(list.first, params['category'])
|
76
|
+
puts "Successfully generated Contract #{id}" if id
|
77
|
+
puts 'Conditions are tentative. Edit contract detail.' if id
|
78
|
+
else
|
79
|
+
puts 'found multiple customer\'s id. exit'
|
80
|
+
list.each { |item| puts " #{item[:id]} #{item[:label]}" }
|
81
|
+
exit 1
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.describe(args = nil, params = {})
|
87
|
+
if args.empty?
|
88
|
+
list = LucaDeal::Contract.id_completion('', label: 'customer_name')
|
89
|
+
puts 'requires contract id. exit'
|
90
|
+
list.each { |item| puts " #{item[:id]} #{item[:label]}" }
|
91
|
+
exit 1
|
92
|
+
else
|
93
|
+
list = LucaDeal::Contract.id_completion(args[0])
|
94
|
+
case list.length
|
95
|
+
when 1
|
96
|
+
render(LucaDeal::Contract.new.describe(list.first), params)
|
97
|
+
else
|
98
|
+
puts 'found multiple contract id. exit'
|
99
|
+
list.each { |item| puts " #{item[:id]} #{item[:label]}" }
|
100
|
+
exit 1
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def self.delete(args = nil, params = {})
|
106
|
+
if args.empty?
|
107
|
+
list = LucaDeal::Contract.id_completion('', label: 'customer_name')
|
108
|
+
puts 'requires contract id. exit'
|
109
|
+
list.each { |item| puts " #{item[:id]} #{item[:label]}" }
|
110
|
+
exit 1
|
111
|
+
else
|
112
|
+
list = LucaDeal::Contract.id_completion(args[0])
|
113
|
+
case list.length
|
114
|
+
when 1
|
115
|
+
id = LucaDeal::Contract.delete(list.first)
|
116
|
+
else
|
117
|
+
puts 'found multiple contract id. exit'
|
118
|
+
list.each { |item| puts " #{item[:id]} #{item[:label]}" }
|
119
|
+
exit 1
|
120
|
+
end
|
121
|
+
end
|
37
122
|
end
|
38
|
-
else
|
39
123
|
end
|
40
|
-
end
|
41
124
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
125
|
+
class Invoice < LucaCmd
|
126
|
+
def self.create(args = nil, params = {})
|
127
|
+
case params['mode']
|
128
|
+
when 'monthly'
|
129
|
+
date = "#{args[0]}-#{args[1]}-#{args[2] || '1'}" if !args.empty?
|
130
|
+
LucaDeal::Invoice.new(date).monthly_invoice
|
131
|
+
else
|
132
|
+
date = "#{args[1]}-#{args[2]}-#{args[3] || '1'}" if !args.empty?
|
133
|
+
list = LucaDeal::Contract.id_completion(args[0] || '', label: 'customer_name')
|
134
|
+
if args.length != 3
|
135
|
+
puts 'requires contract id & year month. exit'
|
136
|
+
list.each { |item| puts " #{item[:id]} #{item[:label]}" }
|
137
|
+
exit 1
|
138
|
+
else
|
139
|
+
case list.length
|
140
|
+
when 1
|
141
|
+
id = LucaDeal::Invoice.new(date).single_invoice(list.first)
|
142
|
+
else
|
143
|
+
puts 'found multiple contract id. exit'
|
144
|
+
list.each { |item| puts " #{item[:id]} #{item[:label]}" }
|
145
|
+
exit 1
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def self.delete(args = nil, params = {})
|
152
|
+
if args
|
153
|
+
id = LucaDeal::Invoice.delete(args[0])
|
154
|
+
else
|
155
|
+
puts 'requires contract id. exit'
|
156
|
+
exit 1
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def self.export(args = nil, _params = nil)
|
161
|
+
if args
|
162
|
+
args << 28 if args.length == 2 # specify safe last day
|
163
|
+
LucaDeal::Invoice.new(args.join('-')).export_json
|
164
|
+
else
|
165
|
+
LucaDeal::Invoice.new.export_json
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def self.list(args = nil, params = {})
|
170
|
+
date = "#{args[0]}-#{args[1]}-#{args[2] || '1'}" if !args.empty?
|
171
|
+
if args.empty?
|
172
|
+
date = "#{Date.today.year}-#{Date.today.month}-1"
|
173
|
+
count = 3
|
174
|
+
end
|
175
|
+
if params[:html]
|
176
|
+
LucaDeal::Invoice.new(date).preview_stdout
|
177
|
+
else
|
178
|
+
render(LucaDeal::Invoice.new(date).stats(count || 1), params)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def self.mail(args = nil, params = {})
|
183
|
+
date = "#{args[0]}-#{args[1]}-#{args[2] || '1'}" if !args.empty?
|
184
|
+
case params['mode']
|
185
|
+
when 'preview'
|
186
|
+
LucaDeal::Invoice.new(date).preview_mail
|
187
|
+
else
|
188
|
+
LucaDeal::Invoice.new(date).deliver_mail
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
class Fee < LucaCmd
|
194
|
+
def self.create(args = nil, params = {})
|
195
|
+
case params['mode']
|
196
|
+
when 'monthly'
|
197
|
+
date = "#{args[0]}-#{args[1]}-#{args[2] || '1'}" if !args.empty?
|
198
|
+
LucaDeal::Fee.new(date).monthly_fee
|
199
|
+
else
|
200
|
+
puts "not implemented yet"
|
201
|
+
exit 1
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def self.delete(args = nil, params = {})
|
206
|
+
if args
|
207
|
+
id = LucaDeal::Fee.delete(args[0])
|
208
|
+
else
|
209
|
+
puts 'requires contract id. exit'
|
210
|
+
exit 1
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def self.list(args = nil, params = {})
|
215
|
+
date = "#{args[0]}-#{args[1]}-#{args[2] || '1'}" if !args.empty?
|
216
|
+
if args.empty?
|
217
|
+
date = "#{Date.today.year}-#{Date.today.month}-1"
|
218
|
+
count = 3
|
219
|
+
end
|
220
|
+
if params[:html]
|
221
|
+
LucaDeal::Fee.new(date).preview_stdout
|
222
|
+
else
|
223
|
+
render(LucaDeal::Fee.new(date).stats(count || 1), params)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def self.mail(args = nil, params = {})
|
228
|
+
date = "#{args[0]}-#{args[1]}-#{args[2] || '1'}" if !args.empty?
|
229
|
+
case params['mode']
|
230
|
+
when 'preview'
|
231
|
+
LucaDeal::Fee.new(date).preview_mail
|
232
|
+
else
|
233
|
+
LucaDeal::Fee.new(date).deliver_mail
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
def self.render(dat, params)
|
239
|
+
case params[:output]
|
240
|
+
when 'json'
|
241
|
+
puts JSON.dump(dat)
|
242
|
+
when 'nu'
|
243
|
+
LucaSupport::View.nushell(YAML.dump(dat))
|
244
|
+
else
|
245
|
+
puts YAML.dump(dat)
|
246
|
+
end
|
59
247
|
end
|
60
248
|
end
|
61
249
|
|
@@ -65,54 +253,170 @@ end
|
|
65
253
|
|
66
254
|
LucaRecord::Base.valid_project?
|
67
255
|
cmd = ARGV.shift
|
256
|
+
params = {}
|
68
257
|
|
69
258
|
case cmd
|
70
|
-
when
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
params
|
259
|
+
when /customers?/
|
260
|
+
subcmd = ARGV.shift
|
261
|
+
case subcmd
|
262
|
+
when 'create'
|
263
|
+
OptionParser.new do |opt|
|
264
|
+
opt.banner = 'Usage: luca-deal customers create CustomerName'
|
265
|
+
args = opt.parse(ARGV)
|
266
|
+
LucaCmd::Customer.create(args, params)
|
78
267
|
end
|
79
|
-
|
80
|
-
|
268
|
+
when 'list'
|
269
|
+
OptionParser.new do |opt|
|
270
|
+
opt.banner = 'Usage: luca-deal customers list [options]'
|
271
|
+
opt.on('--nu', 'show table in nushell') { |_v| params[:output] = 'nu' }
|
272
|
+
opt.on('-o', '--output VAL', 'output serialized data') { |v| params[:output] = v }
|
273
|
+
args = opt.parse(ARGV)
|
274
|
+
LucaCmd::Customer.list(args, params)
|
275
|
+
end
|
276
|
+
when 'describe'
|
277
|
+
OptionParser.new do |opt|
|
278
|
+
opt.banner = 'Usage: luca-deal customers describe [options] customer_id'
|
279
|
+
opt.on('--nu', 'show table in nushell') { |_v| params[:output] = 'nu' }
|
280
|
+
opt.on('-o', '--output VAL', 'output serialized data') { |v| params[:output] = v }
|
281
|
+
args = opt.parse(ARGV)
|
282
|
+
LucaCmd::Customer.describe(args, params)
|
283
|
+
end
|
284
|
+
when 'delete'
|
285
|
+
LucaCmd::Customer.delete(ARGV)
|
286
|
+
else
|
287
|
+
puts 'Proper subcommand needed.'
|
288
|
+
puts
|
289
|
+
puts 'Usage: luca-deal customer[s] subcommands [--help|options]'
|
290
|
+
puts ' create'
|
291
|
+
puts ' list'
|
292
|
+
puts ' describe: show customer with contracts info'
|
293
|
+
puts ' delete'
|
294
|
+
exit 1
|
81
295
|
end
|
82
|
-
when
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
296
|
+
when /contracts?/
|
297
|
+
subcmd = ARGV.shift
|
298
|
+
case subcmd
|
299
|
+
when 'create'
|
300
|
+
OptionParser.new do |opt|
|
301
|
+
opt.banner = 'Usage: luca-deal contracts create [options] CustomerId'
|
302
|
+
opt.on('--salesfee', 'create contract as sales fee definition') do |_v|
|
303
|
+
params['category'] = 'sales_fee'
|
304
|
+
end
|
305
|
+
args = opt.parse(ARGV)
|
306
|
+
LucaCmd::Contract.create(args, params)
|
89
307
|
end
|
90
|
-
|
91
|
-
|
308
|
+
when 'describe'
|
309
|
+
OptionParser.new do |opt|
|
310
|
+
opt.banner = 'Usage: luca-deal contracts describe [options] customer_id'
|
311
|
+
opt.on('--nu', 'show table in nushell') { |_v| params[:output] = 'nu' }
|
312
|
+
opt.on('-o', '--output VAL', 'output serialized data') { |v| params[:output] = v }
|
313
|
+
args = opt.parse(ARGV)
|
314
|
+
LucaCmd::Contract.describe(args, params)
|
92
315
|
end
|
93
|
-
|
94
|
-
|
316
|
+
when 'delete'
|
317
|
+
LucaCmd::Contract.delete(ARGV)
|
318
|
+
else
|
319
|
+
puts 'Proper subcommand needed.'
|
320
|
+
puts
|
321
|
+
puts 'Usage: luca-deal contract[s] subcommand [--help|options]'
|
322
|
+
puts ' create'
|
323
|
+
puts ' describe: show contract with puroducts or items info'
|
324
|
+
puts ' delete'
|
325
|
+
exit 1
|
95
326
|
end
|
96
|
-
when '
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
327
|
+
when 'export'
|
328
|
+
LucaCmd::Invoice.export(ARGV)
|
329
|
+
when /invoices?/, 'i'
|
330
|
+
subcmd = ARGV.shift
|
331
|
+
case subcmd
|
332
|
+
when 'create'
|
333
|
+
OptionParser.new do |opt|
|
334
|
+
opt.banner = 'Usage: luca-deal invoices create [options] --monthly|contract_id year month [date]'
|
335
|
+
opt.on('--monthly', 'generate monthly data') { |_v| params['mode'] = 'monthly' }
|
336
|
+
args = opt.parse(ARGV)
|
337
|
+
LucaCmd::Invoice.create(args, params)
|
338
|
+
end
|
339
|
+
when 'delete'
|
340
|
+
LucaCmd::Invoice.delete(ARGV)
|
341
|
+
when 'list'
|
342
|
+
OptionParser.new do |opt|
|
343
|
+
opt.banner = 'Usage: luca-deal invoices list [options] year month [date]'
|
344
|
+
opt.on('--nu', 'show table in nushell') { |_v| params[:output] = 'nu' }
|
345
|
+
opt.on('-o', '--output VAL', 'output serialized data') { |v| params[:output] = v }
|
346
|
+
opt.on('--html', 'output html invoices') { |_v| params[:html] = 'monthly' }
|
347
|
+
args = opt.parse(ARGV)
|
348
|
+
LucaCmd::Invoice.list(args, params)
|
349
|
+
end
|
350
|
+
when 'mail'
|
351
|
+
OptionParser.new do |opt|
|
352
|
+
opt.banner = 'Usage: luca-deal invoices mail [options] year month [date]'
|
353
|
+
opt.on('--preview', 'send to preview user') { |_v| params['mode'] = 'preview' }
|
354
|
+
args = opt.parse(ARGV)
|
355
|
+
LucaCmd::Invoice.mail(args, params)
|
356
|
+
end
|
357
|
+
else
|
358
|
+
puts 'Proper subcommand needed.'
|
359
|
+
puts
|
360
|
+
puts 'Usage: luca-deal invoices subcommand [--help|options]'
|
361
|
+
puts ' create'
|
362
|
+
puts ' delete'
|
363
|
+
puts ' list'
|
364
|
+
puts ' mail: send mail with invoice'
|
365
|
+
exit 1
|
106
366
|
end
|
107
367
|
when 'new'
|
108
368
|
params = {}
|
109
369
|
OptionParser.new do |opt|
|
370
|
+
opt.banner = 'Usage: luca-deal new DIR'
|
110
371
|
args = opt.parse(ARGV)
|
111
372
|
new_pj(args, params)
|
112
373
|
end
|
374
|
+
when /fee/
|
375
|
+
subcmd = ARGV.shift
|
376
|
+
case subcmd
|
377
|
+
when 'create'
|
378
|
+
OptionParser.new do |opt|
|
379
|
+
opt.banner = 'Usage: luca-deal fee create [options] year month [date]'
|
380
|
+
opt.on('--monthly', 'generate monthly data') { |_v| params['mode'] = 'monthly' }
|
381
|
+
args = opt.parse(ARGV)
|
382
|
+
LucaCmd::Fee.create(args, params)
|
383
|
+
end
|
384
|
+
when 'delete'
|
385
|
+
LucaCmd::Fee.delete(ARGV)
|
386
|
+
when 'list'
|
387
|
+
OptionParser.new do |opt|
|
388
|
+
opt.banner = 'Usage: luca-deal fee list [options] year month [date]'
|
389
|
+
opt.on('--nu', 'show table in nushell') { |_v| params[:output] = 'nu' }
|
390
|
+
opt.on('-o', '--output VAL', 'output serialized data') { |v| params[:output] = v }
|
391
|
+
opt.on('--html', 'output html invoices') { |_v| params[:html] = 'monthly' }
|
392
|
+
args = opt.parse(ARGV)
|
393
|
+
LucaCmd::Fee.list(args, params)
|
394
|
+
end
|
395
|
+
when 'mail'
|
396
|
+
OptionParser.new do |opt|
|
397
|
+
opt.banner = 'Usage: luca-deal fee mail [options] year month [date]'
|
398
|
+
opt.on('--preview', 'send to preview user') { |_v| params['mode'] = 'preview' }
|
399
|
+
args = opt.parse(ARGV)
|
400
|
+
LucaCmd::Fee.mail(args, params)
|
401
|
+
end
|
402
|
+
else
|
403
|
+
puts 'Proper subcommand needed.'
|
404
|
+
puts
|
405
|
+
puts 'Usage: luca-deal fee subcommand [--help|options]'
|
406
|
+
puts ' create'
|
407
|
+
puts ' delete'
|
408
|
+
puts ' list'
|
409
|
+
puts ' mail: send mail with report'
|
410
|
+
exit 1
|
411
|
+
end
|
113
412
|
else
|
114
|
-
puts '
|
115
|
-
puts
|
116
|
-
puts '
|
117
|
-
puts '
|
413
|
+
puts 'Proper subcommand needed.'
|
414
|
+
puts
|
415
|
+
puts 'Usage: luca-deal subcommand [options]'
|
416
|
+
puts ' customers'
|
417
|
+
puts ' contracts'
|
418
|
+
puts ' invoices'
|
419
|
+
puts ' new: initialize project dir'
|
420
|
+
puts ' export: puts invoice data for LucaBook import'
|
421
|
+
exit 1
|
118
422
|
end
|
data/lib/luca_deal/contract.rb
CHANGED
@@ -43,8 +43,18 @@ module LucaDeal
|
|
43
43
|
end
|
44
44
|
end
|
45
45
|
|
46
|
+
def describe(id)
|
47
|
+
contract = parse_current(self.class.find(id))
|
48
|
+
if contract['products']
|
49
|
+
contract['products'] = contract['products'].map do |product|
|
50
|
+
Product.find(product['id'])
|
51
|
+
end
|
52
|
+
end
|
53
|
+
readable(contract)
|
54
|
+
end
|
55
|
+
|
46
56
|
def generate!(customer_id, mode = 'subscription')
|
47
|
-
|
57
|
+
Customer.find(customer_id) do |customer|
|
48
58
|
current_customer = parse_current(customer)
|
49
59
|
if mode == 'sales_fee'
|
50
60
|
obj = salesfee_template
|
data/lib/luca_deal/customer.rb
CHANGED
@@ -12,14 +12,27 @@ module LucaDeal
|
|
12
12
|
@dirname = 'customers'
|
13
13
|
@required = ['name']
|
14
14
|
|
15
|
-
def initialize
|
15
|
+
def initialize
|
16
16
|
@date = Date.today
|
17
|
-
@pjdir = pjdir || Dir.pwd
|
18
17
|
end
|
19
18
|
|
20
19
|
def list_name
|
21
|
-
|
22
|
-
|
20
|
+
self.class.all.map { |dat| parse_current(dat).sort.to_h }
|
21
|
+
end
|
22
|
+
|
23
|
+
def describe(id)
|
24
|
+
customer = parse_current(self.class.find(id))
|
25
|
+
contracts = Contract.all.select { |contract| contract['customer_id'] == customer['id'] }
|
26
|
+
if !contracts.empty?
|
27
|
+
customer['contracts'] = contracts.map do |c|
|
28
|
+
{
|
29
|
+
'id' => c['id'],
|
30
|
+
'effective' => c['terms']['effective'],
|
31
|
+
'defunct' => c['terms']['defunct']
|
32
|
+
}
|
33
|
+
end
|
34
|
+
end
|
35
|
+
readable(customer)
|
23
36
|
end
|
24
37
|
|
25
38
|
def self.create(obj)
|
data/lib/luca_deal/fee.rb
CHANGED
@@ -14,40 +14,99 @@ module LucaDeal
|
|
14
14
|
|
15
15
|
def initialize(date = nil)
|
16
16
|
@date = issue_date(date)
|
17
|
-
@config = load_config('config.yml')
|
18
17
|
end
|
19
18
|
|
20
19
|
# calculate fee, based on invoices
|
21
20
|
#
|
22
21
|
def monthly_fee
|
23
|
-
|
22
|
+
Contract.asof(@date.year, @date.month, @date.day) do |contract|
|
24
23
|
next if contract.dig('terms', 'category') != 'sales_fee'
|
24
|
+
next if duplicated_contract? contract['id']
|
25
25
|
|
26
26
|
@rate = { 'default' => BigDecimal(contract.dig('rate', 'default')) }
|
27
27
|
@rate['initial'] = contract.dig('rate', 'initial') ? BigDecimal(contract.dig('rate', 'initial')) : @rate['default']
|
28
28
|
|
29
|
-
|
29
|
+
fee = { 'contract_id' => contract['id'], 'items' => [] }
|
30
|
+
fee['customer'] = get_customer(contract['customer_id'])
|
31
|
+
fee['issue_date'] = @date
|
32
|
+
Invoice.asof(@date.year, @date.month) do |invoice|
|
30
33
|
next if invoice.dig('sales_fee', 'id') != contract['id']
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
34
|
+
|
35
|
+
invoice['items'].each do |item|
|
36
|
+
rate = item['type'] == 'initial' ? @rate['initial'] : @rate['default']
|
37
|
+
fee['items'] << {
|
38
|
+
'invoice_id' => invoice['id'],
|
39
|
+
'customer_name' => invoice.dig('customer', 'name'),
|
40
|
+
'name' => item['name'],
|
41
|
+
'price' => item['price'],
|
42
|
+
'qty' => item['qty'],
|
43
|
+
'fee' => item['price'] * item['qty'] * rate
|
44
|
+
}
|
39
45
|
end
|
40
|
-
fee['
|
41
|
-
fee['customer'].delete('to')
|
42
|
-
fee['sales_fee'].merge! subtotal(fee['items'])
|
43
|
-
gen_fee!(fee)
|
46
|
+
fee['sales_fee'] = subtotal(fee['items'])
|
44
47
|
end
|
48
|
+
self.class.create(fee, date: @date, codes: Array(contract['id']))
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def deliver_mail(attachment_type = nil, mode: nil)
|
53
|
+
attachment_type = CONFIG.dig('fee', 'attachment') || :html
|
54
|
+
fees = self.class.asof(@date.year, @date.month)
|
55
|
+
raise "No report for #{@date.year}/#{@date.month}" if fees.count.zero?
|
56
|
+
|
57
|
+
fees.each do |dat, path|
|
58
|
+
next if has_status?(dat, 'mail_delivered')
|
59
|
+
|
60
|
+
mail = compose_mail(dat, mode: mode, attachment: attachment_type.to_sym)
|
61
|
+
LucaSupport::Mail.new(mail, PJDIR).deliver
|
62
|
+
self.class.add_status!(path, 'mail_delivered')
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def preview_mail(attachment_type = nil)
|
67
|
+
deliver_mail(attachment_type, mode: :preview)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Render HTML to console
|
71
|
+
#
|
72
|
+
def preview_stdout
|
73
|
+
self.class.asof(@date.year, @date.month) do |dat, _|
|
74
|
+
@company = set_company
|
75
|
+
fee_vars(dat)
|
76
|
+
puts render_report
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def compose_mail(dat, mode: nil, attachment: :html)
|
81
|
+
@company = set_company
|
82
|
+
fee_vars(dat)
|
83
|
+
|
84
|
+
mail = Mail.new
|
85
|
+
mail.to = dat.dig('customer', 'to') if mode.nil?
|
86
|
+
mail.subject = CONFIG.dig('invoice', 'mail_subject') || 'Your Report is available'
|
87
|
+
if mode == :preview
|
88
|
+
mail.cc = CONFIG.dig('mail', 'preview') || CONFIG.dig('mail', 'from')
|
89
|
+
mail.subject = '[preview] ' + mail.subject
|
90
|
+
end
|
91
|
+
mail.text_part = Mail::Part.new(body: render_erb(search_template('fee-report-mail.txt.erb')), charset: 'UTF-8')
|
92
|
+
mail.attachments[attachment_name(dat, attachment)] = render_report(attachment)
|
93
|
+
mail
|
94
|
+
end
|
95
|
+
|
96
|
+
def render_report(file_type = :html)
|
97
|
+
case file_type
|
98
|
+
when :html
|
99
|
+
render_erb(search_template('fee-report.html.erb'))
|
100
|
+
when :pdf
|
101
|
+
erb2pdf(search_template('fee-report.html.erb'))
|
102
|
+
else
|
103
|
+
raise 'This filetype is not supported.'
|
45
104
|
end
|
46
105
|
end
|
47
106
|
|
48
107
|
def get_customer(id)
|
49
108
|
{}.tap do |res|
|
50
|
-
|
109
|
+
Customer.find(id) do |dat|
|
51
110
|
customer = parse_current(dat)
|
52
111
|
res['id'] = customer['id']
|
53
112
|
res['name'] = customer.dig('name')
|
@@ -58,13 +117,19 @@ module LucaDeal
|
|
58
117
|
end
|
59
118
|
end
|
60
119
|
|
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
120
|
private
|
67
121
|
|
122
|
+
# set variables for ERB template
|
123
|
+
#
|
124
|
+
def fee_vars(fee_dat)
|
125
|
+
@customer = fee_dat['customer']
|
126
|
+
@items = readable(fee_dat['items'])
|
127
|
+
@sales_fee = readable(fee_dat['sales_fee'])
|
128
|
+
@issue_date = fee_dat['issue_date']
|
129
|
+
@due_date = fee_dat['due_date']
|
130
|
+
@amount = readable(fee_dat['sales_fee'].inject(0) { |sum, (_k, v)| sum + v })
|
131
|
+
end
|
132
|
+
|
68
133
|
def lib_path
|
69
134
|
__dir__
|
70
135
|
end
|
@@ -73,25 +138,20 @@ module LucaDeal
|
|
73
138
|
#
|
74
139
|
def set_company
|
75
140
|
{}.tap do |h|
|
76
|
-
h['name'] =
|
77
|
-
h['address'] =
|
78
|
-
h['address2'] =
|
141
|
+
h['name'] = CONFIG.dig('company', 'name')
|
142
|
+
h['address'] = CONFIG.dig('company', 'address')
|
143
|
+
h['address2'] = CONFIG.dig('company', 'address2')
|
79
144
|
end
|
80
145
|
end
|
81
146
|
|
82
147
|
# calc fee & tax amount by tax category
|
83
148
|
#
|
84
149
|
def subtotal(items)
|
85
|
-
{}.tap do |subtotal|
|
150
|
+
{ 'fee' => 0, 'tax' => 0 }.tap do |subtotal|
|
86
151
|
items.each do |i|
|
87
|
-
|
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
|
152
|
+
subtotal['fee'] += i['fee']
|
94
153
|
end
|
154
|
+
subtotal['tax'] = (subtotal['fee'] * load_tax_rate('default')).to_i
|
95
155
|
end
|
96
156
|
end
|
97
157
|
|
@@ -100,12 +160,18 @@ module LucaDeal
|
|
100
160
|
Date.new(base.year, base.month, -1)
|
101
161
|
end
|
102
162
|
|
163
|
+
# TODO: support due_date variation
|
164
|
+
def due_date(date)
|
165
|
+
next_month = date.next_month
|
166
|
+
Date.new(next_month.year, next_month.month, -1)
|
167
|
+
end
|
168
|
+
|
103
169
|
# load Tax Rate from config.
|
104
170
|
#
|
105
171
|
def load_tax_rate(name)
|
106
|
-
return 0 if
|
172
|
+
return 0 if CONFIG.dig('tax_rate', name).nil?
|
107
173
|
|
108
|
-
BigDecimal(take_current(
|
174
|
+
BigDecimal(take_current(CONFIG['tax_rate'], name).to_s)
|
109
175
|
end
|
110
176
|
|
111
177
|
def duplicated_contract?(id)
|
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'
|
@@ -16,26 +17,33 @@ module LucaDeal
|
|
16
17
|
|
17
18
|
def initialize(date = nil)
|
18
19
|
@date = issue_date(date)
|
19
|
-
@pjdir = Pathname(LucaSupport::Config::Pjdir)
|
20
|
-
@config = load_config(@pjdir / 'config.yml')
|
21
20
|
end
|
22
21
|
|
23
|
-
def deliver_mail
|
24
|
-
attachment_type =
|
25
|
-
self.class.asof(@date.year, @date.month)
|
22
|
+
def deliver_mail(attachment_type = nil, mode: nil)
|
23
|
+
attachment_type = CONFIG.dig('invoice', 'attachment') || :html
|
24
|
+
invoices = self.class.asof(@date.year, @date.month)
|
25
|
+
raise "No invoice for #{@date.year}/#{@date.month}" if invoices.count.zero?
|
26
|
+
|
27
|
+
invoices.each do |dat, path|
|
26
28
|
next if has_status?(dat, 'mail_delivered')
|
27
29
|
|
28
|
-
mail = compose_mail(dat, attachment: attachment_type.to_sym)
|
29
|
-
LucaSupport::Mail.new(mail,
|
30
|
+
mail = compose_mail(dat, mode: mode, attachment: attachment_type.to_sym)
|
31
|
+
LucaSupport::Mail.new(mail, PJDIR).deliver
|
30
32
|
self.class.add_status!(path, 'mail_delivered')
|
31
33
|
end
|
32
34
|
end
|
33
35
|
|
34
36
|
def preview_mail(attachment_type = nil)
|
35
|
-
attachment_type
|
36
|
-
|
37
|
-
|
38
|
-
|
37
|
+
deliver_mail(attachment_type, mode: :preview)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Render HTML to console
|
41
|
+
#
|
42
|
+
def preview_stdout
|
43
|
+
self.class.asof(@date.year, @date.month) do |dat, _|
|
44
|
+
@company = set_company
|
45
|
+
invoice_vars(dat)
|
46
|
+
puts render_invoice
|
39
47
|
end
|
40
48
|
end
|
41
49
|
|
@@ -45,20 +53,27 @@ module LucaDeal
|
|
45
53
|
|
46
54
|
mail = Mail.new
|
47
55
|
mail.to = dat.dig('customer', 'to') if mode.nil?
|
48
|
-
mail.subject =
|
56
|
+
mail.subject = CONFIG.dig('invoice', 'mail_subject') || 'Your Invoice is available'
|
49
57
|
if mode == :preview
|
50
|
-
mail.cc =
|
58
|
+
mail.cc = CONFIG.dig('mail', 'preview') || CONFIG.dig('mail', 'from')
|
51
59
|
mail.subject = '[preview] ' + mail.subject
|
52
60
|
end
|
53
61
|
mail.text_part = Mail::Part.new(body: render_erb(search_template('invoice-mail.txt.erb')), charset: 'UTF-8')
|
54
|
-
|
55
|
-
mail.attachments[attachment_name(dat, attachment)] = render_erb(search_template('invoice.html.erb'))
|
56
|
-
elsif attachment == :pdf
|
57
|
-
mail.attachments[attachment_name(dat, attachment)] = erb2pdf(search_template('invoice.html.erb'))
|
58
|
-
end
|
62
|
+
mail.attachments[attachment_name(dat, attachment)] = render_invoice(attachment)
|
59
63
|
mail
|
60
64
|
end
|
61
65
|
|
66
|
+
def render_invoice(file_type = :html)
|
67
|
+
case file_type
|
68
|
+
when :html
|
69
|
+
render_erb(search_template('invoice.html.erb'))
|
70
|
+
when :pdf
|
71
|
+
erb2pdf(search_template('invoice.html.erb'))
|
72
|
+
else
|
73
|
+
raise 'This filetype is not supported.'
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
62
77
|
# Output seriarized invoice data to stdout.
|
63
78
|
# Returns previous N months on multiple count
|
64
79
|
#
|
@@ -87,33 +102,65 @@ module LucaDeal
|
|
87
102
|
'customer' => invoice.dig('customer', 'name'),
|
88
103
|
'subtotal' => amount,
|
89
104
|
'tax' => tax,
|
90
|
-
'due' => invoice.dig('due_date')
|
105
|
+
'due' => invoice.dig('due_date'),
|
106
|
+
'mail' => invoice.dig('status')&.select { |a| a.keys.include?('mail_delivered') }&.first
|
91
107
|
}
|
92
108
|
end
|
93
109
|
stat['issue_date'] = scan_date.to_s
|
94
110
|
stat['count'] = stat['records'].count
|
95
111
|
stat['total'] = stat['records'].inject(0) { |sum, rec| sum + rec.dig('subtotal') }
|
96
112
|
stat['tax'] = stat['records'].inject(0) { |sum, rec| sum + rec.dig('tax') }
|
97
|
-
collection << stat
|
113
|
+
collection << readable(stat)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def export_json
|
120
|
+
[].tap do |res|
|
121
|
+
self.class.asof(@date.year, @date.month) do |dat|
|
122
|
+
item = {}
|
123
|
+
item['date'] = dat['issue_date']
|
124
|
+
item['debit'] = []
|
125
|
+
item['credit'] = []
|
126
|
+
dat['subtotal'].map do |sub|
|
127
|
+
item['debit'] << { 'label' => '売掛金', 'value' => readable(sub['items']) }
|
128
|
+
item['debit'] << { 'label' => '売掛金', 'value' => readable(sub['tax']) }
|
129
|
+
item['credit'] << { 'label' => '売上高', 'value' => readable(sub['items']) }
|
130
|
+
item['credit'] << { 'label' => '売上高', 'value' => readable(sub['tax']) }
|
98
131
|
end
|
132
|
+
item['x-customer'] = dat['customer']['name'] if dat.dig('customer', 'name')
|
133
|
+
item['x-editor'] = 'LucaDeal'
|
134
|
+
res << item
|
99
135
|
end
|
100
|
-
puts
|
136
|
+
puts JSON.dump(res)
|
101
137
|
end
|
102
138
|
end
|
103
139
|
|
140
|
+
def single_invoice(contract_id)
|
141
|
+
contract = Contract.find(contract_id)
|
142
|
+
raise "Invoice already exists for #{contract_id}. exit" if duplicated_contract? contract['id']
|
143
|
+
|
144
|
+
gen_invoice!(invoice_object(contract))
|
145
|
+
end
|
146
|
+
|
104
147
|
def monthly_invoice
|
105
|
-
|
148
|
+
Contract.new(@date.to_s).active do |contract|
|
106
149
|
next if contract.dig('terms', 'billing_cycle') != 'monthly'
|
107
150
|
# TODO: provide another I/F for force re-issue if needed
|
108
151
|
next if duplicated_contract? contract['id']
|
109
152
|
|
110
|
-
|
111
|
-
|
153
|
+
gen_invoice!(invoice_object(contract))
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def invoice_object(contract)
|
158
|
+
{}.tap do |invoice|
|
112
159
|
invoice['contract_id'] = contract['id']
|
113
160
|
invoice['customer'] = get_customer(contract.dig('customer_id'))
|
114
161
|
invoice['due_date'] = due_date(@date)
|
115
162
|
invoice['issue_date'] = @date
|
116
|
-
invoice['sales_fee'] = contract.dig('sales_fee')
|
163
|
+
invoice['sales_fee'] = contract['sales_fee'] if contract.dig('sales_fee')
|
117
164
|
invoice['items'] = get_products(contract['products'])
|
118
165
|
.concat(contract['items']&.map { |i| i['qty'] ||= 1; i } || [])
|
119
166
|
.compact
|
@@ -121,25 +168,13 @@ module LucaDeal
|
|
121
168
|
item.dig('type') == 'initial' && subsequent_month?(contract.dig('terms', 'effective'))
|
122
169
|
end
|
123
170
|
invoice['subtotal'] = subtotal(invoice['items'])
|
124
|
-
|
125
|
-
gen_invoice!(invoice)
|
171
|
+
.map { |k, v| v.tap { |dat| dat['rate'] = k } }
|
126
172
|
end
|
127
173
|
end
|
128
174
|
|
129
|
-
# set variables for ERB template
|
130
|
-
#
|
131
|
-
def invoice_vars(invoice_dat)
|
132
|
-
@customer = invoice_dat['customer']
|
133
|
-
@items = invoice_dat['items']
|
134
|
-
@subtotal = invoice_dat['subtotal']
|
135
|
-
@issue_date = invoice_dat['issue_date']
|
136
|
-
@due_date = invoice_dat['due_date']
|
137
|
-
@amount = @subtotal.inject(0) { |sum, i| sum + i['items'] + i['tax'] }
|
138
|
-
end
|
139
|
-
|
140
175
|
def get_customer(id)
|
141
176
|
{}.tap do |res|
|
142
|
-
|
177
|
+
Customer.find(id) do |dat|
|
143
178
|
customer = parse_current(dat)
|
144
179
|
res['id'] = customer['id']
|
145
180
|
res['name'] = customer.dig('name')
|
@@ -155,7 +190,7 @@ module LucaDeal
|
|
155
190
|
|
156
191
|
[].tap do |res|
|
157
192
|
products.each do |product|
|
158
|
-
|
193
|
+
Product.find(product['id'])['items'].each do |item|
|
159
194
|
item['product_id'] = product['id']
|
160
195
|
item['qty'] ||= 1
|
161
196
|
res << item
|
@@ -166,7 +201,7 @@ module LucaDeal
|
|
166
201
|
|
167
202
|
def gen_invoice!(invoice)
|
168
203
|
id = invoice.dig('contract_id')
|
169
|
-
self.class.
|
204
|
+
self.class.create(invoice, date: @date, codes: Array(id))
|
170
205
|
end
|
171
206
|
|
172
207
|
def issue_date(date)
|
@@ -176,11 +211,23 @@ module LucaDeal
|
|
176
211
|
|
177
212
|
# TODO: support due_date variation
|
178
213
|
def due_date(date)
|
179
|
-
|
214
|
+
next_month = date.next_month
|
215
|
+
Date.new(next_month.year, next_month.month, -1)
|
180
216
|
end
|
181
217
|
|
182
218
|
private
|
183
219
|
|
220
|
+
# set variables for ERB template
|
221
|
+
#
|
222
|
+
def invoice_vars(invoice_dat)
|
223
|
+
@customer = invoice_dat['customer']
|
224
|
+
@items = readable(invoice_dat['items'])
|
225
|
+
@subtotal = readable(invoice_dat['subtotal'])
|
226
|
+
@issue_date = invoice_dat['issue_date']
|
227
|
+
@due_date = invoice_dat['due_date']
|
228
|
+
@amount = readable(invoice_dat['subtotal'].inject(0) { |sum, i| sum + i['items'] + i['tax'] })
|
229
|
+
end
|
230
|
+
|
184
231
|
def lib_path
|
185
232
|
__dir__
|
186
233
|
end
|
@@ -189,9 +236,9 @@ module LucaDeal
|
|
189
236
|
#
|
190
237
|
def set_company
|
191
238
|
{}.tap do |h|
|
192
|
-
h['name'] =
|
193
|
-
h['address'] =
|
194
|
-
h['address2'] =
|
239
|
+
h['name'] = CONFIG.dig('company', 'name')
|
240
|
+
h['address'] = CONFIG.dig('company', 'address')
|
241
|
+
h['address2'] = CONFIG.dig('company', 'address2')
|
195
242
|
end
|
196
243
|
end
|
197
244
|
|
@@ -201,12 +248,12 @@ module LucaDeal
|
|
201
248
|
{}.tap do |subtotal|
|
202
249
|
items.each do |i|
|
203
250
|
rate = i.dig('tax') || 'default'
|
204
|
-
qty = i['qty'] || 1
|
251
|
+
qty = i['qty'] || BigDecimal('1')
|
205
252
|
subtotal[rate] = { 'items' => 0, 'tax' => 0 } if subtotal.dig(rate).nil?
|
206
|
-
subtotal[rate]['items'] +=
|
253
|
+
subtotal[rate]['items'] += i['price'] * qty
|
207
254
|
end
|
208
255
|
subtotal.each do |rate, amount|
|
209
|
-
amount['tax'] = (amount['items'] * load_tax_rate(rate))
|
256
|
+
amount['tax'] = (amount['items'] * load_tax_rate(rate))
|
210
257
|
end
|
211
258
|
end
|
212
259
|
end
|
@@ -214,13 +261,14 @@ module LucaDeal
|
|
214
261
|
# load Tax Rate from config.
|
215
262
|
#
|
216
263
|
def load_tax_rate(name)
|
217
|
-
return 0 if
|
264
|
+
return 0 if CONFIG.dig('tax_rate', name).nil?
|
218
265
|
|
219
|
-
BigDecimal(take_current(
|
266
|
+
BigDecimal(take_current(CONFIG['tax_rate'], name).to_s)
|
220
267
|
end
|
221
268
|
|
222
269
|
def attachment_name(dat, type)
|
223
|
-
|
270
|
+
id = %r{/}.match(dat['id']) ? dat['id'].gsub('/', '') : dat['id'][0, 7]
|
271
|
+
"invoice-#{id}.#{type}"
|
224
272
|
end
|
225
273
|
|
226
274
|
def duplicated_contract?(id)
|
@@ -0,0 +1,54 @@
|
|
1
|
+
<html>
|
2
|
+
<head>
|
3
|
+
<meta charset="utf-8">
|
4
|
+
<style>
|
5
|
+
body { size: A4 }
|
6
|
+
</style>
|
7
|
+
</head>
|
8
|
+
<body>
|
9
|
+
<div style="text-align: right">Issue date: <%= @issue_date %></div>
|
10
|
+
<h1 style="display: block; margin: auto; text-align: center">Fee Report</h1>
|
11
|
+
<section><%= @customer["name"] %></section>
|
12
|
+
<section>Total amount: <%= delimit_num(@amount) %></section>
|
13
|
+
|
14
|
+
<table style="width: 100%">
|
15
|
+
<thead>
|
16
|
+
<th>#</th>
|
17
|
+
<th>Customer Name</th>
|
18
|
+
<th>Item Name</th>
|
19
|
+
<th>qty</th>
|
20
|
+
<th>Sales</th>
|
21
|
+
<th>Fee</th>
|
22
|
+
</thead>
|
23
|
+
<tbody>
|
24
|
+
<% @items.each.with_index(1) do |item, i| %>
|
25
|
+
<tr class="item">
|
26
|
+
<td class="unit"><%= i %></td>
|
27
|
+
<td><%= item["customer_name"] %></td>
|
28
|
+
<td><%= item["name"] %></td>
|
29
|
+
<td class="unit"><%= item["qty"] %></td>
|
30
|
+
<td class="price"><%= delimit_num( item["price"] * item["qty"] ) %></td>
|
31
|
+
<td class="price"><%= delimit_num( item["fee"] ) %></td>
|
32
|
+
</tr>
|
33
|
+
<% end %>
|
34
|
+
<tr>
|
35
|
+
<td class="price" colspan="3">Subtotal</td>
|
36
|
+
<td class="price"><%= delimit_num( @sales_fee['fee'] ) %></td>
|
37
|
+
</tr>
|
38
|
+
<tr>
|
39
|
+
<td class="price" colspan="3">Tax</td>
|
40
|
+
<td class="price"><%= delimit_num( @sales_fee['tax'] ) %></td>
|
41
|
+
</tr>
|
42
|
+
<tr>
|
43
|
+
<td class="price" colspan="3">Total</td>
|
44
|
+
<td class="price"><%= delimit_num( @sales_fee['fee'] + @sales_fee['tax'] ) %></td>
|
45
|
+
</tr>
|
46
|
+
</table>
|
47
|
+
|
48
|
+
<section>
|
49
|
+
<div><%= @company["name"] %></div>
|
50
|
+
<div><%= @company["address"] %></div>
|
51
|
+
<div><%= @company["address2"] %></div>
|
52
|
+
</section>
|
53
|
+
</body>
|
54
|
+
</html>
|
@@ -0,0 +1,52 @@
|
|
1
|
+
<html>
|
2
|
+
<head>
|
3
|
+
<meta charset="utf-8">
|
4
|
+
<style>
|
5
|
+
body { size: A4 }
|
6
|
+
</style>
|
7
|
+
</head>
|
8
|
+
<body>
|
9
|
+
<div style="text-align: right">Issue date: <%= @issue_date %></div>
|
10
|
+
<h1 style="display: block; margin: auto; text-align: center">Invoice</h1>
|
11
|
+
<section><%= @customer["name"] %></section>
|
12
|
+
<section>Total amount: <%= delimit_num(@amount) %></section>
|
13
|
+
|
14
|
+
<table style="width: 100%">
|
15
|
+
<thead>
|
16
|
+
<th>#</th>
|
17
|
+
<th>Item</th>
|
18
|
+
<th>qty</th>
|
19
|
+
<th>Amount</th>
|
20
|
+
</thead>
|
21
|
+
<tbody>
|
22
|
+
<% @items.each.with_index(1) do |item, i| %>
|
23
|
+
<tr class="item">
|
24
|
+
<td class="unit"><%= i %></td>
|
25
|
+
<td><%= item["name"] %></td>
|
26
|
+
<td class="unit"><%= item["qty"] %></td>
|
27
|
+
<td class="price"><%= delimit_num( item["price"] * item["qty"] ) %></td>
|
28
|
+
</tr>
|
29
|
+
<% end %>
|
30
|
+
<% @subtotal.each.with_index(1) do |sub, i| %>
|
31
|
+
<tr>
|
32
|
+
<td class="price" colspan="3">Subtotal</td>
|
33
|
+
<td class="price"><%= delimit_num( sub['items'] ) %></td>
|
34
|
+
</tr>
|
35
|
+
<tr>
|
36
|
+
<td class="price" colspan="3">Tax</td>
|
37
|
+
<td class="price"><%= delimit_num( sub['tax'] ) %></td>
|
38
|
+
</tr>
|
39
|
+
<tr>
|
40
|
+
<td class="price" colspan="3">Total</td>
|
41
|
+
<td class="price"><%= delimit_num( sub['items'] + sub['tax'] ) %></td>
|
42
|
+
</tr>
|
43
|
+
<% end %>
|
44
|
+
</table>
|
45
|
+
|
46
|
+
<section>
|
47
|
+
<div><%= @company["name"] %></div>
|
48
|
+
<div><%= @company["address"] %></div>
|
49
|
+
<div><%= @company["address2"] %></div>
|
50
|
+
</section>
|
51
|
+
</body>
|
52
|
+
</html>
|
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.21
|
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
|
+
date: 2020-12-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: lucarecord
|
@@ -89,6 +89,10 @@ files:
|
|
89
89
|
- lib/luca_deal/setup.rb
|
90
90
|
- lib/luca_deal/templates/.keep
|
91
91
|
- lib/luca_deal/templates/config.yml
|
92
|
+
- lib/luca_deal/templates/fee-report-mail.txt.erb
|
93
|
+
- lib/luca_deal/templates/fee-report.html.erb
|
94
|
+
- lib/luca_deal/templates/invoice-mail.txt.erb
|
95
|
+
- lib/luca_deal/templates/invoice.html.erb
|
92
96
|
- lib/luca_deal/version.rb
|
93
97
|
homepage: https://github.com/chumaltd/luca/tree/master/lucadeal
|
94
98
|
licenses:
|