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 +4 -4
- data/CHANGELOG.md +24 -0
- data/README.md +19 -1
- data/exe/luca-deal +26 -4
- data/lib/luca_deal.rb +1 -0
- data/lib/luca_deal/contract.rb +3 -3
- data/lib/luca_deal/fee.rb +147 -14
- data/lib/luca_deal/invoice.rb +71 -11
- data/lib/luca_deal/no_invoice.rb +20 -0
- data/lib/luca_deal/setup.rb +1 -1
- data/lib/luca_deal/templates/fee-report.html.erb +5 -1
- data/lib/luca_deal/templates/monthly-payment-list.html.erb +54 -0
- data/lib/luca_deal/version.rb +1 -1
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 68b497e68738101eafbfe6eab73c3e0cf8d297cf4676ccfb8cd05efdadc273a3
|
4
|
+
data.tar.gz: fb8ed0c2d5eb0f7621c4436ddad91ece16aa723f2861ecd661423802ec01d0c4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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[
|
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[
|
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('--
|
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[
|
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
|
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'
|
@@ -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 = {
|
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['
|
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('
|
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']
|
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
|
data/lib/luca_deal/invoice.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
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') !=
|
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
|
data/lib/luca_deal/setup.rb
CHANGED
@@ -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(
|
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>
|
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.2.
|
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:
|
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.
|
121
|
+
rubygems_version: 3.2.3
|
120
122
|
signing_key:
|
121
123
|
specification_version: 4
|
122
124
|
summary: Deal with contracts
|