Skip to content

Commit

Permalink
Adds the ability to get attributes for Report Cells
Browse files Browse the repository at this point in the history
Note that this means that Report Cells are no longer core classes
(e.g String), but are Report::Cell instances.

ReportCell is a SimpleDelegator to the type of the underlying
value (usually String from Xero), but this may make strict equality
tests fail.
  • Loading branch information
nikz committed Feb 25, 2018
1 parent 230265f commit 1177ef8
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 116 deletions.
146 changes: 82 additions & 64 deletions lib/xero_gateway/report.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
require_relative './report/cell'

module XeroGateway
class Report

include Money
include Dates

Expand All @@ -17,78 +20,93 @@ def initialize(params={})
end
end

def self.from_xml(report_element)
report = Report.new
report_element.children.each do | element |
case element.name
when 'ReportID' then report.report_id = element.text
when 'ReportName' then report.report_name = element.text
when 'ReportType' then report.report_type = element.text
when 'ReportTitles'
each_title(element) do |title|
report.report_titles << title
end
when 'ReportDate' then report.report_date = Date.parse(element.text)
when 'UpdatedDateUTC' then report.updated_at = parse_date_time_utc(element.text)
when 'Rows'
report.column_names ||= find_body_column_names(element)
each_row_content(element) do |content_hash|
report.body << OpenStruct.new(content_hash)
end
class << self

def from_xml(report_element)
report = Report.new
report_element.children.each do | element |
case element.name
when 'ReportID' then report.report_id = element.text
when 'ReportName' then report.report_name = element.text
when 'ReportType' then report.report_type = element.text
when 'ReportTitles'
each_title(element) do |title|
report.report_titles << title
end
when 'ReportDate' then report.report_date = Date.parse(element.text)
when 'UpdatedDateUTC' then report.updated_at = parse_date_time_utc(element.text)
when 'Rows'
report.column_names ||= find_body_column_names(element)
each_row_content(element) do |content_hash|
report.body << OpenStruct.new(content_hash)
end
end
end
report
end
report
end

private
private

def self.each_row_content(xml_element, &block)
column_names = find_body_column_names(xml_element).keys
xpath_body = REXML::XPath.first(xml_element, "//RowType[text()='Section']").parent
rows_contents = []
xpath_body.elements.each("Rows/Row") do |xpath_cells|
values = find_body_cell_values(xpath_cells)
content_hash = Hash[column_names.zip values]
rows_contents << content_hash
yield content_hash if block_given?
end
rows_contents
end
def each_row_content(xml_element, &block)
column_names = find_body_column_names(xml_element).keys
xpath_body = REXML::XPath.first(xml_element, "//RowType[text()='Section']").parent
rows_contents = []
xpath_body.elements.each("Rows/Row") do |xpath_cells|
values = find_body_cell_values(xpath_cells)
content_hash = Hash[column_names.zip values]
rows_contents << content_hash
yield content_hash if block_given?
end
rows_contents
end

def self.each_title(xml_element, &block)
xpath_titles = REXML::XPath.first(xml_element, "//ReportTitles")
xpath_titles.elements.each("//ReportTitle") do |xpath_title|
title = xpath_title.text.strip
yield title if block_given?
end
end
def each_title(xml_element, &block)
xpath_titles = REXML::XPath.first(xml_element, "//ReportTitles")
xpath_titles.elements.each("//ReportTitle") do |xpath_title|
title = xpath_title.text.strip
yield title if block_given?
end
end

def self.find_body_cell_values(xml_cells)
values = []
xml_cells.elements.each("Cells/Cell") do |xml_cell|
if value = xml_cell.children.first # finds <Value>...</Value>
values << value.text.try(:strip)
next
def find_body_cell_values(xml_cells)
values = []
xml_cells.elements.each("Cells/Cell") do |xml_cell|
if value = xml_cell.children.first # finds <Value>...</Value>
values << Cell.new(value.text.try(:strip), collect_attributes(xml_cell))
next
end
values << nil
end
values
end
values << nil
end
values
end

