lucadeal 0.2.21 → 0.2.25

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: f73de0d5baa72b53fcaeb3ec5ef85c0d3d80a21b7d7531f914892ff476d8eee0
4
- data.tar.gz: 3ecfc52d4a1e2257e926773f3e7db455dccce71e8fe89822fccb91402d0a979e
3
+ metadata.gz: 68b497e68738101eafbfe6eab73c3e0cf8d297cf4676ccfb8cd05efdadc273a3
4
+ data.tar.gz: fb8ed0c2d5eb0f7621c4436ddad91ece16aa723f2861ecd661423802ec01d0c4
5
5
  SHA512:
6
- metadata.gz: 1f8c7a3301a69e5891736379fb9cf4fac3d1cda121e295d77903bb36f9f87ab6485be8d69e4bb398b23cf18af80f47f3d1b4a2f67d2dc3cfba5cc1f9c6fcf5cf
7
- data.tar.gz: 1515a346552be7a7ec33a37bd3af941b57f8be9c890d67ccfde40bf0f0ece6f678a405d0723ad1d04649ae70e745e4dfa5d3a869400314b366fddf576c31c21a
6
+ metadata.gz: 5c8941a80f26d67b4855f2757e00f8241dd0b73f37f0fe2ab8799500388118bec4513fcb398116ba8072889bedf97a083fbb2c4ce3fdd4219cb7a15844785b1c
7
+ data.tar.gz: 91a84225d3d46201f85e9e38bbedbc102b2a3aaf2ccbf0a812db56bc3b502b78b7f89cb4087ec09642db5ff49b4abe58c15f6632bcbee18d471e95f3c1ff2a20
data/CHANGELOG.md CHANGED
@@ -1,3 +1,27 @@
1
+ ## LucaDeal 0.2.25
2
+
3
+ * implement deduction rate for fee calculation.
4
+ * implement `luca-deal fee export`
5
+ * refine export label for luca-book compatibility
6
+ * add `luca-deal invoice create --monthly --with-fee` option.
7
+ * preview_mail can deliver regardless of `mail_delivered` status
8
+ * `luca-deal fee mail` skip no item record by default.
9
+
10
+ ## LucaDeal 0.2.24
11
+
12
+ * add `luca-deal invoices create --monthly --mail`, send payment list after monthly invoice creation.
13
+ * add 'other_payments' tracking with no invoices.
14
+ * can have limit on fee calculation.
15
+ * initial implment of `luca-deal fee list`
16
+
17
+ ## LucaDeal 0.2.23
18
+
19
+ * implement `luca-deal invoices list --mail`: payment list via HTML mail
20
+
21
+ ## LucaDeal 0.2.22
22
+
23
+ * Breaking change: export key 'value' -> 'amount'
24
+
1
25
  ## LucaDeal 0.2.21
2
26
 
3
27
  * Implement `luca-deal fee` subcommands.
data/README.md CHANGED
@@ -70,6 +70,21 @@ $ luca-deal invoice create 1d3 yyyy m
70
70
  Invoice conditions are defined by contracts.
71
71
 
72
72
 
73
+ ### Send Invoice
74
+
75
+ Invoice is implemented with HTML & ERB. Copy [default template](lib/luca_deal/templates/invoice.html.erb) to `templates/` in the data directory, and customize.
76
+ If you want to send invoices in PDF, you need to install `wkhtmltopdf command separately. Send mail command is as bellows:`
77
+
78
+ ```
79
+ $ luca-deal invoice mail yyyy m
80
+ ```
81
+
82
+
83
+ ### Sales Fee
84
+
85
+ You can also manage revenue share program with Fee object. Setup proper contract structure.
86
+
87
+
73
88
  ## Data Structure
74
89
 
