File Coverage

blib/lib/WebService/E4SE.pm
Criterion Covered Total %
statement 22 24 91.6
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 30 32 93.7


line stmt bran cond sub pod time code
1             package WebService::E4SE;
2              
3 1     1   131949 use Moo;
  1         21013  
  1         6  
4 1     1   2364 use Authen::NTLM 1.09;
  1         12762  
  1         85  
5 1     1   988 use LWP::UserAgent 6.02;
  1         42989  
  1         34  
6 1     1   9 use HTTP::Headers;
  1         1  
  1         24  
7 1     1   4 use HTTP::Request;
  1         2  
  1         24  
8 1     1   5 use Scalar::Util ();
  1         3  
  1         21  
9 1     1   5 use URI 1.60;
  1         15  
  1         23  
10 1     1   797 use XML::Compile::SOAP11 2.38;
  0            
  0            
11             use XML::Compile::SOAP11::Client;
12             use XML::Compile::WSDL11;
13             use XML::Compile::Transport::SOAPHTTP;
14             use XML::LibXML;
15              
16             use Carp ();
17             use strictures 2;
18             use namespace::clean;
19             use v5.10;
20              
21             our $AUTHORITY = 'cpan:CAPOEIRAB';
22             our $VERSION = '0.03';
23              
24             has _wsdls => (
25             is => 'lazy',
26             isa => sub { die "Not a HashRef" unless ref($_[0]) eq 'HASH' },
27             default => sub { {} },
28             );
29              
30             has _ua => (
31             is => 'lazy',
32             isa => sub { die "Not an LWP::UserAgent" unless Scalar::Util::blessed($_[0]) && $_[0]->isa('LWP::UserAgent') },
33             default => sub { LWP::UserAgent->new( keep_alive=>1 ) },
34             );
35              
36             has base_url => (
37             is => 'rw',
38             isa => sub {die "Not an URI" unless Scalar::Util::blessed($_[0]) && $_[0]->isa('URI')},
39             required => 1,
40             default => sub {
41             URI->new('http://epicor/e4se')
42             }
43             );
44              
45             has files => (
46             is => 'lazy',
47             isa => sub { die 'Should be an array reference' unless $_[0] && ref($_[0]) eq 'ARRAY' },
48             default => sub {[
49             'ActionCall.asmx',
50             'Attachment.asmx',
51             'BackOfficeAP.asmx',
52             'BackOfficeAR.asmx',
53             'BackOfficeCFG.asmx',
54             'BackOfficeGB.asmx',
55             'BackOfficeGL.asmx',
56             'BackOfficeIV.asmx',
57             'BackOfficeMC.asmx',
58             'Billing.asmx',
59             'Business.asmx',
60             'Carrier.asmx',
61             'CommercialTerms.asmx',
62             'Company.asmx',
63             'ControllingProject.asmx',
64             'CostVersion.asmx',
65             'CRMClientHelper.asmx',
66             'Currency.asmx',
67             'Customer.asmx',
68             'ECSClientHelper.asmx',
69             'Employee.asmx',
70             'ExchangeInterface.asmx',
71             'Expense.asmx',
72             'FinancialsAP.asmx',
73             'FinancialsAR.asmx',
74             'FinancialsCFG.asmx',
75             'FinancialsGB.asmx',
76             'FinancialsGL.asmx',
77             'FinancialsMC.asmx',
78             'FinancialsSync.asmx',
79             'GLAccount.asmx',
80             'IntersiteOrder.asmx',
81             'InventoryLocation.asmx',
82             'Journal.asmx',
83             'Location.asmx',
84             'LotSerial.asmx',
85             'Manufacturer.asmx',
86             'Material.asmx',
87             'MaterialPlan.asmx',
88             'MiscItems.asmx',
89             'MSProject.asmx',
90             'MSProjectEnterpriseCustomFieldsandLookupTables.asmx',
91             'Opportunity.asmx',
92             'Organization.asmx',
93             'PartMaster.asmx',
94             'Partner.asmx',
95             'PriceStructure.asmx',
96             'Project.asmx',
97             'Prospect.asmx',
98             'PSAClientHelper.asmx',
99             'PurchaseOrder.asmx',
100             'Receiving.asmx',
101             'Recognize.asmx',
102             'Resource.asmx',
103             'SalesCycleManagement.asmx',
104             'SalesOrder.asmx',
105             'SalesPerson.asmx',
106             'Shipping.asmx',
107             'Site.asmx',
108             'Supplier.asmx',
109             'svActionCall.asmx',
110             'svAttachment.asmx',
111             'svBackOfficeAP.asmx',
112             'svBackOfficeAR.asmx',
113             'svBackOfficeCFG.asmx',
114             'svBackOfficeGB.asmx',
115             'svBackOfficeGL.asmx',
116             'svBackOfficeIV.asmx',
117             'svBackOfficeMC.asmx',
118             'svBilling.asmx',
119             'svBusiness.asmx',
120             'svCarrier.asmx',
121             'svCommercialTerms.asmx',
122             'svCompany.asmx',
123             'svControllingProject.asmx',
124             'svCostVersion.asmx',
125             'svCRMClientHelper.asmx',
126             'svCurrency.asmx',
127             'svCustomer.asmx',
128             'svECSClientHelper.asmx',
129             'svEmployee.asmx',
130             'svExchangeInterface.asmx',
131             'svExpense.asmx',
132             'svFinancialsAP.asmx',
133             'svFinancialsAR.asmx',
134             'svFinancialsCFG.asmx',
135             'svFinancialsGB.asmx',
136             'svFinancialsGL.asmx',
137             'svFinancialsMC.asmx',
138             'svFinancialsSync.asmx',
139             'svGLAccount.asmx',
140             'svIntersiteOrder.asmx',
141             'svInventoryLocation.asmx',
142             'svJournal.asmx',
143             'svLocation.asmx',
144             'svLotSerial.asmx',
145             'svManufacturer.asmx',
146             'svMaterial.asmx',
147             'svMaterialPlan.asmx',
148             'svMiscItems.asmx',
149             'svMSProject.asmx',
150             'svMSProjectEnterpriseCustomFieldsandLookupTables.asmx',
151             'svOpportunity.asmx',
152             'svOrganization.asmx',
153             'svPartMaster.asmx',
154             'svPartner.asmx',
155             'svPriceStructure.asmx',
156             'svProject.asmx',
157             'svProspect.asmx',
158             'svPSAClientHelper.asmx',
159             'svPurchaseOrder.asmx',
160             'svReceiving.asmx',
161             'svRecognize.asmx',
162             'svResource.asmx',
163             'svSalesCycleManagement.asmx',
164             'svSalesOrder.asmx',
165             'svSalesPerson.asmx',
166             'svShipping.asmx',
167             'svSite.asmx',
168             'svSupplier.asmx',
169             'svSysArtifact.asmx',
170             'svSysDirector.asmx',
171             'svSysDomainInfo.asmx',
172             'svSysNotify.asmx',
173             'svSysSearchManager.asmx',
174             'svSysSecurity.asmx',
175             'svSysWorkflow.asmx',
176             'svTax.asmx',
177             'svTime.asmx',
178             'svUOM.asmx',
179             'SysArtifact.asmx',
180             'SysDirector.asmx',
181             'SysDomainInfo.asmx',
182             'SysNotify.asmx',
183             'SysSearchManager.asmx',
184             'SysSecurity.asmx',
185             'SysWorkflow.asmx',
186             'Tax.asmx',
187             'Time.asmx',
188             'UOM.asmx',
189             ]},
190             );
191              
192             has force_wsdl_reload => (
193             is => 'rw',
194             coerce => sub { return 0 unless $_[0]; return 1 if ref($_[0]); (lc($_[0]) eq 'false') ? 0 : 1},
195             required => 1,
196             default => 0
197             );
198              
199             has password => (
200             is => 'rw',
201             required => 1,
202             default => '',
203             );
204              
205             has realm => (
206             is => 'rw',
207             required => 1,
208             default => '',
209             );
210              
211             has site => (
212             is => 'rw',
213             required => 1,
214             default => 'epicor:80',
215             );
216              
217             has username => (
218             is => 'rw',
219             required => 1,
220             default => '',
221             );
222              
223             sub _get_port {
224             my ( $self, $file ) = @_;
225             return "WSSoap" unless defined($file) and length($file);
226             $file =~ s/\.\w+$//; #strip extension
227             return $file."WSSoap";
228             }
229              
230             sub _valid_file {
231             my ( $self, $file ) = @_;
232             return 0 unless defined $file and length $file;
233             return 1 if (grep {$_ eq $file} @{$self->files});
234             return 0;
235             }
236              
237             sub _wsdl {
238             my ( $self, $file ) = @_;
239             my $wsdl = $self->_wsdls();
240             #if our wsdl is already setup, let's just return
241             return 1 if ( exists($wsdl->{$file}) && defined($wsdl->{$file}) );
242              
243             #wsdl doesn't exist. let's setup the user agent for our transport and move along
244             $self->_ua->credentials( $self->site, $self->realm, $self->username, $self->password );
245              
246             my $res = $self->_ua->get($self->base_url . '/'. $file . '?WSDL' );
247             Carp::croak('Unable to setup WSDL: '.$res->status_lin()) unless $res->is_success;
248              
249             $wsdl->{$file} = XML::Compile::WSDL11->new( $res->decoded_content );
250             Carp::croak( "Unable to create new XML::Compile::WSDL11 object" ) unless $wsdl->{$file};
251              
252             my $trans = XML::Compile::Transport::SOAPHTTP->new(
253             user_agent=> $self->_ua,
254             address => $self->base_url.'/'. $file,
255             );
256             Carp::carp( "Unable to create new XML::Compile::Transport::SOAPHTTP object" ) unless $trans;
257              
258             $wsdl->{$file}->compileCalls(
259             port => $self->_get_port($file),
260             transport => $trans,
261             );
262             $self->_wsdls($wsdl);
263             return 1;
264             }
265              
266             sub call {
267             my ( $self, $file, $function, %parameters ) = @_;
268             unless ( $self->_valid_file($file) ) {
269             Carp::carp( "$file is not a valid web service found in E4SE." );
270             return 0;
271             }
272             my $wsdl = $self->wsdl();
273             if ( $self->force_wsdl_reload() ) {
274             delete($wsdl->{$file}) if exists($wsdl->{$file});
275             $self->force_wsdl_reload(0);
276             }
277             return 0 unless $self->_wsdl($file);
278              
279             return $wsdl->{$file}->call($function,%parameters);
280             }
281              
282             sub get_object {
283             my ( $self, $file ) = @_;
284             unless ( $self->_valid_file($file) ) {
285             Carp::carp( "$file is not a valid web service found in E4SE." );
286             return 0;
287             }
288             my $wsdl = $self->wsdl;
289             if ( $self->force_wsdl_reload() ) {
290             delete($wsdl->{$file}) if exists($wsdl->{$file});
291             $self->force_wsdl_reload(0);
292             }
293             return 0 unless $self->_wsdl($file);
294             return $wsdl->{$file};
295             }
296              
297             sub operations {
298             my ( $self, $file ) = @_;
299             unless ( $self->_valid_file($file) ) {
300             Carp::carp( "$file is not a valid web service found in E4SE." );
301             return [];
302             }
303             if ( $self->force_wsdl_reload() ) {
304             delete($self->wsdl->{$file}) if exists($self->wsdl->{$file});
305             $self->force_wsdl_reload(0);
306             }
307             unless ( $self->_wsdl($file) ) {
308             return [];
309             }
310             my @ops = $self->wsdl->{$file}->operations(port=>$self->_get_port($file));
311             my @ret = ();
312             for my $op ( @ops ) {
313             push @ret, $op->name;
314             }
315             return \@ret;
316             }
317              
318             1; # End of WebService::E4SE
319              
320             =encoding utf8
321              
322             =head1 NAME
323              
324             WebService::E4SE - Communicate with the various Epicor E4SE web services.
325              
326             =head1 SYNOPSIS
327              
328             use WebService::E4SE;
329              
330             # create a new object
331             my $ws = WebService::E4SE->new(
332             username => 'AD\username', # NTLM authentication
333             password => 'A password', # NTLM authentication
334             realm => '', # LWP::UserAgent and Authen::NTLM
335             site => 'epicor:80', # LWP::UserAgent and Authen::NTLM
336             base_url => URL->new('http://epicor/e4se'), # LWP::UserAgent and Authen::NTLM
337             timeout => 30, # LWP::UserAgent
338             );
339              
340             # get an array ref of web service APIs to communicate with
341             my $res = $ws->files();
342             say Dumper $res;
343              
344             # returns a list of method names for the file you wanted to know about.
345             my @operations = $ws->operations('Resource.asmx');
346             say Dumper @operations;
347              
348             # call a method and pass some named parameters to it
349             my ($res,$trace) = $ws->call('Resource.asmx','GetResourceForUserID', userID=>'someuser');
350              
351             # give me the XML::Compile::WSDL11 object
352             my $wsdl = $ws->get_object('Resource.asmx'); #returns the usable XML::Compile::WSDL11 object
353              
354             =head1 DESCRIPTION
355              
356             L allows us to connect to
357             L SOAP-based APIs
358             service to access our data or put in our timesheet.
359              
360             Each action on the software calls a SOAP-based web service API method. Each API
361             call is authenticated via NTLM.
362              
363             There are more than 100 web service files you could work with (.asmx
364             extensions) each having their own set of methods. On your installation of E4SE,
365             you can get a listing of method calls available by visiting one of those files
366             directly (C for example).
367              
368             The module will grab the WSDL from the file you're trying to deal with. It will make
369             use of that WSDL with L. You can force a reload of the WSDL at any
370             time. So, we build the L object and hold onto it for any further
371             calls to that file. These are generated by the calls you make, so hopefully we don't
372             kill you with too many objects. You can work directly with the new L
373             object if you like, or use the abstracted out methods listed below.
374              
375             For transportation, we're using L using
376             L with L.
377              
378             =head1 ATTRIBUTES
379              
380             L makes the following attributes available:
381              
382             =head2 base_url
383              
384             my $url = $ws->base_url;
385             $url = $ws->base_url(URI->new('http://epicor/e4se'));
386              
387             This should be the base L for your E4SE installation.
388              
389             =head2 files
390              
391             my $files = $ws->files;
392             $files = $ws->files(['file1.asmx', 'file2.asmx']);
393             say join ', ', @$files;
394              
395             This is reference to an array of file names that this web service has
396             knowledge of for an E4SE installation. If your installation has some services
397             that we're missing, you can inject them here. This will clobber, not
398             merge/append.
399              
400             =head2 force_wsdl_reload
401              
402             my $force = $ws->force_wsdl_reload;
403             $force = $ws->force_wsdl_reload(1);
404              
405             This attribute is defaulted to false (0). If set to true, the next call to a
406             method that would require a L object will go out to the
407             server and re-grab the WSDL and re-setup that WSDL object no matter if we have
408             already generated it or not. The attribute will be reset to false (0) directly
409             after the next WSDL object setup.
410              
411             =head2 password
412              
413             my $pass = $ws->password;
414             $pass = $ws->password('foobarbaz');
415              
416             This will be your domain password. No attempt to hide this is made.
417              
418             =head2 realm
419              
420             my $realm = $ws->realm;
421             $realm = $ws->realm('MyADRealm');
422              
423             Default is an empty string. This is for the L module and can generally be left blank.
424              
425             =head2 site
426              
427             my $site = $ws->site;
428             $site = $ws->site('epicor:80');
429              
430             This is for the L module. Set this accordingly.
431              
432             =head2 username
433              
434             my $user = $ws->username;
435             $user = $ws->username('AD\myusername');
436              
437             Usually, you need to prefix this with the domain your E4SE installation is using.
438              
439             =head1 METHODS
440              
441             L makes the following methods available:
442              
443             =head2 call
444              
445             use Try::Tiny;
446             try {
447             my ( $res, $trace) = $ws->call('Resource.asmx', 'GetResourceForUserID', %parameters );
448             say Dumper $res;
449             }
450             catch {
451             warn "An error happened: $_";
452             exit(1);
453             }
454              
455             This method will call an API method for the file you want. It will die on
456             errors outside of L's knowledge, otherwise
457             it's just a little wrapper around L->call();
458              
459             Another way to do this would be
460              
461             $ws->get_object('Reource.asmx')->call( 'GetResourceForUserID', %params );
462              
463             =head2 get_object
464              
465             my $wsdl = $ws->get_object('Resource.asmx');
466              
467             This method will return an L object for the file name
468             you supply. This handles going to the file's WSDL URL, grabbing that URL
469             with L and L, and using that WSDL response to
470             setup a new L object.
471              
472             Note that if you have previously setup a L object for that
473             file name, it will just return that object rather than going to the server and
474             requesting a new WSDL.
475              
476             =head2 operations
477              
478             my $available_operations = $ws->operations( $file );
479              
480             This method will return a list of L objects
481             that are available for the given file.
482              
483             =head1 AUTHOR
484              
485             Chase Whitener << >>
486              
487             =head1 BUGS
488              
489             Please report any bugs or feature requests on GitHub L.
490             We appreciate any and all criticism, bug reports, enhancements, or fixes.
491              
492             =head1 SUPPORT
493              
494             You can find documentation for this module with the perldoc command.
495              
496             perldoc WebService::E4SE
497              
498              
499             You can also look for information at:
500              
501             =over 4
502              
503             =item * GitHub
504              
505             L
506              
507             =item * AnnoCPAN: Annotated CPAN documentation
508              
509             L
510              
511             =item * CPAN Ratings
512              
513             L
514              
515             =item * Search CPAN
516              
517             L
518              
519             =back
520              
521             =head1 ACKNOWLEDGEMENTS
522              
523             =head1 LICENSE AND COPYRIGHT
524              
525             Copyright 2015
526             This program is free software, you can redistribute it and/or modify it under
527             the terms of the Artistic License version 2.0.
528              
529             =cut