lucadeal 0.2.25 → 0.4.0

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: 68b497e68738101eafbfe6eab73c3e0cf8d297cf4676ccfb8cd05efdadc273a3
4
- data.tar.gz: fb8ed0c2d5eb0f7621c4436ddad91ece16aa723f2861ecd661423802ec01d0c4
3
+ metadata.gz: 14852f4570ab9b7da28524ffe3c4b9fc2129e96a3f7ade9991cfe2f20c8730e1
4
+ data.tar.gz: 0a002fdffad8e94900f81ca53d130407c2dee4f03770cc7881d22f7313d20603
5
5
  SHA512:
6
- metadata.gz: 5c8941a80f26d67b4855f2757e00f8241dd0b73f37f0fe2ab8799500388118bec4513fcb398116ba8072889bedf97a083fbb2c4ce3fdd4219cb7a15844785b1c
7
- data.tar.gz: 91a84225d3d46201f85e9e38bbedbc102b2a3aaf2ccbf0a812db56bc3b502b78b7f89cb4087ec09642db5ff49b4abe58c15f6632bcbee18d471e95f3c1ff2a20
6
+ metadata.gz: 1046732b7d2256f2384186a38cd6d766d8409c7602959d864782f502b0c87729e24b1510c9662a92ea1dae2eb699edfa5e4b9ad36bd4ffd838bb9b42b1ca1ff7
7
+ data.tar.gz: 4fd222f9031b9032542794d01a81982ca38732ecf38c8dac5ef48a165034dcbcd325646a841595c2b4139d2e30c9471faf5ebeab0b8fab2d140f251e09b5151f
data/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## LucaDeal 0.4.0
2
+
3
+ * implement `luca-deal invoices print` for local HTML/PDF rendering.
4
+ * remove `luca-deal invoices list --html` in favor of `print` sub command.
5
+
6
+ ## LucaDeal 0.3.1
7
+
8
+ * add `luca-deal invoices settle --search-terms` for late payment case.
9
+
10
+ ## LucaDeal 0.3.0
11
+
12
+ * implement `luca-deal reports balance` for unsettled balance by customer
13
+ * implement `luca-deal invoices settle` for import payment data from LucaBook
14
+
1
15
  ## LucaDeal 0.2.25
2
16
 
3
17
  * implement deduction rate for fee calculation.
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:
@@ -167,7 +170,7 @@ Fields for sales fee are as bellows:
167
170
  Invoice is basically auto generated from Customer and Contract objects.
168
171
 
169
172
  | Top level | Second level | Description |
170
- |------------|--------------|------------------------------------------|
173
+ |------------+--------------+------------------------------------------|
171
174
  | id | | uuid |
172
175
  | issue_date | | |
173
176
  | due_date | | |
@@ -183,6 +186,10 @@ Invoice is basically auto generated from Customer and Contract objects.
183
186
  | | qty | quantity. Default: 1. |
184
187
  | | type | |
185
188
  | | product_id | refrence for Product |
189
+ | settled | | |
190
+ | | id | data source id for duplication check |
191
+ | | date | payment date |
192
+ | | amount | payment amount |
186
193
  | subtotal | | Array of subtotal by tax category. |
187
194
  | | items | amount of items |
188
195
  | | tax | amount of tax |
data/exe/luca-deal CHANGED
@@ -177,15 +177,18 @@ class LucaCmd
177
177
  date = "#{Date.today.year}-#{Date.today.month}-1"
178
178
  count = 3
179
179
  end
180
- if params[:html]
181
- LucaDeal::Invoice.new(date).preview_stdout
182
- elsif params[:mail]
180
+ if params[:mail]
183
181
  LucaDeal::Invoice.new(date).stats_email
184
182
  else
185
183
  render(LucaDeal::Invoice.new(date).stats(count || 1), params)
186
184
  end
187
185
  end
188
186
 