75
90
  Records are stored in YAML format. On historical records, see [LucaRecord](../lucarecord/README.md#historical-field).
@@ -120,7 +135,7 @@ Fields for subscription customers are as bellows:
120
135
  | Top level | Second level | | historical | Description |
121
136
  |-----------|---------------|----------|------------|------------------------------------------------------------------------------------------------------|
122
137
  | terms | | | | |
123
- | | billing_cycle | optional | | If 'monthly', invoices are generated on each month. |
138
+ | | billing_cycle | optional | | If 'monthly', invoices are generated on each month. If 'other_payments', no_invoices are generated on each month. `no_invoices` are mostly same as invoices, but not sending email. |
124
139
  | | category | optional | | Default: 'subscription' |
125
140
  | products | | | | Array of products. |
126
141
  | | id | | | reference for Product |
@@ -139,9 +154,12 @@ Fields for sales fee are as bellows:
139
154
  |-----------|--------------|----------|------------|-------------------------------------------------------------------------------------|
140
155
  | terms | | | | |
141
156
  | | category | | | If 'sales_fee', contract is treated as selling commission. |
157
+ | | limit | | | If set, fees are calculated as mas as `limit` months. |
158
+ | | deduction_label | | | Label for deduction. Used on export |
142
159
  | rate | | optional | | |
143
160
  | | default | | | sales fee rate. |
144
161
  | | initial | | | sales fee rate for items of type=initial. |
162
+ | | deduction | | | deduction rate(if any) multiplied by fee |
145
163
 
146
164
 
147
165
  ### Invoice
data/exe/luca-deal CHANGED
@@ -124,10 +124,15 @@ class LucaCmd
124
124
 
125
125
  class Invoice < LucaCmd
126
126
  def self.create(args = nil, params = {})
127
- case params['mode']
127
+ case params[:mode]
128
128
  when 'monthly'
129
129
  date = "#{args[0]}-#{args[1]}-#{args[2] || '1'}" if !args.empty?
130
130
  LucaDeal::Invoice.new(date).monthly_invoice
131
+ LucaDeal::NoInvoice.new(date).monthly_invoice
132
+ LucaDeal::Fee.new(date).monthly_fee if params[:fee]
133
+ if params[:mail]
134
+ LucaDeal::Invoice.new(date).stats_email
135
+ end
131
136
  else
132
137
  date = "#{args[1]}-#{args[2]}-#{args[3] || '1'}" if !args.empty?
133
138
  list = LucaDeal::Contract.id_completion(args[0] || '', label: 'customer_name')
@@ -174,6 +179,8 @@ class LucaCmd
174
179
  end
175
180
  if params[:html]
176
181
  LucaDeal::Invoice.new(date).preview_stdout
182
+ elsif params[:mail]
183
+ LucaDeal::Invoice.new(date).stats_email
177
184
  else
178
185
  render(LucaDeal::Invoice.new(date).stats(count || 1), params)
179
186
  end
@@ -192,7 +199,7 @@ class LucaCmd
192
199
 
193
200
  class Fee < LucaCmd
194
201
  def self.create(args = nil, params = {})
195
- case params['mode']
202
+ case params[:mode]
196
203
  when 'monthly'
197
204
  date = "#{args[0]}-#{args[1]}-#{args[2] || '1'}" if !args.empty?
198
205
  LucaDeal::Fee.new(date).monthly_fee
@@ -211,6 +218,15 @@ class LucaCmd
211
218
  end
212
219
  end
213
220
 
221
+ def self.export(args = nil, _params = nil)
222
+ if args
223
+ args << 28 if args.length == 2 # specify safe last day
224
+ LucaDeal::Fee.new(args.join('-')).export_json
225
+ else
226
+ LucaDeal::Fee.new.export_json
227
+ end
228
+ end
229
+
214
230
  def self.list(args = nil, params = {})
215
231
  date = "#{args[0]}-#{args[1]}-#{args[2] || '1'}" if !args.empty?
216
232
  if args.empty?
@@ -332,7 +348,9 @@ when /invoices?/, 'i'
332
348
  when 'create'
333
349
  OptionParser.new do |opt|
334
350
  opt.banner = 'Usage: luca-deal invoices create [options] --monthly|contract_id year month [date]'
335
- opt.on('--monthly', 'generate monthly data') { |_v| params['mode'] = 'monthly' }
351
+ opt.on('--mail', 'send payment list by email. Only works with --monthly') { |_v| params[:mail] = true }
352
+ opt.on('--monthly', 'generate monthly data') { |_v| params[:mode] = 'monthly' }
353
+ opt.on('--with-fee', 'generate sales fee data after monthly invoice creation') { |_v| params[:fee] = true }
336
354
  args = opt.parse(ARGV)
337
355
  LucaCmd::Invoice.create(args, params)
338
356
  end
@@ -344,6 +362,7 @@ when /invoices?/, 'i'
344
362
  opt.on('--nu', 'show table in nushell') { |_v| params[:output] = 'nu' }
345
363
  opt.on('-o', '--output VAL', 'output serialized data') { |v| params[:output] = v }
346
364
  opt.on('--html', 'output html invoices') { |_v| params[:html] = 'monthly' }
365
+ opt.on('--mail', 'send payment list by email') { |_v| params[:mail] = true }
347
366
  args = opt.parse(ARGV)
348
367
  LucaCmd::Invoice.list(args, params)
349
368
  end
@@ -377,12 +396,14 @@ when /fee/
377
396
  when 'create'
378
397
  OptionParser.new do |opt|
379
398
  opt.banner = 'Usage: luca-deal fee create [options] year month [date]'
380
- opt.on('--monthly', 'generate monthly data') { |_v| params['mode'] = 'monthly' }
399
+ opt.on('--monthly', 'generate monthly data') { |_v| params[:mode] = 'monthly' }
381
400
  args = opt.parse(ARGV)
382
401
  LucaCmd::Fee.create(args, params)
383
402
  end
384
403
  when 'delete'
385
404
  LucaCmd::Fee.delete(ARGV)
405
+ when 'export'
406
+ LucaCmd::Fee.export(ARGV)
386
407
  when 'list'
387
408
  OptionParser.new do |opt|
388
409
  opt.banner = 'Usage: luca-deal fee list [options] year month [date]'
@@ -416,6 +437,7 @@ else
416
437
  puts ' customers'
417
438
  puts ' contracts'
418
439
  puts ' invoices'
440
+ puts ' fee'
419
441
  puts ' new: initialize project dir'
420
442
  puts ' export: puts invoice data for LucaBook import'
421
443
  exit 1
data/lib/luca_deal.rb CHANGED
@@ -8,6 +8,7 @@ module LucaDeal
8
8
  autoload :Contract, 'luca_deal/contract'
9
9
  autoload :Fee, 'luca_deal/fee'
10
10
  autoload :Invoice, 'luca_deal/invoice'
11
+ autoload :NoInvoice, 'luca_deal/no_invoice'
11
12
  autoload :Product, 'luca_deal/product'
12
13
  autoload :Setup, 'luca_deal/setup'
13
14
  end
@@ -69,11 +69,11 @@ module LucaDeal
69
69
  end
70
70
 
71
71
  def active_period?(dat)
72
- unless dat.dig('defunct').nil?
73
- defunct = dat.dig('defunct').respond_to?(:year) ? dat.dig('defunct') : Date.parse(dat.dig('defunct'))
72
+ unless dat['defunct'].nil?
73
+ defunct = dat['defunct'].respond_to?(:year) ? dat['defunct'] : Date.parse(dat['defunct'])
74
74
  return false if @date > defunct
75
75
  end
76
- effective = dat.dig('effective').respond_to?(:year) ? dat.dig('effective') : Date.parse(dat.dig('effective'))
76
+ effective = dat['effective'].respond_to?(:year) ? dat['effective'] : Date.parse(dat['effective'])
77
77
  @date >= effective
78
78
  end
79
79
 
data/lib/luca_deal/fee.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'luca_deal/version'
2
2
 
3
3
  require 'mail'
4
+ require 'json'
4
5
  require 'yaml'
5
6
  require 'pathname'
6
7
  require 'bigdecimal'
@@ -25,41 +26,58 @@ module LucaDeal
25
26
 
26
27
  @rate = { 'default' => BigDecimal(contract.dig('rate', 'default')) }
27
28
  @rate['initial'] = contract.dig('rate', 'initial') ? BigDecimal(contract.dig('rate', 'initial')) : @rate['default']
29
+ limit = contract.dig('terms', 'limit')
28
30
 
29
- fee = { 'contract_id' => contract['id'], 'items' => [] }
31
+ fee = {
32
+ 'contract_id' => contract['id'],
33
+ 'items' => [],
34
+ 'sales_fee' => {
35
+ 'fee' => 0,
36
+ 'tax' => 0,
37
+ 'deduction' => 0,
38
+ 'deduction_label' => contract.dig('terms', 'deduction_label')
39
+ }
40
+ }
30
41
  fee['customer'] = get_customer(contract['customer_id'])
31
42
  fee['issue_date'] = @date
32
43
  Invoice.asof(@date.year, @date.month) do |invoice|
33
44
  next if invoice.dig('sales_fee', 'id') != contract['id']
45
+ next if exceed_limit?(invoice, limit)
34
46
 
35
47
  invoice['items'].each do |item|
36
48
  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
- }
49
+ fee['items'] << fee_record(invoice, item, rate)
45
50
  end
