lucadeal 0.2.16 → 0.2.23

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.
@@ -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)