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