46
- fee['sales_fee'] = subtotal(fee['items'])
51
+ subtotal(fee['items']).each{ |k, v| fee['sales_fee'][k] += v }
47
52
  end
53
+ NoInvoice.asof(@date.year, @date.month) do |no_invoice|
54
+ next if no_invoice.dig('sales_fee', 'id') != contract['id']
55
+ next if exceed_limit?(no_invoice, limit)
56
+
57
+ no_invoice['items'].each do |item|
58
+ rate = item['type'] == 'initial' ? @rate['initial'] : @rate['default']
59
+ fee['items'] << fee_record(no_invoice, item, rate)
60
+ end
61
+ subtotal(fee['items']).each{ |k, v| fee['sales_fee'][k] += v }
62
+ end
63
+ deduction_rate = contract.dig('rate', 'deduction')
64
+ fee['sales_fee']['deduction'] = -1 * (fee['sales_fee']['fee'] * deduction_rate).floor if deduction_rate
48
65
  self.class.create(fee, date: @date, codes: Array(contract['id']))
49
66
  end
50
67
  end
51
68
 
52
- def deliver_mail(attachment_type = nil, mode: nil)
69
+ def deliver_mail(attachment_type = nil, mode: nil, skip_no_item: true)
53
70
  attachment_type = CONFIG.dig('fee', 'attachment') || :html
