lucadeal 0.2.16 → 0.2.23

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- LucaDeal::Customer.find(customer_id) do |customer|
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
@@ -12,14 +12,27 @@ 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) }
22
- YAML.dump(list).tap { |l| puts l }
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)
@@ -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)
@@ -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 = @config.dig('invoice', 'attachment') || :html
25
- 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|
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, @pjdir).deliver
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 ||= @config.dig('invoice', 'attachment') || :html
36
- self.class.asof(@date.year, @date.month) do |dat, _path|
37
- mail = compose_mail(dat, mode: :preview, attachment: attachment_type.to_sym)
38
- 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
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 = @config.dig('invoice', 'mail_subject') || 'Your Invoice is available'
56
+ mail.subject = CONFIG.dig('invoice', 'mail_subject') || 'Your Invoice is available'
49
57
  if mode == :preview
50
- mail.cc = @config.dig('mail', 'preview') || @config.dig('mail', 'from')
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
- if attachment == :html
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,28 +102,83 @@ 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
+ # send payment list to preview address or from address.
120
+ #
121
+ def stats_email
122
+ {}.tap do |res|
123
+ stats(2).each.with_index(1) do |stat, i|
124
+ @issue_date = stat['issue_date'] if i == 1
125
+ stat['records'].each do |record|
126
+ res[record['customer']] ||= {}
127
+ res[record['customer']]['customer_name'] ||= record['customer']
128
+ res[record['customer']]["amount#{i}"] ||= record['subtotal']
129
+ res[record['customer']]["tax#{i}"] ||= record['tax']
130
+ end
131
+ end
132
+ @invoices = res.values
133
+ end
134
+
135
+ mail = Mail.new
136
+ mail.to = CONFIG.dig('mail', 'preview') || CONFIG.dig('mail', 'from')
137
+ mail.subject = 'Check monthly payment list'
138
+ mail.html_part = Mail::Part.new(body: render_erb(search_template('monthly-payment-list.html.erb')), content_type: 'text/html; charset=UTF-8')
139
+ LucaSupport::Mail.new(mail, PJDIR).deliver
140
+ end
141
+
142
+ def export_json
143
+ [].tap do |res|
144
+ self.class.asof(@date.year, @date.month) do |dat|
145
+ item = {}
146
+ item['date'] = dat['issue_date']
147
+ item['debit'] = []
148
+ item['credit'] = []
149
+ dat['subtotal'].map do |sub|
150
+ item['debit'] << { 'label' => '売掛金', 'amount' => readable(sub['items']) }
151
+ item['debit'] << { 'label' => '売掛金', 'amount' => readable(sub['tax']) }
152
+ item['credit'] << { 'label' => '売上高', 'amount' => readable(sub['items']) }
153
+ item['credit'] << { 'label' => '売上高', 'amount' => readable(sub['tax']) }
98
154
  end
155
+ item['x-customer'] = dat['customer']['name'] if dat.dig('customer', 'name')
156
+ item['x-editor'] = 'LucaDeal'
157
+ res << item
99
158
  end
100
- puts YAML.dump(LucaSupport::Code.readable(collection))
159
+ puts JSON.dump(res)
101
160
  end
102
161
  end
103
162
 
163
+ def single_invoice(contract_id)
164
+ contract = Contract.find(contract_id)
165
+ raise "Invoice already exists for #{contract_id}. exit" if duplicated_contract? contract['id']
166
+
167
+ gen_invoice!(invoice_object(contract))
168
+ end
169
+
104
170
  def monthly_invoice
105
- LucaDeal::Contract.new(@date.to_s).active do |contract|
171
+ Contract.new(@date.to_s).active do |contract|
106
172
  next if contract.dig('terms', 'billing_cycle') != 'monthly'
107
173
  # TODO: provide another I/F for force re-issue if needed
108
174
  next if duplicated_contract? contract['id']
109
175
 