187
+ def self.report(args = nil, params = {})
188
+ date = Date.new(args[0].to_i, args[1].to_i, 1)
189
+ render(LucaDeal::Invoice.report(date, detail: params[:detail], due: params[:due]), params)
190
+ end
191
+
189
192
  def self.mail(args = nil, params = {})
190
193
  date = "#{args[0]}-#{args[1]}-#{args[2] || '1'}" if !args.empty?
191
194
  case params['mode']
@@ -195,6 +198,21 @@ class LucaCmd
195
198
  LucaDeal::Invoice.new(date).deliver_mail
196
199
  end
197
200
  end
201
+
202
+ def self.print(args = nil, params = {})
203
+ if args.length >= 2
204
+ date = "#{args[0]}-#{args[1]}-#{args[2] || '1'}" if !args.empty?
205
+ LucaDeal::Invoice.new(date).print(nil, params)
206
+ else
207
+ date = "#{Date.today.year}-#{Date.today.month}-#{Date.today.day}"
208
+ LucaDeal::Invoice.new(date).print(args[0], params)
209
+ end
210
+ end
211
+
212
+ def self.settle(args = nil, params = nil)
213
+ str = args[0].nil? ? STDIN.read : File.read(args[0])
214
+ LucaDeal::Invoice.settle(str, params[:term])
215
+ end
198
216
  end
199
217
 
200
218
  class Fee < LucaCmd
@@ -257,6 +275,12 @@ class LucaCmd
257
275
  puts JSON.dump(dat)
258
276
  when 'nu'
259
277
  LucaSupport::View.nushell(YAML.dump(dat))
278
+ when 'csv'
279
+ str = CSV.generate(String.new, col_sep: "\t") do |row|
280
+ row << dat.first.keys
281
+ dat.each { |d| row << d.values }
282
+ end
283
+ puts str
260
284
  else
261
285
  puts YAML.dump(dat)
262
286
  end
@@ -361,7 +385,6 @@ when /invoices?/, 'i'
361
385
  opt.banner = 'Usage: luca-deal invoices list [options] year month [date]'
362
386
  opt.on('--nu', 'show table in nushell') { |_v| params[:output] = 'nu' }
363
387
  opt.on('-o', '--output VAL', 'output serialized data') { |v| params[:output] = v }
364
- opt.on('--html', 'output html invoices') { |_v| params[:html] = 'monthly' }
365
388
  opt.on('--mail', 'send payment list by email') { |_v| params[:mail] = true }
366
389
  args = opt.parse(ARGV)
367
390
  LucaCmd::Invoice.list(args, params)
@@ -373,6 +396,21 @@ when /invoices?/, 'i'
373
396
  args = opt.parse(ARGV)
374
397
  LucaCmd::Invoice.mail(args, params)
375
398
  end
399
+ when 'print'
400
+ OptionParser.new do |opt|
401
+ opt.banner = 'Usage: luca-deal invoices print [options] <invoice_id | year month>'
402
+ opt.on('--pdf', 'output PDF invoices. wkhtmlpdf is required') { |_v| params[:output] = :pdf }
403
+ args = opt.parse(ARGV)
404
+ LucaCmd::Invoice.print(args, params)
405
+ end
406
+ when 'settle'
407
+ params[:term] = 1
408
+ OptionParser.new do |opt|
409
+ opt.banner = 'Usage: luca-deal invoices settle [filepath]'
410
+ opt.on('--search-term VAL', 'search invoice N months before payment date. default: 1') { |v| params[:term] = v.to_i }
411
+ args = opt.parse(ARGV)
412
+ LucaCmd::Invoice.settle(args, params)
413
+ end
376
414
  else
377
415
  puts 'Proper subcommand needed.'
378
416
  puts
@@ -381,6 +419,30 @@ when /invoices?/, 'i'
381
419
  puts ' delete'
382
420
  puts ' list'
383
421
  puts ' mail: send mail with invoice'