54
71
  fees = self.class.asof(@date.year, @date.month)
55
72
  raise "No report for #{@date.year}/#{@date.month}" if fees.count.zero?
56
73
 
57
74
  fees.each do |dat, path|
58
75
  next if has_status?(dat, 'mail_delivered')
76
+ next if skip_no_item && dat['items'].empty?
59
77
 
60
78
  mail = compose_mail(dat, mode: mode, attachment: attachment_type.to_sym)
61
79
  LucaSupport::Mail.new(mail, PJDIR).deliver
62
- self.class.add_status!(path, 'mail_delivered')
80
+ self.class.add_status!(path, 'mail_delivered') if mode.nil?
63
81
  end
64
82
  end
65
83
 
@@ -83,7 +101,7 @@ module LucaDeal
83
101
 
84
102
  mail = Mail.new
85
103
  mail.to = dat.dig('customer', 'to') if mode.nil?
86
- mail.subject = CONFIG.dig('invoice', 'mail_subject') || 'Your Report is available'
104
+ mail.subject = CONFIG.dig('fee', 'mail_subject') || 'Your Report is available'
87
105
  if mode == :preview
88
106
  mail.cc = CONFIG.dig('mail', 'preview') || CONFIG.dig('mail', 'from')
89
107
  mail.subject = '[preview] ' + mail.subject
@@ -93,6 +111,76 @@ module LucaDeal
93
111
  mail
94
112
  end
95
113
 
