lucadeal 0.2.25 → 0.3.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: 060bcea890091dbe70e03718d1a8871bccf4873db86922d505e0e2c3ffefd94a
4
+ data.tar.gz: 3d202e5711a7e1d28cadb5a4debbda18900b7ab7ea4d6e5e413d52980c1cb84f
5
5
  SHA512:
6
- metadata.gz: 5c8941a80f26d67b4855f2757e00f8241dd0b73f37f0fe2ab8799500388118bec4513fcb398116ba8072889bedf97a083fbb2c4ce3fdd4219cb7a15844785b1c
7
- data.tar.gz: 91a84225d3d46201f85e9e38bbedbc102b2a3aaf2ccbf0a812db56bc3b502b78b7f89cb4087ec09642db5ff49b4abe58c15f6632bcbee18d471e95f3c1ff2a20
6
+ metadata.gz: 7a61af30d3b434edf10ab8553b3f878646354dd6e838a93405483b476ab3d8456713a745c0eb2a99eefdbc233ed662a87d4645c7a94b375924e2d8f347208c00
7
+ data.tar.gz: ca88eff9ed7c7c8dd815089ccdb34bd8dcd0813ae58dbb3b026db4833373a4cb33cb2b91eea27fce67b599a9a43b6e27796ed14ef1c2da6c60cf7308e389ea7f
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## LucaDeal 0.3.0
2
+
3
+ * implement `luca-deal reports balance` for unsettled balance by customer
4
+ * implement `luca-deal invoices settle` for import payment data from LucaBook
5
+
1
6
  ## LucaDeal 0.2.25
2
7
 
3
8
  * implement deduction rate for fee calculation.
data/README.md CHANGED
@@ -167,7 +167,7 @@ Fields for sales fee are as bellows:
167
167
  Invoice is basically auto generated from Customer and Contract objects.
168
168
 
169
169
  | Top level | Second level | Description |
170
- |------------|--------------|------------------------------------------|
170
+ |------------+--------------+------------------------------------------|
171
171
  | id | | uuid |
172
172
  | issue_date | | |
173
173
  | due_date | | |
@@ -183,6 +183,10 @@ Invoice is basically auto generated from Customer and Contract objects.
183
183
  | | qty | quantity. Default: 1. |
184
184
  | | type | |
185
185
  | | product_id | refrence for Product |
186
+ | settled | | |
187
+ | | id | data source id for duplication check |
188
+ | | date | payment date |
189
+ | | amount | payment amount |
186
190
  | subtotal | | Array of subtotal by tax category. |
187
191
  | | items | amount of items |
188
192
  | | tax | amount of tax |
data/exe/luca-deal CHANGED
@@ -186,6 +186,11 @@ class LucaCmd
186
186
  end
187
187
  end
188
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
+
189
194
  def self.mail(args = nil, params = {})
190
195
  date = "#{args[0]}-#{args[1]}-#{args[2] || '1'}" if !args.empty?
191
196
  case params['mode']
@@ -195,6 +200,11 @@ class LucaCmd
195
200
  LucaDeal::Invoice.new(date).deliver_mail
196
201
  end
197
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)
207
+ end
198
208
  end
199
209
 
200
210
  class Fee < LucaCmd
@@ -257,6 +267,12 @@ class LucaCmd
257
267
  puts JSON.dump(dat)
258
268
  when 'nu'
259
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
260
276
  else
261
277
  puts YAML.dump(dat)
262
278
  end
@@ -373,6 +389,12 @@ when /invoices?/, 'i'
373
389
  args = opt.parse(ARGV)
374
390
  LucaCmd::Invoice.mail(args, params)
375
391
  end
392
+ when 'settle'
393
+ OptionParser.new do |opt|
394
+ opt.banner = 'Usage: luca-deal invoices settle [filepath]'
395
+ args = opt.parse(ARGV)
396
+ end
397
+ LucaCmd::Invoice.settle(ARGV)
376
398
  else
377
399
  puts 'Proper subcommand needed.'
378
400
  puts
@@ -381,6 +403,29 @@ when /invoices?/, 'i'
381
403
  puts ' delete'
382
404
  puts ' list'
383
405
  puts ' mail: send mail with invoice'
406
+ puts ' settle'
407
+ exit 1
408
+ end
409
+ when /reports?/, 'r'
410
+ subcmd = ARGV.shift
411
+ case subcmd
412
+ when 'balance'
413
+ params[:detail] = false
414
+ params[:due] = false
415
+ OptionParser.new do |opt|
416
+ opt.banner = 'Usage: luca-deal r[eports] balance [options] [year month]'
417
+ opt.on('--nu', 'show table in nushell') { |_v| params[:output] = 'nu' }
418
+ opt.on('-o', '--output VAL', 'output serialized data') { |v| params[:output] = v }
419
+ opt.on('--detail', 'show detail info') { |_v| params[:detail] = true }
420
+ opt.on('--force-due', 'respect due date over actual payment') { |_v| params[:due] = true }
421
+ args = opt.parse(ARGV)
422
+ LucaCmd::Invoice.report(args, params)
423
+ end
424
+ else
425
+ puts 'Proper subcommand needed.'
426
+ puts
427
+ puts 'Usage: luca-deal r[eports] subcommand [--help|options]'
428
+ puts ' balance'
384
429
  exit 1
385
430
  end
386
431
  when 'new'
@@ -436,8 +481,9 @@ else
436
481
  puts 'Usage: luca-deal subcommand [options]'
437
482
  puts ' customers'
438
483
  puts ' contracts'
439
- puts ' invoices'
484
+ puts ' i[nvoices]'
440
485
  puts ' fee'
486
+ puts ' r[eports]'
441
487
  puts ' new: initialize project dir'
442
488
  puts ' export: puts invoice data for LucaBook import'
443
489
  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'
@@ -76,6 +77,113 @@ module LucaDeal
76
77
  end
77
78
  end
78
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
+
79
187
  # Output seriarized invoice data to stdout.
80
188
  # Returns previous N months on multiple count
81
189
  #
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LucaDeal
4
- VERSION = '0.2.25'
4
+ VERSION = '0.3.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.3.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-03-11 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.2.5
122
+ signing_key:
123
123
  specification_version: 4
124
124
  summary: Deal with contracts
125
125
  test_files: []