lucadeal 0.2.19 → 0.2.21

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 36ce9f92d92079f0049ec346da2921116e7cde669314bccca3653c28dca33b51
4
- data.tar.gz: b2f591e9bfb9710fa45cdb1d3582f19997d392e84e639531e4b15e77d6b284c7
3
+ metadata.gz: f73de0d5baa72b53fcaeb3ec5ef85c0d3d80a21b7d7531f914892ff476d8eee0
4
+ data.tar.gz: 3ecfc52d4a1e2257e926773f3e7db455dccce71e8fe89822fccb91402d0a979e
5
5
  SHA512:
6
- metadata.gz: 8e2ca7f2a7063d44d29013cdd7448f807c58dbabca341cdeb1a8a118e24ca826ca94ac608f1c4a56eb16cb41cb039a0b39d2b07cb011fdd4a57e4b04c24a7d92
7
- data.tar.gz: 692b888b1209de7fa6329ffbc7edd1ef7ad84be530b2dad7fcaad64b5c621d0da424e3ac90f21e9db7113057599497c91b61d70f479cf48bdea6bee2c49e1f0e
6
+ metadata.gz: 1f8c7a3301a69e5891736379fb9cf4fac3d1cda121e295d77903bb36f9f87ab6485be8d69e4bb398b23cf18af80f47f3d1b4a2f67d2dc3cfba5cc1f9c6fcf5cf
7
+ data.tar.gz: 1515a346552be7a7ec33a37bd3af941b57f8be9c890d67ccfde40bf0f0ece6f678a405d0723ad1d04649ae70e745e4dfa5d3a869400314b366fddf576c31c21a
@@ -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
 
@@ -5,8 +5,8 @@ require 'date'
5
5
  require 'optparse'
6
6
  require 'luca_deal'
7
7
 
8
- module LucaCmd
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
- id = LucaDeal::Customer.new.describe(list.first)
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
- id = LucaDeal::Contract.new.describe(list.first)
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
- puts 'not implemented mode'
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
- LucaDeal::Invoice.new(date).stats(count || 1)
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 'create'
276
+ when 'describe'
194
277
  OptionParser.new do |opt|
195
- opt.banner = 'Usage: luca-deal customers create CustomerName'
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.create(args, params)
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
- LucaCmd::Contract.describe(ARGV)
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
@@ -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
- LucaDeal::Product.find(product['id'])
50
+ Product.find(product['id'])
51
51
  end
52
52
  end
53
- YAML.dump(readable(contract)).tap{ |d| puts d }
53
+ readable(contract)
54
54
  end
55
55
 
56
56
  def generate!(customer_id, mode = 'subscription')
57
- LucaDeal::Customer.find(customer_id) do |customer|
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
@@ -12,19 +12,17 @@ module LucaDeal
12
12
  @dirname = 'customers'
13
13
  @required = ['name']
14
14
 
15
- def initialize(pjdir = nil)
15
+ def initialize
16
16
  @date = Date.today
17
- @pjdir = pjdir || Dir.pwd
18
17
  end
19
18
 
20
19
  def list_name
21
- list = self.class.all.map { |dat| parse_current(dat).sort.to_h }
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 = LucaDeal::Contract.all.select { |contract| contract['customer_id'] == customer['id'] }
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
- YAML.dump(readable(customer)).tap{ |d| puts d }
35
+ readable(customer)
38
36
  end
39
37
 
40
38
  def self.create(obj)
@@ -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
- LucaDeal::Contract.asof(@date.year, @date.month, @date.day) do |contract|
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
- LucaDeal::Invoice.asof(@date.year, @date.month) do |invoice|
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
- 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
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['id'] = issue_random_id
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
- LucaDeal::Customer.find(id) do |dat|
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'] = @config.dig('company', 'name')
77
- h['address'] = @config.dig('company', 'address')
78
- h['address2'] = @config.dig('company', '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
- 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
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 @config.dig('tax_rate', name).nil?
172
+ return 0 if CONFIG.dig('tax_rate', name).nil?
107
173
 
108
- BigDecimal(take_current(@config['tax_rate'], name).to_s)
174
+ BigDecimal(take_current(CONFIG['tax_rate'], name).to_s)
109
175
  end
110
176
 
111
177
  def duplicated_contract?(id)
@@ -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 = @config.dig('invoice', 'attachment') || :html
26
- self.class.asof(@date.year, @date.month) do |dat, path|
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, @pjdir).deliver
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 ||= @config.dig('invoice', 'attachment') || :html
37
- self.class.asof(@date.year, @date.month) do |dat, _path|
38
- mail = compose_mail(dat, mode: :preview, attachment: attachment_type.to_sym)
39
- LucaSupport::Mail.new(mail, @pjdir).deliver
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 = @config.dig('invoice', 'mail_subject') || 'Your Invoice is available'
56
+ mail.subject = CONFIG.dig('invoice', 'mail_subject') || 'Your Invoice is available'
50
57
  if mode == :preview
51
- mail.cc = @config.dig('mail', 'preview') || @config.dig('mail', 'from')
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
- if attachment == :html
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' => LucaSupport::Code.readable(sub['items']) }
115
- item['debit'] << { 'label' => '売掛金', 'value' => LucaSupport::Code.readable(sub['tax']) }
116
- item['credit'] << { 'label' => '売上高', 'value' => LucaSupport::Code.readable(sub['items']) }
117
- item['credit'] << { 'label' => '売上高', 'value' => LucaSupport::Code.readable(sub['tax']) }
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
- LucaDeal::Contract.new(@date.to_s).active do |contract|
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
- invoice = {}
134
- invoice['id'] = issue_random_id
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
- .map { |k, v| v.tap { |dat| dat['rate'] = k } }
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
- LucaDeal::Customer.find(id) do |dat|
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
- LucaDeal::Product.find(product['id'])['items'].each do |item|
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
- Date.new(date.year, date.month + 1, -1)
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'] = @config.dig('company', 'name')
216
- h['address'] = @config.dig('company', 'address')
217
- h['address2'] = @config.dig('company', '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 @config.dig('tax_rate', name).nil?
264
+ return 0 if CONFIG.dig('tax_rate', name).nil?
241
265
 
242
- BigDecimal(take_current(@config['tax_rate'], name).to_s)
266
+ BigDecimal(take_current(CONFIG['tax_rate'], name).to_s)
243
267
  end
244
268
 
245
269
  def attachment_name(dat, type)
246
- "invoice-#{dat.dig('id')[0, 7]}.#{type}"
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,6 @@
1
+ Your monthly fee report is available.
2
+ Please find the document attaced at the bottom of this email.
3
+ --
4
+ <%= @company['name'] %>
5
+ <%= @company['address'] %>
6
+ <%= @company['address2'] %>
@@ -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,6 @@
1
+ Your monthly invoice is available.
2
+ Please find the document attaced at the bottom of this email.
3
+ --
4
+ <%= @company['name'] %>
5
+ <%= @company['address'] %>
6
+ <%= @company['address2'] %>
@@ -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>
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LucaDeal
4
- VERSION = '0.2.19'
4
+ VERSION = '0.2.21'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lucadeal
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.19
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-19 00:00:00.000000000 Z
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: