Class: Quickbooks::Model

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

Overview

Model steps above related Element classes give you a way to manage your Quickbooks data like objects rather than by managing the communication of those objects.

Defined Under Namespace

Classes: ModelProperty

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(attrs = {}) ⇒ Model

Paints up a new QuickBooks model object with the attributes you give it. Marked as new, so when you hit save, it will actually create it.



215
216
217
218
219
# File 'lib/quickbooks/model.rb', line 215

def initialize(attrs={})
  @new_record = :init
  self.attributes = attrs
  @new_record = true
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method_name, *args) ⇒ Object (private)



556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
# File 'lib/quickbooks/model.rb', line 556

def method_missing(method_name, *args)
  if method_name.to_s =~ /^[A-Z]/
    if method_name.to_s =~ /=$/
      # Set property
      self[method_name.to_s.gsub(/\=$/,'')] = *args
    elsif args.empty?
      # Get property
      self[method_name.to_s.gsub(/\=$/,'')]
    else
      raise NoMethodError, "undefined method `#{method_name}' for #{self.inspect}:#{self.class.name}", caller[0..-1]
    end
  else
    raise NoMethodError, "undefined method `#{method_name}' for #{self.inspect}:#{self.class.name}", caller[0..-1]
  end
end

Class Attribute Details

.associationsObject (readonly)

Emulated Associations



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

def associations
  @associations
end

.propertiesObject (readonly)

Returns the value of attribute properties.



50
51
52
# File 'lib/quickbooks/model.rb', line 50

def properties
  @properties
end

Class Method Details

.all(args = {}) ⇒ Object

Request an array of whatever QuickBooks objects match the query. The args presented should be attributes with which to construct a *QueryRq for whatever model you’re in.



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/quickbooks/model.rb', line 123

def all(args={})
  process_finder_args(args)
  # This includes DataExt if user has asked for it.
  if args.has_key?(:IncludeExtData)
    data_scope = args.delete(:IncludeExtData)
    args[:OwnerID] = [data_scope == true ? '0' : data_scope] if data_scope
  end
  # Create a #{short_name}Query out of the filters, and send it to quickbooks for a response; instantiate the response objects.
  query = Quickbooks.get_constant("#{short_name}QueryRq").new(args)
  if caller.join =~ /lib\/quickbooks\.rb:\d+:in `qbxml'/
    Quickbooks.requestify(query).to_xml
  else
    catch :response do
      response = Quickbooks.execute(query)[:QBXMLMsgsRs]["#{short_name}QueryRs"][0]
      (response.nil? || response["#{short_name}Ret"].nil? || response["#{short_name}Ret"].empty?) ?
        [] : response["#{short_name}Ret"].collect {|r| r.to_model}
    end
  end
end

.create_combined_elements!Object



104
105
106
107
108
109
110
111
112
113
114
# File 'lib/quickbooks/model.rb', line 104

def create_combined_elements!
  # If this runs more than once it's okay; but it shouldn't ever because the attributes are written in order so write_xsd are done last.
  if read_xsd
    @properties = {}
    @associations = {}
    [read_xsd, read_write_xsd, write_xsd].compact.inject([]) {|a,e| a.concat(e.children)}.each do |prop_xsd|
      @properties[prop_xsd.name.to_sym] = ModelProperty.new(Quickbooks.get_constant(prop_xsd.name).short_name, write_xsd && write_xsd.include?(prop_xsd.name), read_write_xsd && read_write_xsd.include?(prop_xsd.name), read_xsd && read_xsd.include?(prop_xsd.name))
      @associations[prop_xsd.name[0..-4].to_sym] = @properties[prop_xsd.name.to_sym] if prop_xsd.name =~ /Ref$/
    end
  end
end

.each(args = {}) ⇒ Object

Iterates through all records matching *args, but only grabs 70 at a time from QuickBooks.



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
# File 'lib/quickbooks/model.rb', line 163

def each(args={})
  process_finder_args(args)
  query = Quickbooks.get_constant("#{short_name}QueryRq").new(args)
  started_valid = query.valid?
  query[:iterator] = 'Start'
  query[:MaxReturned] = 70
  query.delete(:iterator, :MaxReturned) if started_valid && !query.valid?
  if caller.join =~ /lib\/quickbooks\.rb:\d+:in `qbxml'/
    Quickbooks.requestify(query).to_xml
  else
    catch :response do
      result = Quickbooks.execute(query)
      ary = result[:QBXMLMsgsRs]["#{short_name}QueryRs"][0]["#{short_name}Ret"].each {|e| yield e.to_model}
      if iteratorID = result[:QBXMLMsgsRs]["#{short_name}QueryRs"][0][:iteratorID]
        query[:iteratorID] = iteratorID
        query[:iterator] = 'Continue'
        until(result[:QBXMLMsgsRs]["#{short_name}QueryRs"][0][:iteratorRemainingCount].to_i == 0)
          result = Quickbooks.execute(query)
          ary.concat result[:QBXMLMsgsRs]["#{short_name}QueryRs"][0]["#{short_name}Ret"].each {|e| yield e.to_model}
        end
      end
      ary
    end
  end
