lewt 0.5.12
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 +7 -0
- data/LICENSE.md +22 -0
- data/README.md +238 -0
- data/bin/lewt +10 -0
- data/lib/config/customers.yml +33 -0
- data/lib/config/enterprise.yml +54 -0
- data/lib/config/settings.yml +10 -0
- data/lib/config/templates/invoice.html.liquid +63 -0
- data/lib/config/templates/invoice.text.liquid +40 -0
- data/lib/config/templates/meta.html.liquid +0 -0
- data/lib/config/templates/meta.text.liquid +1 -0
- data/lib/config/templates/metastat.html.liquid +2 -0
- data/lib/config/templates/metastat.text.liquid +8 -0
- data/lib/config/templates/report.html.liquid +29 -0
- data/lib/config/templates/report.text.liquid +15 -0
- data/lib/config/templates/style.css +461 -0
- data/lib/extension.rb +158 -0
- data/lib/extensions/calendar-timekeeping/apple_extractor.rb +63 -0
- data/lib/extensions/calendar-timekeeping/calendar-timekeeping.rb +65 -0
- data/lib/extensions/calendar-timekeeping/extractor.rb +62 -0
- data/lib/extensions/calendar-timekeeping/gcal_extractor.rb +61 -0
- data/lib/extensions/calendar-timekeeping/ical_extractor.rb +52 -0
- data/lib/extensions/liquid-renderer.rb +106 -0
- data/lib/extensions/metastat/metamath.rb +108 -0
- data/lib/extensions/metastat/metastat.rb +161 -0
- data/lib/extensions/simple-expenses.rb +112 -0
- data/lib/extensions/simple-invoices.rb +93 -0
- data/lib/extensions/simple-milestones.rb +102 -0
- data/lib/extensions/simple-reports.rb +81 -0
- data/lib/extensions/store.rb +81 -0
- data/lib/lewt.rb +233 -0
- data/lib/lewt_book.rb +29 -0
- data/lib/lewt_ledger.rb +149 -0
- data/lib/lewtopts.rb +170 -0
- data/tests/LEWT Schedule.ics +614 -0
- data/tests/expenses.csv +1 -0
- data/tests/milestones.csv +1 -0
- data/tests/run_tests.rb +14 -0
- data/tests/tc_Billing.rb +29 -0
- data/tests/tc_CalExt.rb +44 -0
- data/tests/tc_Lewt.rb +37 -0
- data/tests/tc_LewtExtension.rb +31 -0
- data/tests/tc_LewtLedger.rb +38 -0
- data/tests/tc_LewtOpts.rb +26 -0
- metadata +158 -0
@@ -0,0 +1,112 @@
|
|
1
|
+
require "csv"
|
2
|
+
|
3
|
+
# Author:: Jason Wijegooneratne (mailto:code@jwije.com)
|
4
|
+
# Copyright:: Copyright (c) 2014 Jason Wijegooneratne
|
5
|
+
# License:: MIT. See LICENSE.md distributed with the source code for more information.
|
6
|
+
|
7
|
+
|
8
|
+
module LEWT
|
9
|
+
|
10
|
+
# The Simple Expenses LEWT Extensions allows you to manage you expenses in a CSV file. The CSV
|
11
|
+
# file itself must conform to the following specification:
|
12
|
+
#
|
13
|
+
# ===Row Indexes:
|
14
|
+
# [0] Date
|
15
|
+
# [1] Description
|
16
|
+
# [2] Context
|
17
|
+
# [3] Cost
|
18
|
+
#
|
19
|
+
# The key <tt>expenses_filepath</tt> can be added to your settings file to change the location where this extension looks for the CSV.
|
20
|
+
|
21
|
+
class SimpleExpenses < LEWT::Extension
|
22
|
+
|
23
|
+
# Registers this extension
|
24
|
+
def initialize
|
25
|
+
options = {
|
26
|
+
:include_own => {
|
27
|
+
:definition => "toggles including own business expenses from csv file",
|
28
|
+
:default => false,
|
29
|
+
:short_flag => "-i"
|
30
|
+
}
|
31
|
+
}
|
32
|
+
super({:cmd => "expenses", :options => options })
|
33
|
+
end
|
34
|
+
|
35
|
+
# Extracts data from the expenses CSV file.
|
36
|
+
# options [Hash]:: The options hash passed to this function by the Lewt program.
|
37
|
+
def extract( options )
|
38
|
+
@targets = get_matched_customers( options[:target] )
|
39
|
+
@dStart = options[:start]
|
40
|
+
@dEnd = options[:end]
|
41
|
+
@category = 'Expenses'
|
42
|
+
@include_own_expenses = options[:include_own]
|
43
|
+
exFile = lewt_settings["expenses_filepath"]
|
44
|
+
return get_expenses ( exFile )
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
# Read file at filepath and parses it expecting the format presented in this classes header.
|
49
|
+
# filepath [String]:: The CSV filepath as a string.
|
50
|
+
def get_expenses ( filepath )
|
51
|
+
# ROWS:
|
52
|
+
# [0]Date [1]Description [2]Context [3]Cost
|
53
|
+
count = 0
|
54
|
+
data = LEWT::LEWTBook.new
|
55
|
+
CSV.foreach(filepath) do |row|
|
56
|
+
if count > 0
|
57
|
+
date = Time.parse(row[0])
|
58
|
+
desc = row[1]
|
59
|
+
context = row[2]
|
60
|
+
cost = row[3].to_f * -1
|
61
|
+
if self.is_target_date?( date ) == true && self.is_target_context?(context) == true
|
62
|
+
# create ledger entry and append to books
|
63
|
+
row_data = LEWT::LEWTLedger.new({
|
64
|
+
:date_start => date,
|
65
|
+
:date_end => date,
|
66
|
+
:category => @category,
|
67
|
+
:entity => context,
|
68
|
+
:description => desc,
|
69
|
+
:quantity => 1,
|
70
|
+
:unit_cost => cost
|
71
|
+
})
|
72
|
+
data.push(row_data)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
# increment our row index counter
|
76
|
+
count += 1
|
77
|
+
end
|
78
|
+
|
79
|
+
# return our data as per specification!
|
80
|
+
return data
|
81
|
+
end
|
82
|
+
|
83
|
+
# Checks whether event date is within target range
|
84
|
+
# date [DateTime]:: The date to check
|
85
|
+
# returns: Boolean
|
86
|
+
def is_target_date? ( date )
|
87
|
+
d = date.to_date
|
88
|
+
check = false
|
89
|
+
if d >= @dStart.to_date && d <= @dEnd.to_date
|
90
|
+
check = true
|
91
|
+
end
|
92
|
+
return check
|
93
|
+
end
|
94
|
+
|
95
|
+
# Checks if the context field in the CSV matches any of our target clients names or alias'
|
96
|
+
# context [String]:: The context field as a string.
|
97
|
+
def is_target_context?(context)
|
98
|
+
match = false
|
99
|
+
@targets.each do |t|
|
100
|
+
reg = [ t['alias'], t['name'] ]
|
101
|
+
if @include_own_expenses == true
|
102
|
+
reg.concat [ @enterprise["alias"], @enterprise["name"] ]
|
103
|
+
end
|
104
|
+
regex = Regexp.new( reg.join("|"), Regexp::IGNORECASE )
|
105
|
+
match = regex.match(context) != nil ? true : false;
|
106
|
+
break if match != false
|
107
|
+
end
|
108
|
+
return match
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require "yaml"
|
2
|
+
require "securerandom"
|
3
|
+
|
4
|
+
# Author:: Jason Wijegooneratne (mailto:code@jwije.com)
|
5
|
+
# Copyright:: Copyright (c) 2014 Jason Wijegooneratne
|
6
|
+
# License:: MIT. See LICENSE.md distributed with the source code for more information.
|
7
|
+
|
8
|
+
# The Billing LEWT Extension handles processing invoices from extracted data and returns them as a hash
|
9
|
+
# for rendering.
|
10
|
+
|
11
|
+
module LEWT
|
12
|
+
|
13
|
+
class SimpleInvoices < LEWT::Extension
|
14
|
+
|
15
|
+
attr_reader :data
|
16
|
+
|
17
|
+
# Sets up this extensions command
|
18
|
+
def initialize
|
19
|
+
super({:cmd => "invoice"})
|
20
|
+
end
|
21
|
+
|
22
|
+
# Processes the provided extract data into an invoice for the given targets.
|
23
|
+
# options [Hash]:: An hash containing run-time options passed to this extension by LEWT.
|
24
|
+
# data [LEWTBook]:: A hash-like object containing all the extracted data in the LEWTLedger format.
|
25
|
+
# returns [Array]: The invoice data as an array of hashes.
|
26
|
+
def process ( options, data )
|
27
|
+
matchData = get_matched_customers( options[:target] )
|
28
|
+
bills = Array.new
|
29
|
+
matchData.each do |client|
|
30
|
+
bills.push( generateBill( client, data) )
|
31
|
+
end
|
32
|
+
return bills
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
|
37
|
+
# Generates a UID for this invoice based of the customer it is being sent to
|
38
|
+
def generate_id
|
39
|
+
if !lewt_settings.has_key?("invoice_id_counter")
|
40
|
+
self.write_settings("settings.yml", "invoice_id_counter", 0)
|
41
|
+
end
|
42
|
+
id = lewt_settings["invoice_id_counter"].to_i + 1
|
43
|
+
self.write_settings("settings.yml", "invoice_id_counter", id)
|
44
|
+
return "#{id.to_s}-" + SecureRandom.hex(4)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Generates a bill for the given client.
|
48
|
+
# client [Hash]:: The client to calculate the invoice for.
|
49
|
+
# data [LEWTBook]:: The data preloaded into the LEWTBook format.
|
50
|
+
# returns [Hash]: The invoice data as a hash.
|
51
|
+
def generateBill(client, data)
|
52
|
+
bill = {
|
53
|
+
"date_created" => DateTime.now.strftime("%d/%m/%y"),
|
54
|
+
"id" => generate_id,
|
55
|
+
# "date_begin"=> @events.dateBegin.strftime("%d/%m/%y"),
|
56
|
+
# "date_end"=> @events.dateEnd.strftime("%d/%m/%y"),
|
57
|
+
"billed_to" => client,
|
58
|
+
"billed_from" => enterprise,
|
59
|
+
"items" => [
|
60
|
+
# eg: { description, duration, rate, total
|
61
|
+
],
|
62
|
+
"sub-total" => 0,
|
63
|
+
"tax" => nil,
|
64
|
+
"total" => nil
|
65
|
+
}
|
66
|
+
# loop events and filter for requested entity (client)
|
67
|
+
data.each do |row|
|
68
|
+
if row[:entity] == client["name"]
|
69
|
+
item = {
|
70
|
+
"description" => row[:description],
|
71
|
+
"duration" => row[:quantity],
|
72
|
+
"rate" => row[:unit_cost],
|
73
|
+
"total" => row[:total] < 0 ? row[:total] * -1 : row[:total],
|
74
|
+
"start" => row[:date_start].strftime("%d/%m/%y %l:%M%P"),
|
75
|
+
"end" => row[:date_end].strftime("%d/%m/%y %l:%M%P")
|
76
|
+
}
|
77
|
+
bill["items"].push( item );
|
78
|
+
bill["sub-total"] += item["total"]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
bill["sub-total"] = bill["sub-total"].round(2)
|
82
|
+
bill["tax"] = (bill["sub-total"] * enterprise["invoice-tax"]).round(2)
|
83
|
+
bill["total"] = (bill["sub-total"] + bill["tax"]).round(2)
|
84
|
+
|
85
|
+
return bill;
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require "csv"
|
2
|
+
|
3
|
+
# Author:: Jason Wijegooneratne (mailto:code@jwije.com)
|
4
|
+
# Copyright:: Copyright (c) 2014 Jason Wijegooneratne
|
5
|
+
# License:: MIT. See LICENSE.md distributed with the source code for more information.
|
6
|
+
|
7
|
+
|
8
|
+
module LEWT
|
9
|
+
|
10
|
+
# Extracts milestone payment data from a CSV file.
|
11
|
+
#
|
12
|
+
# ===Row Indexes:
|
13
|
+
# [0] Id
|
14
|
+
# [1] Date
|
15
|
+
# [2] Description
|
16
|
+
# [3] Context
|
17
|
+
# [4] Amount
|
18
|
+
#
|
19
|
+
# The key <tt>milstones_filepath</tt> can be added to your settings file to change the location where this extension looks for the CSV.
|
20
|
+
|
21
|
+
class SimpleMilestones < LEWT::Extension
|
22
|
+
|
23
|
+
# Sets up this extension and regsters its run-time options.
|
24
|
+
def initialize
|
25
|
+
@category = "Milestone Income"
|
26
|
+
super({:cmd => "milestones"})
|
27
|
+
end
|
28
|
+
|
29
|
+
# Extracts data from the milestones CSV file.
|
30
|
+
# options [Hash]:: The options hash passed to this function by the Lewt program.
|
31
|
+
def extract( options )
|
32
|
+
matchData = get_matched_customers( options[:target] )
|
33
|
+
@dStart = options[:start].to_date
|
34
|
+
@dEnd = options[:end].to_date
|
35
|
+
@targets = self.get_matched_customers(options[:target])
|
36
|
+
exFile = lewt_settings["milestones_filepath"]
|
37
|
+
return get_milestones ( exFile )
|
38
|
+
end
|
39
|
+
|
40
|
+
# Read file at filepath and parses it expecting the format presented in this classes header.
|
41
|
+
# filepath [String]:: The CSV filepath as a string.
|
42
|
+
def get_milestones ( filepath )
|
43
|
+
# ROWS:
|
44
|
+
# [0]Id [1]Date [2]Description [3]Context [4]Amount
|
45
|
+
count = 0
|
46
|
+
data = LEWT::LEWTBook.new
|
47
|
+
|
48
|
+
CSV.foreach(filepath) do |row|
|
49
|
+
if count > 0
|
50
|
+
id = row[0]
|
51
|
+
date = Time.parse( row[1] )
|
52
|
+
desc = row[2]
|
53
|
+
context = row[3]
|
54
|
+
amount = row[4].to_f
|
55
|
+
|
56
|
+
if self.is_target_date?( date ) == true && self.is_target_context?(context) == true
|
57
|
+
# create ledger entry and append to books
|
58
|
+
row_data = LEWT::LEWTLedger.new({
|
59
|
+
:date_start => date,
|
60
|
+
:date_end => date,
|
61
|
+
:category => @category,
|
62
|
+
:entity => context,
|
63
|
+
:description => desc,
|
64
|
+
:quantity => 1,
|
65
|
+
:unit_cost => amount
|
66
|
+
})
|
67
|
+
data.push(row_data)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
# increment our row index counter
|
71
|
+
count += 1
|
72
|
+
end
|
73
|
+
return data
|
74
|
+
end
|
75
|
+
|
76
|
+
# Checks if the context field in the CSV matches any of our target clients names or alias'
|
77
|
+
# context [String]:: The context field as a string.
|
78
|
+
def is_target_context?(context)
|
79
|
+
match = false
|
80
|
+
@targets.each do |t|
|
81
|
+
reg = [ t['alias'], t['name'] ]
|
82
|
+
regex = Regexp.new( reg.join("|"), Regexp::IGNORECASE )
|
83
|
+
match = regex.match(context) != nil ? true : false;
|
84
|
+
break if match != false
|
85
|
+
end
|
86
|
+
return match
|
87
|
+
end
|
88
|
+
|
89
|
+
# Checks whether event date is within target range
|
90
|
+
# date [DateTime]:: The date to check
|
91
|
+
# returns: Boolean
|
92
|
+
def is_target_date?(date)
|
93
|
+
d = date.to_date
|
94
|
+
check = false
|
95
|
+
if d >= @dStart && d <= @dEnd
|
96
|
+
check = true
|
97
|
+
end
|
98
|
+
return check
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# Author:: Jason Wijegooneratne (mailto:code@jwije.com)
|
2
|
+
# Copyright:: Copyright (c) 2014 Jason Wijegooneratne
|
3
|
+
# License:: MIT. See LICENSE.md distributed with the source code for more information.
|
4
|
+
|
5
|
+
module LEWT
|
6
|
+
|
7
|
+
# The Reports LEWT Extension processes ledger data into a brief report.
|
8
|
+
|
9
|
+
class SimpleReports < LEWT::Extension
|
10
|
+
|
11
|
+
# Registers this extension.
|
12
|
+
def initialize
|
13
|
+
super({:cmd => "report"})
|
14
|
+
end
|
15
|
+
|
16
|
+
# Called on Lewt process cycle and uses the ledger data to compose a report.
|
17
|
+
# options [Hash]:: The options hash passed to this function by the Lewt program.
|
18
|
+
# data [LEWTBook]:: The data in LEWTBook format
|
19
|
+
def process ( options, data )
|
20
|
+
targets = get_matched_customers( options[:target] )
|
21
|
+
return make_report(targets, data)
|
22
|
+
end
|
23
|
+
|
24
|
+
# This method handles the bulk of the calculation required in compiling the report.
|
25
|
+
# targets [Hash]:: The target client(s) to operate on
|
26
|
+
# data [LEWTBook]:: The data in LEWTBook format
|
27
|
+
def make_report ( targets, data )
|
28
|
+
report = {
|
29
|
+
"date_created" => DateTime.now.strftime("%d/%m/%y"),
|
30
|
+
"included_customers" => targets,
|
31
|
+
"revenue" => 0,
|
32
|
+
"expenses" => 0,
|
33
|
+
"income" => 0,
|
34
|
+
"taxes" => Array.new,
|
35
|
+
"hours" => 0
|
36
|
+
}
|
37
|
+
data.each do |row|
|
38
|
+
if row[:category].downcase.match /income/
|
39
|
+
report["revenue"] += row[:total]
|
40
|
+
# check if category Hourly Income. If so add quantity to our 'hours' counter.
|
41
|
+
if row[:category].downcase.match /hourly/
|
42
|
+
report["hours"] += row[:quantity]
|
43
|
+
end
|
44
|
+
elsif row[:category].downcase.match /expense/
|
45
|
+
report["expenses"] += row[:total]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# remember expenses is a negative amount to begin with so don't subtract it!
|
50
|
+
report["income"] = report["revenue"] + report["expenses"]
|
51
|
+
tax_levees = enterprise["tax_levees"]
|
52
|
+
tax_total = 0
|
53
|
+
|
54
|
+
if tax_levees != nil
|
55
|
+
tax_levees.each do |tax|
|
56
|
+
if tax["applies_to"] == "income"
|
57
|
+
# do income tax
|
58
|
+
if report["income"] > tax["lower_threshold"]
|
59
|
+
taxable = [ report["income"], tax["upper_threshold"]].min - tax["lower_threshold"]
|
60
|
+
damage = (taxable * tax["rate"] + ( tax["flatrate"] || 0 )).round(2)
|
61
|
+
tax_total += damage
|
62
|
+
report["taxes"].push({ "amount" => damage, "name" => tax["name"], "rate" => tax["rate"] })
|
63
|
+
end
|
64
|
+
elsif tax["applies_to"] == "revenue"
|
65
|
+
# do GST's
|
66
|
+
damage = report["revenue"] * tax["rate"] + ( tax["flatrate"] || 0 )
|
67
|
+
tax_total += damage
|
68
|
+
report["taxes"].push({ "amount" => damage, "name" => tax["name"], "rate" => tax["rate"] })
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
report["bottom_line"] = (report["income"] - tax_total).round(2)
|
73
|
+
return report
|
74
|
+
end
|
75
|
+
|
76
|
+
def calculate_income_tax (income, tax)
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require "yaml"
|
2
|
+
|
3
|
+
# Author:: Jason Wijegooneratne (mailto:code@jwije.com)
|
4
|
+
# Copyright:: Copyright (c) 2014 Jason Wijegooneratne
|
5
|
+
# License:: MIT. See LICENSE.md distributed with the source code for more information.
|
6
|
+
|
7
|
+
|
8
|
+
module LEWT
|
9
|
+
|
10
|
+
# The Store LEWT Extension handles persisting data across sessions. It hooks into the 'process' operation to
|
11
|
+
# obtain the raw extract data, likewise it hooks into 'render' to get the process data. Whilst in 'render' it
|
12
|
+
# also handles writing the data to the file-system, so to save data you must invoke it with the --render flag!
|
13
|
+
# The data is saved to some configurable paths (see source code).
|
14
|
+
#
|
15
|
+
# Furthermore store can also re-extract previously persisted data from the file system for re-use! This is handy
|
16
|
+
# if you need to re-generate some output (ie: an invoice that required a quick edit), or if you would like to use
|
17
|
+
# store data built up overtime for bulk operations such as analytics & reporting.
|
18
|
+
|
19
|
+
class Store < LEWT::Extension
|
20
|
+
|
21
|
+
# Sets up this extensions command name and run-time options.
|
22
|
+
def initialize
|
23
|
+
options = {
|
24
|
+
:store_filename => {
|
25
|
+
:definition => "File name to save as",
|
26
|
+
:type => String
|
27
|
+
},
|
28
|
+
:store_hook => {
|
29
|
+
:definition => "Tell store whether to save either the 'extract' or 'process' data",
|
30
|
+
:default => "process",
|
31
|
+
:type => String
|
32
|
+
}
|
33
|
+
}
|
34
|
+
super({:cmd => "store", :options => options})
|
35
|
+
end
|
36
|
+
|
37
|
+
# This method is not yet implemented. This method (should) extracts previously stored data for reuse.
|
38
|
+
# options [Hash]:: A hash that is passed to this extension by the main LEWT program containing ru-time options.
|
39
|
+
def extract( options )
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
# Captures the extract data and converts it to a YML string storing it as a property on this object.
|
44
|
+
# Returns an empty array so as not to interupt the process loop.
|
45
|
+
# options [Hash]:: A hash that is passed to this extension by the main LEWT program containing ru-time options.
|
46
|
+
# data [Hash]:: The extracted data as a hash.
|
47
|
+
def process( options, data )
|
48
|
+
@extractData = data.to_yaml
|
49
|
+
return []
|
50
|
+
end
|
51
|
+
|
52
|
+
# Captures proess data and converts it to a YML string. This method also handles the
|
53
|
+
# actual writing of data to the file system. The options 'store_hook' toggles exract or
|
54
|
+
# process targeting.
|
55
|
+
# options [Hash]:: A hash that is passed to this extension by the main LEWT program containing ru-time options.
|
56
|
+
# data [Array]:: The processed data as an array of hashes.
|
57
|
+
def render( options, data )
|
58
|
+
@processData = data.to_yaml
|
59
|
+
name = options[:store_filename]
|
60
|
+
yml = options[:store_hook] == "extract" ? @extractData : @processData
|
61
|
+
name != nil ? store(yml, name ) : [yml]
|
62
|
+
end
|
63
|
+
|
64
|
+
protected
|
65
|
+
|
66
|
+
# Writes the given YAML string to a file at path/name.yml, this method will overwrite the
|
67
|
+
# file if it already exists.
|
68
|
+
# yml [String]:: A YAML string of data.
|
69
|
+
# path [String]:: The path to store too.
|
70
|
+
# name [String]:: The name of the file to save as.
|
71
|
+
def store ( yml, name )
|
72
|
+
storefile = File.new( name, "w")
|
73
|
+
storefile.puts(yml)
|
74
|
+
storefile.close
|
75
|
+
return [yml]
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
end
|