File Coverage

blib/lib/OpenERP/OOM/Object/Base.pm
Criterion Covered Total %
statement 26 244 10.6
branch 0 102 0.0
condition 0 6 0.0
subroutine 9 39 23.0
pod 18 18 100.0
total 53 409 12.9


line stmt bran cond sub pod time code
1              
2             use 5.010;
3 1     1   5856 use Carp;
  1         3  
4 1     1   5 use Data::Dumper;
  1         2  
  1         66  
5 1     1   5 use List::MoreUtils qw/uniq/;
  1         2  
  1         45  
6 1     1   5 use Moose;
  1         3  
  1         10  
7 1     1   630 use Try::Tiny;
  1         3  
  1         7  
8 1     1   5507 use Try::Tiny::Retry;
  1         3  
  1         57  
9 1     1   6 use Time::HiRes qw/usleep/;
  1         3  
  1         81  
10 1     1   7 use Switch::Plain;
  1         1  
  1         9  
11 1     1   121  
  1         3  
  1         7  
12             extends 'Moose::Object';
13             with 'OpenERP::OOM::DynamicUtils';
14              
15             =head1 NAME
16              
17             OpenERP::OOM::Class::Base
18              
19             =head1 SYNOPSYS
20              
21             my $obj = $schema->class('Name')->create(\%args);
22              
23             :say $obj->id;
24              
25             $obj->name('New name');
26             $obj->update;
27              
28             $obj->delete;
29              
30             =head1 DESCRIPTION
31              
32             Provides a base set of properties and methods for OpenERP::OOM objects (update, delete, etc).
33              
34             =head1 PROPERTIES
35              
36             =head2 id
37              
38             Returns the OpenERP ID of an object.
39              
40             say $obj->id;
41              
42             =head2 BUILD
43              
44             The BUILD method sets up the methods for the links to the attached objects.
45              
46             =cut
47              
48             has 'id' => (
49             isa => 'Int',
50             is => 'ro',
51             );
52              
53             my $self = shift;
54              
55 0     0 1   # Add methods to follow links
56             my $links = $self->meta->link;
57             while (my ($name, $link) = each %$links) {
58 0           sswitch ($link->{type}) {
59 0           case ('single'): {
60             $self->meta->add_method(
61             $name,
62             sub {
63             my $obj = shift;
64             $obj->{"_$name"} //= $obj->class->schema->link($link->{class})->retrieve($link->{args}, $obj->{$link->{key}});
65 0     0      
66 0   0       unless ($obj->{"_$name"}) {
67             # FIXME: If $obj->{"_$name"} is undefined, we have a data integrity problem.
68 0 0         # Either the linked data is missing, or the key in the OpenERP object is missing.
69             die "Error linking to OpenERP object " . $obj->id . " of class " . ref($obj);
70             }
71 0            
72             # NOTE: this only links up the object from the linked object
73             # if it has a _source attribute
74             #
75             # has _source => (is => 'rw');
76              
77             if ($obj->{"_$name"}->can('_source')) {
78             # set the _source attribute to point back
79 0 0         # to the linked object.
80             $obj->{"_$name"}->_source($obj);
81             }
82 0            
83             return $obj->{"_$name"};
84             }
85 0           )
86             }
87             case ('multiple'): {
88 0           $self->meta->add_method(
89 0 0         $name,
    0          
90             sub {
91             return $self->class->schema->link($link->{class})->retrieve_list($link->{args}, $self->{$link->{key}});
92             }
93 0     0     )
94             }
95             }
96 0           }
97 0           }
98              
99              
100             #-------------------------------------------------------------------------------
101              
102             =head1 METHODS
103              
104             =head2 update
105              
106             Updates an object in OpenERP after its properties have been changed.
107              
108             $obj->name('New name');
109             $obj->update;
110              
111             Also allows a hashref to be passed to update multiple properties:
112              
113             $obj->update({
114             name => 'new name',
115             ref => 'new reference',
116             price => 'new price',
117             });
118              
119             =cut
120              
121             my $self = shift;
122              
123             if (my $update = shift) {
124 0     0 1   while (my ($param, $value) = each %$update) {
125             $self->$param($value);
126 0 0         }
127 0           }
128 0           my $context = $self->class->_get_context(shift);
129              
130             my $object;
131 0           foreach my $attribute ($self->dirty_attributes) {
132             next if ($attribute eq 'id');
133 0           next if ($attribute =~ '^_');
134 0            
135 0 0         $object->{$attribute} = $self->{$attribute};
136 0 0         }
137              
138 0           my $relationships = $self->meta->relationship;
139             while (my ($name, $rel) = each %$relationships) {
140             if ($object->{$rel->{key}}) {
141 0           sswitch ($rel->{type}) {
142 0           case ('one2many'): {
143 0 0         delete $object->{$rel->{key}}; # Don't update one2many relationships
144             }
145             case ('many2many'): {
146 0           $object->{$rel->{key}} = [[6,0,$object->{$rel->{key}}]];
147             }
148 0 0         }
    0          
149 0           }
150             }
151 0            
152             # Force Str parameters to be object type RPC::XML::string
153             foreach my $attribute ($self->meta->get_all_attributes) {
154             if (exists $object->{$attribute->name}) {
155             $object->{$attribute->name} = $self->prepare_attribute_for_send($attribute->type_constraint, $object->{$attribute->name});
156 0           }
157 0 0         }
158 0            
159             $self->class->_with_retries(sub {
160             $self->class->schema->client->object_execute_kw(
161             'write',
162             $self->model,
163 0     0     [ # positional args
164             $self->id,
165             $object,
166             ],
167             { # keyword args
168             context => $context,
169             },
170             );
171             });
172             $self->refresh;
173              
174 0           return $self;
175 0           }
176              
177 0           #-------------------------------------------------------------------------------
178              
179             =head2 update_single
180              
181             Updates OpenERP with a single property of an object.
182              
183             $obj->name('New name');
184             $obj->status('Active');
185              
186             $obj->update_single('name'); # Only the 'name' property is updated
187              
188             =cut
189              
190             my ($self, $property) = @_;
191             my $value = $self->{$property};
192              
193             # Check to see if the property is the key to a many2many relationship
194 0     0 1   my $relationships = $self->meta->relationship;
195 0           my ($key) = grep { $relationships->{$_}->{key} eq $property } keys %$relationships;
196             if($key)
197             {
198 0           my $rel = $relationships->{$key};
199 0           if ($rel->{type} eq 'many2many') {
  0            
200 0 0         $value = [[6,0,$value]];
201             }
202 0           }
203 0 0          
204 0           # Force Str parameters to be object type RPC::XML::string
205             foreach my $attribute ($self->meta->get_all_attributes) {
206             if ($attribute->name eq $property) {
207             $value = $self->prepare_attribute_for_send($attribute->type_constraint, $value);
208             }
209 0           }
210 0 0          
211 0           $self->class->schema->client->update($self->model, $self->id, {$property => $value});
212             return $self;
213             }
214              
215 0           #-------------------------------------------------------------------------------
216 0            
217             =head2 refresh
218              
219             Reloads an object's properties from OpenERP.
220              
221             $obj->refresh;
222              
223             =cut
224              
225             my $self = shift;
226              
227             my $new = $self->class->retrieve($self->id);
228              
229             foreach my $attribute ($self->meta->get_all_attributes) {
230 0     0 1   my $name = $attribute->name;
231             $self->{$name} = ($new->$name);
232 0           }
233             $self->mark_all_clean; # reset the dirty attribute
234 0            
235 0           return $self;
236 0           }
237              
238 0            
239             #-------------------------------------------------------------------------------
240 0            
241             =head2 delete
242              
243             Deletes an object from OpenERP.
244              
245             my $obj = $schema->class('Partner')->retrieve(60);
246             $obj->delete;
247              
248             =cut
249              
250             my $self = shift;
251              
252             $self->class->schema->client->delete($self->model, $self->id);
253             }
254              
255             {
256 0     0 1   my $self = shift;
257              
258 0           my $id = $self->class->schema->client->copy($self->model, $self->id);
259             # now load the new invoice and return it
260             return $id;
261             }
262              
263 0     0     =head2 copy
264              
265 0           Clone the current object, returning the new object.
266              
267 0           This is equivalent to pressing duplicate in the OpenERP user interface.
268              
269             =cut
270              
271             {
272             my ($self, @args) = @_;
273             my $args = shift;
274             my $id = $self->_copy;
275             # passing args through allows for field refinement.
276             my $clone = $self->class->retrieve($id, @args);
277             return $clone;
278             }
279              
280 0     0 1   #-------------------------------------------------------------------------------
281 0            
282 0           =head2 print
283              
284 0           This is a debug method.
285 0            
286             =cut
287              
288             my $self = shift;
289              
290             say "Print called";
291             }
292              
293              
294             #-------------------------------------------------------------------------------
295              
296             =head2 real_create_related
297 0     0 1    
298             This actually does the create related via OpenERP.
299 0            
300             I'm not sure in what scenarios you should use it versus the scenario's you
301             shouldn't. Suck it and see.
302              
303             It will create calls like this,
304              
305             # DEBUG_RPC:rpc.request:('execute', 'db', 1, '*', ('stock.partial.picking', 'write', [1], {'product_moves_out': [(0, 0, {'prodlot_id': False, 'product_id': 16, 'product_uom': 1, 'quantity': 10.0})]}, {'lang': 'en_GB', 'search_default_available': 1, 'project_id': False, 'tz': False, '__last_update': {'stock.partial.picking,1': False}, 'active_model': 'ir.ui.menu', 'section_id': False, 'contact_display': 'partner_address', 'active_ids': [3], 'active_id': 316}))
306              
307             Note that it will not return the object created.
308              
309             =cut
310              
311             {
312             my $self = shift;
313             my $relation_name = shift;
314             my $object = shift;
315             my $context = $self->class->_get_context(shift);
316              
317             # find relationship class
318             my $class = $self->relationship_class($relation_name);
319             my $data = $class->_collapse_data_to_ids($object);
320              
321             $self->class->schema->client->update($self->model, $self->id, {$relation_name => [[ 0, 0, $data ]]}, $context);
322 0     0 1    
323 0           # FIXME: need to check what happens to existing data
324 0           # how do you add multiple objects ?
325 0           return;
326             }
327              
328 0           =head2 create_related
329 0            
330             Creates a related or linked object.
331 0            
332             $obj->create_related('address',{
333             street => 'Drury Lane',
334             postcode => 'CV21 3DE',
335 0           });
336              
337             =cut
338              
339             my ($self, $relation_name, $object) = @_;
340              
341             ### Creating related object
342             ### $relation_name
343             ### with initial data:
344             ### $object
345             my $created_obj;
346              
347             if (my $relation = $self->meta->relationship->{$relation_name}) {
348             sswitch ($relation->{type}) {
349             case ('one2many'): {
350 0     0 1   my $class = $self->meta->name;
351             if ($class =~ m/(.*?)::(\w+)$/) {
352             my ($base, $name) = ($1, $2);
353             my $related_class = $base . "::" . $relation->{class};
354              
355             $self->ensure_class_loaded($related_class);
356 0           my $related_meta = $related_class->meta->relationship;
357              
358 0 0         my $far_end_relation;
    0          
359             REL: for my $key (keys %$related_meta) {
360             my $value = $related_meta->{$key};
361 0           if ($value->{class} eq $name) {
362 0 0         $far_end_relation = $key;
363 0           last REL;
364 0           }
365             }
366 0            
367 0           if ($far_end_relation) {
368             my $foreign_key = $related_meta->{$far_end_relation}->{key};
369 0            
370 0           ### Far end relation exists
371 0           $created_obj = $self->class->schema->class($relation->{class})->create({
372 0 0         %$object,
373 0           $foreign_key => $self->id,
374 0           });
375              
376             $self->refresh;
377             } else {
378 0 0         my $new_object = $self->class->schema->class($relation->{class})->create($object);
379 0            
380             $created_obj = $new_object;
381             $self->refresh;
382 0            
383             unless (grep {$new_object->id} @{$self->{$relation->{key}}}) {
384             push @{$self->{$relation->{key}}}, $new_object->id;
385             $self->update;
386             }
387 0           }
388             }
389 0           }
390             case ('many2many'): {
391 0           say "create_related many2many";
392 0           }
393             case ('many2one'): {
394 0 0         say "create_related many2one";
  0            
  0            
395 0           }
  0            
396 0           }
397             } elsif ($relation = $self->meta->link->{$relation_name}) {
398             sswitch ($relation->{type}) {
399             case ('single'): {
400             ### Creating linked object
401             try {
402 0           my $id = $self->class->schema->link($relation->{class})->create($relation->{args}, $object);
403             $created_obj = $id;
404 0 0         ### Linked object created with key $id
    0          
    0          
405 0           $self->{$relation->{key}} = $id;
406             $self->update_single($relation->{key});
407 0           undef $self->{"_$relation_name"};
408             } catch {
409             die "Error creating linked object: $_[0]";
410             };
411             }
412             case ('multiple'): {
413 0     0     say "create_linked multiple";
414 0           }
415             }
416 0           }
417 0           else {
418 0           croak "Can not find relation $relation_name";
419             }
420 0     0     return $created_obj if $created_obj;
421 0           }
422              
423 0 0         {
    0          
424 0           my $val = shift;
425             return ref $val ? $val->id : $val;
426 0           }
427              
428             =head2 find_related
429 0            
430             Finds a property related to the current object.
431 0 0          
432             my $line = $po->find_related('order_lines', [ 'id', '=', 1 ]);
433              
434             This only works with relationships to OpenERP objects (i.e. not DBIC) and
435             to one2many relationships where the other side of the relationship has a field
436 0     0     pointing back to the object you are searching from.
437 0 0          
438             In any other case the method will croak.
439              
440             If the search criteria return more than one result it will whine.
441              
442             =cut
443              
444             my ($self) = shift;
445             my @results = $self->search_related(@_);
446             if(scalar @results > 1)
447             {
448             # should this just croak?
449             carp 'find_related returned more than 1 result';
450             }
451             if(@results)
452             {
453             return $results[0];
454             }
455             }
456              
457 0     0 1   =head2 relationship_class
458 0            
459 0 0         Returns the OpenERP::OOM::Class object for the relationship passed in.
460              
461             Obviously this only works for the OpenERP relationships. It will croak
462 0           if you ask for a relationship to a DBIC object.
463              
464 0 0         =cut
465              
466 0           {
467             my ($self, $relationship) = @_;
468             if (my $relation = $self->meta->relationship->{$relationship}) {
469             my $type = $relation->{type};
470             croak 'Cannot get a class for a DBIC relationship' if $type eq 'single'
471             || $type eq 'multiple';
472             my $class = $relation->{class};
473             return $self->class->schema->class($class);
474             }
475             croak "Unable to find relation $relationship";
476             }
477              
478             =head2 search_related
479              
480             Searches for objects of a relation associated with this object.
481 0     0 1    
482 0 0         my @lines = $po->search_related('order_lines', [ 'state', '=', 'draft' ]);
483 0            
484 0 0 0       This only works with relationships to OpenERP objects (i.e. not DBIC) and
485             to one2many relationships where the other side of the relationship has a field
486 0           pointing back to the object you are searching from.
487 0            
488             In any other case the method will croak.
489 0            
490             =cut
491              
492             my ($self, $relation_name, @search) = @_;
493              
494             # find the relation details and add it to the search criteria.
495             if (my $relation = $self->meta->relationship->{$relation_name}) {
496             sswitch ($relation->{type}) {
497             case ('one2many'): {
498             my $class = $self->meta->name;
499             if ($class =~ m/(.*?)::(\w+)$/) {
500             my ($base, $name) = ($1, $2);
501             my $related_class = $self->class->schema->class($relation->{class});
502             my $related_meta = $related_class->object->meta->relationship;
503              
504             my $far_end_relation;
505             REL: for my $key (keys %$related_meta) {
506             my $value = $related_meta->{$key};
507 0     0 1   if ($value->{class} eq $name) {
508             $far_end_relation = $key;
509             last REL;
510 0 0         }
    0          
511             }
512              
513 0           if ($far_end_relation) {
514 0 0          
515 0           my $foreign_key = $related_meta->{$far_end_relation}->{key};
516 0            
517 0           push @search, [ $foreign_key, '=', $self->id ];
518             return $related_class->search(@search);
519 0            
520 0           } else {
521 0           # well, perhaps we could fix this, but I can't be bothered at the moment.
522 0 0         croak 'Unable to search_related without relationship back';
523 0           }
524 0           }
525             }
526             case ('many2many'): {
527             croak 'Unable to search_related many2many relationships';
528 0 0         }
529             case ('many2one'): {
530 0           croak 'Unable to search_related many2one relationships';
531             }
532 0           }
533 0           } elsif ($relation = $self->meta->link->{$relation_name}) {
534             croak 'Unable to search_related outside NonOpenERP';
535             }
536              
537 0           croak 'Unable to search_related'; # beat up the lame programmer who did this.
538             }
539              
540              
541             #-------------------------------------------------------------------------------
542 0            
543             =head2 add_related
544 0 0          
    0          
    0          
