Class: Quickbooks::Element

Inherits:
Object show all
Defined in:
lib/quickbooks/element.rb

Overview

Element is the parent of all QBXML elements (tags) that have children. This includes the outer wrapping QBXML tag, request and response tags such as CustomerAddRq and AccountModRs, data return tags such as AccountRet, and any data fields inside them that also have children. If you don’t know what those tags are, don’t worry, Model takes care of knowing how to handle them for you.

Element acts a lot like a Model but without the record-handling parts, and also acts a little like a Property.

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(attrs = {}) ⇒ Element

Create a new Element object, such as Account.new or ItemInventory.new. Marks item as a new record, so .save will send a create command to Quickbooks.



84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/quickbooks/element.rb', line 84

def initialize(attrs={})
  attrs = attrs.attributes if attrs.is_a?(Element)
  attrs = attrs.attributes.values if attrs.is_a?(Model)
  if attrs.is_a?(Array)
    # Allows just passing an array of attributes in.
    set_attrs = attrs.select {|a| self.class.xsd.index(a.class.short_name)}
    reject_attrs = attrs - set_attrs
    raise AttributeAssignmentError, "Attribute#{'s' if reject_attrs.length > 1} not available: #{reject_attrs.join(', ')}!\nAvailable attributes: #{self.class.xsd.children.collect {|i| i.name}.join(', ')}" unless reject_attrs.empty?
    attributes.concat(set_attrs.sort! {|a,b| Quickbooks::Element.order(self,a,b) })
  else
    self.attributes = attrs
  end
end

Class Attribute Details

.associationsObject (readonly)

Returns the value of attribute associations.



17
18
19
# File 'lib/quickbooks/element.rb', line 17

def associations
  @associations
end

.optionsObject (readonly)

Returns the value of attribute options.



17
18
19
# File 'lib/quickbooks/element.rb', line 17

def options
  @options
end

.propertiesObject (readonly)

Returns the value of attribute properties.



17
18
19
# File 'lib/quickbooks/element.rb', line 17

def properties
  @properties
end

.xsdObject

Returns the value of attribute xsd.



18
19
20
# File 'lib/quickbooks/element.rb', line 18

def xsd
  @xsd
end

Class Method Details

.instantiate(obj_or_attrs = {}) ⇒ Object

This is usually used internally, but is safe to use if you need it for edge cases. It is meant to create a new Element object as if you just read it from Quickbooks – marks it as not a new record. Call on an Element class, such as QB::Customer.instantiate(attributes).



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/quickbooks/element.rb', line 43

def instantiate(obj_or_attrs={})
  obj = allocate
  if obj_or_attrs.is_a?(Hash)
    obj_or_attrs.each do |key,value|
      key = key.to_s
      if options.include?(key)
        obj[key] = value
        next
      end
      next unless xsd.include?(key)
      attr_klass = QB[key]
      # Add the new value
      if value.is_a?(ElementCollection)
        value.each do |v|
          obj.attributes << v
        end
      else
        value = value.is_a?(attr_klass) ? value : attr_klass.new(value)
        obj.attributes << value if xsd.index(value.class.short_name)
      end
    end
  elsif obj_or_attrs.respond_to?(:to_element) && elem = obj_or_attrs.to_element && elem.is_a?(self)
    return elem
  else
    raise ArgumentError, "must supply a hash of arguments or a model object"
  end
  # Sort the values to the proper order
  suffix = short_name =~ /(Add|Mod|Ret)$/ ? $1 : ''
  obj.attributes.sort! do |a,b|
    begin
      Quickbooks::Element.order(obj,a,b)
    rescue => e
      raise e, "-> Comparing #{a.class.short_name.inspect} <=> #{b.class.short_name.inspect} in #{obj.class.short_name} XSD"
    end
  end
  obj.send(:clean!)
  obj
end

.order(obj, a, b) ⇒ Object

:nodoc:



28
29
30
31
32
33
34
35
# File 'lib/quickbooks/element.rb', line 28