end

.first(args = {}) ⇒ Object

Request a single QuickBooks object that matches the query. The args presented should be attributes with which to construct a *QueryRq for whatever model you’re in. This simply adds the property :MaxReturned => 1, if the request is still valid with it.



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/quickbooks/model.rb', line 144

def first(args={})
  process_finder_args(args)
  # Create a #{short_name}Query out of the filters, and send it to quickbooks for a response; instantiate the response objects.
  query = QB["#{short_name}QueryRq"].new(args)
  started_valid = query.valid?
  query[:MaxReturned] = 1
  query.delete(:MaxReturned) if started_valid && !query.valid?
  if caller.join =~ /lib\/quickbooks\.rb:\d+:in `qbxml'/
    Quickbooks.requestify(query).to_xml
  else
    catch :response do
      response = Quickbooks.execute(query)[:QBXMLMsgsRs]["#{short_name}QueryRs"][0]
      response.nil? || response["#{short_name}Ret"].nil? || response["#{short_name}Ret"][0].nil? ?
        nil : response["#{short_name}Ret"][0].to_model
    end
  end
end

.helpObject



52
53
54
# File 'lib/quickbooks/model.rb', line 52

def help
  "QB::#{short_name}.query_xsd for available query params\nQB::#{short_name}.write_xsd for create properties\nQB::#{short_name}.read_write_xsd for modifiable properties\nTry one of the above options to study this particular model's properties."
end

.instantiate(attrs = {}) ⇒ Object

Should be used only internally, but if you need this - use it to paint a new QuickBooks model object that thinks it already exists in QuickBooks.



190
191
192
193
194
195
196
197
# File 'lib/quickbooks/model.rb', line 190

def instantiate(attrs={})
  obj = allocate
  obj.instance_variable_set(:@new_record, nil)
  obj.attributes = attrs
  obj.instance_variable_set(:@new_record, false)
  obj.send :clean!
  obj
end

.item_typeObject

Replies whether it’s a :List or a :Txn item.



200
201
202
# File 'lib/quickbooks/model.rb', line 200

def item_type
  @properties.has_key?(:ListID) ? :List : :Txn
end

.query_attributesObject



96
97
98
99
100
101
102
# File 'lib/quickbooks/model.rb', line 96

def query_attributes
  if query_xsd
    query_xsd.attributes
  else
    raise RuntimeError, "You must first initialize query_xsd."
  end
end

.query_xsd(xsd = nil) ⇒ Object

Set or retrieve the XSD data for the Model’s write elements – properties that can be used in creating a new object. This is also the controller for validation of the object when you’re creating a new object.

Raises:

  • (ArgumentError)


90
91
92
93
94
# File 'lib/quickbooks/model.rb', line 90

def query_xsd(xsd=nil)
  raise ArgumentError, "must be an Quickbooks::XSD::Element" if !xsd.nil? && !xsd.is_a?(Quickbooks::XSD::Element)
  @query_xsd = xsd if xsd
  @query_xsd ||= nil
end

.read_write_xsd(xsd = nil) ⇒ Object

Set or retrieve the XSD data for the Model’s read/write elements – properties that can be used in modifying objects. This is also the controller for validation of the object when you’re modifying an existing object.

Raises:

  • (ArgumentError)


68
69
70
71
72
73
74
75
# File 'lib/quickbooks/model.rb', line 68

def read_write_xsd(xsd=nil)
  raise ArgumentError, "must be an Quickbooks::XSD::Element" unless xsd.nil? || xsd.is_a?(Quickbooks::XSD::Element)
  if xsd
    @read_write_xsd = xsd
    create_combined_elements!
  end
  @read_write_xsd ||= nil
end

.read_xsd(xsd = nil) ⇒ Object

Set or retrieve the XSD data for the Model’s read elements – properties that are readable.

Raises:

  • (ArgumentError)


57
58
59
60
61
62
63
64
# File 'lib/quickbooks/model.rb', line 57

def read_xsd(xsd=nil)
  raise ArgumentError, "must be an Quickbooks::XSD::Element" unless xsd.nil? || xsd.is_a?(Quickbooks::XSD::Element)
  if xsd
    @read_xsd = xsd
    create_combined_elements!
  end
  @read_xsd ||= nil
end

.write_xsd(xsd = nil) ⇒ Object

Set or retrieve the XSD data for the Model’s write elements – properties that can be used in creating a new object. This is also the controller for validation of the object when you’re creating a new object.

Raises:

  • (ArgumentError)


79
80
81
82
83
84
85
86
# File 'lib/quickbooks/model.rb', line 79

def write_xsd(xsd=nil)
  raise ArgumentError, "must be an Quickbooks::XSD::Element" if !xsd.nil? && !xsd.is_a?(Quickbooks::XSD::Element)
  if xsd
    @write_xsd = xsd
    create_combined_elements!
  end
  @write_xsd ||= nil
end

Instance Method Details

#[](key) ⇒ Object

Access attribute values by key. Also access associations this way.



268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/quickbooks/model.rb', line 268

def [](key)
  return self.attributes = key if key.is_a?(Hash)
  key = case key # make a string
  when String
    key.to_sym
  when Symbol
    key
  when Module
    key.short_name.to_sym
  end
  return get_associated(key) if self.class.associations.has_key?(key)
  attr_klass = begin
    QB[key.to_s]
  rescue RuntimeError
    raise "Invalid key '#{key.inspect}'"
  end
  repeatable = (new_record? ? self.class.write_xsd : (@new_record == false ? self.class.read_write_xsd : self.class.read_xsd)).repeatable?(key.to_s)
  attributes[key] = ElementCollection.new(self, key) if repeatable && !attributes[key].is_a?(ElementCollection)
  attributes[key]
end

#[]=(key, value) ⇒ Object

Set attribute values by key. Also set associated objects here.

Raises:

  • (RuntimeError)


290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/quickbooks/model.rb', line 290

def []=(key,value)
  key = case key # make a symbol
  when String
    key.to_sym
  when Symbol
    key
  when Module
    key.short_name.to_sym
  end
  return associate(key, value) if self.class.associations.include?(key)
  raise RuntimeError, "'#{key}' is not a valid property name for #{self.class.name}!" unless self.class.properties.has_key?(key)
  raise RuntimeError, "'#{key}' cannot be assigned on creation" if new_record? && !self.class.properties[key].addable?
  raise RuntimeError, "'#{key}' cannot be modified"             if @new_record == false && !self.class.properties[key].writable?
  # Instantiate the value into the attribute class, if necessary
  attr_klass = QB[key.to_s]
  # If it *should* be an ElementCollection, it shouldn't be set here at all!
  if (new_record? ? self.class.write_xsd : (@new_record == false ? self.class.read_write_xsd : self.class.read_xsd)).repeatable?(key.to_s)
    if @new_record.in?(nil, :init) || value.is_a?(Array)
      attributes[key] = ElementCollection.new(self, key, value)
      attributes[key].send(:dirty!)
    else
      if attributes[key].nil? || attributes[key].empty?
        attributes[key] = ElementCollection.new(self, key, [value])
        attributes[key].send(:dirty!)
      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 singular value directly over an element that already contains multiple values. Use \"model[:#{key}] << value\" to append, or wrap the value in an array to explicitly replace the current contents -- \"model[:#{key}] = [ value ]\"."
      end
    end
  else
    value = @new_record.nil? ? attr_klass.instantiate(value) : attr_klass.new(value) unless value.is_a?(attr_klass)
    value.send(:dirty!)
    attributes[key] = value
  end
  remove_incorrect_associated(key.to_s.sub(/Ref$/,'').to_sym, value) if self.class.associations.include?(key.to_s.sub(/Ref$/,'').to_sym)
end

#add_error(msg) ⇒ Object

:nodoc:



426
427
428
# File 'lib/quickbooks/model.rb', line 426

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

#attributesObject

Just return the attributes hash.



251
252
253
# File 'lib/quickbooks/model.rb', line 251

def attributes
  @attributes ||= {}
end

#attributes=(attrs) ⇒ Object

Pass a hash of attributes, and it’ll set each one one by one via []=.

Raises:

  • (ArgumentError)


256
257
258
259
260
261
262
263
264
265
# File 'lib/quickbooks/model.rb', line 256

def attributes=(attrs)
  raise ArgumentError, "must be a hash" unless attrs.is_a?(Hash)
  attrs.each do |k,v|
    if self.class.properties.has_key?(k.to_sym) || self.class.associations.has_key?(k.to_sym)
      self[k.to_sym] = v
    else
      raise "Model #{self.class.short_name} does not have property #{k}!"
    end
  end
end

#can_ref?Boolean

Do I have a relative *Ref class?

Returns:



362
363
364
# File 'lib/quickbooks/model.rb', line 362

def can_ref?
  Quickbooks.get_constant(self.class.short_name.gsub(/(Add|Mod|Ret)/,'') + 'Ref') && true rescue false
end

#clean_attributesObject

Return only the attributes that have not been modified.



351
352
353
# File 'lib/quickbooks/model.rb', line 351

def clean_attributes
  attributes.except(dirty_attributes.keys)
end

#delete(*keys) ⇒ Object

Completely remove an attribute value from the object. This is different from setting the attribute to nil.



329
330
331
332
333
334
335
336
337
338
339
340
341
# File 'lib/quickbooks/model.rb', line 329

def delete(*keys)
  keys.each do |key|
    key = case key # make a string
    when String
      key.to_sym
    when Symbol
      key
    when Module
      key.short_name.to_sym
    end
    attributes.delete(key)
  end
end

#destroy!Object

That’s right. Just get rid of that object. QuickBooks knows how to handle things, and it won’t let you destroy items if it’ll mess things up.



504
505
506
507
508
509
510
511
512
513
# File 'lib/quickbooks/model.rb', line 504

def destroy!
  query = Quickbooks.get_constant("#{self.class.item_type}DelRq").new("#{self.class.item_type}DelType" => self.class.short_name, "#{self.class.item_type}ID" => self["#{self.class.item_type}ID"])
  if caller.join =~ /lib\/quickbooks\.rb:\d+:in `qbxml'/
    Quickbooks.requestify(query).to_xml
  else
    catch :response do
      Quickbooks.execute(query)
    end
  end
