diff --git a/lib/xero_gateway/report.rb b/lib/xero_gateway/report.rb index b5593045..f197ab95 100644 --- a/lib/xero_gateway/report.rb +++ b/lib/xero_gateway/report.rb @@ -1,5 +1,8 @@ +require_relative './report/cell' + module XeroGateway class Report + include Money include Dates @@ -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 ... - 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 ... + 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 ... - names_map[column_key] = column_name - end - names_map + # Collects "" 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 ... + names_map[column_key] = column_name + end + names_map + end end end diff --git a/lib/xero_gateway/report/cell.rb b/lib/xero_gateway/report/cell.rb new file mode 100644 index 00000000..b1a95fe0 --- /dev/null +++ b/lib/xero_gateway/report/cell.rb @@ -0,0 +1,28 @@ +module XeroGateway + class Report + + # Adds #attributes to the cells we're grabbing, since Xero Report Cells use XML like: + # + # Interest Income (270) + # + # + # e9482110-7245-4a76-bfe2-14500495a076 + # account + # + # + # + # + # We delegate to the topmost "" 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 \ No newline at end of file diff --git a/test/stub_responses/reports/bank_statement.xml b/test/stub_responses/reports/bank_statement.xml index d8cf0b8f..ef53452d 100644 --- a/test/stub_responses/reports/bank_statement.xml +++ b/test/stub_responses/reports/bank_statement.xml @@ -97,10 +97,12 @@ 2014-05-01T00:00:00 - - asasdas-asdf-asdf-asdf-asdfasdfasdfas - statementID - + + + asasdas-asdf-asdf-asdf-asdfasdfasdfas + statementID + + Eft diff --git a/test/unit/report_test.rb b/test/unit/report_test.rb index 2abe6006..e46eee16 100644 --- a/test/unit/report_test.rb +++ b/test/unit/report_test.rb @@ -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