114
+ # Output seriarized fee data to stdout.
115
+ # Returns previous N months on multiple count
116
+ #
117
+ # === Example YAML output
118
+ # ---
119
+ # - records:
120
+ # - customer: Example Co.
121
+ # subtotal: 100000
122
+ # tax: 10000
123
+ # due: 2020-10-31
124
+ # issue_date: '2020-09-30'
125
+ # count: 1
126
+ # total: 100000
127
+ # tax: 10000
128
+ #
129
+ def stats(count = 1)
130
+ [].tap do |collection|
131
+ scan_date = @date.next_month
132
+ count.times do
133
+ scan_date = scan_date.prev_month
134
+ {}.tap do |stat|
135
+ stat['records'] = self.class.asof(scan_date.year, scan_date.month).map do |fee|
136
+ {
137
+ 'customer' => fee.dig('customer', 'name'),
138
+ 'client' => fee['items'].map{ |item| item.dig('customer_name') }.join(' / '),
139
+ 'subtotal' => fee.dig('sales_fee', 'fee'),
140
+ 'tax' => fee.dig('sales_fee', 'tax'),
141
+ 'due' => fee.dig('due_date'),
142
+ 'mail' => fee.dig('status')&.select { |a| a.keys.include?('mail_delivered') }&.first
143
+ }
144
+ end
145
+ stat['issue_date'] = scan_date.to_s
146
+ stat['count'] = stat['records'].count
147
+ stat['total'] = stat['records'].inject(0) { |sum, rec| sum + rec.dig('subtotal') }
148
+ stat['tax'] = stat['records'].inject(0) { |sum, rec| sum + rec.dig('tax') }
149
+ collection << readable(stat)
150
+ end
151
+ end
152
+ end
153
+ end
154
+
155
+ def export_json
156
+ labels = export_labels
157
+ [].tap do |res|
158
+ self.class.asof(@date.year, @date.month) do |dat|
159
+ item = {}
160
+ item['date'] = dat['issue_date']
161
+ item['debit'] = []
162
+ item['credit'] = []
163
+ sub = dat['sales_fee']
164
+ if readable(sub['fee']) != 0
165
+ item['debit'] << { 'label' => labels[:debit][:fee], 'amount' => readable(sub['fee']) }
166
+ item['credit'] << { 'label' => labels[:credit][:fee], 'amount' => readable(sub['fee']) }
167
+ end
168
+ if readable(sub['tax']) != 0
169
+ item['debit'] << { 'label' => labels[:debit][:tax], 'amount' => readable(sub['tax']) }
170
+ item['credit'] << { 'label' => labels[:credit][:tax], 'amount' => readable(sub['tax']) }
171
+ end
172
+ if readable(sub['deduction']) != 0
173
+ item['debit'] << { 'label' => labels[:debit][:deduction], 'amount' => readable(sub['deduction'] * -1) }
174
+ item['credit'] << { 'label' => sub['deduction_label'] || labels[:credit][:deduction], 'amount' => readable(sub['deduction'] * -1) }
175
+ end
176
+ item['x-customer'] = dat['customer']['name'] if dat.dig('customer', 'name')
177
+ item['x-editor'] = 'LucaDeal'
178
+ res << item
179
+ end
180
+ puts JSON.dump(res)
181
+ end
182
+ end
183
+
96
184
  def render_report(file_type = :html)
97
185
  case file_type
98
186
  when :html
@@ -127,13 +215,32 @@ module LucaDeal
127
215
  @sales_fee = readable(fee_dat['sales_fee'])
128
216
  @issue_date = fee_dat['issue_date']
129
217
  @due_date = fee_dat['due_date']
130
- @amount = readable(fee_dat['sales_fee'].inject(0) { |sum, (_k, v)| sum + v })
218
+ @amount = readable(fee_dat['sales_fee']
219
+ .reject{ |k, _v| k == 'deduction_label' }
220
+ .inject(0) { |sum, (_k, v)| sum + v })
131
221
  end
132
222
 
133
223
  def lib_path
134
224
  __dir__
135
225
  end
136
226
 
227
+ # TODO: load labels from CONFIG before country defaults
228
+ #
229
+ def export_labels
230
+ case CONFIG['country']
231
+ when 'jp'
232
+ {
233
+ debit: { fee: '支払手数料', tax: '支払手数料', deduction: '未払費用' },
234
+ credit: { fee: '未払費用', tax: '未払費用', deduction: '雑収入' }
235
+ }
236
+ else
237
+ {
238
+ debit: { fee: 'Fees and commisions', tax: 'Fees and commisions', deduction: 'Accounts payable - other' },
239
+ credit: { fee: 'Accounts payable - other', tax: 'Accounts payable - other', deduction: 'Miscellaneous income' }
240
+ }
241
+ end
242
+ end
243
+
137
244
  # load user company profile from config.
138
245
  #
139
246
  def set_company
@@ -155,6 +262,11 @@ module LucaDeal
155
262
  end
156
263
  end
157
264
 
265
+ def attachment_name(dat, type)
266
+ id = %r{/}.match(dat['id']) ? dat['id'].gsub('/', '') : dat['id'][0, 7]
267
+ "feereport-#{id}.#{type}"
268
+ end
269
+
158
270
  def issue_date(date)
159
271
  base = date.nil? ? Date.today : Date.parse(date)
160
272
  Date.new(base.year, base.month, -1)
@@ -174,11 +286,32 @@ module LucaDeal
174
286
  BigDecimal(take_current(CONFIG['tax_rate'], name).to_s)
175
287
  end
176
288
 
289
+ # Fees are unique contract_id in each month
290
+ # If update needed, remove the target fee file.
291
+ #
177
292
  def duplicated_contract?(id)
