diff --git a/lib/xero_gateway/line_item.rb b/lib/xero_gateway/line_item.rb index 51a3770e..f763f081 100644 --- a/lib/xero_gateway/line_item.rb +++ b/lib/xero_gateway/line_item.rb @@ -3,73 +3,75 @@ module XeroGateway class LineItem include Money - + TAX_TYPE = Account::TAX_TYPE unless defined?(TAX_TYPE) # Any errors that occurred when the #valid? method called. attr_reader :errors # All accessible fields - attr_accessor :line_item_id, :description, :quantity, :unit_amount, :item_code, :tax_type, :tax_amount, :account_code, :tracking - + attr_accessor :line_item_id, :description, :quantity, :unit_amount, :discount_rate, :item_code, :tax_type, :tax_amount, :account_code, :tracking + def initialize(params = {}) @errors ||= [] @tracking ||= [] @quantity = 1 @unit_amount = BigDecimal.new('0') - + params.each do |k,v| self.send("#{k}=", v) end end - + # Validate the LineItem record according to what will be valid by the gateway. # - # Usage: + # Usage: # line_item.valid? # Returns true/false - # + # # Additionally sets line_item.errors array to an array of field/error. def valid? @errors = [] - + if !line_item_id.nil? && line_item_id !~ GUID_REGEX @errors << ['line_item_id', 'must be blank or a valid Xero GUID'] end - + unless description @errors << ['description', "can't be blank"] end - + if tax_type && !TAX_TYPE[tax_type] @errors << ['tax_type', "must be one of #{TAX_TYPE.keys.join('/')}"] end - + @errors.size == 0 end - + def has_tracking? return false if tracking.nil? - + if tracking.is_a?(Array) return tracking.any? else return tracking.is_a?(TrackingCategory) end end - + # Deprecated (but API for setter remains). # # As line_amount must equal quantity * unit_amount for the API call to pass, this is now # automatically calculated in the line_amount method. def line_amount=(value) end - + # Calculate the line_amount as quantity * unit_amount as this value must be correct # for the API call to succeed. def line_amount - quantity * unit_amount + total = quantity * unit_amount + total = total * (1 - (discount_rate / BigDecimal.new(100))) if discount_rate + total end - + def to_xml(b = Builder::XmlMarkup.new) b.LineItem { b.Description description @@ -79,6 +81,7 @@ def to_xml(b = Builder::XmlMarkup.new) b.TaxType tax_type if tax_type b.TaxAmount tax_amount if tax_amount b.LineAmount line_amount if line_amount + b.DiscountRate discount_rate if discount_rate b.AccountCode account_code if account_code if has_tracking? b.Tracking { @@ -92,7 +95,7 @@ def to_xml(b = Builder::XmlMarkup.new) end } end - + def self.from_xml(line_item_element) line_item = LineItem.new line_item_element.children.each do |element| @@ -105,6 +108,7 @@ def self.from_xml(line_item_element) when "TaxType" then line_item.tax_type = element.text when "TaxAmount" then line_item.tax_amount = BigDecimal.new(element.text) when "LineAmount" then line_item.line_amount = BigDecimal.new(element.text) + when "DiscountRate" then line_item.discount_rate = BigDecimal.new(element.text) when "AccountCode" then line_item.account_code = element.text when "Tracking" then element.children.each do | tracking_element | @@ -113,13 +117,13 @@ def self.from_xml(line_item_element) end end line_item - end + end def ==(other) - [:description, :quantity, :unit_amount, :tax_type, :tax_amount, :line_amount, :account_code, :item_code].each do |field| + [:description, :quantity, :unit_amount, :tax_type, :tax_amount, :line_amount, :discount_rate, :account_code, :item_code].each do |field| return false if send(field) != other.send(field) end return true end - end + end end diff --git a/test/unit/invoice_test.rb b/test/unit/invoice_test.rb index cac19d00..dd98901c 100644 --- a/test/unit/invoice_test.rb +++ b/test/unit/invoice_test.rb @@ -68,6 +68,18 @@ class InvoiceTest < Test::Unit::TestCase parsed_invoice = XeroGateway::Invoice.from_xml(invoice_element) assert_equal 'http://example.com?with=params&and=more', parsed_invoice.url end + + should "work with line_item discount rates" do + invoice = create_test_invoice + invoice.line_items.first.discount_rate = 27 + invoice_as_xml = invoice.to_xml + + invoice_element = REXML::XPath.first(REXML::Document.new(invoice_as_xml), "/Invoice") + result_invoice = XeroGateway::Invoice.from_xml(invoice_element) + + assert_equal(invoice, result_invoice) + assert_equal 27, result_invoice.line_items.first.discount_rate + end end # Tests the sub_total calculation and that setting it manually doesn't modify the data. @@ -154,6 +166,26 @@ def test_line_amount_calculation assert_equal(quantity * line_item.unit_amount, line_item.line_amount) end + def test_line_amount_discount_calculation + invoice = create_test_invoice + line_item = invoice.line_items.first + line_item.discount_rate = 12.5 + + # Make sure that everything adds up to begin with. + expected_amount = line_item.quantity * line_item.unit_amount * 0.875 + assert_equal(expected_amount, line_item.line_amount) + + # Change the line_amount and check that it doesn't modify anything. + line_item.line_amount = expected_amount * 10 + assert_equal(expected_amount, line_item.line_amount) + + # Change the quantity and check that the line_amount has been updated. + quantity = line_item.quantity + 2 + line_item.quantity = quantity + assert_not_equal(expected_amount, line_item.line_amount) + assert_equal(quantity * line_item.unit_amount * 0.875, line_item.line_amount) + end + # Ensure that the totalling methods don't raise exceptions, even when # invoice.line_items is empty. def test_totalling_methods_when_line_items_empty @@ -286,7 +318,7 @@ def test_instantiate_invoice_with_default_line_amount_types def test_optional_params eur_code = "EUR" eur_rate = 1.80 - + invoice = create_test_invoice(:url => 'http://example.com', :branding_theme_id => 'a94a78db-5cc6-4e26-a52b-045237e56e6e', :currency_code => eur_code, :currency_rate => eur_rate) assert_equal 'http://example.com', invoice.url assert_equal 'a94a78db-5cc6-4e26-a52b-045237e56e6e', invoice.branding_theme_id