lucadeal 0.2.24 → 0.3.1

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: 0e4fa52c476838906c3ad849ab1bc4a9bf58926a526034553a90d6c6299d7945
4
- data.tar.gz: c3e434b8677a0b502cfbef70dc8d936bbc2dd3d7f0be0b6b1e2edea8294baa2c
3
+ metadata.gz: 599780f0a61b27bcea93724823c02bdfdd52b7b04a028265c00bc2fe1e0e865c
4
+ data.tar.gz: 7b3a09097cb6543b3b07718c7ecb35fd21bcdafbe851eb6240a33707a2e7ddf3
5
5
  SHA512:
6
- metadata.gz: ae66f02ef1861eeb9bf217dc5547b05bdd384a9d25bddd8d12dfd62ab2fea63e3f051c699dc267620488949659c98e772799d027dd27101b2d214b8517b55ba2
7
- data.tar.gz: 5ec46b9f38cb9bfda178896e2319d728cd41df86d6fb389342a4e9759351e64608abe33980a7cf35e5a8f25459223521c9a69bf356dc61fad55d8cb83588cdda
6
+ metadata.gz: 2a8c83638b3ee0d8d9afb02bb555ee4bc679b89873b677a5f75751a9d75412479e6900be700f06dc6d44473d9a3149a11d173e5bb0b39ca45aa8055da22da120
7
+ data.tar.gz: 4012470b9d9a1e671f1d0d4bcfd7144b4c4e097f282f099c518396706e946f313d22b35955c0999acf40753e284a0c9b48d7fec6d514492e477ee082e5623785
data/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ ## LucaDeal 0.3.1
2
+
3
+ * add `luca-deal invoices settle --search-terms` for late payment case.
4
+
5
+ ## LucaDeal 0.3.0
6
+
7
+ * implement `luca-deal reports balance` for unsettled balance by customer
8
+ * implement `luca-deal invoices settle` for import payment data from LucaBook
9
+
10
+ ## LucaDeal 0.2.25
11
+
12
+ * implement deduction rate for fee calculation.
13
+ * implement `luca-deal fee export`
14
+ * refine export label for luca-book compatibility
15
+ * add `luca-deal invoice create --monthly --with-fee` option.
16
+ * preview_mail can deliver regardless of `mail_delivered` status
17
+ * `luca-deal fee mail` skip no item record by default.
18
+
1
19
  ## LucaDeal 0.2.24
2
20
 