178
293
  self.class.asof(@date.year, @date.month, @date.day) do |_f, path|
179
294
  return true if path.include?(id)
180
295
  end
181
296
  false
182
297
  end
298
+
299
+ def fee_record(invoice, item, rate)
300
+ {
301
+ 'invoice_id' => invoice['id'],
302
+ 'customer_name' => invoice.dig('customer', 'name'),
303
+ 'name' => item['name'],
304
+ 'price' => item['price'],
305
+ 'qty' => item['qty'],
306
+ 'fee' => item['price'] * item['qty'] * rate
307
+ }
308
+ end
309
+
310
+ def exceed_limit?(invoice, limit)
311
+ return false if limit.nil?
312
+
313
+ contract_start = Contract.find(invoice['contract_id']).dig('terms', 'effective')
314
+ contract_start.next_month(limit).prev_day < @date
315
+ end
183
316
  end
184
317
  end
@@ -20,21 +20,23 @@ module LucaDeal
20
20
  end
21
21
 
22
22
  def deliver_mail(attachment_type = nil, mode: nil)
23
- attachment_type = CONFIG.dig('invoice', 'attachment') || :html
24
23
  invoices = self.class.asof(@date.year, @date.month)
25
24
  raise "No invoice for #{@date.year}/#{@date.month}" if invoices.count.zero?
26
25
 
27
26
  invoices.each do |dat, path|
28
27
  next if has_status?(dat, 'mail_delivered')
29
28
 
30
- mail = compose_mail(dat, mode: mode, attachment: attachment_type.to_sym)
31
- LucaSupport::Mail.new(mail, PJDIR).deliver
32
- self.class.add_status!(path, 'mail_delivered')
29
+ deliver_one(dat, path, mode: mode, attachment_type: attachment_type)
33
30
  end
34
31
  end
35
32
 
36
33
  def preview_mail(attachment_type = nil)
37
- deliver_mail(attachment_type, mode: :preview)
34
+ invoices = self.class.asof(@date.year, @date.month)
35
+ raise "No invoice for #{@date.year}/#{@date.month}" if invoices.count.zero?
36
+
37
+ invoices.each do |dat, path|
38
+ deliver_one(dat, path, mode: :preview, attachment_type: attachment_type)
39
+ end
38
40
  end
39
41
 
40
42
  # Render HTML to console
@@ -116,7 +118,37 @@ module LucaDeal
116
118
  end
117
119
  end
118
120
 
121
+ # send payment list to preview address or from address.
122
+ #
123
+ def stats_email
124
+ {}.tap do |res|
125
+ stats(3).each.with_index(1) do |stat, i|
126
+ stat['records'].each do |record|
127
+ res[record['customer']] ||= {}
128
+ res[record['customer']]['customer_name'] ||= record['customer']
129
+ res[record['customer']]["amount#{i}"] ||= record['subtotal']
130
+ res[record['customer']]["tax#{i}"] ||= record['tax']
131
+ end
132
+ if i == 1
133
+ @issue_date = stat['issue_date']
134
+ @total_amount = stat['total']
135
+ @total_tax = stat['tax']
136
+ @total_count = stat['count']
137
+ end
138
+ end
139
+ @invoices = res.values
140
+ end
141
+ @company = CONFIG.dig('company', 'name')
142
+
143
+ mail = Mail.new
144
+ mail.to = CONFIG.dig('mail', 'preview') || CONFIG.dig('mail', 'from')
145
+ mail.subject = 'Check monthly payment list'
146
+ mail.html_part = Mail::Part.new(body: render_erb(search_template('monthly-payment-list.html.erb')), content_type: 'text/html; charset=UTF-8')
147
+ LucaSupport::Mail.new(mail, PJDIR).deliver
148
+ end
149
+
119
150
  def export_json
151
+ labels = export_labels
120
152
  [].tap do |res|
121
153
  self.class.asof(@date.year, @date.month) do |dat|
122
154
  item = {}
@@ -124,10 +156,14 @@ module LucaDeal
124
156
  item['debit'] = []
125
157
  item['credit'] = []
126
158
  dat['subtotal'].map do |sub|
