appraisermetrics_report_service 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ Gemfile.lock
16
+ *.pdf
17
+ /todo.txt
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,78 @@
1
+ appraisermetrics-report-service
2
+ ===============================
3
+
4
+ Report Service app for AM app.
5
+
6
+
7
+
8
+ ## gem dependencies
9
+ * prawn - https://github.com/prawnpdf/prawn - license: choice of GPLv2 or GPLv3
10
+ * prawn/table - https://github.com/prawnpdf/prawn-table - license: choice of GPLv2 or GPLv3
11
+ * money - https://github.com/RubyMoney/money - license: https://github.com/RubyMoney/money/blob/master/LICENSE
12
+ * rspec for tests
13
+ * pdf-reader (for specs) - https://github.com/yob/pdf-reader - license: MIT
14
+ * the Date ruby module, should be included in any standard ruby app
15
+
16
+
17
+ ## usage
18
+
19
+ ```ruby
20
+ # will need a deploy key
21
+ gem 'appraisermetrics_report_service', git: 'https://github.com/StackPointCloud/appraisermetrics-report-service.git'
22
+ ```
23
+
24
+ ```ruby
25
+ require 'appraisermetrics_report_service'
26
+ ```
27
+
28
+ ## ClosedSale
29
+ ```ruby
30
+ c = ClosedSale.new do
31
+ write_content(ruby_hash_object, images, office_logo) # => returns ClosedSaleObject which inherits from Prawn::Document
32
+ end
33
+
34
+ # ruby_hash_object is identical to Comp document, with references to the Mongo Shell removed (ISODate, ObjectID)
35
+ # images takes an array of image paths. [0] should be the profile image for the property
36
+ # office_logo takes a file path
37
+ ```
38
+
39
+ ## EvalReport
40
+ ```ruby
41
+ # generate an eval report
42
+ r = EvalReport.new do
43
+ write_content(subject, comparables, images, logo) # images is a hash, subject is a hash, comparables is an array, logo is a filepath
44
+ end
45
+
46
+ # image hash construction: {subject_photos: [], regional_maps: [], topo_maps: [], ag_sales_map: String}
47
+ ```
48
+
49
+ ## Output
50
+ ```ruby
51
+ # render it to file
52
+ r.render_file("some_name.pdf") => local directory pdf called "some_name" with extension .pdf
53
+ ```
54
+
55
+ ```ruby
56
+ # stringIO
57
+ StringIO.new(r.render) => <StringIO>
58
+
59
+
60
+ ```
61
+ ## Testing/Dev
62
+ * Rspec suite covers table methods invidually, as well as helper methods.
63
+ * The `pdf-reader` gem is used in specs to read content into tests
64
+ * The `Sampler` class is used to generate sample ruby objects to be used in specs.
65
+
66
+ To generate test pdf reports:
67
+
68
+ ```ruby
69
+ irb
70
+
71
+ load './spec/test_data/sampler'
72
+
73
+ # closed sale
74
+ Sampler.test_closed_sale
75
+
76
+ # eval report
77
+ Sampler.test_eval_report
78
+ ```
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'appraisermetrics_report_service/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "appraisermetrics_report_service"
8
+ spec.version = AppraisermetricsReportService::VERSION
9
+ spec.authors = ["StackPoint"]
10
+ spec.email = ["fletcher.wilkens1@gmail.com"] # feel free to change this
11
+ spec.summary = ["A service to generate pdf reports"]
12
+ spec.homepage = ""
13
+
14
+ spec.files = `git ls-files -z`.split("\x0")
15
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
16
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
17
+ spec.require_paths = ["lib"]
18
+
19
+ spec.add_development_dependency "bundler", "~> 1.6"
20
+ spec.add_development_dependency "rake", "~> 10.0"
21
+ spec.add_development_dependency "rspec"
22
+ spec.add_development_dependency "pdf-reader"
23
+ spec.add_dependency "prawn-table"
24
+ spec.add_dependency "prawn"
25
+ spec.add_dependency "money"
26
+ end
@@ -0,0 +1,44 @@
1
+ transmittal:
2
+ "In fulfillment of our agreement and under XI of FDIC Appraisal and Evaluation Guidelines setting forth circumstances under which evaluations of real property are allowed:
3
+ The Agencies’ appraisal regulations permit an institution to obtain an appropriate evaluation of real property collateral in lieu of an appraisal for transactions that qualify for certain exemptions. These exemptions include a transaction that:
4
+ \n \n
5
+ • Has a transaction value equal to or less than the appraisal threshold of $250,000.
6
+ \n \n
7
+ • Is a business loan with a transaction value equal to or less than the business loan threshold of $1 million, and is not dependent on the sale of, or rental income derived from, real estate as the primary source of repayment.
8
+ \n \n
9
+ • Involves an existing extension of credit at the lending institution, provided that:
10
+ \n \n
11
+ There has been no obvious and material change in market conditions or physical aspects of the property that threaten the adequacy of the institution’s real estate collateral protection after the transaction, even with the advancement of new monies; or
12
+ \n \n
13
+ There is no advancement of new monies other than funds necessary to cover reasonable closing costs.
14
+ \n \n
15
+ I hereby submit the attached report which contains the FDIC Evaluation Content Requirements identified in the scope of work section."
16
+
17
+ scope_of_work1:
18
+ "This Evaluation is presented in a concise report format, is intended to comply with the reporting requirements outlined under the Interagency Appraisal and Evaluation Guidelines issued on December 10, 2010, as previously set forth.
19
+ \n \nThe scope of this evaluation required collecting primary and secondary data relevant to the subject property, including its current and projected use.
20
+ \n \nThe evaluation provides an estimate of the property’s market value in its actual physical condition, use and zoning designation as of the effective date of the evaluation (the date that the analysis was completed) with any limiting conditions.
21
+ \n \nSale data have been selected based on similar significant variables thoroughly analyzed and confirmed by qualified professionals, including certified real estate appraisers, county assessors, buyers, sellers and Realtors, and are believed to be reliable qualifying market data and have been appropriately sourced.
22
+ \n \nA physical inspection of the subject property was made by the person signing this report based on the following research:"
23
+
24
+ scope_of_work2:
25
+ "From the research gathered, the person signing this report has completed and following analysis:"
26
+
27
+ scope_of_work3:
28
+ "The review and description of the area (neighborhood) including general and local market conditions have been provided.
29
+ \nThis evaluation employs only the Sales Comparison Approach. Any inclusion of income projections are for internal purposes and do not infer a net operating income from which a value can be derived."
30
+
31
+ valuation_process:
32
+ "The subject property has been valued “as-is” as of the effective date of the Evaluation. The analysis of value that follows is based on a relative comparison of the subject’s primary property type and secondary property type. This is accomplished by identifying the same property classifications on sales of similar-type properties located in the subject’s immediate vicinity that were being used in a similar manner to the subject when they sold. This analysis does not include an analysis of highest and best use because the evaluator is not an appraiser and because this evaluation in not an appraisal.
33
+ \n \nSimilar property types have been selected in order to represent a reasonably supported value of the subject property by indicators derived from recent sales of similar properties. This method employs generally accepted procedures and methods used to conclude an estimate of value, however should not be construed to meet appraisal standards, guidelines, and methodologies."
34
+
35
+ comp_method:
36
+ "An opinion of value has been developed by comparing the subject property to similar, recently sold properties in the surrounding or competing area. This approach relies on the idea that when a property is replaceable in the market its value tends to be set at the cost of acquiring an equally desirable substitute property, assuming that no costly delay is encountered in making the substitution.
37
+ \n \nBy analyzing sales with similar physical and economic properties that qualify as arm’s-length transactions between willing and knowledgeable buyers and sellers, we can identify value ranges and price trends."
38
+
39
+ val_method:
40
+ "This evaluation includes an estimate of value by direct comparison. The opinions and conclusion set forth herein are limited to the Client’s use and may not be understood without additional information contained in my work file or an expanded scope of work or a complete appraisal analysis which may produce a different result."
41
+
42
+ assumptions_conditions:
43
+ "THIS IS AN EVALUATION AND SHOULD NOT BE CONSIDERED TO BE AN APPRAISAL. This report complies with the Interagency Appraisal and Evaluation Guidelines Final Guidance that became effective on December 10, 2010, the date it was published in the Federal register, jointly by the Comptroller of the Currency, the Board of Governors of the Federal Reserve System, the Federal Deposit Insurance Corporation, the Department of the Treasury Office of Thrift Supervision and the National Credit Union Administration (“Federal Guidelines”). Accordingly this Report may be relied upon by Customer in accordance with the Federal Guidelines.
44
+ \n \nThis is an agricultural Evaluation Report. As such, it presents only brief discussions or statements of the data and analysis used to estimate the value conclusion. The scope of the assignment and report content is specific to the needs of the client and for the intended use as stated by the client. The Evaluator(s) listed below are not responsible for unauthorized use of this report."
@@ -0,0 +1,8 @@
1
+ module AppraisermetricsReportService
2
+ require 'prawn'
3
+ require 'prawn/table'
4
+ require 'money'
5
+ require 'report_utils'
6
+ require 'closed_sale'
7
+ require 'eval_report'
8
+ end
@@ -0,0 +1,3 @@
1
+ module AppraisermetricsReportService
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,706 @@
1
+ class ClosedSale < Prawn::Document
2
+ require 'prawn'
3
+ require 'prawn/table'
4
+ require 'date'
5
+
6
+ include ReportUtils
7
+
8
+ def initialize()
9
+ super()
10
+ end
11
+
12
+ def write_content(report_doc, images, office_logo) # the call to add content to pdf
13
+ font_size 6
14
+ @rep = report_doc
15
+ @property_images = images
16
+ @office_logo = office_logo
17
+
18
+ ## page 1 ##
19
+
20
+ # render office logo image if present
21
+ render_logo
22
+
23
+ # rendering bounding boxes and tables
24
+ meta_data
25
+ property_class
26
+ profile_image
27
+ property_type_and_address
28
+ transaction_summary
29
+ land_classification
30
+ physical_overview
31
+ cash_flow
32
+ income_expense
33
+
34
+ start_new_page
35
+
36
+ ## page 2 ##
37
+ comments
38
+ legal_transaction
39
+ # move to full width table
40
+ transaction_history
41
+ utilities
42
+ improvements
43
+ water_rights
44
+
45
+ start_new_page
46
+ ## page 3 ##
47
+ water_distribution
48
+ crop_yield
49
+ estimated_productivity
50
+ perm_plantings
51
+ externalities
52
+
53
+ start_new_page
54
+ ## page 4 ##
55
+ additional_images
56
+
57
+
58
+ end
59
+
60
+ def render_logo
61
+ if @office_logo
62
+ image @office_logo, height: 25
63
+ end
64
+ end
65
+
66
+
67
+ def meta_data
68
+ bounding_box([0, 690], width: 270, height: 55) do
69
+ meta_data = [
70
+ ["Record Number:", @rep[:sequence]],
71
+ ["Record Created By:", @rep[:record_created_by]],
72
+ ["Date Created or Last Modified:", datemaker(@rep[:record_edited])]
73
+ ]
74
+
75
+ table(meta_data) do # record meta data table with image
76
+ columns(0..1).width = 135
77
+ columns([0]).style(font_style: :bold)
78
+ cells.style(border_width: 0)
79
+ cells.padding = [1, 2.5]
80
+ end
81
+ stroke_bounds
82
+ end
83
+ end
84
+
85
+ def profile_image
86
+ bounding_box([0, 635], width: 270, height: 275) do
87
+ if @property_images && @property_images.any?
88
+ image "#{@property_images.first}", width: 265
89
+ else
90
+ text "No Image"
91
+ end
92
+ stroke_bounds
93
+ end
94
+ end
95
+
96
+ def land_classification
97
+ bounding_box([0, 360], width: 270, height: 180) do
98
+ classification_data = [
99
+ [{content: "Land Classification Breakdown", colspan: 4}],
100
+ ["Land Class:", "Acres:", "$/Acre:", "Total:"],
101
+ ]
102
+
103
+ land_classes = @rep[:landclassifications]
104
+ if land_classes # node could be nil
105
+
106
+ total_acres = 0 # value to add to in loop
107
+ total_dollar = 0
108
+
109
+ land_classes.each do |l|
110
+ if l # array could be empty
111
+ if l[:priceperacre] # priceperacre could be nil, avoid nomethod errors below by checking
112
+ m = Money.new(l[:priceperacre][:cents], l[:priceperacre][:currency_iso])
113
+ per_acre_string = m.format(no_cents: true)
114
+ total_string = (m * l[:numacres]).format(no_cents: true)
115
+
116
+ total_acres += no_nil_number(l[:numacres]) # add to total
117
+ total_dollar += m.to_f * l[:numacres]
118
+
119
+ classification_data.push(
120
+ [
121
+ l[:landclass],
122
+ '%.2f' % no_nil_number(l[:numacres]), # => adds .00 to acres
123
+ per_acre_string,
124
+ total_string
125
+ ]
126
+ )
127
+ end
128
+ end
129
+ end # end block
130
+
131
+ total_dollar_string = Money.new(total_dollar * 100, 'USD').format(no_cents: true)
132
+ total_per_acre = Money.new((total_dollar / total_acres) * 100, 'USD')
133
+ total_per_acre_string = total_per_acre.format(no_cents: true)
134
+
135
+ # push totals into table
136
+ classification_data.push(
137
+ ["", "", "", ""], # line space
138
+ ["Totals", '%.2f' % total_acres, total_per_acre_string, total_dollar_string]
139
+ )
140
+
141
+ if @rep[:imprv_unitprice]
142
+ improv_alloc = Money.new(@rep[:imprv_unitprice][:cents], 'USD')
143
+ else
144
+ improv_alloc = Money.new(0, 'USD')
145
+ end
146
+
147
+ classification_data.push([
148
+ "Total $ to Imprv", moneymaker(@rep[:total_imprv_budget], true),
149
+ "Imprv $/AC Alloc", improv_alloc.format(no_cents: :true)],
150
+ [{content: "Total Land and Improvement $/AC", colspan: 3}, (total_per_acre + improv_alloc).format(no_cents: :true)])
151
+ end
152
+
153
+ table(classification_data) do # land classification breakdown table
154
+ columns(0..1).width = 68
155
+ columns(2..3).width = 67
156
+ columns(1..3).style(align: :right)
157
+ cells.style(border_width: 0)
158
+ cells.padding = [1, 2.5]
159
+ row(0).style(align: :center, background_color: 'd7d7d7', font_size: 8, font_style: :bold)
160
+ row(1).style(align: :right, font_style: :bold, border_bottom_width: 0.5)
161
+ row(-3).style(border_bottom_width: 0.5, font_style: :bold)
162
+ end
163
+ stroke_bounds
164
+ end
165
+ end
166
+
167
+ def cash_flow
168
+ bounding_box([0, 180], width: 270, height: 180) do
169
+ cash_flow_data = [
170
+ [{content: "Cash Flow - Production Analysis", colspan: 4}],
171
+ ["Income Source", "Stable $/Unit", "Stablilized Yield/Unit", "No. of Source, i.e. AC"]
172
+ ]
173
+
174
+ incomes = @rep[:incomes]
175
+
176
+ if incomes # node could be nil
177
+ incomes.each do |i|
178
+ if i # array could be nil
179
+ cash_flow_data.push(
180
+ [
181
+ i[:incomesrc],
182
+ moneymaker(i[:stabcashperunit], false),
183
+ '%.2f' % i[:stabilizedyield],
184
+ '%.2f' % i[:totalunits]
185
+ ]
186
+ )
187
+ end
188
+ end
189
+ end
190
+
191
+ table(cash_flow_data) do
192
+ columns(0..1).width = 68
193
+ columns(2..3).width = 67
194
+ columns(1..3).style(align: :right, font_style: :bold)
195
+ column(0).style(align: :left, font_style: :bold)
196
+ row(0).style(align: :center, background_color: 'd7d7d7', font_size: 8)
197
+ cells.style(border_width: 0)
198
+ row(1).style(border_bottom_width: 0.5, align: :center, font_style: nil)
199
+ cells.padding = [1, 2.5]
200
+ end
201
+ stroke_bounds
202
+ end
203
+ end
204
+
205
+ def comments
206
+ bounding_box([0, 690], width: 270, height: 125) do
207
+ comments_data = [
208
+ ["Comments"],
209
+ [@rep[:general_comments]]
210
+ ]
211
+
212
+ table(comments_data) do
213
+ row(0).style align: :center
214
+ column(0).width = 270
215
+ row(0).style(align: :center, background_color: 'd7d7d7', font_size: 8, font_style: :bold)
216
+ cells.padding = [1, 2.5]
217
+ cells.style(border_width: 0)
218
+ end
219
+ stroke_bounds
220
+ end
221
+ end
222
+
223
+ def property_class
224
+ bounding_box([270, 690], width: 270, height: 55) do
225
+ property_data = [
226
+ ["Property Classification:", @rep[:land_use_zone]],
227
+ ["Transaction Type:", @rep[:type_of_transaction]],
228
+ ["Property Name:", @rep[:property_name]]
229
+ ]
230
+
231
+ table(property_data) do
232
+ column(0).style(align: :right)
233
+ column([1]).style(font_style: :bold)
234
+ columns(0..1).width = 135
235
+ cells.style(border_width: 0)
236
+ cells.padding = [1, 2.5]
237
+ end
238
+ stroke_bounds
239
+ end
240
+ end
241
+
242
+ def property_type_and_address
243
+ bounding_box([270, 635], height: 145, width: 270) do
244
+ pos = @rep[:position] # lat and long
245
+ if pos
246
+ lat, long = pos[0], pos[1]
247
+ else
248
+ lat, long = " ", " "
249
+ end
250
+
251
+ property_inclusions = no_nil_array(@rep[:property_inclusions]).join(" ")
252
+ type_and_address_data = [
253
+ [{content: "Property Type and Address", colspan: 4}],
254
+ ["Property Name:", @rep[:property_name], "Primary Land Use:", @rep[:primary_ag_use]],
255
+ ["What does this property include?",
256
+ {content: property_inclusions, colspan: 3}],
257
+ ["Address:", {content: "#{@rep[:property_address_number]}" + " #{@rep[:street_or_road_name]}", colspan: 3}],
258
+ ["City:", @rep[:city], "County:", @rep[:ordinance_authority]],
259
+ ["State:", no_nil_string(@rep[:county_state]).strip.chars.last(2).join(""), "Zip Code:", @rep[:zip_code]],
260
+ ["Township/Range:","#{@rep[:townshipINT]}" + "#{@rep[:rangeINT]}", "Section(s):", @rep[:legal_description]],
261
+ ["Latitude:", lat, "Longitude:", long],
262
+ [{content: "Property Location and Directions:" + "#{@rep[:directions_or_ownership_comments]}", colspan: 4}]
263
+ ]
264
+
265
+ table(type_and_address_data) do
266
+ columns(0..1).width = 68
267
+ columns(2..3).width = 67
268
+ columns([0, 2]).style(align: :right)
269
+ columns([1, 3]).style(font_style: :bold)
270
+ cells.padding = [1, 2.5]
271
+ cells.style(border_width: 0)
272
+ row(0).style(align: :center, background_color: 'd7d7d7', font_size: 8, font_style: :bold)
273
+ end
274
+ stroke_bounds
275
+ end
276
+ end
277
+
278
+ def transaction_summary
279
+ bounding_box([270, 490], height: 130, width: 270) do
280
+ price = moneymaker(@rep[:sale_price], true)
281
+ adjustment = moneymaker(@rep[:sale_adjustment], true)
282
+ ce_sale_price = moneymaker(@rep[:cesaleprice], true)
283
+ transaction_data = [
284
+ [{content: "Transaction Summary", colspan: 4}],
285
+ ["Sale Price $:", price, "Sale Date:", datemaker(@rep[:sale_date])],
286
+ ["Cond. Adjust $:", '(' + adjustment +')', "Adj. CE Sale $:", ce_sale_price],
287
+ ["Unit Description:", @rep[:unit], "No. of Units:", '%.2f' % no_nil_number(@rep[:num_of_units])],
288
+ ["Instrument No.:", @rep[:public_rec_ref_numer], "$/Unit:", moneymaker(@rep[:cesaleunitprice], true)],
289
+ ["Seller:", @rep[:grantor], "Buyer:", @rep[:grantee]],
290
+ ["Extent of Verification:", {content: no_nil_array(@rep[:verifications]).join(', '), colspan: 3}],
291
+ [{content: "#{@rep[:grantor]}" + " #{@rep[:information_source_contact]}",
292
+ colspan: 2}, "Date Inspected:", datemaker(@rep[:date_inspected])
293
+ ]
294
+ ]
295
+
296
+ table(transaction_data) do
297
+ columns(0..1).width = 68
298
+ columns(2..3).width = 67
299
+ columns([0, 2]).style align: :right
300
+ cells.padding = [1, 2.5]
301
+ cells.style(border_width: 0)
302
+ columns([1, 3]).style(font_style: :bold)
303
+ row(0).style(align: :center, background_color: 'd7d7d7', font_size: 8, font_style: :bold)
304
+ end
305
+ stroke_bounds
306
+ end
307
+ end
308
+
309
+ def physical_overview
310
+ bounding_box([270, 360], height: 180, width: 270) do
311
+ overview_data = [
312
+ [{content: "Physical Overview", colspan: 4}],
313
+ ["Farming Practice:", @rep[:farming_practices], {content: "Predominant Soil Type and Description:", colspan: 2}],
314
+ ["Ann. Rainfall-Inches:", @rep[:avg_precipitation], {content: @rep[:soils], colspan: 2, rowspan: 3}],
315
+ ["Avg Frost Free Days:", @rep[:growing_season]],
316
+ ["Elevation(Range):", @rep[:elevation]],
317
+ ["Predom Top Slope:", @rep[:topography], "Legal Access:", {content: @rep[:legal_access], rowspan: 2}],
318
+ ["Corn Suitability Rating:", @rep[:CSR2]],
319
+ ["Land Use Zone:", @rep[:land_use_zone], "Physical Access:", @rep[:physical_access]],
320
+ ["Flood Zone:", no_nil_array(@rep[:flood_zone]).join(", "), "Wetlands:", @rep[:wetlands]],
321
+ [{content: "Land Development Potential, Change or Use, or Limitations Comments:", colspan: 4}],
322
+ [{content: "#{@rep[:development_potential]}" , colspan: 4}]
323
+ ]
324
+
325
+ table(overview_data) do
326
+ columns(0..1).width = 68
327
+ columns(2..3).width = 67
328
+ columns([0, 2]).style(align: :right)
329
+ columns([1, 3]).style(font_style: :bold)
330
+ cells.padding = [1, 2.5]
331
+ cells.style(border_width: 0)
332
+ row(0).style(align: :center, background_color: 'd7d7d7', font_size: 8, font_style: :bold)
333
+ row(9).style(align: :left)
334
+ end
335
+ stroke_bounds
336
+ end
337
+ end
338
+
339
+ def income_expense
340
+ bounding_box([270, 180], height: 180, width: 270) do
341
+
342
+ income = Money.new(no_nil_number(@rep[:eff_gross_income]) * 100, 'USD')
343
+ expense = Money.new(no_nil_number(@rep[:total_expenses]) * 100, 'USD')
344
+ noi = income - expense
345
+ noi_string = noi.format(no_cents: :true)
346
+ expense_ratio = "#{((expense / income) * 100).round(2)}" + "%"
347
+
348
+ income_expense_data = [
349
+ [{content: "Income/Expense Information", colspan: 4}],
350
+ ["Total Farm Income:", income.format(no_cents: true), "Total Income to LL:", income.format(no_cents: true)],
351
+ ["Total Farm Expenses:", expense.format(no_cents: true), "Total Exp pd by LL:", expense.format(no_cents: true)],
352
+ ["NOI:", noi_string, "Expense Ratio:", expense_ratio],
353
+ ["Cap Rate or OAR", "#{@rep[:overall_capitalization_rate]}%", " ", " "],
354
+ [{content: "Cash Flow Comments:", colspan: 4}],
355
+ [{content: @rep[:income_comments], colspan: 4}],
356
+ ["Annual Water $/Acre:", moneymaker(@rep[:irrig_cost_per_acre], true)],
357
+ ["Annual Pumping $/Acre:", moneymaker(@rep[:pumping_cost_per_acre], true)]
358
+ ]
359
+
360
+ table(income_expense_data) do
361
+ columns(0..1).width = 68
362
+ columns(2..3).width = 67
363
+ columns(0..3).style(align: :right)
364
+ cells.padding = [1, 2.5]
365
+ cells.style(border_width: 0)
366
+ columns([1, 3]).style(font_style: :bold)
367
+ row(0).style(align: :center, background_color: 'd7d7d7', font_size: 8, font_style: :bold)
368
+ row([5, 6]).style(align: :left)
369
+ end
370
+ stroke_bounds
371
+ end
372
+ end
373
+
374
+ def legal_transaction
375
+ bounding_box([270, 690], height: 125, width: 270) do
376
+
377
+ taxes = @rep[:taxes]
378
+ if taxes # node could be nil
379
+ if taxes[0] # array could be empty
380
+ tax_parcel_no = taxes[0][:tax_parcel_no]
381
+ assessed_value = moneymaker(taxes[0][:assessed_value], true)
382
+ real_estate_taxes = moneymaker(taxes[0][:RET], true)
383
+ end
384
+ else # deal with nil values to avoid no method errors
385
+ tax_parcel_no, assessed_value, real_estate_taxes = "", "", ""
386
+ end
387
+
388
+ legal_transaction_data = [
389
+ [{content: "Legal/Transaction Detail", colspan: 4}],
390
+ ["Tax Parcel ID(s)", tax_parcel_no, " ", " "],
391
+ [" ", " ", " ", " "],
392
+ ["Year of Assessment:", @rep[:year_of_assessment], "Year of RE Taxes:", @rep[:year_of_real_estate_taxes]],
393
+ ["Assessed Value:", assessed_value, "RE Taxes", real_estate_taxes],
394
+ ["Financing:", @rep[:financing_type], "Property Rights:", @rep[:property_rights]],
395
+ ["At Market Trans?:", @rep[:atmarket_trans], "Days on Market:", @rep[:exposure_period]],
396
+ [{content: "Transaction History Comments:", colspan: 4}],
397
+ [{content: @rep[:listing_comments], colspan: 4}]
398
+ ]
399
+
400
+ table(legal_transaction_data) do
401
+ columns(0..1).width = 68
402
+ columns(2..3).width = 67
403
+ columns([1, 3]).style(font_style: :bold)
404
+ cells.padding = [1, 2.5]
405
+ cells.style(border_width: 0)
406
+ row(0).style(align: :center, background_color: 'd7d7d7', font_size: 8, font_style: :bold)
407
+ end
408
+ stroke_bounds
409
+ end
410
+ end
411
+
412
+ def transaction_history
413
+ bounding_box([0, 565], height: 125, width: 540) do
414
+ transaction_history_data = [
415
+ [{content: "Transaction History", colspan: 4}],
416
+ ["Historic Transaction Type", "Transaction Description", "Date", "Sale Price, List Price, or List Price"]
417
+ ]
418
+
419
+ transactions = @rep[:historyrecords]
420
+
421
+ if transactions # node could be nil
422
+ transactions.each do |t|
423
+ if t # array could be nil
424
+ transaction_history_data.push(
425
+ [t[:transType], t[:transDescr], datemaker(t[:transDate]), moneymaker(t[:price], true)]
426
+ )
427
+ end
428
+ end
429
+ end
430
+
431
+ table(transaction_history_data) do
432
+ columns(0..3).width = 135
433
+ columns(0..3).style(align: :right, font_style: :bold)
434
+ cells.padding = [1, 2.5]
435
+ cells.style(border_width: 0)
436
+ row(0).style(align: :center, background_color: 'd7d7d7', font_size: 8, font_style: :bold)
437
+ row(1).style(border_bottom_width: 0.5, font_style: nil)
438
+ end
439
+ stroke_bounds
440
+ end
441
+ end
442
+
443
+ def utilities
444
+ bounding_box([0, 440], height: 125, width: 540) do
445
+ utilities_data = [
446
+ [{content: "Description of Utilities", colspan: 4}],
447
+ ["Utility Description", "Service Availability", "Service Provider", "Comments"]
448
+ ]
449
+
450
+ utilities = @rep[:utilities]
451
+ if utilities # node could be nil
452
+ utilities.each do |u|
453
+ if u # array could be empty
454
+ utilities_data.push(
455
+ [u[:description], u[:availbility], u[:provider], u[:comments]]
456
+ )
457
+ end
458
+ end
459
+ end
460
+
461
+ table(utilities_data) do
462
+ columns(0..3).width = 135
463
+ columns(0..3).style(align: :right, font_style: :bold)
464
+ cells.padding = [1, 2.5]
465
+ cells.style(border_width: 0)
466
+ row(0).style(align: :center, background_color: 'd7d7d7', font_size: 8, font_style: :bold)
467
+ row(1).style(border_bottom_width: 0.5, font_style: nil)
468
+ end
469
+ stroke_bounds
470
+ end
471
+ end
472
+
473
+ def improvements
474
+ bounding_box([0, 315], height: 220, width: 540) do
475
+
476
+ improvements_array = @rep[:improvements]
477
+
478
+ sorted_array = flip_improvements_array(improvements_array)
479
+
480
+ table(sorted_array) do
481
+ columns(0..7).width = 67.5
482
+ columns(1..7).style(font_style: :bold)
483
+ row(0).style(align: :center, background_color: 'd7d7d7', font_size: 8, font_style: :bold)
484
+ cells.padding = [1, 2.5]
485
+ cells.style(border_width: 0)
486
+ row(1).style(border_bottom_width: 0.5, font_style: nil)
487
+ end
488
+ stroke_bounds
489
+ end
490
+ end
491
+
492
+ def water_rights
493
+ bounding_box([0, 95], height: 95, width: 540) do
494
+
495
+ water_rights_data = [
496
+ [{content: "Water Rights", colspan: 8}],
497
+ ["Water Right No.", "Water Right", "Water Source", "Priority Date", "Beneficial Use", "No. Acres Irrigated", "Annual Volume Ac-Ft", "Period of Use"]
498
+ ]
499
+
500
+ rights = @rep[:waterrights]
501
+ if rights # node could be nil
502
+ rights.each do |r|
503
+ if r # array could be empty
504
+ water_rights_data.push(
505
+ [r[:waterrightNum], r[:waterRight], r[:waterSrc], datemaker(r[:priorityDate]), r[:purpose], r[:numIrrAcres], r[:annVolume]]
506
+ )
507
+ end
508
+ end
509
+ end
510
+
511
+ table(water_rights_data) do
512
+ columns(0..7).width = 67.5
513
+ columns(0..7).style(font_style: :bold)
514
+ row(0).style(align: :center, background_color: 'd7d7d7', font_size: 8, font_style: :bold)
515
+ cells.padding = [1, 2.5]
516
+ cells.style(border_width: 0)
517
+ row(1).style(border_bottom_width: 0.5, font_style: nil)
518
+ end
519
+
520
+ stroke_bounds
521
+ end
522
+ end
523
+
524
+ def water_distribution
525
+ bounding_box([0, 690], height: 100, width: 540) do
526
+
527
+ distribution_data = [
528
+ [{content: "Water Distribution System", colspan: 8}],
529
+ ["Water Distrib. Equip.", "Manuf.", "Make/Brand", "Type", "Description", "Yr. Manufactured", "Remaining Ec. Life", "No. Acres Irrigated"]
530
+ ]
531
+
532
+ distributions = @rep[:waterdistributions]
533
+
534
+ if distributions # node could be nil
535
+ distributions.each do |d|
536
+ if d # array could be empty
537
+ distribution_data.push(
538
+ [d[:waterdistrEq], d[:manufacturer], d[:brand], d[:eqType], d[:descr], d[:yearManuf], d[:remainingEcLife], d[:irrAcres]]
539
+ )
540
+ end
541
+ end
542
+ end
543
+
544
+ table(distribution_data) do
545
+ columns(0..7).width = 67.5
546
+ columns(0..7).style(font_style: :bold)
547
+ row(0).style(align: :center, background_color: 'd7d7d7', font_size: 8, font_style: :bold)
548
+ cells.padding = [1, 2.5]
549
+ cells.style(border_width: 0)
550
+ row(1).style(border_bottom_width: 0.5, font_style: nil)
551
+ end
552
+
553
+ stroke_bounds
554
+ end
555
+ end
556
+
557
+ def crop_yield
558
+ bounding_box([0, 590], height: 135, width: 540) do
559
+
560
+ crops = @rep[:crops]
561
+
562
+ crop_data = [
563
+ [{content: "Crop Yield Summary", colspan: 7}],
564
+ ["Crop Year", "Commodity Identified", "Unit of Measure", "Average Yield", " ", " ", ", "]
565
+ ]
566
+
567
+ if crops # node could be nil
568
+ crops.each do |c|
569
+ if c # array could be empty
570
+ crop_data.push(
571
+ [c[:year], c[:commodity], c[:unit], c[:avgyield]]
572
+ )
573
+ end
574
+ end
575
+ end
576
+
577
+ table(crop_data) do
578
+ column(0).width = 135
579
+ columns(1..6).width = 67.5
580
+ columns(0..6).style(font_style: :bold)
581
+ row(0).style(align: :center, background_color: 'd7d7d7', font_size: 8, font_style: :bold)
582
+ cells.padding = [1, 2.5]
583
+ cells.style(border_width: 0)
584
+ row(1).style(border_bottom_width: 0.5, font_style: nil)
585
+ end
586
+ stroke_bounds
587
+ end
588
+ end
589
+
590
+ def estimated_productivity
591
+ bounding_box([0, 455], height: 120, width: 540) do
592
+ estimatedproductivity = @rep[:estimatedproductivity]
593
+
594
+ production_data = [
595
+ [{content: "Estimated Productivity", colspan: 4}],
596
+ ["Commodity Identification", "Unit Measure", "Estimated Yield", " "]
597
+ ]
598
+
599
+ if estimatedproductivity && estimatedproductivity.any?
600
+ estimatedproductivity.each do |p|
601
+ production_data.push(
602
+ ["#{p[:commodity]}", "#{p[:unit]}", "#{p[:estmtdyield]}"]
603
+ )
604
+ end
605
+ end
606
+
607
+ table(production_data) do
608
+ columns(0..3).width = 135
609
+ columns(0..3).style(font_style: :bold)
610
+ cells.padding = [1, 2.5]
611
+ cells.style(border_width: 0)
612
+ row(0).style(align: :center, background_color: 'd7d7d7', font_size: 8, font_style: :bold)
613
+ row(1).style(border_bottom_width: 0.5, font_style: nil)
614
+ end
615
+
616
+ stroke_bounds
617
+ end
618
+ end
619
+
620
+ def perm_plantings
621
+ bounding_box([0, 335], height: 135, width: 540) do
622
+ planting_data = [
623
+ [{content: "Permanent Plantings", colspan: 8}],
624
+ ["Planting", "Variety", "Ac. Type", "No. of Acres", "Average Age", "Plants/Acre", "Unit Description", "Average Yield"]
625
+ ]
626
+
627
+ plantings = @rep[:plantings]
628
+
629
+ if plantings # node could be nil
630
+ plantings.each do |p|
631
+ if p # array could be empty
632
+ planting_data.push(
633
+ [p[:planting], p[:variety], p[:acres], p[:numacres], p[:avgage], p[:plantsacre], p[:unitdescr1], p[:avgproduction]]
634
+ )
635
+ end
636
+ end
637
+ end
638
+
639
+ table(planting_data) do
640
+ columns(0..7).width = 67.5
641
+ columns(0..7).style(font_style: :bold)
642
+ row(0).style(align: :center, background_color: 'd7d7d7', font_size: 8, font_style: :bold)
643
+ cells.padding = [1, 2.5]
644
+ cells.style(border_width: 0)
645
+ row(1).style(border_bottom_width: 0.5, font_style: nil)
646
+ end
647
+
648
+ stroke_bounds
649
+ end
650
+ end
651
+
652
+ def externalities
653
+ bounding_box([0, 200], height: 200, width: 540) do
654
+
655
+ extern_data = ReportUtils.conditional_externs_array(@rep) # instance method to populate dynamic array
656
+ table(extern_data) do
657
+ columns(0..3).width = 135
658
+ columns([1, 3]).style(font_style: :bold)
659
+ row(0).style(align: :center, background_color: 'd7d7d7', font_size: 8, font_style: :bold)
660
+ cells.padding = [1, 2.5]
661
+ cells.style(border_width: 0)
662
+ end
663
+
664
+ stroke_bounds
665
+ end
666
+ end
667
+
668
+ def additional_images
669
+ if @property_images && @property_images.any?
670
+ if @property_images[1]
671
+ bounding_box([0, 690], height: 330, width: 250) do
672
+ image @property_images[1], width: 240
673
+ end
674
+ end
675
+
676
+ if @property_images[2]
677
+ bounding_box([270, 690], height: 330, width: 250) do
678
+ image @property_images[2], width: 240
679
+ end
680
+ end
681
+
682
+ if @property_images[3]
683
+ bounding_box([0, 340], height: 330, width: 250) do
684
+ image @property_images[3], width: 240
685
+ end
686
+ end
687
+
688
+ if @property_images[4]
689
+ bounding_box([270, 340], height: 330, width: 250) do
690
+ image @property_images[4], width: 240
691
+ end
692
+ end
693
+ end
694
+ end
695
+ end
696
+
697
+ # misc. notes
698
+
699
+ # the first page of the report is split into two columns using bounding boxes, each is 270 p wide
700
+
701
+ ##### tables ######
702
+ # - 4 column layout [68, 67, 67, 68]
703
+ # - 3 column tables can be used if needed [90, 90, 90]
704
+ # - 2 column layout [135, 135]
705
+ # - for dynamic layouts within an existing layout, pass colspan and content to data array like so:
706
+ # - {content: "double span", colspan: 2} => spans two columns with content