def order(obj, a, b) #:nodoc:
  c = (
    obj.class.xsd.index(a.class.short_name) || obj.class.xsd.index(a.class.short_name + obj.send(:suffix))
  ) <=> (
    obj.class.xsd.index(b.class.short_name) || obj.class.xsd.index(b.class.short_name + obj.send(:suffix))
  )
  c == 0 ? a.instance_variable_get(:@collection_index).to_i <=> b.instance_variable_get(:@collection_index).to_i : c
end

.pluralizeObject



13
14
15
# File 'lib/quickbooks/element.rb', line 13

def self.pluralize
  Quickbooks::Elements
end

.unrefObject

Provides the class equivalent to the current class but without the Ref suffix.



38
39
40
# File 'lib/quickbooks/element.rb', line 38

def unref
  QB[short_name[0..-4]]
end

Instance Method Details

#[](key) ⇒ Object

Access an attribute value by its type – use a string, a symbol, or its Class constant.



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/quickbooks/element.rb', line 139

def [](key)
  return self.attributes = key if key.is_a?(Hash)
  key = case key
  when String
    key
  when Symbol
    key.to_s
  when Module
    key.short_name
  else
    raise RuntimeError, "boy, that is a weird key (type:#{key.class.name})"
  end
  return get_associated(key) if self.class.associations.include?(key)
  return options.has_key?(key) ? options[key] : self.class.options[key].default if self.class.options.include?(key)
  vals = attributes.select {|a| a.class.short_name == key}
  property_xsd = self.class.xsd.find(key.to_s)
  property_xsd ? (self.class.xsd.repeatable?(key.to_s) ? vals : vals[0]) : nil
end

#[]=(key, value) ⇒ Object

Set the value of an attribute. If key refers to an association, it associates the given value. If it is an option, it sets the option. Otherwise if it’s a valid attribute, it sets the attribute value.

Note: If multiple of this attribute are allowed, you must send an array of values as the value.



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/quickbooks/element.rb', line 162

def []=(key,value)
  key = case key
  when String
    key
  when Symbol
    key.to_s
  when Module
    key.short_name
  else
    raise RuntimeError, "man is that a weird kind of key to use (type:#{key.class.name})!"
  end
  return associate(key, value) if self.class.associations.include?(key)
  unless self.class.xsd.include?(key)
    if self.class.short_name =~ /(Add|Mod|Ret)$/ && self.class.xsd.include?(key + $1)
      key = key + $1
    else
      return options[key] = value if self.class.options.include?(key)
      raise Quickbooks::InvalidAttributeError, "'#{key}' is not a valid property or option name for #{self.class.name}!"
    end
  end
  # Instantiate the value into the attribute class, if necessary
  attr_klass = Quickbooks.get_constant(key.to_s)
  if self.class.xsd.repeatable?(key.to_s)
    if value.is_a?(ElementCollection) || value.is_a?(Array)
      attributes.reject! {|a| a.is_a?(attr_klass)}
      value.each do |val|
        val = attr_klass.new(val) unless val.is_a?(attr_klass)
        val.send(:dirty!)
        attributes << val
      end
    else
      # If it *should* be an array element, it shouldn't be set here as a single value. This is just for safeguard, so that syntax
      # always shows what is going on. For an array element, set it with: object.some_attr = [value]
      raise RuntimeError, "You can't set a single value into a multi-value element to using equals(=). Use \"element[:#{key}] << value\" to append, or wrap the value in an array -- \"element[:#{key}] = [ value ]\" -- if you want to completely replace the current value set."
    end
  else
    value = attr_klass.new(value) unless value.is_a?(attr_klass)
    value.send(:dirty!)
    # Remove the previous value
    attributes.reject! {|a| a.is_a?(attr_klass)}
    # Add the new value
    attributes << value
  end
  # Sort the values to the proper order
  attributes.sort! {|a,b| Quickbooks::Element.order(self, a, b) }
end

#add_error(msg) ⇒ Object

:nodoc:



114
115
116
# File 'lib/quickbooks/element.rb', line 114

def add_error(msg) #:nodoc:
  errors << [nil, msg]
end

#attributesObject

An array of attribute values. Each value is of course instantiated as its own type (some subclass of Element or Property). These attributes are automatically kept in order, and can be accessed somewhat like a hash using the #[] method.