127
- item['debit'] << { 'label' => '売掛金', 'value' => readable(sub['items']) }
128
- item['debit'] << { 'label' => '売掛金', 'value' => readable(sub['tax']) }
129
- item['credit'] << { 'label' => '売上高', 'value' => readable(sub['items']) }
130
- item['credit'] << { 'label' => '売上高', 'value' => readable(sub['tax']) }
159
+ if readable(sub['items']) != 0
160
+ item['debit'] << { 'label' => labels[:debit][:items], 'amount' => readable(sub['items']) }
161
+ item['credit'] << { 'label' => labels[:credit][:items], 'amount' => readable(sub['items']) }
162
+ end
163
+ if readable(sub['tax']) != 0
164
+ item['debit'] << { 'label' => labels[:debit][:tax], 'amount' => readable(sub['tax']) }
165
+ item['credit'] << { 'label' => labels[:credit][:tax], 'amount' => readable(sub['tax']) }
166
+ end
131
167
  end
132
168
  item['x-customer'] = dat['customer']['name'] if dat.dig('customer', 'name')
133
169
  item['x-editor'] = 'LucaDeal'
@@ -144,9 +180,9 @@ module LucaDeal
144
180
  gen_invoice!(invoice_object(contract))
145
181
  end
146
182
 
147
- def monthly_invoice
183
+ def monthly_invoice(target = 'monthly')
148
184
  Contract.new(@date.to_s).active do |contract|
149
- next if contract.dig('terms', 'billing_cycle') != 'monthly'
185
+ next if contract.dig('terms', 'billing_cycle') != target
150
186
  # TODO: provide another I/F for force re-issue if needed
151
187
  next if duplicated_contract? contract['id']
152
188
 
@@ -228,10 +264,34 @@ module LucaDeal
228
264
  @amount = readable(invoice_dat['subtotal'].inject(0) { |sum, i| sum + i['items'] + i['tax'] })
229
265
  end
230
266
 
267
+ def deliver_one(invoice, path, mode: nil, attachment_type: nil)
268
+ attachment_type ||= CONFIG.dig('invoice', 'attachment') || :html
269
+ mail = compose_mail(invoice, mode: mode, attachment: attachment_type.to_sym)
270
+ LucaSupport::Mail.new(mail, PJDIR).deliver
271
+ self.class.add_status!(path, 'mail_delivered') if mode.nil?
272
+ end
273
+
231
274
  def lib_path
232
275
  __dir__
233
276
  end
234
277
 
278
+ # TODO: load labels from CONFIG before country defaults
279
+ #
280
+ def export_labels
281
+ case CONFIG['country']
282
+ when 'jp'
283
+ {
284
+ debit: { items: '売掛金', tax: '売掛金' },
285
+ credit: { items: '売上高', tax: '売上高' }
286
+ }
287
+ else
288
+ {
289
+ debit: { items: 'Accounts receivable - trade', tax: 'Accounts receivable - trade' },
290
+ credit: { items: 'Amount of Sales', tax: 'Amount of Sales' }
291
+ }
292
+ end
293
+ end
294
+
235
295
  # load user company profile from config.
236
296
  #
237
297
  def set_company
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'luca_deal/invoice'
4
+
5
+ module LucaDeal #:nodoc:
6
+ # Invoice compatible transactions for other payment methods.
7
+ #
8
+ class NoInvoice < Invoice
9
+ @dirname = 'no_invoices'
10
+
11
+ def monthly_invoice
12
+ super('other_payments')
13
+ end
14
+
15
+ # Override not to send mail to customer.
16
+ #
17
+ def deliver_mail(attachment_type = nil, mode: nil)
18
+ end
19
+ end
20
+ end
@@ -12,7 +12,7 @@ module LucaDeal
12
12
  FileUtils.cp("#{__dir__}/templates/config.yml", 'config.yml') unless File.exist?('config.yml')
13
13
  Dir.mkdir('data') unless Dir.exist?('data')
14
14
  Dir.chdir('data') do
15
- %w[contracts customers invoices].each do |subdir|
15
+ %w[contracts customers invoices no_invoices].each do |subdir|
16
16
  Dir.mkdir(subdir) unless Dir.exist?(subdir)
17
17
  end
18
18
  end
@@ -39,9 +39,13 @@
39
39
  <td class="price" colspan="3">Tax</td>
40
40
  <td class="price"><%= delimit_num( @sales_fee['tax'] ) %></td>
41
41
  </tr>
