From a80c87be9beefa22a9751c8e6d42e2dfe144dc97 Mon Sep 17 00:00:00 2001 From: Jared Armstrong Date: Mon, 25 Jan 2016 22:47:34 +0000 Subject: [PATCH 1/3] Add tracking category options data --- lib/xero_gateway/tracking_category.rb | 52 ++++++++++++++++++++------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/lib/xero_gateway/tracking_category.rb b/lib/xero_gateway/tracking_category.rb index 00cad322..f813f121 100644 --- a/lib/xero_gateway/tracking_category.rb +++ b/lib/xero_gateway/tracking_category.rb @@ -18,19 +18,43 @@ # module XeroGateway class TrackingCategory - attr_accessor :tracking_category_id, :name, :options - + attr_accessor :tracking_category_id, :name, :status, :options + attr_accessor :all_options + + class Option + attr_accessor :tracking_option_id, :name, :status + + def initialize(params = {}) + params.each do |k,v| + self.send("#{k}=", v) + end + end + + def self.from_xml(option_element) + option = Option.new + option_element.children.each do |element| + case(element.name) + when "TrackingOptionID" then option.tracking_option_id = element.text + when "Name" then option.name = element.text + when "Status" then option.status = element.text + end + end + option + end + end + def initialize(params = {}) @options = [] + @all_options = [] params.each do |k,v| self.send("#{k}=", v) end end - + def option options[0] if options.size == 1 end - + def to_xml(b = Builder::XmlMarkup.new) b.TrackingCategory { b.TrackingCategoryID tracking_category_id unless tracking_category_id.nil? @@ -45,43 +69,45 @@ def to_xml(b = Builder::XmlMarkup.new) else b.Option { b.Name self.options.to_s - } + } end } } end - + # When a tracking category is serialized as part of an invoice it may only have a single # option, and the Options tag is omitted def to_xml_for_invoice_messages(b = Builder::XmlMarkup.new) b.TrackingCategory { b.TrackingCategoryID self.tracking_category_id unless tracking_category_id.nil? b.Name self.name - b.Option self.options.is_a?(Array) ? self.options.first : self.options.to_s - } + b.Option self.options.is_a?(Array) ? self.options.first : self.options.to_s + } end - + def self.from_xml(tracking_category_element) tracking_category = TrackingCategory.new tracking_category_element.children.each do |element| case(element.name) when "TrackingCategoryID" then tracking_category.tracking_category_id = element.text when "Name" then tracking_category.name = element.text + when "Status" then tracking_category.status = element.text when "Options" then element.children.each do |option_child| tracking_category.options << option_child.children.detect {|c| c.name == "Name"}.text + tracking_category.all_options << Option.from_xml(option_child) end when "Option" then tracking_category.options << element.text end end - tracking_category - end - + tracking_category + end + def ==(other) [:tracking_category_id, :name, :options].each do |field| return false if send(field) != other.send(field) end return true - end + end end end From 00edb79182fd987548053374d67ffc48d3472ac3 Mon Sep 17 00:00:00 2001 From: Jared Armstrong Date: Thu, 28 Jan 2016 17:06:47 +0000 Subject: [PATCH 2/3] Add support for Items. (Also refactors some records to use a default BaseRecord) --- lib/xero_gateway.rb | 2 + lib/xero_gateway/base_record.rb | 97 +++++++++++++++++++++++++++++ lib/xero_gateway/currency.rb | 62 +++---------------- lib/xero_gateway/gateway.rb | 9 +++ lib/xero_gateway/item.rb | 26 ++++++++ lib/xero_gateway/organisation.rb | 98 +++++++----------------------- lib/xero_gateway/response.rb | 1 + lib/xero_gateway/tax_rate.rb | 74 ++++------------------ test/integration/get_items_test.rb | 25 ++++++++ test/stub_responses/item.xml | 14 +++++ test/stub_responses/items.xml | 53 ++++++++++++++++ test/unit/item_test.rb | 50 +++++++++++++++ 12 files changed, 320 insertions(+), 191 deletions(-) create mode 100644 lib/xero_gateway/base_record.rb create mode 100644 lib/xero_gateway/item.rb create mode 100644 test/integration/get_items_test.rb create mode 100644 test/stub_responses/item.xml create mode 100644 test/stub_responses/items.xml create mode 100644 test/unit/item_test.rb diff --git a/lib/xero_gateway.rb b/lib/xero_gateway.rb index 35dc45ed..75f5fa69 100644 --- a/lib/xero_gateway.rb +++ b/lib/xero_gateway.rb @@ -18,6 +18,7 @@ require File.join(File.dirname(__FILE__), 'xero_gateway', 'money') require File.join(File.dirname(__FILE__), 'xero_gateway', 'line_item_calculations') require File.join(File.dirname(__FILE__), 'xero_gateway', 'response') +require File.join(File.dirname(__FILE__), 'xero_gateway', 'base_record') require File.join(File.dirname(__FILE__), 'xero_gateway', 'account') require File.join(File.dirname(__FILE__), 'xero_gateway', 'accounts_list') require File.join(File.dirname(__FILE__), 'xero_gateway', 'tracking_category') @@ -36,6 +37,7 @@ require File.join(File.dirname(__FILE__), 'xero_gateway', 'organisation') require File.join(File.dirname(__FILE__), 'xero_gateway', 'tax_rate') require File.join(File.dirname(__FILE__), 'xero_gateway', 'currency') +require File.join(File.dirname(__FILE__), 'xero_gateway', 'item') require File.join(File.dirname(__FILE__), 'xero_gateway', 'error') require File.join(File.dirname(__FILE__), 'xero_gateway', 'oauth') require File.join(File.dirname(__FILE__), 'xero_gateway', 'exceptions') diff --git a/lib/xero_gateway/base_record.rb b/lib/xero_gateway/base_record.rb new file mode 100644 index 00000000..36ee77bc --- /dev/null +++ b/lib/xero_gateway/base_record.rb @@ -0,0 +1,97 @@ +module XeroGateway + class BaseRecord + class_attribute :element_name + class_attribute :attribute_definitions + + class << self + def attributes(hash) + hash.each do |k, v| + attribute k, v + end + end + + def attribute(name, value) + self.attribute_definitions ||= {} + self.attribute_definitions[name] = value + + case value + when Hash + value.each do |k, v| + attribute("#{name}#{k}", v) + end + else + attr_accessor name.underscore + end + end + + def from_xml(base_element) + new.from_xml(base_element) + end + + def xml_element + element_name || self.name.split('::').last + end + end + + def initialize(params = {}) + params.each do |k,v| + self.send("#{k}=", v) if respond_to?("#{k}=") + end + end + + def ==(other) + to_xml == other.to_xml + end + + def to_xml + builder = Builder::XmlMarkup.new + builder.__send__(self.class.xml_element) do + to_xml_attributes(builder) + end + end + + def from_xml(base_element) + from_xml_attributes(base_element) + self + end + + def from_xml_attributes(element, attribute = nil, attr_definition = self.class.attribute_definitions) + if Hash === attr_definition + element.children.each do |child| + next unless child.respond_to?(:name) + + child_attribute = child.name + child_attr_definition = attr_definition[child_attribute] + next unless child_attr_definition + + from_xml_attributes(child, "#{attribute}#{child_attribute}", child_attr_definition) + end + + return + end + + value = case attr_definition + when :boolean then element.text == "true" + when :float then element.text.to_f + when :integer then element.text.to_i + else element.text + end if element.text.present? + + send("#{attribute.underscore}=", value) + end + + def to_xml_attributes(builder = Builder::XmlMarkup.new, path = nil, attr_definitions = self.class.attribute_definitions) + attr_definitions.each do |attr, value| + case value + when Hash + builder.__send__(attr) do + to_xml_attributes(builder, "#{path}#{attr}", value) + end + else + builder.__send__(attr, send("#{path}#{attr}".underscore)) + end + end + end + + end +end diff --git a/lib/xero_gateway/currency.rb b/lib/xero_gateway/currency.rb index 7398016b..4a315e66 100644 --- a/lib/xero_gateway/currency.rb +++ b/lib/xero_gateway/currency.rb @@ -1,56 +1,10 @@ module XeroGateway - class Currency - - unless defined? ATTRS - ATTRS = { - "Code" => :string, # 3 letter alpha code for the currency – see list of currency codes - "Description" => :string, # Name of Currency - } - end - - attr_accessor *ATTRS.keys.map(&:underscore) - - def initialize(params = {}) - params.each do |k,v| - self.send("#{k}=", v) - end - end - - def ==(other) - ATTRS.keys.map(&:underscore).each do |field| - return false if send(field) != other.send(field) - end - return true - end - - def to_xml - b = Builder::XmlMarkup.new - - b.Currency do - ATTRS.keys.each do |attr| - eval("b.#{attr} '#{self.send(attr.underscore.to_sym)}'") - end - end - end - - def self.from_xml(currency_element) - Currency.new.tap do |currency| - currency_element.children.each do |element| - - attribute = element.name - underscored_attribute = element.name.underscore - - raise "Unknown attribute: #{attribute}" unless ATTRS.keys.include?(attribute) - - case (ATTRS[attribute]) - when :boolean then currency.send("#{underscored_attribute}=", (element.text == "true")) - when :float then currency.send("#{underscored_attribute}=", element.text.to_f) - else currency.send("#{underscored_attribute}=", element.text) - end - - end - end - end - + class Currency < BaseRecord + + attributes({ + "Code" => :string, # 3 letter alpha code for the currency – see list of currency codes + "Description" => :string, # Name of Currency + }) + end -end \ No newline at end of file +end diff --git a/lib/xero_gateway/gateway.rb b/lib/xero_gateway/gateway.rb index c648754e..572daaa1 100644 --- a/lib/xero_gateway/gateway.rb +++ b/lib/xero_gateway/gateway.rb @@ -557,6 +557,14 @@ def get_tax_rates parse_response(response_xml, {}, {:request_signature => 'GET/tax_rates'}) end + # + # Gets all Items for a specific organisation in Xero + # + def get_items + response_xml = http_get(@client, "#{xero_url}/Items") + parse_response(response_xml, {}, {:request_signature => 'GET/items'}) + end + # # Create Payment record in Xero # @@ -761,6 +769,7 @@ def parse_response(raw_response, request = {}, options = {}) when "CreditNotes" then element.children.each {|child| response.response_item << CreditNote.from_xml(child, self, {:line_items_downloaded => options[:request_signature] != "GET/CreditNotes"}) } when "Accounts" then element.children.each {|child| response.response_item << Account.from_xml(child) } when "TaxRates" then element.children.each {|child| response.response_item << TaxRate.from_xml(child) } + when "Items" then element.children.each {|child| response.response_item << Item.from_xml(child) } when "Currencies" then element.children.each {|child| response.response_item << Currency.from_xml(child) } when "Organisations" then response.response_item = Organisation.from_xml(element.children.first) # Xero only returns the Authorized Organisation when "TrackingCategories" then element.children.each {|child| response.response_item << TrackingCategory.from_xml(child) } diff --git a/lib/xero_gateway/item.rb b/lib/xero_gateway/item.rb new file mode 100644 index 00000000..c637dc97 --- /dev/null +++ b/lib/xero_gateway/item.rb @@ -0,0 +1,26 @@ +module XeroGateway + class Item < BaseRecord + attributes({ + "Code" => :string, + "InventoryAssetAccountCode" => :string, + "Name" => :string, + "IsSold" => :boolean, + "IsPurchased" => :boolean, + "Description" => :string, + "PurchaseDescription" => :string, + "IsTrackedAsInventory" => :boolean, + "TotalCostPool" => :float, + "QuantityOnHand" => :integer, + + "SalesDetails" => { + "UnitPrice" => :float, + "AccountCode" => :string + }, + + "PurchaseDetails" => { + "UnitPrice" => :float, + "AccountCode" => :string + } + }) + end +end diff --git a/lib/xero_gateway/organisation.rb b/lib/xero_gateway/organisation.rb index a90b508a..346f3c14 100644 --- a/lib/xero_gateway/organisation.rb +++ b/lib/xero_gateway/organisation.rb @@ -1,78 +1,24 @@ module XeroGateway - class Organisation - - unless defined? ATTRS - ATTRS = { - "Name" => :string, # Display name of organisation shown in Xero - "LegalName" => :string, # Organisation name shown on Reports - "PaysTax" => :boolean, # Boolean to describe if organisation is registered with a local tax authority i.e. true, false - "Version" => :string, # See Version Types - "BaseCurrency" => :string, # Default currency for organisation. See Currency types - "OrganisationType" => :string, # only returned for "real" (i.e non-demo) companies - "OrganisationStatus" => :string, - "IsDemoCompany" => :boolean, - "APIKey" => :string, # returned if organisations are linked via Xero Network - "CountryCode" => :string, - "TaxNumber" => :string, - "FinancialYearEndDay" => :string, - "FinancialYearEndMonth" => :string, - "PeriodLockDate" => :string, - "CreatedDateUTC" => :string, - "ShortCode" => :string, - "Timezone" => :string, - "LineOfBusiness" => :string - } - end - - attr_accessor *ATTRS.keys.map(&:underscore) - - def initialize(params = {}) - params.each do |k,v| - self.send("#{k}=", v) - end - end - - def ==(other) - ATTRS.keys.map(&:underscore).each do |field| - return false if send(field) != other.send(field) - end - return true - end - - def to_xml - b = Builder::XmlMarkup.new - - b.Organisation do - ATTRS.keys.each do |attr| - eval("b.#{attr} '#{self.send(attr.underscore.to_sym)}'") - end - end - end - - def self.from_xml(organisation_element) - Organisation.new.tap do |org| - organisation_element.children.each do |element| - - attribute = element.name - underscored_attribute = element.name.underscore - - if ATTRS.keys.include?(attribute) - - case (ATTRS[attribute]) - when :boolean then org.send("#{underscored_attribute}=", (element.text == "true")) - when :float then org.send("#{underscored_attribute}=", element.text.to_f) - else org.send("#{underscored_attribute}=", element.text) - end - - else - - warn "Ignoring unknown attribute: #{attribute}" - - end - - end - end - end - + class Organisation < BaseRecord + attributes({ + "Name" => :string, # Display name of organisation shown in Xero + "LegalName" => :string, # Organisation name shown on Reports + "PaysTax" => :boolean, # Boolean to describe if organisation is registered with a local tax authority i.e. true, false + "Version" => :string, # See Version Types + "BaseCurrency" => :string, # Default currency for organisation. See Currency types + "OrganisationType" => :string, # only returned for "real" (i.e non-demo) companies + "OrganisationStatus" => :string, + "IsDemoCompany" => :boolean, + "APIKey" => :string, # returned if organisations are linked via Xero Network + "CountryCode" => :string, + "TaxNumber" => :string, + "FinancialYearEndDay" => :string, + "FinancialYearEndMonth" => :string, + "PeriodLockDate" => :string, + "CreatedDateUTC" => :string, + "ShortCode" => :string, + "Timezone" => :string, + "LineOfBusiness" => :string + }) end -end \ No newline at end of file +end diff --git a/lib/xero_gateway/response.rb b/lib/xero_gateway/response.rb index 9f97d459..c4c2cfc8 100644 --- a/lib/xero_gateway/response.rb +++ b/lib/xero_gateway/response.rb @@ -23,6 +23,7 @@ def array_wrapped_response_item alias_method :accounts, :array_wrapped_response_item alias_method :tracking_categories, :array_wrapped_response_item alias_method :tax_rates, :array_wrapped_response_item + alias_method :items, :array_wrapped_response_item alias_method :currencies, :array_wrapped_response_item alias_method :payments, :array_wrapped_response_item diff --git a/lib/xero_gateway/tax_rate.rb b/lib/xero_gateway/tax_rate.rb index d614ca55..dcb944b0 100644 --- a/lib/xero_gateway/tax_rate.rb +++ b/lib/xero_gateway/tax_rate.rb @@ -1,63 +1,15 @@ module XeroGateway - class TaxRate - - unless defined? ATTRS - ATTRS = { - "Name" => :string, - "TaxType" => :string, - "CanApplyToAssets" => :boolean, - "CanApplyToEquity" => :boolean, - "CanApplyToExpenses" => :boolean, - "CanApplyToLiabilities" => :boolean, - "CanApplyToRevenue" => :boolean, - "DisplayTaxRate" => :float, - "EffectiveRate" => :float - } - end - - attr_accessor *ATTRS.keys.map(&:underscore) - - def initialize(params = {}) - params.each do |k,v| - self.send("#{k}=", v) - end - end - - def ==(other) - ATTRS.keys.map(&:underscore).each do |field| - return false if send(field) != other.send(field) - end - return true - end - - def to_xml - b = Builder::XmlMarkup.new - - b.TaxRate do - ATTRS.keys.each do |attr| - eval("b.#{attr} '#{self.send(attr.underscore.to_sym)}'") - end - end - end - - def self.from_xml(tax_rate_element) - TaxRate.new.tap do |tax_rate| - tax_rate_element.children.each do |element| - - attribute = element.name - underscored_attribute = element.name.underscore - - next if !ATTRS.keys.include?(attribute) - - case (ATTRS[attribute]) - when :boolean then tax_rate.send("#{underscored_attribute}=", (element.text == "true")) - when :float then tax_rate.send("#{underscored_attribute}=", element.text.to_f) - else tax_rate.send("#{underscored_attribute}=", element.text) - end - - end - end - end - + class TaxRate < BaseRecord + attributes({ + "Name" => :string, + "TaxType" => :string, + "CanApplyToAssets" => :boolean, + "CanApplyToEquity" => :boolean, + "CanApplyToExpenses" => :boolean, + "CanApplyToLiabilities" => :boolean, + "CanApplyToRevenue" => :boolean, + "DisplayTaxRate" => :float, + "EffectiveRate" => :float + }) end -end \ No newline at end of file +end diff --git a/test/integration/get_items_test.rb b/test/integration/get_items_test.rb new file mode 100644 index 00000000..16d4c03e --- /dev/null +++ b/test/integration/get_items_test.rb @@ -0,0 +1,25 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class GetItemsTest < Test::Unit::TestCase + include TestHelper + + def setup + @gateway = XeroGateway::Gateway.new(CONSUMER_KEY, CONSUMER_SECRET) + + if STUB_XERO_CALLS + @gateway.xero_url = "DUMMY_URL" + + @gateway.stubs(:http_get).with {|client, url, params| url =~ /Items$/ }.returns(get_file_as_string("items.xml")) + end + end + + def test_get_items + result = @gateway.get_items + assert result.success? + assert !result.response_xml.nil? + + assert result.items.size > 0 + assert_equal XeroGateway::Item, result.items.first.class + assert_equal "An Untracked Item", result.items.first.name + end +end diff --git a/test/stub_responses/item.xml b/test/stub_responses/item.xml new file mode 100644 index 00000000..0dfcba1d --- /dev/null +++ b/test/stub_responses/item.xml @@ -0,0 +1,14 @@ + + Merino-2011-LG + Full Tracked Item + 2011 Merino Sweater - LARGE + 2011 Merino Sweater - LARGE + + 149.0000 + 300 + + + 299.0000 + 200 + + diff --git a/test/stub_responses/items.xml b/test/stub_responses/items.xml new file mode 100644 index 00000000..5a9b1c28 --- /dev/null +++ b/test/stub_responses/items.xml @@ -0,0 +1,53 @@ + + ace3dea1-4025-48cf-a91f-50759c32653e + OK + Xero Gateway Test + 2009-10-04T03:29:54.6788414Z + + + 19b79d12-0ae1-496e-9649-cbd04b15c7c5 + UnTrackedThing + I sell this untracked thing + I buy this untracked thing + 2015-09-22T22:37:55.527 + + 20.0000 + 400 + NONE + + + 40.0000 + 200 + OUTPUT2 + + An Untracked Item + false + true + true + + + 90a72d44-43e4-410d-a68b-1139ef0c0c07 + TrackedThing + I sell this tracked thing + I purchase this tracked thing + 2015-09-22T22:42:30.547 + + 20.0000 + 430 + NONE + + + 40.0000 + 200 + OUTPUT2 + + Tracked Thing + true + 630 + 200.00 + 10.0000 + true + true + + + diff --git a/test/unit/item_test.rb b/test/unit/item_test.rb new file mode 100644 index 00000000..e13544bb --- /dev/null +++ b/test/unit/item_test.rb @@ -0,0 +1,50 @@ +require File.join(File.dirname(__FILE__), '../test_helper.rb') + +class ItemTest < Test::Unit::TestCase + + # Tests that a item can be converted into XML that Xero can understand, and then converted back to a item + def test_build_and_parse_xml + item = create_test_item + + # Generate the XML message + item_as_xml = item.to_xml + + # Parse the XML message and retrieve the account element + item_element = REXML::XPath.first(REXML::Document.new(item_as_xml), "/Item") + + # Build a new account from the XML + result_item = XeroGateway::Item.from_xml(item_element) + + # Check the account details + assert_equal item, result_item + end + + def test_load_item_xml + xml_text = File.read("test/stub_responses/item.xml") + xml_doc = REXML::Document.new(xml_text) + xml = REXML::XPath.first(xml_doc, "/Item") + + item = XeroGateway::Item.from_xml(xml) + + assert_equal "Merino-2011-LG", item.code + assert_equal "Full Tracked Item", item.name + assert_equal "2011 Merino Sweater - LARGE", item.description + assert_equal "2011 Merino Sweater - LARGE", item.purchase_description + assert_equal 149.0, item.purchase_details_unit_price + assert_equal "300", item.purchase_details_account_code + assert_equal 299.0, item.sales_details_unit_price + assert_equal "200", item.sales_details_account_code + end + + + private + + def create_test_item + XeroGateway::Item.new.tap do |item| + item.code = "Merino-2011-LG" + item.name = "Full Tracked Item" + item.purchase_details_unit_price = 149.0 + item.purchase_details_account_code = "300" + end + end +end From 22a8c3fde04d06ff700ea4b917c777b4287c5aa1 Mon Sep 17 00:00:00 2001 From: Jared Armstrong Date: Sat, 6 Feb 2016 18:01:17 +0000 Subject: [PATCH 3/3] Add ItemID to Items --- lib/xero_gateway/item.rb | 1 + test/stub_responses/item.xml | 1 + test/unit/item_test.rb | 1 + 3 files changed, 3 insertions(+) diff --git a/lib/xero_gateway/item.rb b/lib/xero_gateway/item.rb index c637dc97..b0f25e6a 100644 --- a/lib/xero_gateway/item.rb +++ b/lib/xero_gateway/item.rb @@ -1,6 +1,7 @@ module XeroGateway class Item < BaseRecord attributes({ + "ItemID" => :string, "Code" => :string, "InventoryAssetAccountCode" => :string, "Name" => :string, diff --git a/test/stub_responses/item.xml b/test/stub_responses/item.xml index 0dfcba1d..0eabf4c7 100644 --- a/test/stub_responses/item.xml +++ b/test/stub_responses/item.xml @@ -1,4 +1,5 @@ + 19b79d12-0ae1-496e-9649-cbd04b15c7c5 Merino-2011-LG Full Tracked Item 2011 Merino Sweater - LARGE diff --git a/test/unit/item_test.rb b/test/unit/item_test.rb index e13544bb..8e556464 100644 --- a/test/unit/item_test.rb +++ b/test/unit/item_test.rb @@ -26,6 +26,7 @@ def test_load_item_xml item = XeroGateway::Item.from_xml(xml) + assert_equal "19b79d12-0ae1-496e-9649-cbd04b15c7c5", item.item_id assert_equal "Merino-2011-LG", item.code assert_equal "Full Tracked Item", item.name assert_equal "2011 Merino Sweater - LARGE", item.description