110
- invoice = {}
111
- invoice['id'] = issue_random_id
176
+ gen_invoice!(invoice_object(contract))
177
+ end
178
+ end
179
+
180
+ def invoice_object(contract)
181
+ {}.tap do |invoice|
112
182
  invoice['contract_id'] = contract['id']
113
183
  invoice['customer'] = get_customer(contract.dig('customer_id'))
114
184
  invoice['due_date'] = due_date(@date)
@@ -121,25 +191,13 @@ module LucaDeal
121
191
  item.dig('type') == 'initial' && subsequent_month?(contract.dig('terms', 'effective'))
122
192
  end
123
193
  invoice['subtotal'] = subtotal(invoice['items'])
124
- .map { |k, v| v.tap { |dat| dat['rate'] = k } }
125
- gen_invoice!(invoice)
194
+ .map { |k, v| v.tap { |dat| dat['rate'] = k } }
126
195
  end
127
196
  end
128
197
 
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
198
  def get_customer(id)
141
199
  {}.tap do |res|
142
- LucaDeal::Customer.find(id) do |dat|
200
+ Customer.find(id) do |dat|
143
201
  customer = parse_current(dat)
144
202
  res['id'] = customer['id']
145
203
  res['name'] = customer.dig('name')
@@ -155,7 +213,7 @@ module LucaDeal
155
213
 
156
214
  [].tap do |res|
157
215
  products.each do |product|
158
- LucaDeal::Product.find(product['id'])['items'].each do |item|
216
+ Product.find(product['id'])['items'].each do |item|
159
217
  item['product_id'] = product['id']
160
218
  item['qty'] ||= 1
161
219
  res << item
@@ -176,11 +234,23 @@ module LucaDeal
176
234
 
177
235
  # TODO: support due_date variation
178
236
  def due_date(date)
179
- Date.new(date.year, date.month + 1, -1)
237
+ next_month = date.next_month
238
+ Date.new(next_month.year, next_month.month, -1)
180
239
  end
181
240
 
182
241
  private
183
242
 
243
+ # set variables for ERB template
244
+ #
245
+ def invoice_vars(invoice_dat)
246
+ @customer = invoice_dat['customer']
247
+ @items = readable(invoice_dat['items'])
248
+ @subtotal = readable(invoice_dat['subtotal'])
249
+ @issue_date = invoice_dat['issue_date']
250
+ @due_date = invoice_dat['due_date']
251
+ @amount = readable(invoice_dat['subtotal'].inject(0) { |sum, i| sum + i['items'] + i['tax'] })
252
+ end
253
+
184
254
  def lib_path
185
255
  __dir__
186
256
  end
@@ -189,9 +259,9 @@ module LucaDeal
189
259
  #
190
260
  def set_company
191
261
  {}.tap do |h|
192
- h['name'] = @config.dig('company', 'name')
193
- h['address'] = @config.dig('company', 'address')
194
- h['address2'] = @config.dig('company', 'address2')
262
+ h['name'] = CONFIG.dig('company', 'name')
263
+ h['address'] = CONFIG.dig('company', 'address')
264
+ h['address2'] = CONFIG.dig('company', 'address2')
195
265
  end
196
266
  end
197
267
 
@@ -214,13 +284,14 @@ module LucaDeal
214
284
  # load Tax Rate from config.
215
285
  #
216
286
  def load_tax_rate(name)
217
- return 0 if @config.dig('tax_rate', name).nil?
287
+ return 0 if CONFIG.dig('tax_rate', name).nil?
218
288
 
219
- BigDecimal(take_current(@config['tax_rate'], name).to_s)
289
+ BigDecimal(take_current(CONFIG['tax_rate'], name).to_s)
220
290
  end
221
291
 
222
292
  def attachment_name(dat, type)
223
- "invoice-#{dat.dig('id')[0, 7]}.#{type}"
293
+ id = %r{/}.match(dat['id']) ? dat['id'].gsub('/', '') : dat['id'][0, 7]
294
+ "invoice-#{id}.#{type}"
224
295
  end
225
296
 
226
297
  def duplicated_contract?(id)