end

#dirty?Boolean

Have any of my values, or my descendents’ values, been changed?

Returns:



356
357
358
359
# File 'lib/quickbooks/model.rb', line 356

def dirty?
  # Test for any dirty elements
  @dirty || attributes.any? {|k,v| v.dirty?}
end

#dirty_attributes(include_required = false) ⇒ Object

Return only the attributes that have been changed or whose descendents have changed. If include_required is true, then whatever is necessary to make the set of attributes valid is returned as well.



344
345
346
347
348
# File 'lib/quickbooks/model.rb', line 344

def dirty_attributes(include_required=false)
  Hash[*((include_required ?
    attributes.select {|k,v| v.dirty? || (new_record? ? self.class.write_xsd : self.class.read_write_xsd).required?(k) || !(new_record? ? self.class.write_xsd : self.class.read_write_xsd).validate(self.class.short_name => attributes.except(v.class.short_name))} :
    attributes.select {|k,v| v.dirty?}).flatten)]
end

#errorsObject

The errors returned by the latest validate call.



423
424
425
# File 'lib/quickbooks/model.rb', line 423

def errors
  @errors ||= []
end

#idObject

Returns the ListID or TxnID value respective of its type.



237
238
239
# File 'lib/quickbooks/model.rb', line 237

def id
  self[:"#{self.class.item_type}ID"]