545 0           Adds a related or linked object to a one2many, many2many, or multiple relationship.
546              
547 0           my $partner = $schema->class('Partner')->find(...);
548             my $category = $schema->class('PartnerCategory')->find(...);
549 0            
550             $partner->add_related('category', $category);
551              
552 0           =cut
553              
554             my ($self, $relation_name, $object) = @_;
555              
556             if (my $relation = $self->meta->relationship->{$relation_name}) {
557             sswitch ($relation->{type}) {
558             case ('one2many'): {
559             # FIXME - is this the same process as adding a many2many relationship?
560             }
561             case ('many2many'): {
562             push @{$self->{$relation->{key}}}, _id($object);
563             $self->{$relation->{key}} = [uniq @{$self->{$relation->{key}}}];
564             $self->update_single($relation->{key});
565             }
566             }
567             } elsif ($relation = $self->meta->link->{$relation_name}) {
568             sswitch ($relation->{type}) {
569             case ('multiple'): {
570 0     0 1   # FIXME - handle linked as well as related objects
571             }
572 0 0         }
    0          
573             }
574             }
575              
576              
577 0 0         #-------------------------------------------------------------------------------
    0          
578 0            
  0            
579 0           =head2 set_related
  0            
580 0            
581             Like the DBIx::Class set_related. Sets up a link to a related object.
582 0            
583             =cut
584              
585 0 0         my ($self, $relation_name, $object) = @_;
586              
587             if (my $relation = $self->meta->relationship->{$relation_name}) {
588 0           sswitch ($relation->{type}) {
589             case ('many2one'): {
590             $self->{$relation->{key}} = $object ? _id($object) : undef;
591             $self->update_single($relation->{key});
592             }
593             case ('many2many'): {
594             my @array;
595             if($object)
596             {
597             if(ref $object eq 'ARRAY')
598             {
599             @array = map { _id($_) } @$object;
600             }
601             else
602 0     0 1   {
603             push @array, _id($object);
604 0 0         }
605             }
606             $self->{$relation->{key}} = \@array;
607 0 0         $self->update_single($relation->{key});
608 0           }
609             default: {
610             carp "Cannot use set_related() on a $_ relationship";
611 0           }
612 0 0         }
613             } else {
614 0 0         carp "Relation '$relation_name' does not exist!";
615             }
616 0           }
  0            