# returns something like { column_1: "Amount", column_2: "Description", ... }
def self.find_body_column_names(body)
header = REXML::XPath.first(body, "//RowType[text()='Header']")
names_map = {}
column_count = 0
header.parent.elements.each("Cells/Cell") do |header_cell|
column_count += 1
column_key = "column_#{column_count}".to_sym
column_name = nil
name_value = header_cell.children.first
column_name = name_value.text.strip unless name_value.blank? # finds <Value>...</Value>
names_map[column_key] = column_name
end
names_map
# Collects "<Attribute>" elements into a hash
def collect_attributes(xml_cell)
Array.wrap(xml_cell.elements["Attributes/Attribute"]).inject({}) do |hash, xml_attribute|
if (key = xml_attribute.elements["Id"].try(:text)) &&
(value = xml_attribute.elements["Value"].try(:text))

hash[key] = value
end
hash
end.symbolize_keys
end

# returns something like { column_1: "Amount", column_2: "Description", ... }
def find_body_column_names(body)
header = REXML::XPath.first(body, "//RowType[text()='Header']")
names_map = {}
column_count = 0
header.parent.elements.each("Cells/Cell") do |header_cell|
column_count += 1
column_key = "column_#{column_count}".to_sym
column_name = nil
name_value = header_cell.children.first
column_name = name_value.text.strip unless name_value.blank? # finds <Value>...</Value>
names_map[column_key] = column_name
end
names_map
end
end

end
Expand Down
28 changes: 28 additions & 0 deletions lib/xero_gateway/report/cell.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module XeroGateway
class Report

# Adds #attributes to the cells we're grabbing, since Xero Report Cells use XML like:
# <Cell>
# <Value>Interest Income (270)</Value>
# <Attributes>
# <Attribute>
# <Value>e9482110-7245-4a76-bfe2-14500495a076</Value>
# <Id>account</Id>
# </Attribute>
# </Attributes>
# </Cell>
#
# We delegate to the topmost "<Value>" class and decorate with an "attributes" hash
# for the "attribute: value" pairs
class Cell < SimpleDelegator
attr_reader :attributes, :value

def initialize(value, new_attributes = {})
@value = value
@attributes = new_attributes
super(value)
end
end

end
end
10 changes: 6 additions & 4 deletions test/stub_responses/reports/bank_statement.xml
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,12 @@
<Value>2014-05-01T00:00:00</Value>
</Cell>
<Cell>
<Attribute>
<Value>asasdas-asdf-asdf-asdf-asdfasdfasdfas</Value>
<Id>statementID</Id>
</Attribute>
<Attributes>
<Attribute>
<Value>asasdas-asdf-asdf-asdf-asdfasdfasdfas</Value>
<Id>statementID</Id>
</Attribute>
</Attributes>
</Cell>
<Cell>
<Value>Eft </Value>
Expand Down
117 changes: 69 additions & 48 deletions test/unit/report_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,60 +19,81 @@ class ReportTest < Test::Unit::TestCase
end

context :from_xml do
setup do
xml_response = get_file("reports/bank_statement.xml")
xml_response.gsub!(/\n +/,'')
xml_doc = REXML::Document.new(xml_response)
xpath_report = XPath.first(xml_doc, "//Report")
@report = XeroGateway::Report.from_xml(xpath_report)
end
context "with a bank statement report" do
setup do
@report = make_report_from_xml("bank_statement")
end

should "create a bank statement report" do
assert @report.is_a?(XeroGateway::Report)
assert_equal [], @report.errors
assert_equal Date.parse("27 May 2014"), @report.report_date
assert_equal "BankStatement", @report.report_id
assert_equal "Bank Statement", @report.report_name
expected_titles = ["Bank Statement", "Business Bank Account", "Demo Company (NZ)", "From 1 May 2014 to 27 May 2014"]
assert_equal expected_titles, @report.report_titles
assert_equal "BankStatement", @report.report_type
assert_equal Time.parse("2014-05-26 22:36:07 +0000").to_i, @report.updated_at.to_i
expected_names = { :column_1=>"Date", :column_2=>"Description", :column_3=>"Reference", :column_4=>"Reconciled", :column_5=>"Source", :column_6=>"Amount", :column_7=>"Balance" }
assert_equal expected_names, @report.column_names