123
124
125
# File 'lib/quickbooks/element.rb', line 123

def attributes
  @attributes ||= []
end

#attributes=(attrs) ⇒ Object

Sets whatever attributes you give it using []=. Does not remove attributes not given.



128
129
130
131
132
133
134
135
136
# File 'lib/quickbooks/element.rb', line 128

def attributes=(attrs)
  attrs.each do |k,v|
    if self.class.xsd.include?(k.to_s)
      self[k.to_s] = v
    else
      raise AttributeAssignmentError, "Attribute not available: #{k.to_s}!\nAvailable attributes: #{self.class.xsd.children.collect {|i| i.name}.join(', ')}"
    end
  end
end

#clean_attributesObject

The attributes that have not been changed.



243
244
245
# File 'lib/quickbooks/element.rb', line 243

def clean_attributes
  attributes.reject {|a| a.dirty?}
end

#delete(*keys) ⇒ Object

Remove an attribute from the object. Setting an attribute to nil will be an attribute set to nil, but if you remove the attribute completely, it won’t be considered in create/update operations.



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/quickbooks/element.rb', line 211

def delete(*keys)
  keys.each do |key|
    key = case key
    when String
      key
    when Symbol
      key.to_s
    when Module
      key.short_name
    else
      raise RuntimeError, "boy, that is a weird key (type:#{key.class.name})"
    end
    unless self.class.xsd.include?(key) || (self.class.short_name =~ /(Add|Mod|Ret)$/ && self.class.xsd.include?(key + $1))
      return options.delete(key) if self.class.options.include?(key)
      raise RuntimeError, "'#{key}' is not a valid property or option name for #{self.class.name}!"
    end
    # Instantiate the value into the attribute class, if necessary
    attr_klass = Quickbooks.get_constant(key.to_s)

    attributes.reject! {|a| a.is_a?(attr_klass)}
  end
end

#dirty?Boolean

Has anything in this object (or its descendents) changed?

Returns:



248
249
250
251
# File 'lib/quickbooks/element.rb', line 248

def dirty?
  # Test for any dirty elements
  @dirty || attributes.any? {|e| e.dirty?}
end

#dirty_attributes(include_required = false) ⇒ Object

The attributes that have been changed.



235
236
237
238
239
240
# File 'lib/quickbooks/element.rb', line 235

def dirty_attributes(include_required=false)
  attr_hash = attributes_hash # (Speedier when we only do it once)
  include_required ?
    attributes.select {|a| a.dirty? || self.class.xsd.required?(a.class.short_name) || !self.class.xsd.validate(self.class.short_name => attr_hash.except(a.class.short_name))} :
    attributes.select {|a| a.dirty?}
end

#errorsObject

Holds any errors from the last time valid? is run.



111
112
113
# File 'lib/quickbooks/element.rb', line 111

def errors
  @errors ||= []
end

#inspectObject

:nodoc:



118
119
120
# File 'lib/quickbooks/element.rb', line 118

def inspect #:nodoc:
  "<#{self.class.short_name}:##{object_id}#{' ' + options.collect {|k,v| "#{k}=#{v.inspect}"}.join(' ')}#{" [#{@collection_index}]" if instance_variables.include?('@collection_index')}\n  #{attributes.collect {|a| a.inspect}.join("\n").gsub(/\n/, "\n  ")}>"
end

#new_record?Boolean

I don’t remember why this nil? had to be modified. If the need pops back up, CREATE A TEST to show the need!! def nil? #:nodoc:

if self.class.xsd.include?('FullName') && self.class.xsd.include?('ListID') || (self.class.xsd.include?('TxnID') && !self.class.xsd.name =~ /DataExt/)
  self[:FullName].nil? && self[:ListID].nil? && self[:TxnID].nil?
else
  super
end

end

Returns:



330
331
332
# File 'lib/quickbooks/element.rb', line 330

def new_record?
  
end

#save_associationsObject

:nodoc:



309
310
311
312
313
314
315
316
317
318
319
# File 'lib/quickbooks/element.rb', line 309