3
21
  * add `luca-deal invoices create --monthly --mail`, send payment list after monthly invoice creation.
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # LucaDeal
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/lucadeal.svg)](https://badge.fury.io/rb/lucadeal)
4
+ [![doc](https://img.shields.io/badge/doc-rubydoc-green.svg)](https://www.rubydoc.info/gems/lucadeal/index)
5
+ ![license](https://img.shields.io/github/license/chumaltd/luca)
4
6
 
5
7
  LucaDeal is Sales contract management application.
6
8
 
@@ -10,6 +12,7 @@ Add this line to your application's Gemfile:
10
12
 
11
13
  ```ruby
12
14
  gem 'lucadeal'
15
+ gem 'mail' # If you don't use mail functionality, you can remove this line.
13
16
  ```
14
17
 
15
18
  And then execute:
@@ -155,9 +158,11 @@ Fields for sales fee are as bellows:
155
158
  | terms | | | | |
156
159
  | | category | | | If 'sales_fee', contract is treated as selling commission. |
157
160
  | | limit | | | If set, fees are calculated as mas as `limit` months. |
161
+ | | deduction_label | | | Label for deduction. Used on export |
158
162
  | rate | | optional | | |
159
163
  | | default | | | sales fee rate. |
160
164
  | | initial | | | sales fee rate for items of type=initial. |
165
+ | | deduction | | | deduction rate(if any) multiplied by fee |
161
166
 
162
167
 
163
168
  ### Invoice
@@ -165,7 +170,7 @@ Fields for sales fee are as bellows:
165
170
  Invoice is basically auto generated from Customer and Contract objects.
166
171
 
167
172
  | Top level | Second level | Description |
168
- |------------|--------------|------------------------------------------|
173
+ |------------+--------------+------------------------------------------|
169
174
  | id | | uuid |
170
175
  | issue_date | | |
171
176
  | due_date | | |
@@ -181,6 +186,10 @@ Invoice is basically auto generated from Customer and Contract objects.
181
186
  | | qty | quantity. Default: 1. |
182
187
  | | type | |
183
188
  | | product_id | refrence for Product |
189
+ | settled | | |
190
+ | | id | data source id for duplication check |
191
+ | | date | payment date |
192
+ | | amount | payment amount |
184
193
  | subtotal | | Array of subtotal by tax category. |
185
194
  | | items | amount of items |
186
195
  | | tax | amount of tax |
data/exe/luca-deal CHANGED
@@ -124,11 +124,12 @@ 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
131
  LucaDeal::NoInvoice.new(date).monthly_invoice
132
+ LucaDeal::Fee.new(date).monthly_fee if params[:fee]
132
133
  if params[:mail]
133
134
  LucaDeal::Invoice.new(date).stats_email
134
135
  end
@@ -185,6 +186,11 @@ class LucaCmd
185
186
  end
186
187
  end
187
188
 
189
+ def self.report(args = nil, params = {})
190
+ date = Date.new(args[0].to_i, args[1].to_i, 1)
191
+ render(LucaDeal::Invoice.report(date, detail: params[:detail], due: params[:due]), params)
192
+ end
193
+
188
194
  def self.mail(args = nil, params = {})
189
195
  date = "#{args[0]}-#{args[1]}-#{args[2] || '1'}" if !args.empty?
190
196
  case params['mode']
@@ -194,11 +200,16 @@ class LucaCmd
194
200
  LucaDeal::Invoice.new(date).deliver_mail
195
201
  end
196
202
  end
203
+
204
+ def self.settle(args = nil, params = nil)
205
+ str = args[0].nil? ? STDIN.read : File.read(args[0])
206
+ LucaDeal::Invoice.settle(str, params[:term])
207
+ end
197
208
  end
198
209
 
199
210
  class Fee < LucaCmd
200
211
  def self.create(args = nil, params = {})
201
- case params['mode']
212
+ case params[:mode]
202
213
  when 'monthly'
203
214
  date = "#{args[0]}-#{args[1]}-#{args[2] || '1'}" if !args.empty?
204
215
  LucaDeal::Fee.new(date).monthly_fee
@@ -217,6 +228,15 @@ class LucaCmd
217
228
  end
218
229
  end
219
230
 
231
+ def self.export(args = nil, _params = nil)
232
+ if args
233
+ args << 28 if args.length == 2 # specify safe last day
234
+ LucaDeal::Fee.new(args.join('-')).export_json
235
+ else
236
+ LucaDeal::Fee.new.export_json
237
+ end
238
+ end
239
+
220
240
  def self.list(args = nil, params = {})
221
241
  date = "#{args[0]}-#{args[1]}-#{args[2] || '1'}" if !args.empty?
222
242
  if args.empty?
@@ -247,6 +267,12 @@ class LucaCmd
247
267
  puts JSON.dump(dat)
248
268
  when 'nu'
249
269
  LucaSupport::View.nushell(YAML.dump(dat))
270
+ when 'csv'
271
+ str = CSV.generate(String.new, col_sep: "\t") do |row|
272
+ row << dat.first.keys
273
+ dat.each { |d| row << d.values }
274
+ end
275
+ puts str
250
276
  else
251
277
  puts YAML.dump(dat)
252
278
  end
@@ -338,8 +364,9 @@ when /invoices?/, 'i'
338
364
  when 'create'
339
365
  OptionParser.new do |opt|
340
366
  opt.banner = 'Usage: luca-deal invoices create [options] --monthly|contract_id year month [date]'
341
- opt.on('--monthly', 'generate monthly data') { |_v| params['mode'] = 'monthly' }
342
367
  opt.on('--mail', 'send payment list by email. Only works with --monthly') { |_v| params[:mail] = true }
368
+ opt.on('--monthly', 'generate monthly data') { |_v| params[:mode] = 'monthly' }
369
+ opt.on('--with-fee', 'generate sales fee data after monthly invoice creation') { |_v| params[:fee] = true }
343
370
  args = opt.parse(ARGV)
344
371
  LucaCmd::Invoice.create(args, params)
345
372
  end
@@ -362,6 +389,14 @@ when /invoices?/, 'i'
362
389
  args = opt.parse(ARGV)
363
390
  LucaCmd::Invoice.mail(args, params)
364
391
  end
392
+ when 'settle'
393
+ params[:term] = 1
394
+ OptionParser.new do |opt|
395
+ opt.banner = 'Usage: luca-deal invoices settle [filepath]'
396
+ opt.on('--search-term VAL', 'search invoice N months before payment date. default: 1') { |v| params[:term] = v.to_i }
397
+ args = opt.parse(ARGV)
398
+ LucaCmd::Invoice.settle(args, params)
399
+ end
365
400
  else
366
401
  puts 'Proper subcommand needed.'
367
402
  puts
@@ -370,6 +405,29 @@ when /invoices?/, 'i'
370
405
  puts ' delete'
371
406
  puts ' list'
372
407
  puts ' mail: send mail with invoice'
408
+ puts ' settle'
409
+ exit 1
410
+ end
411
+ when /reports?/, 'r'
412
+ subcmd = ARGV.shift
413
+ case subcmd
414
+ when 'balance'
415
+ params[:detail] = false
416
+ params[:due] = false
417
+ OptionParser.new do |opt|
418
+ opt.banner = 'Usage: luca-deal r[eports] balance [options] [year month]'
419
+ opt.on('--nu', 'show table in nushell') { |_v| params[:output] = 'nu' }
420
+ opt.on('-o', '--output VAL', 'output serialized data') { |v| params[:output] = v }
421
+ opt.on('--detail', 'show detail info') { |_v| params[:detail] = true }
422
+ opt.on('--force-due', 'respect due date over actual payment') { |_v| params[:due] = true }
423
+ args = opt.parse(ARGV)
424
+ LucaCmd::Invoice.report(args, params)
425
+ end
426
+ else
427
+ puts 'Proper subcommand needed.'
428
+ puts
429
+ puts 'Usage: luca-deal r[eports] subcommand [--help|options]'
430
+ puts ' balance'
373
431
  exit 1
374
432
  end
375
433
  when 'new'
@@ -385,12 +443,14 @@ when /fee/
385
443
  when 'create'
386
444
  OptionParser.new do |opt|
387
445
  opt.banner = 'Usage: luca-deal fee create [options] year month [date]'
388
- opt.on('--monthly', 'generate monthly data') { |_v| params['mode'] = 'monthly' }
446
+ opt.on('--monthly', 'generate monthly data') { |_v| params[:mode] = 'monthly' }
389
447
  args = opt.parse(ARGV)
390
448
  LucaCmd::Fee.create(args, params)
391
449
  end
392
450
  when 'delete'
393
451
  LucaCmd::Fee.delete(ARGV)
452
+ when 'export'
453
+ LucaCmd::Fee.export(ARGV)
394
454
  when 'list'
395
455
  OptionParser.new do |opt|
396
456
  opt.banner = 'Usage: luca-deal fee list [options] year month [date]'
@@ -423,7 +483,9 @@ else
423
483
  puts 'Usage: luca-deal subcommand [options]'
424
484
  puts ' customers'
425
485
  puts ' contracts'
426
- puts ' invoices'
486
+ puts ' i[nvoices]'
487
+ puts ' fee'
488
+ puts ' r[eports]'
427
489
  puts ' new: initialize project dir'
428
490
  puts ' export: puts invoice data for LucaBook import'
429
491
  exit 1
@@ -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'
@@ -27,7 +28,16 @@ module LucaDeal
27
28
  @rate['initial'] = contract.dig('rate', 'initial') ? BigDecimal(contract.dig('rate', 'initial')) : @rate['default']
28
29
  limit = contract.dig('terms', 'limit')
29
30
 
30
- 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
+ }
31
41
  fee['customer'] = get_customer(contract['customer_id'])
32
42
  fee['issue_date'] = @date
33
43
  Invoice.asof(@date.year, @date.month) do |invoice|
@@ -38,33 +48,36 @@ module LucaDeal
38
48
  rate = item['type'] == 'initial' ? @rate['initial'] : @rate['default']
39
49
  fee['items'] << fee_record(invoice, item, rate)
40
50
  end
41
- fee['sales_fee'] = subtotal(fee['items'])
51
+ subtotal(fee['items']).each{ |k, v| fee['sales_fee'][k] += v }
42
52
  end
43
53
  NoInvoice.asof(@date.year, @date.month) do |no_invoice|
44
54
  next if no_invoice.dig('sales_fee', 'id') != contract['id']
45
- next if exceed_limit?(invoice, limit)
55
+ next if exceed_limit?(no_invoice, limit)
46
56
 
47
57
  no_invoice['items'].each do |item|
48
58
  rate = item['type'] == 'initial' ? @rate['initial'] : @rate['default']
49
59
  fee['items'] << fee_record(no_invoice, item, rate)
50
60
  end
51
- fee['sales_fee'] = subtotal(fee['items'])
61
+ subtotal(fee['items']).each{ |k, v| fee['sales_fee'][k] += v }
52
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
53
65
  self.class.create(fee, date: @date, codes: Array(contract['id']))
54
66
  end
55
67
  end
56
68
 
57
- def deliver_mail(attachment_type = nil, mode: nil)
69
+ def deliver_mail(attachment_type = nil, mode: nil, skip_no_item: true)
58
70
  attachment_type = CONFIG.dig('fee', 'attachment') || :html
59
71
  fees = self.class.asof(@date.year, @date.month)
60
72
  raise "No report for #{@date.year}/#{@date.month}" if fees.count.zero?
61
73
 
62
74
  fees.each do |dat, path|
63
75
  next if has_status?(dat, 'mail_delivered')
76
+ next if skip_no_item && dat['items'].empty?
64
77
 
65
78
  mail = compose_mail(dat, mode: mode, attachment: attachment_type.to_sym)
66
79
  LucaSupport::Mail.new(mail, PJDIR).deliver
67
- self.class.add_status!(path, 'mail_delivered')
80
+ self.class.add_status!(path, 'mail_delivered') if mode.nil?
68
81
  end
69
82
  end
70
83
 
@@ -88,7 +101,7 @@ module LucaDeal
88
101
 
89
102
  mail = Mail.new
90
103
  mail.to = dat.dig('customer', 'to') if mode.nil?
91
- mail.subject = CONFIG.dig('invoice', 'mail_subject') || 'Your Report is available'
104
+ mail.subject = CONFIG.dig('fee', 'mail_subject') || 'Your Report is available'
92
105
  if mode == :preview
93
106
  mail.cc = CONFIG.dig('mail', 'preview') || CONFIG.dig('mail', 'from')
94
107
  mail.subject = '[preview] ' + mail.subject
@@ -139,6 +152,35 @@ module LucaDeal
139
152
  end
140
153
  end
141
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
+
142
184
  def render_report(file_type = :html)
143
185
  case file_type
144
186
  when :html
@@ -173,13 +215,32 @@ module LucaDeal
173
215
  @sales_fee = readable(fee_dat['sales_fee'])
174
216
  @issue_date = fee_dat['issue_date']
175
217
  @due_date = fee_dat['due_date']
176
- @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 })
177
221
  end
178
222
 
179
223
  def lib_path
180
224
  __dir__
181
225
  end
182
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
+
183
244
  # load user company profile from config.
184
245
  #
185
246
  def set_company
@@ -201,6 +262,11 @@ module LucaDeal
201
262
  end
202
263
  end
203
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
+
204
270
  def issue_date(date)
205
271
  base = date.nil? ? Date.today : Date.parse(date)
206
272
  Date.new(base.year, base.month, -1)
@@ -5,6 +5,7 @@ require 'json'
5
5
  require 'yaml'
6
6
  require 'pathname'
7
7
  require 'bigdecimal'
8
+ require 'luca_support/code'
8
9
  require 'luca_support/config'
9
10
  require 'luca_support/mail'
10
11
  require 'luca_deal/contract'
@@ -20,21 +21,23 @@ module LucaDeal
20
21
  end
21
22
 
22
23
  def deliver_mail(attachment_type = nil, mode: nil)
23
- attachment_type = CONFIG.dig('invoice', 'attachment') || :html
24
24
  invoices = self.class.asof(@date.year, @date.month)
25
25
  raise "No invoice for #{@date.year}/#{@date.month}" if invoices.count.zero?
26
26
 
27
27
  invoices.each do |dat, path|
28
28
  next if has_status?(dat, 'mail_delivered')
29
29
 
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')
30
+ deliver_one(dat, path, mode: mode, attachment_type: attachment_type)
33
31
  end
34
32
  end
35
33
 
36
34
  def preview_mail(attachment_type = nil)
37
- deliver_mail(attachment_type, mode: :preview)
35
+ invoices = self.class.asof(@date.year, @date.month)
36
+ raise "No invoice for #{@date.year}/#{@date.month}" if invoices.count.zero?
37
+
38
+ invoices.each do |dat, path|
39
+ deliver_one(dat, path, mode: :preview, attachment_type: attachment_type)
40
+ end
38
41
  end
39
42
 
40
43
  # Render HTML to console
@@ -74,6 +77,113 @@ module LucaDeal
74
77
  end
75
78
  end
76
79
 
80
+ def self.report(date, scan_years = 10, detail: false, due: false)
81
+ fy_end = Date.new(date.year, date.month, -1)
82
+ if detail
83
+ customers = {}.tap do |h|
84
+ Customer.all.each { |c| h[c['name']] = c }
85
+ end
86
+ end
87
+ [].tap do |res|
88
+ items = {}
89
+ head = date.prev_year(scan_years)
90
+ e = Enumerator.new do |yielder|
91
+ while head <= date
92
+ yielder << head
93
+ head = head.next_month
94
+ end
95
+ end
96
+ e.each do |d|
97
+ asof(d.year, d.month).map do |invoice|
98
+ if invoice['settled']
99
+ next if !due
100
+ settle_date = invoice['settled']['date'].class.name == "String" ? Date.parse(invoice['settled']['date']) : invoice['settled']['date']
101
+ next if (settle_date && settle_date <= fy_end)
102
+ end
103
+
104
+ customer = invoice.dig('customer', 'name')
105
+ items[customer] ||= { 'unsettled' => BigDecimal('0'), 'invoices' => [] }
106
+ items[customer]['unsettled'] += (invoice.dig('subtotal', 0, 'items') + invoice.dig('subtotal', 0, 'tax')||0)
107
+ items[customer]['invoices'] << invoice
108
+ end
109
+ end
110
+ items.each do |k, item|
111
+ row = {
112
+ 'customer' => k,
113
+ 'unsettled' => LucaSupport::Code.readable(item['unsettled']),
114
+ }
115
+ if detail
116
+ row['address'] = %Q(#{customers.dig(k, 'address')}#{customers.dig(k, 'address2')})
117
+ row['invoices'] = item['invoices'].map{ |i| { 'id' => i['id'], 'issue' => i['issue_date'].to_s } }
118
+ end
119
+ res << row
120
+ end
121
+ res.sort! { |a, b| b['unsettled'] <=> a['unsettled'] }
122
+ end
123
+ end
124
+
125
+ # === JSON Format:
126
+ # [
127
+ # {
128
+ # "journals" : [
129
+ # {
130
+ # "id": "2021A/U001",
131
+ # "header": "customer name",
132
+ # "diff": -20000
133
+ # }
134
+ # ]
135
+ # }
136
+ # ]
137
+ #
138
+ def self.settle(io, payment_terms = 1)
139
+ customers = {}.tap do |h|
140
+ Customer.all.each { |c| h[c['name']] = c }
141
+ end
142
+ contracts = {}.tap do |h|
143
+ Contract.all.each { |c| h[c['customer_id']] ||= []; h[c['customer_id']] << c }
144
+ end
145
+ JSON.parse(io).each do |d|
146
+ next if d['journals'].nil?
147
+
148
+ d['journals'].each do |j|
149
+ next if j['diff'] >= 0
150
+
151
+ if j['header'] == 'others'
152
+ STDERR.puts "#{j['id']}: no customer header found. skip"
153
+ next
154
+ end
155
+
156
+ ord = customers.map do |k, v|
157
+ [v, LucaSupport::Code.match_score(j['header'], k, 2)]
158
+ end
159
+ customer = ord.max { |x, y| x[1] <=> y[1] }.dig(0, 'id')
160
+
161
+ if customer
162
+ contract = contracts[customer].length == 1 ? contracts.dig(customer, 0, 'id') : nil
163
+ date = Date.parse(j['date'])
164
+ invoices = term(date.prev_month(payment_terms).year, date.prev_month(payment_terms).month, date.year, date.month, contract)
165
+ invoices.each do |invoice, _path|
166
+ next if invoice['customer']['id'] != customer
167
+ next if invoice['issue_date'] > date
168
+ if Regexp.new("^LucaBook/#{j['id']}").match invoice.dig('settled', 'id')||''
169
+ break
170
+ end
171
+
172
+ invoice['settled'] = {
173
+ 'id' => "LucaBook/#{j['id']}",
174
+ 'date' => j['date'],
175
+ 'amount' => j['diff']
176
+ }
177
+ save(invoice)
178
+ break
179
+ end
180
+ else
181
+ STDERR.puts "#{j['id']}: no customer found"
182
+ end
183
+ end
184
+ end
185
+ end
186
+
77
187
  # Output seriarized invoice data to stdout.
78
188
  # Returns previous N months on multiple count
79
189
  #
@@ -146,6 +256,7 @@ module LucaDeal
146
256
  end
147
257
 
148
258
  def export_json
259
+ labels = export_labels
149
260
  [].tap do |res|
150
261
  self.class.asof(@date.year, @date.month) do |dat|
151
262
  item = {}
@@ -153,10 +264,14 @@ module LucaDeal
153
264
  item['debit'] = []
154
265
  item['credit'] = []
155
266
  dat['subtotal'].map do |sub|
156
- item['debit'] << { 'label' => '売掛金', 'amount' => readable(sub['items']) }
157
- item['debit'] << { 'label' => '売掛金', 'amount' => readable(sub['tax']) }
158
- item['credit'] << { 'label' => '売上高', 'amount' => readable(sub['items']) }
159
- item['credit'] << { 'label' => '売上高', 'amount' => readable(sub['tax']) }
267
+ if readable(sub['items']) != 0
268
+ item['debit'] << { 'label' => labels[:debit][:items], 'amount' => readable(sub['items']) }
269
+ item['credit'] << { 'label' => labels[:credit][:items], 'amount' => readable(sub['items']) }
270
+ end
271
+ if readable(sub['tax']) != 0
272
+ item['debit'] << { 'label' => labels[:debit][:tax], 'amount' => readable(sub['tax']) }
273
+ item['credit'] << { 'label' => labels[:credit][:tax], 'amount' => readable(sub['tax']) }
274
+ end
160
275
  end
161
276
  item['x-customer'] = dat['customer']['name'] if dat.dig('customer', 'name')
162
277
  item['x-editor'] = 'LucaDeal'
@@ -257,10 +372,34 @@ module LucaDeal
257
372
  @amount = readable(invoice_dat['subtotal'].inject(0) { |sum, i| sum + i['items'] + i['tax'] })
258
373
  end
259
374
 
375
+ def deliver_one(invoice, path, mode: nil, attachment_type: nil)
376
+ attachment_type ||= CONFIG.dig('invoice', 'attachment') || :html
377
+ mail = compose_mail(invoice, mode: mode, attachment: attachment_type.to_sym)
378
+ LucaSupport::Mail.new(mail, PJDIR).deliver
379
+ self.class.add_status!(path, 'mail_delivered') if mode.nil?
380
+ end
381
+
260
382
  def lib_path
261
383
  __dir__
262
384
  end
263
385
 
386
+ # TODO: load labels from CONFIG before country defaults
387
+ #
388
+ def export_labels
389
+ case CONFIG['country']
390
+ when 'jp'
391
+ {
392
+ debit: { items: '売掛金', tax: '売掛金' },
393
+ credit: { items: '売上高', tax: '売上高' }
394
+ }
395
+ else
396
+ {
397
+ debit: { items: 'Accounts receivable - trade', tax: 'Accounts receivable - trade' },
398
+ credit: { items: 'Amount of Sales', tax: 'Amount of Sales' }
399
+ }
400
+ end
401
+ end
402
+
264
403
  # load user company profile from config.
265
404
  #
266
405
  def set_company
@@ -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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LucaDeal
4
- VERSION = '0.2.24'
4
+ VERSION = '0.3.1'
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.24
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuma Takahiro
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-02-25 00:00:00.000000000 Z
11
+ date: 2022-05-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lucarecord
@@ -68,7 +68,7 @@ dependencies:
68
68
  version: 12.3.3
69
69
  description: 'Deal with contracts
70
70
 
71
- '
71
+ '
72
72
  email:
73
73
  - co.chuma@gmail.com
74
74
  executables:
@@ -118,7 +118,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
118
118
  - !ruby/object:Gem::Version
119
119
  version: '0'
120
120
  requirements: []
121
- rubygems_version: 3.2.3
121
+ rubygems_version: 3.3.5
122
122
  signing_key:
123
123
  specification_version: 4
124
124
  summary: Deal with contracts