lucadeal 0.2.19 → 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 +10 -0
- data/README.md +6 -0
- data/exe/luca-deal +147 -17
- data/lib/luca_deal/contract.rb +3 -3
- data/lib/luca_deal/customer.rb +4 -6
- data/lib/luca_deal/fee.rb +100 -34
- data/lib/luca_deal/invoice.rb +74 -49
- 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
@@ -1,3 +1,13 @@
|
|
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
|
+
|
1
11
|
## LucaDeal 0.2.19
|
2
12
|
|
3
13
|
* CLI id completion on Customer delete, Contract create/delete
|
data/README.md
CHANGED
@@ -61,6 +61,12 @@ Monthly invoices are generated with `invoice create --monthly` sub command. Targ
|
|
61
61
|
$ luca-deal invoice create --monthly [yyyy m]
|
62
62
|
```
|
63
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
|
+
|
64
70
|
Invoice conditions are defined by contracts.
|
65
71
|
|
66
72
|
|
data/exe/luca-deal
CHANGED
@@ -5,8 +5,8 @@ require 'date'
|
|
5
5
|
require 'optparse'
|
6
6
|
require 'luca_deal'
|
7
7
|
|
8
|
-
|
9
|
-
class Customer
|
8
|
+
class LucaCmd
|
9
|
+
class Customer < LucaCmd
|
10
10
|
def self.create(args = nil, params = {})
|
11
11
|
if args
|
12
12
|
id = LucaDeal::Customer.create(name: args[0])
|
@@ -28,7 +28,7 @@ module LucaCmd
|
|
28
28
|
list = LucaDeal::Customer.id_completion(args[0])
|
29
29
|
case list.length
|
30
30
|
when 1
|
31
|
-
|
31
|
+
render(LucaDeal::Customer.new.describe(list.first), params)
|
32
32
|
else
|
33
33
|
puts 'found multiple contract id. exit'
|
34
34
|
list.each { |item| puts " #{item[:id]} #{item[:label]}" }
|
@@ -57,11 +57,11 @@ module LucaCmd
|
|
57
57
|
end
|
58
58
|
|
59
59
|
def self.list(args = nil, params = {})
|
60
|
-
LucaDeal::Customer.new.list_name
|
60
|
+
render(LucaDeal::Customer.new.list_name, params)
|
61
61
|
end
|
62
62
|
end
|
63
63
|
|
64
|
-
class Contract
|
64
|
+
class Contract < LucaCmd
|
65
65
|
def self.create(args = nil, params = {})
|
66
66
|
if args.empty?
|
67
67
|
list = LucaDeal::Customer.id_completion('')
|
@@ -93,7 +93,7 @@ module LucaCmd
|
|
93
93
|
list = LucaDeal::Contract.id_completion(args[0])
|
94
94
|
case list.length
|
95
95
|
when 1
|
96
|
-
|
96
|
+
render(LucaDeal::Contract.new.describe(list.first), params)
|
97
97
|
else
|
98
98
|
puts 'found multiple contract id. exit'
|
99
99
|
list.each { |item| puts " #{item[:id]} #{item[:label]}" }
|
@@ -122,14 +122,29 @@ module LucaCmd
|
|
122
122
|
end
|
123
123
|
end
|
124
124
|
|
125
|
-
class Invoice
|
125
|
+
class Invoice < LucaCmd
|
126
126
|
def self.create(args = nil, params = {})
|
127
|
-
date = "#{args[0]}-#{args[1]}-#{args[2] || '1'}" if !args.empty?
|
128
127
|
case params['mode']
|
129
128
|
when 'monthly'
|
129
|
+
date = "#{args[0]}-#{args[1]}-#{args[2] || '1'}" if !args.empty?
|
130
130
|
LucaDeal::Invoice.new(date).monthly_invoice
|
131
131
|
else
|
132
|
-
|
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
|
133
148
|
end
|
134
149
|
end
|
135
150
|
|
@@ -157,7 +172,11 @@ module LucaCmd
|
|
157
172
|
date = "#{Date.today.year}-#{Date.today.month}-1"
|
158
173
|
count = 3
|
159
174
|
end
|
160
|
-
|
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
|
161
180
|
end
|
162
181
|
|
163
182
|
def self.mail(args = nil, params = {})
|
@@ -170,6 +189,62 @@ module LucaCmd
|
|
170
189
|
end
|
171
190
|
end
|
172
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
|
247
|
+
end
|
173
248
|
end
|
174
249
|
|
175
250
|
def new_pj(args = nil, params = {})
|
@@ -184,20 +259,28 @@ case cmd
|
|
184
259
|
when /customers?/
|
185
260
|
subcmd = ARGV.shift
|
186
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)
|
267
|
+
end
|
187
268
|
when 'list'
|
188
269
|
OptionParser.new do |opt|
|
189
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 }
|
190
273
|
args = opt.parse(ARGV)
|
191
274
|
LucaCmd::Customer.list(args, params)
|
192
275
|
end
|
193
|
-
when '
|
276
|
+
when 'describe'
|
194
277
|
OptionParser.new do |opt|
|
195
|
-
opt.banner = 'Usage: luca-deal customers
|
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 }
|
196
281
|
args = opt.parse(ARGV)
|
197
|
-
LucaCmd::Customer.
|
282
|
+
LucaCmd::Customer.describe(args, params)
|
198
283
|
end
|
199
|
-
when 'describe'
|
200
|
-
LucaCmd::Customer.describe(ARGV)
|
201
284
|
when 'delete'
|
202
285
|
LucaCmd::Customer.delete(ARGV)
|
203
286
|
else
|
@@ -223,7 +306,13 @@ when /contracts?/
|
|
223
306
|
LucaCmd::Contract.create(args, params)
|
224
307
|
end
|
225
308
|
when 'describe'
|
226
|
-
|
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)
|
315
|
+
end
|
227
316
|
when 'delete'
|
228
317
|
LucaCmd::Contract.delete(ARGV)
|
229
318
|
else
|
@@ -242,7 +331,7 @@ when /invoices?/, 'i'
|
|
242
331
|
case subcmd
|
243
332
|
when 'create'
|
244
333
|
OptionParser.new do |opt|
|
245
|
-
opt.banner = 'Usage: luca-deal invoices create [options] year month [date]'
|
334
|
+
opt.banner = 'Usage: luca-deal invoices create [options] --monthly|contract_id year month [date]'
|
246
335
|
opt.on('--monthly', 'generate monthly data') { |_v| params['mode'] = 'monthly' }
|
247
336
|
args = opt.parse(ARGV)
|
248
337
|
LucaCmd::Invoice.create(args, params)
|
@@ -252,6 +341,9 @@ when /invoices?/, 'i'
|
|
252
341
|
when 'list'
|
253
342
|
OptionParser.new do |opt|
|
254
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' }
|
255
347
|
args = opt.parse(ARGV)
|
256
348
|
LucaCmd::Invoice.list(args, params)
|
257
349
|
end
|
@@ -279,6 +371,44 @@ when 'new'
|
|
279
371
|
args = opt.parse(ARGV)
|
280
372
|
new_pj(args, params)
|
281
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
|
282
412
|
else
|
283
413
|
puts 'Proper subcommand needed.'
|
284
414
|
puts
|
data/lib/luca_deal/contract.rb
CHANGED
@@ -47,14 +47,14 @@ module LucaDeal
|
|
47
47
|
contract = parse_current(self.class.find(id))
|
48
48
|
if contract['products']
|
49
49
|
contract['products'] = contract['products'].map do |product|
|
50
|
-
|
50
|
+
Product.find(product['id'])
|
51
51
|
end
|
52
52
|
end
|
53
|
-
|
53
|
+
readable(contract)
|
54
54
|
end
|
55
55
|
|
56
56
|
def generate!(customer_id, mode = 'subscription')
|
57
|
-
|
57
|
+
Customer.find(customer_id) do |customer|
|
58
58
|
current_customer = parse_current(customer)
|
59
59
|
if mode == 'sales_fee'
|
60
60
|
obj = salesfee_template
|
data/lib/luca_deal/customer.rb
CHANGED
@@ -12,19 +12,17 @@ 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
|
-
YAML.dump(list).tap { |l| puts l }
|
20
|
+
self.class.all.map { |dat| parse_current(dat).sort.to_h }
|
23
21
|
end
|
24
22
|
|
25
23
|
def describe(id)
|
26
24
|
customer = parse_current(self.class.find(id))
|
27
|
-
contracts =
|
25
|
+
contracts = Contract.all.select { |contract| contract['customer_id'] == customer['id'] }
|
28
26
|
if !contracts.empty?
|
29
27
|
customer['contracts'] = contracts.map do |c|
|
30
28
|
{
|
@@ -34,7 +32,7 @@ module LucaDeal
|
|
34
32
|
}
|
35
33
|
end
|
36
34
|
end
|
37
|
-
|
35
|
+
readable(customer)
|
38
36
|
end
|
39
37
|
|
40
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(fee, date: @date, codes: 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
@@ -17,26 +17,33 @@ module LucaDeal
|
|
17
17
|
|
18
18
|
def initialize(date = nil)
|
19
19
|
@date = issue_date(date)
|
20
|
-
@pjdir = Pathname(LucaSupport::Config::Pjdir)
|
21
|
-
@config = load_config(@pjdir / 'config.yml')
|
22
20
|
end
|
23
21
|
|
24
|
-
def deliver_mail
|
25
|
-
attachment_type =
|
26
|
-
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|
|
27
28
|
next if has_status?(dat, 'mail_delivered')
|
28
29
|
|
29
|
-
mail = compose_mail(dat, attachment: attachment_type.to_sym)
|
30
|
-
LucaSupport::Mail.new(mail,
|
30
|
+
mail = compose_mail(dat, mode: mode, attachment: attachment_type.to_sym)
|
31
|
+
LucaSupport::Mail.new(mail, PJDIR).deliver
|
31
32
|
self.class.add_status!(path, 'mail_delivered')
|
32
33
|
end
|
33
34
|
end
|
34
35
|
|
35
36
|
def preview_mail(attachment_type = nil)
|
36
|
-
attachment_type
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
40
47
|
end
|
41
48
|
end
|
42
49
|
|
@@ -46,20 +53,27 @@ module LucaDeal
|
|
46
53
|
|
47
54
|
mail = Mail.new
|
48
55
|
mail.to = dat.dig('customer', 'to') if mode.nil?
|
49
|
-
mail.subject =
|
56
|
+
mail.subject = CONFIG.dig('invoice', 'mail_subject') || 'Your Invoice is available'
|
50
57
|
if mode == :preview
|
51
|
-
mail.cc =
|
58
|
+
mail.cc = CONFIG.dig('mail', 'preview') || CONFIG.dig('mail', 'from')
|
52
59
|
mail.subject = '[preview] ' + mail.subject
|
53
60
|
end
|
54
61
|
mail.text_part = Mail::Part.new(body: render_erb(search_template('invoice-mail.txt.erb')), charset: 'UTF-8')
|
55
|
-
|
56
|
-
mail.attachments[attachment_name(dat, attachment)] = render_erb(search_template('invoice.html.erb'))
|
57
|
-
elsif attachment == :pdf
|
58
|
-
mail.attachments[attachment_name(dat, attachment)] = erb2pdf(search_template('invoice.html.erb'))
|
59
|
-
end
|
62
|
+
mail.attachments[attachment_name(dat, attachment)] = render_invoice(attachment)
|
60
63
|
mail
|
61
64
|
end
|
62
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
|
+
|
63
77
|
# Output seriarized invoice data to stdout.
|
64
78
|
# Returns previous N months on multiple count
|
65
79
|
#
|
@@ -96,10 +110,9 @@ module LucaDeal
|
|
96
110
|
stat['count'] = stat['records'].count
|
97
111
|
stat['total'] = stat['records'].inject(0) { |sum, rec| sum + rec.dig('subtotal') }
|
98
112
|
stat['tax'] = stat['records'].inject(0) { |sum, rec| sum + rec.dig('tax') }
|
99
|
-
collection << stat
|
113
|
+
collection << readable(stat)
|
100
114
|
end
|
101
115
|
end
|
102
|
-
puts YAML.dump(LucaSupport::Code.readable(collection))
|
103
116
|
end
|
104
117
|
end
|
105
118
|
|
@@ -111,10 +124,10 @@ module LucaDeal
|
|
111
124
|
item['debit'] = []
|
112
125
|
item['credit'] = []
|
113
126
|
dat['subtotal'].map do |sub|
|
114
|
-
item['debit'] << { 'label' => '売掛金', 'value' =>
|
115
|
-
item['debit'] << { 'label' => '売掛金', 'value' =>
|
116
|
-
item['credit'] << { 'label' => '売上高', 'value' =>
|
117
|
-
item['credit'] << { 'label' => '売上高', 'value' =>
|
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']) }
|
118
131
|
end
|
119
132
|
item['x-customer'] = dat['customer']['name'] if dat.dig('customer', 'name')
|
120
133
|
item['x-editor'] = 'LucaDeal'
|
@@ -124,14 +137,25 @@ module LucaDeal
|
|
124
137
|
end
|
125
138
|
end
|
126
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
|
+
|
127
147
|
def monthly_invoice
|
128
|
-
|
148
|
+
Contract.new(@date.to_s).active do |contract|
|
129
149
|
next if contract.dig('terms', 'billing_cycle') != 'monthly'
|
130
150
|
# TODO: provide another I/F for force re-issue if needed
|
131
151
|
next if duplicated_contract? contract['id']
|
132
152
|
|
133
|
-
|
134
|
-
|
153
|
+
gen_invoice!(invoice_object(contract))
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def invoice_object(contract)
|
158
|
+
{}.tap do |invoice|
|
135
159
|
invoice['contract_id'] = contract['id']
|
136
160
|
invoice['customer'] = get_customer(contract.dig('customer_id'))
|
137
161
|
invoice['due_date'] = due_date(@date)
|
@@ -144,25 +168,13 @@ module LucaDeal
|
|
144
168
|
item.dig('type') == 'initial' && subsequent_month?(contract.dig('terms', 'effective'))
|
145
169
|
end
|
146
170
|
invoice['subtotal'] = subtotal(invoice['items'])
|
147
|
-
|
148
|
-
gen_invoice!(invoice)
|
171
|
+
.map { |k, v| v.tap { |dat| dat['rate'] = k } }
|
149
172
|
end
|
150
173
|
end
|
151
174
|
|
152
|
-
# set variables for ERB template
|
153
|
-
#
|
154
|
-
def invoice_vars(invoice_dat)
|
155
|
-
@customer = invoice_dat['customer']
|
156
|
-
@items = invoice_dat['items']
|
157
|
-
@subtotal = invoice_dat['subtotal']
|
158
|
-
@issue_date = invoice_dat['issue_date']
|
159
|
-
@due_date = invoice_dat['due_date']
|
160
|
-
@amount = @subtotal.inject(0) { |sum, i| sum + i['items'] + i['tax'] }
|
161
|
-
end
|
162
|
-
|
163
175
|
def get_customer(id)
|
164
176
|
{}.tap do |res|
|
165
|
-
|
177
|
+
Customer.find(id) do |dat|
|
166
178
|
customer = parse_current(dat)
|
167
179
|
res['id'] = customer['id']
|
168
180
|
res['name'] = customer.dig('name')
|
@@ -178,7 +190,7 @@ module LucaDeal
|
|
178
190
|
|
179
191
|
[].tap do |res|
|
180
192
|
products.each do |product|
|
181
|
-
|
193
|
+
Product.find(product['id'])['items'].each do |item|
|
182
194
|
item['product_id'] = product['id']
|
183
195
|
item['qty'] ||= 1
|
184
196
|
res << item
|
@@ -199,11 +211,23 @@ module LucaDeal
|
|
199
211
|
|
200
212
|
# TODO: support due_date variation
|
201
213
|
def due_date(date)
|
202
|
-
|
214
|
+
next_month = date.next_month
|
215
|
+
Date.new(next_month.year, next_month.month, -1)
|
203
216
|
end
|
204
217
|
|
205
218
|
private
|
206
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
|
+
|
207
231
|
def lib_path
|
208
232
|
__dir__
|
209
233
|
end
|
@@ -212,9 +236,9 @@ module LucaDeal
|
|
212
236
|
#
|
213
237
|
def set_company
|
214
238
|
{}.tap do |h|
|
215
|
-
h['name'] =
|
216
|
-
h['address'] =
|
217
|
-
h['address2'] =
|
239
|
+
h['name'] = CONFIG.dig('company', 'name')
|
240
|
+
h['address'] = CONFIG.dig('company', 'address')
|
241
|
+
h['address2'] = CONFIG.dig('company', 'address2')
|
218
242
|
end
|
219
243
|
end
|
220
244
|
|
@@ -237,13 +261,14 @@ module LucaDeal
|
|
237
261
|
# load Tax Rate from config.
|
238
262
|
#
|
239
263
|
def load_tax_rate(name)
|
240
|
-
return 0 if
|
264
|
+
return 0 if CONFIG.dig('tax_rate', name).nil?
|
241
265
|
|
242
|
-
BigDecimal(take_current(
|
266
|
+
BigDecimal(take_current(CONFIG['tax_rate'], name).to_s)
|
243
267
|
end
|
244
268
|
|
245
269
|
def attachment_name(dat, type)
|
246
|
-
|
270
|
+
id = %r{/}.match(dat['id']) ? dat['id'].gsub('/', '') : dat['id'][0, 7]
|
271
|
+
"invoice-#{id}.#{type}"
|
247
272
|
end
|
248
273
|
|
249
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:
|