def save_associations #:nodoc:
  # first save any of my associated items that aren't already existing
  self.class.associations.each_key do |association_name|
    if instance_variable_get("@#{association_name}") && self[association_name].new_record?
      self[association_name].save
      self[association_name] = self[association_name] # re-assigns the associated Ref
    end
  end
  # then save any of my children's associated items that aren't already existing
  attributes.each { |attv| attv.save_associations if attv.respond_to?(:save_associations) }
end

#to_dirty_xml(include_required = false) ⇒ Object

Dumps only the changed data into QBXML. Useful for updating only specific attributes without touching the rest. Set include_required to true if you need valid qbxml to send.



300
301
302
# File 'lib/quickbooks/element.rb', line 300

def to_dirty_xml(include_required=false)
  '<' + self.class.short_name + '>' +  ("\n" + dirty_attributes(include_required).collect {|a| a.to_dirty_xml(include_required)}.join("\n")).gsub(/\n( *)</, "\n  \\1<") + "\n</" + self.class.short_name + '>'
end

#to_elementObject

Returns self. Just a convenience method for interoperability with Model.



254
255
256
# File 'lib/quickbooks/element.rb', line 254

def to_element
  self
end

#to_modelObject

Converts the element to a model if it is an appropriate class type.



275
276
277
278
279
280
281
282
283
284
285
286
# File 'lib/quickbooks/element.rb', line 275

def to_model
  # List clean attributes
  clean_attrs = attributes_to_hash(clean_attributes).inject({}) {|h,(k,v)| h[k.gsub(/(Add|Mod|Ret)$/,'')] = v.respond_to?(:to_model) ? v.to_model : v; h}
  # List dirty attributes
  dirty_attrs = attributes_to_hash(dirty_attributes).inject({}) {|h,(k,v)| h[k.gsub(/(Add|Mod|Ret)$/,'')] = v.respond_to?(:to_model) ? v.to_model : v; h}
  # Set up the corresponding element with corresponding properties
  model = model_klass.instantiate(clean_attrs)
  model.attributes = dirty_attrs
  # Transfer the order index (for attribute sorting) from elements that were in a collection.
  model.instance_variable_set(:@collection_index, @collection_index) if instance_variables.include?('@collection_index')
  model
end

#to_update_elementObject



288
289
290
291
292
# File 'lib/quickbooks/element.rb', line 288

def to_update_element
  update_element = self.class.new
  update_element.attributes.replace(dirty_attributes(true))
  update_element
end

#to_xmlObject

Dumps all data into QBXML.



295
296
297
# File 'lib/quickbooks/element.rb', line 295

def to_xml
  '<' + self.class.short_name + [[nil] + options.collect {|k,v| "#{k}=#{v.inspect}"}].join(' ') + '>' +  ("\n" + attributes.collect {|a| a.to_xml}.join("\n")).gsub(/\n( *)</, "\n  \\1<") + "\n</" + self.class.short_name + '>'
end

#unrefObject

Attemps to get the Model object being referenced, if this is a Ref class.



259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/quickbooks/element.rb', line 259

def unref
  @retrieve_full ||= begin
    filter = {}
    case
    when self[:ListID]
      filter[:ListID]   = [self[:ListID]]
    when self[:TxnID]
      filter[:TxnID]    = [self[:TxnID]]
    when self[:FullName]
      filter[:FullName] = [self[:FullName]]
    end
    self.class.unref.first(filter)
  end
end

#update_xmlObject

Simply calls to_dirty_xml(true). Returns the minimal xml necessary to update the record in QuickBooks.



305
306
307
# File 'lib/quickbooks/element.rb', line 305

def update_xml
  to_dirty_xml(true)
end

#valid?Boolean

Validate all the necessary elements and attributes are included, using the object class’s spec

Returns:



99
100
101
# File 'lib/quickbooks/element.rb', line 99

def valid?
  validate.perfect?
end

#validateObject

Use this instead of valid? to get the Valean result instead of just a true or false value.



104
105
106
107
108
# File 'lib/quickbooks/element.rb', line 104

def validate
  r = self.class.xsd.validate(self)
  errors.replace(r.errors)
  r
end