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