422
+ puts ' print: render invoices into HTML/PDF'
423
+ puts ' settle'
424
+ exit 1
425
+ end
426
+ when /reports?/, 'r'
427
+ subcmd = ARGV.shift
428
+ case subcmd
429
+ when 'balance'
430
+ params[:detail] = false
431
+ params[:due] = false
432
+ OptionParser.new do |opt|
433
+ opt.banner = 'Usage: luca-deal r[eports] balance [options] [year month]'
434
+ opt.on('--nu', 'show table in nushell') { |_v| params[:output] = 'nu' }
435
+ opt.on('-o', '--output VAL', 'output serialized data') { |v| params[:output] = v }
436
+ opt.on('--detail', 'show detail info') { |_v| params[:detail] = true }
437
+ opt.on('--force-due', 'respect due date over actual payment') { |_v| params[:due] = true }
438
+ args = opt.parse(ARGV)
439
+ LucaCmd::Invoice.report(args, params)
440
+ end
441
+ else
442
+ puts 'Proper subcommand needed.'
443
+ puts
444
+ puts 'Usage: luca-deal r[eports] subcommand [--help|options]'
445
+ puts ' balance'
384
446
  exit 1
385
447
  end
386
448
  when 'new'
@@ -436,8 +498,9 @@ else
436
498
  puts 'Usage: luca-deal subcommand [options]'
437
499
  puts ' customers'
438
500
  puts ' contracts'
439
- puts ' invoices'
501
+ puts ' i[nvoices]'
440
502
  puts ' fee'
503
+ puts ' r[eports]'
441
504
  puts ' new: initialize project dir'
442
505
  puts ' export: puts invoice data for LucaBook import'
443
506
  exit 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'
@@ -39,13 +40,26 @@ module LucaDeal
39
40
  end
40
41
  end
41
42
 
42
- # Render HTML to console
43
+ # Render HTML/PDF to files
44
+ # TODO: change output dir
43
45
  #
44
- def preview_stdout
45
- self.class.asof(@date.year, @date.month) do |dat, _|
46
+ def print(id = nil, params = {})
47
+ filetype = params[:output] || :html
48
+ if id
49
+ dat = self.class.find(id)
46
50
  @company = set_company
47
51
  invoice_vars(dat)
48
- puts render_invoice
52
+ File.open(attachment_name(dat, filetype), 'w') do |f|
53
+ f.puts render_invoice(filetype)
54
+ end
55
+ else
56
+ self.class.asof(@date.year, @date.month) do |dat, _|
57
+ @company = set_company
58
+ invoice_vars(dat)
59
+ File.open(attachment_name(dat, filetype), 'w') do |f|
60
+ f.puts render_invoice(filetype)
61
+ end
62
+ end
49
63
  end
50
64
  end
51
65
 
@@ -76,6 +90,113 @@ module LucaDeal
76
90
  end
77
91
  end
78
92
 