should "create a bank statement report" do
assert @report.is_a?(XeroGateway::Report)
assert_equal [], @report.errors
assert_equal Date.parse("27 May 2014"), @report.report_date
assert_equal "BankStatement", @report.report_id
assert_equal "Bank Statement", @report.report_name
expected_titles = ["Bank Statement", "Business Bank Account", "Demo Company (NZ)", "From 1 May 2014 to 27 May 2014"]
assert_equal expected_titles, @report.report_titles
assert_equal "BankStatement", @report.report_type
assert_equal Time.parse("2014-05-26 22:36:07 +0000").to_i, @report.updated_at.to_i
expected_names = { :column_1=>"Date", :column_2=>"Description", :column_3=>"Reference", :column_4=>"Reconciled", :column_5=>"Source", :column_6=>"Amount", :column_7=>"Balance" }
assert_equal expected_names, @report.column_names
###
# REPORT BODY
assert @report.body.is_a?(Array)

###
# REPORT BODY
assert @report.body.is_a?(Array)
# First = Opening Balance
first_statement = @report.body.first
assert_equal "2014-05-01T00:00:00", first_statement.column_1
assert_equal "Opening Balance", first_statement.column_2
assert_equal nil, first_statement.column_3
assert_equal nil, first_statement.column_4
assert_equal nil, first_statement.column_5
assert_equal nil, first_statement.column_6
assert_equal "15461.97", first_statement.column_7

# First = Opening Balance
first_statement = @report.body.first
assert_equal "2014-05-01T00:00:00", first_statement.column_1
assert_equal "Opening Balance", first_statement.column_2
assert_equal nil, first_statement.column_3
assert_equal nil, first_statement.column_4
assert_equal nil, first_statement.column_5
assert_equal nil, first_statement.column_6
assert_equal "15461.97", first_statement.column_7
# Second = Bank Transaction/Statement
second_statement = @report.body.second
assert_equal "2014-05-01T00:00:00", second_statement.column_1
assert_equal "Ridgeway Banking Corporation", second_statement.column_2
assert_equal "Fee", second_statement.column_3
assert_equal "No", second_statement.column_4
assert_equal "Import", second_statement.column_5
assert_equal "-15.00", second_statement.column_6
assert_equal "15446.97", second_statement.column_7

# Third
third_statement = @report.body.third
assert_equal nil, third_statement.column_2.value # no description, but other attributes
assert_equal "Eft", third_statement.column_3
assert_equal "No", third_statement.column_4
assert_equal "Import", third_statement.column_5
assert_equal "-15.75", third_statement.column_6
assert_equal "15431.22", third_statement.column_7
end
end

# Second = Bank Transaction/Statement
second_statement = @report.body.second
assert_equal "2014-05-01T00:00:00", second_statement.column_1
assert_equal "Ridgeway Banking Corporation", second_statement.column_2
assert_equal "Fee", second_statement.column_3
assert_equal "No", second_statement.column_4
assert_equal "Import", second_statement.column_5
assert_equal "-15.00", second_statement.column_6
assert_equal "15446.97", second_statement.column_7
context "with a trial balance report" do
setup do
@report = make_report_from_xml("trial_balance")
end

# Third
third_statement = @report.body.third
assert_equal nil, third_statement.column_2 # no description, but other attributes
assert_equal "Eft", third_statement.column_3
assert_equal "No", third_statement.column_4
assert_equal "Import", third_statement.column_5
assert_equal "-15.75", third_statement.column_6
assert_equal "15431.22", third_statement.column_7
should "set attributes on individual cells" do
first_statement = @report.body.first
assert_equal "Sales (200)", first_statement.column_1.value
assert_equal({ account: "7d05a53d-613d-4eb2-a2fc-dcb6adb80b80" }, first_statement.column_1.attributes)
end
end

end

private

def make_report_from_xml(report_name = "bank_statement")
xml_response = get_file("reports/#{report_name}.xml")
xml_response.gsub!(/\n +/,'')
xml_doc = REXML::Document.new(xml_response)
xpath_report = XPath.first(xml_doc, "//Report")
XeroGateway::Report.from_xml(xpath_report)
end

end

0 comments on commit 1177ef8

Please sign in to comment.