File Coverage

blib/lib/OpenERP/OOM/Class/Base.pm
Criterion Covered Total %
statement 35 226 15.4
branch 0 82 0.0
condition 0 21 0.0
subroutine 14 42 33.3
pod 14 14 100.0
total 63 385 16.3


line stmt bran cond sub pod time code
1             package OpenERP::OOM::Class::Base;
2              
3 3     3   131420 use 5.010;
  3         19  
4 3     3   15 use Carp;
  3         6  
  3         179  
5 3     3   449 use Moose;
  3         396525  
  3         33  
6 3     3   25138 use RPC::XML;
  3         981072  
  3         134  
7 3     3   23 use DateTime;
  3         5  
  3         64  
8 3     3   16 use DateTime::Format::Strptime;
  3         24  
  3         22  
9 3     3   1434 use MooseX::NotRequired;
  3         1048  
  3         79  
10 3     3   18 use Try::Tiny;
  3         4  
  3         145  
11 3     3   1147 use Try::Tiny::Retry;
  3         3322  
  3         177  
12 3     3   450 use Time::HiRes qw/usleep/;
  3         1135  
  3         15  
13              
14             extends 'Moose::Object';
15             with 'OpenERP::OOM::DynamicUtils';
16              
17             =head1 NAME
18              
19             OpenERP::OOM::Class::Base
20              
21             =head1 SYNOPSYS
22              
23             my $obj = $schema->class('Name')->create(\%args);
24            
25             foreach my $obj ($schema->class('Name')->search(@query)) {
26             ...
27             }
28              
29             =head1 DESCRIPTION
30              
31             Provides a base set of methods for OpenERP::OOM classes (search, create, etc).
32              
33             =cut
34              
35             has 'schema' => (
36             is => 'ro',
37             );
38              
39             has 'object_class' => (
40             is => 'ro',
41             lazy => 1,
42             builder => '_build_object_class',
43             );
44              
45             sub _build_object_class {
46 0     0   0 my $self = shift;
47            
48             # if you get this blow up it probably means the class doesn't compile for some
49             # reason. Run the t/00-load.t tests. If they pass check you have a use_ok
50             # statement for all your modules.
51 0 0       0 die 'Your code doesn\'t compile llamma' if !$self->can('object');
52 0         0 $self->ensure_class_loaded($self->object);
53            
54 0     0   0 $self->object->meta->add_method('class' => sub{return $self});
  0         0  
55            
56 0         0 return $self->object->new;
57             }
58              
59             #-------------------------------------------------------------------------------
60              
61             =head2 search
62              
63             Searches OpenERP and returns a list of objects matching a given query.
64              
65             my @list = $schema->class('Name')->search(
66             ['name', 'ilike', 'OpusVL'],
67             ['active', '=', 1],
68             );
69              
70             The query is formatted as a list of array references, each specifying a
71             column name, operator, and value. The objects returned will be those where
72             all of these sub-queries match.
73              
74             Searches can be performed against OpenERP fields, linked objects (e.g. DBIx::Class
75             relationships), or a combination of both.
76              
77             my @list = $schema->class('Name')->search(
78             ['active', '=', 1],
79             ['details', {status => 'value'}, {}],
80             )
81              
82             In this example, 'details' is a linked DBIx::Class object with a column called
83             'status'.
84              
85             An optional 'search context' can also be provided at the end of the query list, e.g.
86              
87             my @list = $schema->class('Location')->search(
88             ['usage' => '=' => 'internal'],
89             ['active' => '=' => 1],
90             {
91             active_id => $self->id,
92             active_ids => [$self->id],
93             active_model => 'product.product',
94             full => 1,
95             product_id => $self->id,
96             search_default_in_location => 1,
97             section_id => undef,
98             tz => undef,
99             }
100             );
101              
102             Supplying a context further restricts the search, for example to narrow down a
103             'stock by location' query to 'stock of a specific product by location'.
104              
105             Following the search context, an arrayref of options can be given to return a
106             paged set of results:
107              
108             {
109             limit => 10, # Return max 10 results
110             offset => 20, # Start at result 20
111             }
112              
113             =head2 raw_search
114              
115             This is the same as search but it doesn't turn the results into objects. This
116             is useful if your search is likely to have returned fields that aren't part of
117             the object. Queries like those used by the Stock By Location report are likely
118             to return stock levels as well as the location details for example.
119              
120             =cut
121              
122             sub raw_search {
123 0     0 1 0 my $self = shift;
124 0         0 return $self->_raw_search(0, @_);
125             }
126              
127             =head2 search_limited_fields
128              
129             This is an alternative version of search that only fills in the required fields
130             of the object.
131              
132             # avoid pulling the whole attachement down for a search
133             my @a = $attachments->search_limited_fields([
134             qw/res_model res_name type url create_uid create_date
135             datas_fname description name res_id/
136             ], [
137             res_model => '=' => 'product.template',
138             res_id => '=' => 1,
139             ]);
140              
141             This allows you to avoid pulling down problem fields. The most obvious example
142             is get a list of attachments for an object, without pulling down all the data
143             for the attachement.
144              
145             =cut
146              
147             sub search_limited_fields {
148 0     0 1 0 my $self = shift;
149 0         0 return $self->_search_limited_fields(1, @_);
150             }
151              
152             sub _search_limited_fields {
153 0     0   0 my $self = shift;
154 0         0 my $objects = shift;
155 0         0 my $fields = shift;
156              
157 0         0 my $ids = $self->_raw_search(1, @_);
158 0 0 0     0 return wantarray ? () : undef unless ( defined $ids && ref $ids eq 'ARRAY' && scalar @$ids >= 1 );
    0 0        
159 0         0 my ($context) = grep { ref $_ eq 'HASH' } @_;
  0         0  
160 0         0 return $self->_retrieve_list($objects, $ids, $context, $fields);
161             }
162              
163             sub _raw_search {
164 0     0   0 my ($self, $ids_only, @args) = @_;
165             ### Initial search args: @args
166            
167 0         0 my @search;
168 0   0     0 while (@args && ref $args[0] ne 'HASH') {push @search, shift @args}
  0         0  
169            
170             # Loop through each search criteria, and if it is a linked object
171             # search, replace it with a translated OpenERP search parameter.
172 0         0 foreach my $criteria (@search) {
173 0 0       0 if(ref $criteria eq 'ARRAY') {
174 0         0 my $search_field = $criteria->[0];
175              
176 0 0       0 if (my $link = $self->object_class->meta->link->{$search_field}) {
177 0 0       0 if ($self->schema->link($link->{class})->can('search')) {
178 0         0 my @results = $self->schema->link($link->{class})->search($link->{args}, @$criteria[1 .. @$criteria-1]);
179              
180 0 0       0 if (@results) {
181             ### Adding to OpenERP search:
182             ### $link->{key}
183             ### IN
184             ### join(', ', @results)
185 0         0 $criteria = [$link->{key}, 'in', \@results];
186             } else {
187 0         0 return; # No results found, so no point searching in OpenERP
188             }
189             } else {
190 0         0 carp "Cannot search for link type " . $link->{class};
191             }
192             }
193             }
194             }
195            
196 0         0 my $context = $self->_get_context(shift @args);
197 0         0 my $options = shift @args;
198 0 0       0 $options = {} unless $options;
199             ### Search: @search
200             ### Search context: $context
201             ### Search options: $options
202 0 0       0 if($ids_only)
203             {
204 0         0 return $self->schema->client->search($self->object_class->model,[@search], $context, $options->{offset}, $options->{limit}, $options->{order});
205             }
206              
207 0         0 my $objects = $self->schema->client->search_detail($self->object_class->model,[@search], $context, $options->{offset}, $options->{limit}, $options->{order});
208              
209 0 0       0 if ($objects) {
210 0         0 foreach my $attribute ($self->object_class->meta->get_all_attributes) {
211 0 0 0     0 if($attribute->type_constraint && $attribute->type_constraint =~ /DateTime/)
212             {
213 0         0 map { $_->{$attribute->name} = $self->_parse_datetime($_->{$attribute->name}) } @$objects;
  0         0  
214             }
215             }
216 0         0 return $objects;
217             } else {
218 0         0 return undef;
219             }
220             }
221              
222             sub search
223             {
224 0     0 1 0 my $self = shift;
225 0         0 my $objects = $self->raw_search(@_);
226 0 0       0 if($objects) {
227 0         0 return map {$self->object_class->new($_)} @$objects;
  0         0  
228             } else {
229 0 0       0 return wantarray ? () : undef;
230             }
231             }
232              
233             =head2 is_not_null
234              
235             Returns search criteria for a not null search. i.e. equivalend to $field is not null in SQL.
236              
237             $self->search($self->is_not_null('x_department'), [ 'other_field', '=', 3 ]);
238              
239             =cut
240              
241             sub is_not_null
242             {
243 0     0 1 0 my $self = shift;
244 0         0 my $field = shift;
245 0         0 return [ $field, '!=', RPC::XML::boolean->new(0) ];
246             }
247              
248             =head2 null
249              
250             Returns a 'null' for use in OpenERP calls and objects. (Actually this is a False value).
251              
252             =cut
253              
254 0     0 1 0 sub null { RPC::XML::boolean->new(0) }
255              
256             =head2 is_null
257              
258             Returns search criteria for an is null search. i.e. equivalend to $field is null in SQL.
259              
260             $self->search($self->is_null('x_department'), [ 'other_field', '=', 3 ]);
261              
262             =cut
263              
264             sub is_null
265             {
266 0     0 1 0 my $self = shift;
267 0         0 my $field = shift;
268 0         0 return [ $field, '=', RPC::XML::boolean->new(0) ];
269             }
270              
271             #-------------------------------------------------------------------------------
272              
273             =head2 find
274              
275             Returns the first object matching a given query.
276              
277             my $obj = $schema->class('Name')->find(['id', '=', 32]);
278              
279             Will return C<undef> if no objects matching the query are found.
280              
281             =cut
282              
283             sub find {
284 0     0 1 0 my $self = shift;
285            
286             #my $ids = $self->schema->client->search($self->object_class->model,[@_]);
287 0         0 my $ids = $self->raw_search(@_);
288            
289 0 0       0 if ($ids->[0]) {
290             #return $self->retrieve($ids->[0]);
291 0         0 return $self->object_class->new($ids->[0]);
292             }
293             }
294              
295              
296             =head2 get_options
297              
298             This returns the options for available for a selection field. It will croak if you
299             try to give it a field that isn't an option.
300              
301             =cut
302              
303             sub get_options
304             {
305 0     0 1 0 my $self = shift;
306 0         0 my $field = shift;
307              
308 0         0 my $model_info = $self->schema->client->model_fields($self->object_class->model);
309 0         0 my $field_info = $model_info->{$field};
310 0 0       0 croak 'Can only get options for selection objects' unless $field_info->{type} eq 'selection';
311 0         0 my $options = $field_info->{selection};
312 0         0 return $options;
313             }
314              
315             #-------------------------------------------------------------------------------
316              
317             =head2 retrieve
318              
319             Returns an object by ID.
320              
321             my $obj = $schema->class('Name')->retrieve(32);
322              
323             =cut
324              
325             sub retrieve {
326 0     0 1 0 my ($self, $id, @args) = @_;
327            
328             # FIXME - This should probably be in a try/catch block
329 0         0 my $context = $self->_get_context(shift @args);
330 0         0 $self->_ensure_object_fields(\@args);
331 0 0       0 if (my $object = $self->schema->client->read_single($self->object_class->model, $id, $context, @args))
332             {
333 0         0 return $self->_inflate_object($self->object, $object);
334             }
335             }
336              
337             sub _ensure_object_fields
338             {
339 0     0   0 my $self = shift;
340 0         0 my $args = shift;
341              
342 0 0       0 unless(@$args)
343             {
344 0         0 my @fields;
345 0         0 foreach my $attribute ($self->object_class->meta->get_all_attributes)
346             {
347 0         0 my $name = $attribute->name;
348 0 0       0 push @fields, $name unless $name =~ /^_/;
349             }
350 0         0 push @$args, \@fields;
351             }
352             }
353              
354             sub _get_context
355             {
356 0     0   0 my $self = shift;
357 0         0 my $context = shift;
358              
359 0         0 my %translation = ( lang => $self->schema->lang );
360 0 0       0 if($context)
361             {
362             # merge the context with our language for translation.
363 0         0 @translation{keys %$context} = values %$context;
364             }
365 0         0 $context = \%translation;
366 0         0 return $context;
367             }
368              
369             sub _inflate_object
370             {
371 0     0   0 my $self = shift;
372 0         0 my $object_class = shift;
373 0         0 my $object = shift;
374              
375 0         0 foreach my $attribute ($self->object_class->meta->get_all_attributes) {
376 0 0 0     0 if($attribute->type_constraint && $attribute->type_constraint =~ /DateTime/)
377             {
378 0         0 $object->{$attribute->name} = $self->_parse_datetime($object->{$attribute->name});
379             }
380             }
381 0         0 return $object_class->new($object);
382             }
383              
384             sub _do_strptime {
385 0     0   0 my ($self, $string, $format) = @_;
386 0 0       0 return unless $string;
387 0         0 my $parser = DateTime::Format::Strptime->new(pattern => $format, time_zone => 'UTC');
388 0         0 return $parser->parse_datetime($string);
389             }
390              
391             sub _parse_datetime {
392 0     0   0 my ($self, $string) = @_;
393 0   0     0 return $self->_do_strptime($string, '%Y-%m-%d %H:%M:%S') // $self->_do_strptime($string, '%Y-%m-%d');
394             }
395              
396             =head2 default_values
397              
398             Returns an instance of the object filled in with the default values suggested by OpenERP.
399              
400             =cut
401             sub default_values
402             {
403 0     0 1 0 my $self = shift;
404 0         0 my $context = shift;
405             # do a default_get
406              
407 0         0 my @fields = map { $_->name } $self->object_class->meta->get_all_attributes;
  0         0  
408 0         0 my $object = $self->schema->client->get_defaults($self->object_class->model, \@fields, $context);
409 0         0 my $class = MooseX::NotRequired::make_optional_subclass($self->object);
410 0         0 return $self->_inflate_object($class, $object);
411             }
412              
413             =head2 create_related_object_for_DBIC
414              
415             Creates a related DBIC object for an object of this class (before the object
416             is created).
417              
418             It returns a transaction guard alongside the id so that if the corresponding
419             object fails to create it can be aborted.
420              
421             This can make the link up smoother as you know the id of the object to refer
422             to in OpenERP before creating the OpenERP object. It also allows for failures
423             to be dealt with more reliably.
424              
425             my ($id, $guard) = $self->create_related_object_for_DBIC('details', $details);
426             # Create the object
427             $object->{x_dbic_link_id} = $id;
428             $object->{default_code} = sprintf("OBJ%06d", $id);
429              
430             my $prod = $self->$orig($object);
431             $guard->commit;
432              
433             =cut
434              
435             sub create_related_object_for_DBIC
436             {
437 0     0 1 0 my ($self, $relation_name, $data) = @_;
438 0         0 my $object = $self->object_class;
439 0         0 my $relation = $object->meta->link->{$relation_name};
440 0 0       0 if($relation)
441             {
442 0 0       0 die 'Wrong type of relation' unless $relation->{class} eq 'DBIC';
443 0         0 my $link = $self->schema->link($relation->{class});
444 0         0 my $guard = $link->dbic_schema->storage->txn_scope_guard;
445 0         0 my $id = $link->create($relation->{args}, $data);
446 0         0 return ($id, $guard);
447             }
448             else
449             {
450 0         0 die 'Unable to find relation';
451             }
452             }
453             #-------------------------------------------------------------------------------
454              
455             =head2 retrieve_list
456              
457             Takes a reference to a list of object IDs and returns a list of objects.
458              
459             my @list = $schema->class('Name')->retrieve_list([32, 15, 60]);
460              
461             =cut
462              
463             sub retrieve_list {
464 0     0 1 0 my $self = shift;
465 0         0 return $self->_retrieve_list(1, @_);
466             }
467              
468             sub _retrieve_list {
469 0     0   0 my ($self, $inflate_objects, $ids, @args) = @_;
470            
471 0         0 my $context = $self->_get_context(shift @args);
472 0         0 $self->_ensure_object_fields(\@args);
473 0 0       0 if (my $objects = $self->schema->client->read($self->object_class->model, $ids, $context, @args)) {
474 0         0 foreach my $attribute ($self->object_class->meta->get_all_attributes) {
475 0 0 0     0 if($attribute->type_constraint && $attribute->type_constraint =~ /DateTime/)
476             {
477 0         0 map { $_->{$attribute->name} = $self->_parse_datetime($_->{$attribute->name}) } @$objects;
  0         0  
478             }
479             }
480 0         0 my %id_map = map { $_->{id} => $_ } @$objects;
  0         0  
481 0         0 my @sorted = map { $id_map{$_} } @$ids;
  0         0  
482 0 0       0 return map {$self->object_class->new($_)} @sorted if $inflate_objects;
  0         0  
483 0         0 return @sorted;
484             }
485             }
486              
487              
488             #-------------------------------------------------------------------------------
489              
490             sub _collapse_data_to_ids
491             {
492 0     0   0 my ($self, $object_data) = @_;
493              
494 0         0 my $relationships = $self->object_class->meta->relationship;
495 0         0 while (my ($name, $rel) = each %$relationships) {
496 0 0       0 if ($rel->{type} eq 'one2many') {
497 0 0       0 if ($object_data->{$name}) {
498 0         0 $object_data->{$rel->{key}} = $self->_id($rel, $object_data->{$name});
499 0 0       0 delete $object_data->{$name} if $name ne $rel->{key};
500             }
501             }
502            
503 0 0       0 if ($rel->{type} eq 'many2one') {
504 0 0       0 if ($object_data->{$name}) {
505 0         0 $object_data->{$rel->{key}} = $self->_id($rel, $object_data->{$name});
506 0 0       0 delete $object_data->{$name} if $name ne $rel->{key};
507             }
508             }
509 0 0       0 if ($rel->{type} eq 'many2many') {
510 0 0       0 if ($object_data->{$name}) {
511 0         0 my $val = $object_data->{$name};
512 0         0 my @ids;
513 0 0       0 if(ref $val eq 'ARRAY')
514             {
515             # they passed in an arrayref.
516 0         0 my $objects = $val;
517 0         0 @ids = map { $self->_id($rel, $_) } @$objects;
  0         0  
518             }
519             else
520             {
521             # assume it's a single object.
522 0         0 push @ids, $self->_id($rel, $val);
523             }
524 0         0 $object_data->{$rel->{key}} = [[ 6, 0, \@ids ]];
525 0 0       0 delete $object_data->{$name} if $name ne $rel->{key};
526             }
527             }
528             }
529             # Force Str parameters to be object type RPC::XML::string
530 0         0 foreach my $attribute ($self->object_class->meta->get_all_attributes) {
531 0 0       0 if (exists $object_data->{$attribute->name}) {
532 0         0 $object_data->{$attribute->name} = $self->prepare_attribute_for_send($attribute->type_constraint, $object_data->{$attribute->name});
533             }
534             }
535 0         0 return $object_data;
536             }
537              
538             sub _id
539             {
540 0     0   0 my $self = shift;
541 0         0 my $rel = shift;
542 0         0 my $val = shift;
543 0         0 my $ref = ref $val;
544 0 0       0 if($ref)
545             {
546             # FIXME: this is close to what I want but I need to be doing it with the class
547             # that corresponds to the relation we're delving into.
548 0 0       0 if($ref eq 'HASH')
    0          
549             {
550 0         0 my $class = $self->schema->class($rel->{class});
551 0         0 return [[ 0, 0, $class->_collapse_data_to_ids($val) ]];
552             }
553             elsif($ref eq 'ARRAY')
554             {
555             # this should allow us to do child objects too.
556 0         0 my $class = $self->schema->class($rel->{class});
557 0         0 my @expanded = map { [ 0, 0, $class->_collapse_data_to_ids($_) ] } @$val;
  0         0  
558 0         0 return \@expanded;
559             }
560             else
561             {
562 0         0 return $val->id;
563             }
564             }
565 0         0 return $val;
566             }
567              
568             =head2 create
569              
570             Creates a new instance of an object in OpenERP.
571              
572             my $obj = $schema->class('Name')->create({
573             name => 'OpusVL',
574             active => 1,
575             });
576              
577             Takes a hashref of object parameters.
578              
579             Returns the new object or C<undef> if it could not be created.
580              
581             =cut
582              
583             sub create {
584 0     0 1 0 my ($self, $object_data, @args) = @_;
585              
586             ### Create called with initial object data:
587             ### $object_data;
588            
589 0         0 $object_data = $self->_collapse_data_to_ids($object_data);
590              
591             ### To
592             ### $object_data;
593 0         0 my $id;
594             $self->_with_retries(sub {
595 0     0   0 $id = $self->schema->client->create($self->object_class->model, $object_data, @args);
596 0         0 });
597 0 0       0 if ($id)
598             {
599 0         0 return $self->retrieve($id);
600             }
601             }
602              
603             sub _with_retries
604             {
605 4     4   11593 my $self = shift;
606 4         7 my $call = shift;
607             retry
608             {
609 17     17   31631308 $call->();
610             }
611 16     16   3344 retry_if {/current transaction is aborted, commands ignored until end of transaction block/}
612             catch
613             {
614 3     3   128 die $_; # rethrow the unhandled exception
615 4         33 };
616             }
617              
618              
619             #-------------------------------------------------------------------------------
620              
621             =head2 execute
622              
623             Performs an execute in OpenERP on the class level.
624              
625             $c->model('OpenERP')->class('Invoice')->execute('build_invoice', $args);
626              
627             Please look at L<OpenERP::OOM::Object::Base> for more information on C<execute>
628              
629             =cut
630              
631             sub execute {
632 0     0 1   my $self = shift;
633 0           my $action = shift;
634 0           my @params = @_;
635 0           my @args = ($action, $self->object_class->model, @params);
636 0           my $retval;
637             $self->_with_retries(sub {
638 0     0     $retval = $self->schema->client->object_execute(@args);
639 0           });
640 0           return $retval;
641             }
642              
643             #-------------------------------------------------------------------------------
644              
645              
646             1;