93
+ def self.report(date, scan_years = 10, detail: false, due: false)
94
+ fy_end = Date.new(date.year, date.month, -1)
95
+ if detail
96
+ customers = {}.tap do |h|
97
+ Customer.all.each { |c| h[c['name']] = c }
98
+ end
99
+ end
100
+ [].tap do |res|
101
+ items = {}
102
+ head = date.prev_year(scan_years)
103
+ e = Enumerator.new do |yielder|
104
+ while head <= date
105
+ yielder << head
106
+ head = head.next_month
107
+ end
108
+ end
109
+ e.each do |d|
110
+ asof(d.year, d.month).map do |invoice|
111
+ if invoice['settled']
112
+ next if !due
113
+ settle_date = invoice['settled']['date'].class.name == "String" ? Date.parse(invoice['settled']['date']) : invoice['settled']['date']
114
+ next if (settle_date && settle_date <= fy_end)
115
+ end
116
+
117
+ customer = invoice.dig('customer', 'name')
118
+ items[customer] ||= { 'unsettled' => BigDecimal('0'), 'invoices' => [] }
119
+ items[customer]['unsettled'] += (invoice.dig('subtotal', 0, 'items') + invoice.dig('subtotal', 0, 'tax')||0)
120
+ items[customer]['invoices'] << invoice
121
+ end
122
+ end
123
+ items.each do |k, item|
124
+ row = {
125
+ 'customer' => k,
126
+ 'unsettled' => LucaSupport::Code.readable(item['unsettled']),
127
+ }
128
+ if detail
129
+ row['address'] = %Q(#{customers.dig(k, 'address')}#{customers.dig(k, 'address2')})
130
+ row['invoices'] = item['invoices'].map{ |i| { 'id' => i['id'], 'issue' => i['issue_date'].to_s } }
131
+ end
132
+ res << row
133
+ end
134
+ res.sort! { |a, b| b['unsettled'] <=> a['unsettled'] }
135
+ end
136
+ end
137
+
138
+ # === JSON Format:
139
+ # [
140
+ # {
141
+ # "journals" : [
142
+ # {
143
+ # "id": "2021A/U001",
144
+ # "header": "customer name",
145
+ # "diff": -20000
146
+ # }
147
+ # ]
148
+ # }
149
+ # ]
150
+ #
151
+ def self.settle(io, payment_terms = 1)
152
+ customers = {}.tap do |h|
153
+ Customer.all.each { |c| h[c['name']] = c }
154
+ end
155
+ contracts = {}.tap do |h|
156
+ Contract.all.each { |c| h[c['customer_id']] ||= []; h[c['customer_id']] << c }
157
+ end
158
+ JSON.parse(io).each do |d|
159
+ next if d['journals'].nil?
160
+
161
+ d['journals'].each do |j|
162
+ next if j['diff'] >= 0
163
+
164
+ if j['header'] == 'others'
165
+ STDERR.puts "#{j['id']}: no customer header found. skip"
166
+ next
167
+ end
168
+
169
+ ord = customers.map do |k, v|
170
+ [v, LucaSupport::Code.match_score(j['header'], k, 2)]
171
+ end
172
+ customer = ord.max { |x, y| x[1] <=> y[1] }.dig(0, 'id')
173
+
174
+ if customer
175
+ contract = contracts[customer].length == 1 ? contracts.dig(customer, 0, 'id') : nil
176
+ date = Date.parse(j['date'])
177
+ invoices = term(date.prev_month(payment_terms).year, date.prev_month(payment_terms).month, date.year, date.month, contract)
178
+ invoices.each do |invoice, _path|
179
+ next if invoice['customer']['id'] != customer
180
+ next if invoice['issue_date'] > date
181
+ if Regexp.new("^LucaBook/#{j['id']}").match invoice.dig('settled', 'id')||''
182
+ break
183
+ end
184
+
185
+ invoice['settled'] = {
186
+ 'id' => "LucaBook/#{j['id']}",
187
+ 'date' => j['date'],
188
+ 'amount' => j['diff']
189
+ }
190
+ save(invoice)
191
+ break
192
+ end
193
+ else
194
+ STDERR.puts "#{j['id']}: no customer found"
195
+ end
196
+ end
197
+ end
198
+ end
199
+
79
200
  # Output seriarized invoice data to stdout.
80
201
  # Returns previous N months on multiple count
81
202
  #
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LucaDeal
4
- VERSION = '0.2.25'
4
+ VERSION = '0.4.0'
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.25
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuma Takahiro
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-07-30 00:00:00.000000000 Z
11
+ date: 2022-05-26 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:
@@ -103,7 +103,7 @@ metadata:
103
103
  homepage_uri: https://github.com/chumaltd/luca/tree/master/lucadeal
104
104
  source_code_uri: https://github.com/chumaltd/luca/tree/master/lucadeal
105
105
  changelog_uri: https://github.com/chumaltd/luca/tree/master/lucadeal/CHANGELOG.md
106
- post_install_message:
106
+ post_install_message:
107
107
  rdoc_options: []
108
108
  require_paths:
109
109
  - lib
@@ -118,8 +118,8 @@ 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
122
- signing_key:
121
+ rubygems_version: 3.3.5
122
+ signing_key:
123
123
  specification_version: 4
124
124
  summary: Deal with contracts
125
125
  test_files: []