end

#inspectObject

:nodoc:



221
222
223
# File 'lib/quickbooks/model.rb', line 221

def inspect #:nodoc:
  "<#{self.class.short_name}:##{object_id}\n  #{attributes.collect {|k,v| v.inspect}.join("\n").gsub(/\n/, "\n  ")}>"
end

#load_extended_data(owner = '0') ⇒ Object

Use this to load extended data if you need to import into this same object.



485
486
487
# File 'lib/quickbooks/model.rb', line 485

def load_extended_data(owner='0')
  attributes[:DataExt] = self.class.first(:"#{self.class.item_type}ID" => [id], :OwnerID => [owner])[:DataExt]
end

#new_record?Boolean

true if new record, false if already exists.

For deeper inspection, @new_record is set to:

  • :init if being initialized

  • true if new_record

  • nil if being instantiated

  • false if not new_record

Returns:



232
233
234
# File 'lib/quickbooks/model.rb', line 232

def new_record?
  !!@new_record
end

#properties(key = nil) ⇒ Object

Display a list of the available attribute names



242
243
244
245
246
247
248
# File 'lib/quickbooks/model.rb', line 242

def properties(key=nil)
  if key
    self.class.properties[key]
  else
    self.class.properties.keys
  end
end

#saveObject

Save the object to QuickBooks, whether that means creating a new record or updating an existing one.



