lucadeal 0.2.21 → 0.2.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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