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 +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +10 -1
- data/exe/luca-deal +67 -5
- data/lib/luca_deal/contract.rb +3 -3
- data/lib/luca_deal/fee.rb +74 -8
- data/lib/luca_deal/invoice.rb +148 -9
- data/lib/luca_deal/templates/fee-report.html.erb +5 -1
- data/lib/luca_deal/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 599780f0a61b27bcea93724823c02bdfdd52b7b04a028265c00bc2fe1e0e865c
|
4
|
+
data.tar.gz: 7b3a09097cb6543b3b07718c7ecb35fd21bcdafbe851eb6240a33707a2e7ddf3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
[](https://badge.fury.io/rb/lucadeal)
|
4
|
+
[](https://www.rubydoc.info/gems/lucadeal/index)
|
5
|
+

|
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[
|
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[
|
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[
|
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 '
|
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
|
data/lib/luca_deal/contract.rb
CHANGED
@@ -69,11 +69,11 @@ module LucaDeal
|
|
69
69
|
end
|
70
70
|
|
71
71
|
def active_period?(dat)
|
72
|
-
unless dat
|
73
|
-
defunct = dat
|
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
|
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 = {
|
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['
|
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?(
|
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['
|
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('
|
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']
|
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)
|
data/lib/luca_deal/invoice.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
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(
|
48
|
+
<td class="price"><%= delimit_num(@sales_fee['fee'] + @sales_fee['tax'] + @sales_fee['deduction']) %></td>
|
45
49
|
</tr>
|
46
50
|
</table>
|
47
51
|
|
data/lib/luca_deal/version.rb
CHANGED
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.
|
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:
|
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.
|
121
|
+
rubygems_version: 3.3.5
|
122
122
|
signing_key:
|
123
123
|
specification_version: 4
|
124
124
|
summary: Deal with contracts
|