42
+ <tr>
43
+ <td class="price" colspan="3">Deduction</td>
44
+ <td class="price"><%= delimit_num( @sales_fee['deduction'] ) %></td>
45
+ </tr>
42
46
  <tr>
43
47
  <td class="price" colspan="3">Total</td>
44
- <td class="price"><%= delimit_num( @sales_fee['fee'] + @sales_fee['tax'] ) %></td>
48
+ <td class="price"><%= delimit_num(@sales_fee['fee'] + @sales_fee['tax'] + @sales_fee['deduction']) %></td>
45
49
  </tr>
46
50
  </table>
47
51
 
@@ -0,0 +1,54 @@
1
+ <html>
2
+ <head>
3
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
4
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
5
+ <style>
6
+ td { text-align: right; line-height: 2em; min-width: 6em }
7
+ thead th, thead td { text-align: center }
8
+ thead { border-bottom: solid 1px #aaa }
9
+ tr#total { border-top: solid 1px #aaa }
10
+ tr.sub { font-size: .8em; color: #aaa }
11
+ .past { color: #777 }
12
+ </style>
13
+ </head>
14
+ <body>
15
+ <div style="margin: 1em 0"><%= @company %></div>
16
+ <div style="margin: 1em 0">Issue date: <%= @issue_date %></div>
17
+ <table>
18
+ <thead>
19
+ <tr>
20
+ <th>#</th>
21
+ <th>Customer</th>
22
+ <th>This month</th>
23
+ <th>Last Month</th>
24
+ <th>2 Month ago</th>
25
+ </tr>
26
+ <tr class="sub">
27
+ <th></th>
28
+ <th></th>
29
+ <th>Amount / Tax</th>
30
+ <th class="past">Amount / Tax</th>
31
+ <th class="past">Amount / Tax</th>
32
+ </tr>
33
+ </thead>
34
+ <tbody>
35
+ <% @invoices.each.with_index(1) do |invoice, i| %>
36
+ <tr>
37
+ <th><%= i %></th>
38
+ <td><%= invoice["customer_name"] %></td>
39
+ <td><%= delimit_num(invoice["amount1"]) %><br /><%= delimit_num(invoice["tax1"]) %></td>
40
+ <td class="past"><%= delimit_num(invoice["amount2"]) %><br /><%= delimit_num(invoice["tax2"]) %></td>
41
+ <td class="past"><%= delimit_num(invoice["amount3"]) %><br /><%= delimit_num(invoice["tax3"]) %></td>
42
+ </tr>
43
+ <% end %>
44
+ <tr id="total">
45
+ <td></td>
46
+ <td>Total (<%= @total_count %> records)</td>
47
+ <td><%= delimit_num(@total_amount) %><br /><%= delimit_num(@total_tax) %></td>
48
+ <td></td>
49
+ <td></td>
50
+ </tr>
51
+ </tbody>
52
+ </table>
53
+ </body>
54
+ </html>
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LucaDeal
4
- VERSION = '0.2.21'
4
+ VERSION = '0.2.25'
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.21
4
+ version: 0.2.25
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-12-01 00:00:00.000000000 Z
11
+ date: 2021-07-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lucarecord
@@ -85,6 +85,7 @@ files:
85
85
  - lib/luca_deal/customer.rb
86
86
  - lib/luca_deal/fee.rb
87
87
  - lib/luca_deal/invoice.rb
88
+ - lib/luca_deal/no_invoice.rb
88
89
  - lib/luca_deal/product.rb
89
90
  - lib/luca_deal/setup.rb
90
91
  - lib/luca_deal/templates/.keep
@@ -93,6 +94,7 @@ files:
93
94
  - lib/luca_deal/templates/fee-report.html.erb
94
95
  - lib/luca_deal/templates/invoice-mail.txt.erb
95
96
  - lib/luca_deal/templates/invoice.html.erb
97
+ - lib/luca_deal/templates/monthly-payment-list.html.erb
96
98
  - lib/luca_deal/version.rb
97
99
  homepage: https://github.com/chumaltd/luca/tree/master/lucadeal
98
100
  licenses:
@@ -116,7 +118,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
116
118
  - !ruby/object:Gem::Version
117
119
  version: '0'
118
120
  requirements: []
119
- rubygems_version: 3.1.2
121
+ rubygems_version: 3.2.3
120
122
  signing_key:
121
123
  specification_version: 4
122
124
  summary: Deal with contracts