617              
618             =head2 execute_workflow
619              
620 0           Performs an exec_workflow in OpenERP.
621              
622             $self->execute_workflow('purchase_confirm');
623 0            
624 0           Is likely to translate to something like this,
625              
626 0 0         # DEBUG_RPC:rpc.request:('exec_workflow', 'db', 1, '*', ('purchase.order', 'purchase_confirm', 24))
    0          
627 0            
628             The 24 is the id of the object.
629 0            
630             =cut
631 0            
632             {
633             my ($self, $workflow) = @_;
634              
635             retry
636             {
637             $self->class->schema->client->object_exec_workflow($workflow, $self->model, $self->id);
638             }
639             retry_if {/current transaction is aborted, commands ignored until end of transaction block/}
640             catch
641             {
642             die $_; # rethrow the unhandled exception
643             };
644             }
645              
646             =head2 execute
647              
648             Performs an execute in OpenERP.
649              
650             $self->execute('action_process');
651 0     0 1    
652             Is likely to translate to something like this,
653              
654             # DEBUG_RPC:rpc.request:('execute', 'gooner', 1, '*', ('stock.picking', 'action_process', [26], {'lang': 'en_GB', 'search_default_available': 1, 'active_ids': [316], 'tz': False, 'active_model': 'ir.ui.menu', 'section_id': False, 'contact_display': 'partner_address', 'project_id': False, 'active_id': 316}))
655 0     0      
656             The 26 is the id of the object.
657 0     0      
658             =cut
659              
660 0     0     {
661 0           my $self = shift;
662             my $action = shift;
663             my @params = @_;
664              
665             my @args = ($action, $self->model, [$self->id], @params);
666             my $retval;
667             $self->class->_with_retries(sub {
668             $retval = $self->class->schema->client->object_execute(@args);
669             });
670             return $retval;
671             }
672              
673             =head2 executex
674              
675             Similar to execute but it allows you to specify any number of parameters.
676              
677             Primarily created to prevent any compatibility problems with other callers.
678             Although I'm not entirely sure if there are any.
679              
680 0     0 1   $self->executex('add_invoices_to_payment', [1,2], [3,4]);
681 0            
682 0           Translates roughly to
683              
684 0           execute_kw(..., 'payment.order', 'add_invoices_to_payment', [5], [1, 2], [3, 4])
685 0            
686             Stick a hash on the end of the list of params to pass a context object.
687 0     0      
688 0           =cut
689 0            
690             {
691             my ($self, $action, @rest) = @_;
692              
693             my @args = ($action, $self->model, [$self->id]);
694             push @args, @rest if @rest;
695             my $retval;
696             $self->class->_with_retries(sub {
697             $retval = $self->class->schema->client->object_execute(@args);
698             });
699             return $retval;
700             }
701              
702             =head2 get_report
703              
704             To print a purchase order we need to send a report, then get it, then display it, then print it (and you don't want to know about all the traffic behind the scenes...)
705              
706             The first step looks like this:
707              
708             # DEBUG_RPC:rpc.request:('report', 'aquarius_openerp_jj_staging', 1, '*', (u'purchase.quotation', [1], {'model': u'purchase.order', 'id': 1, 'report_type': u'pdf'}, {'lang': u'en_GB', 'active_ids': [1], 'tz': False, 'active_model': u'purchase.order', 'section_id': False, 'search_default_draft': 1, 'project_id': False, 'active_id': 1}))
709              
710             =cut
711 0     0 1    
712             {
713 0           my $self = shift;
714 0 0         my $report_id = shift;
715 0            
716             my $id = $self->class->schema->client->report_report($report_id, $self->id,
717 0     0     {
718 0           model => $self->model,
719 0           id => $self->id,
720             report_type => 'pdf',
721             }, @_);
722              
723             # the report_report function returns only a report id, which is all we need to pass to the next function call
724             # but report_report_get don't work first time (?!) so we need to call it recursively until with get an answer
725             my $data;
726             while(!$data)
727             {
728             $data = $self->class->schema->client->report_report_get($id);
729             sleep 1;
730             }
731             return $data;
732             }
733              
734 0     0 1    
735 0           1;