431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
# File 'lib/quickbooks/model.rb', line 431

def save
  save_associations
  query = Quickbooks.get_constant("#{element_klass.short_name}Rq").new([ to_element.to_update_element ])
  if caller.join =~ /lib\/quickbooks\.rb:\d+:in `qbxml'/
    Quickbooks.requestify(query).to_xml
  else
    catch :response do
      @last_response = Quickbooks.execute(query)
      response = @last_response[:QBXMLMsgsRs]["#{element_klass.short_name}Rs"][0]
      reload = if response.nil? || response["#{self.class.short_name}Ret"].nil? || response["#{self.class.short_name}Ret"].nil?
        raise "Response doesn't make sense after saving #{inspect}: #{@last_response.inspect} (#{@last_response[:QBXMLMsgsRs]["#{element_klass.short_name}Rs"][0].nil?})"
      else
        response["#{self.class.short_name}Ret"].to_model
      end
      @attributes = reload.attributes
      true
    end
  end
end

#save_associationsObject

Save any unsaved or dirty associations.



490
491
492
493
494
495
496
497
498
499
500
501
# File 'lib/quickbooks/model.rb', line 490

def save_associations
  # first save any of my associated items that aren't already existing
  self.class.associations.each_key do |association_name|
    instance_variable_set("@#{association_name}", nil) unless self.instance_variables.include?("@#{association_name}") # just to avoid needless warnings.
    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 { |k,attv| attv.save_associations if attv.respond_to?(:save_associations) }
end

#to_dirty_xml(include_required = false) ⇒ Object



406
407
408
# File 'lib/quickbooks/model.rb', line 406

def to_dirty_xml(include_required=false)
  to_element.to_dirty_xml(include_required)
end

#to_elementObject

Turn me into an element (ending in *Add or *Mod, depending on my new_record? situation).



385
386
387
388
389
390
391
392
393
394
395
396
# File 'lib/quickbooks/model.rb', line 385

def to_element
  # List clean attributes
  clean_attrs = clean_attributes.collect {|k,v| v.respond_to?(:to_element) ? v.to_element : v}.to_hash_via {|e| [(e.is_a?(ElementCollection) ? e.type.short_name : e.class.short_name), e]}
  # List dirty attributes
  dirty_attrs = dirty_attributes.collect {|k,v| v.respond_to?(:to_element) ? v.to_element : v}.to_hash_via {|e| [(e.is_a?(ElementCollection) ? e.type.short_name : e.class.short_name), e]}
  # Set up the corresponding element with corresponding properties
  element = element_klass.instantiate(clean_attrs)
  element.attributes = dirty_attrs
  # Transfer the order index (for attribute sorting) from elements that were in a collection.
  element.instance_variable_set(:@collection_index, @collection_index) if instance_variables.include?('@collection_index')
  element
end

#to_refObject

Make a Ref object out of me.



367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
# File 'lib/quickbooks/model.rb', line 367

def to_ref
  begin
    attrs = {}
    idsym = :"#{self.class.item_type}ID"
    if self[idsym]
      attrs[idsym] = self[idsym]
    elsif self[:FullName]
      attrs[:FullName] = self[:FullName]
    else
      return nil # Not able to ref
    end
    Quickbooks.get_constant(self.class.short_name.gsub(/(Add|Mod|Ret)/,'') + 'Ref').new(attrs)
  rescue NameError => e
    nil
  end
end

#to_update_elementObject

I don’t think this is actually being used anywhere…



399
400
401
# File 'lib/quickbooks/model.rb', line 399

def to_update_element
  to_element.to_update_element
end

#to_xmlObject



403
404
405
# File 'lib/quickbooks/model.rb', line 403

def to_xml
  to_element.to_xml
end

#update_extended_data(key, value, owner = nil) ⇒ Object

Update some extended data by key and value. By default, just include the key and value, and the owner id of ‘0’ will be used. However, if you want to specify a different owner, just add that on as a third parameter.



454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
# File 'lib/quickbooks/model.rb', line 454

def update_extended_data(key, value, owner=nil)
  # First, see if this key already exists or not.
  load_extended_data unless attributes[:DataExt]

  attrs = {
    # Use the same OwnerID that was supplied for getting the DataExt.
    :OwnerID => owner || ((attributes[:DataExt] && attributes[:DataExt][0]) ? attributes[:DataExt][0][:OwnerID] : '0'),
    :DataExtName => key,
    :"#{self.class.short_name == 'Company' ? 'Other' : self.class.item_type}DataExtType" => self.class.short_name,
    :DataExtValue => value
  }
  case self.class.item_type
  when :List
    attrs[:ListObjRef] = {:ListID => id}
  when :Txn
    attrs[:TxnID] = id
    # We don't need to bother with LineID extended data.
    # attrs[:TxnLineID]
  end

  de = if self[:DataExt] && self[:DataExt].select {|de| de[:DataExtName] == key}
    # Modify it.
    QB::DataExtModRq.new(:DataExtMod => attrs)
  else
    # Create it.
    QB::DataExtAddRq.new(:DataExtAdd => attrs)
  end
  Quickbooks.execute(de)
  load_extended_data
end

#valid?Boolean

Am I completely valid?

Returns:



411
412
413
# File 'lib/quickbooks/model.rb', line 411

def valid?
  validate.perfect?
end

#validateObject

Use this to validate the object and get the Valean result instead of just a true/false value.



416
417
418
419
420
# File 'lib/quickbooks/model.rb', line 416

def validate
  r = new_record? ? self.class.write_xsd.validate(self) : self.class.read_write_xsd.validate(self)
  errors.replace(r.errors)
  r
end