js-mw-api 2.5.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- js_mw_api/__init__.py +0 -0
- js_mw_api/mwapi.py +1226 -0
- js_mw_api-2.5.0.3.dist-info/METADATA +19 -0
- js_mw_api-2.5.0.3.dist-info/RECORD +5 -0
- js_mw_api-2.5.0.3.dist-info/WHEEL +4 -0
js_mw_api/__init__.py
ADDED
|
File without changes
|
js_mw_api/mwapi.py
ADDED
|
@@ -0,0 +1,1226 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
|
+
import os
|
|
4
|
+
import json
|
|
5
|
+
from bs4 import BeautifulSoup
|
|
6
|
+
import pandas as pd
|
|
7
|
+
import re
|
|
8
|
+
import urllib.parse
|
|
9
|
+
from cryptography.fernet import Fernet
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
# TODO: Adjust functions to use MorawareSession.session as a common requests session, rather than creating new sessions for each request. This will allow for better session management and less overhead. Although this can cause issues with multithreading, so will need to be tested.
|
|
13
|
+
# Can be done by optionally passing the session to the functions, and using it in the requests.get/post calls.
|
|
14
|
+
|
|
15
|
+
DEFAULTMORAWARECREDS = {'user': None,
|
|
16
|
+
'pwd': None}
|
|
17
|
+
|
|
18
|
+
JOBCARD_FIELDMAPPING = {
|
|
19
|
+
'Notes' : '149',
|
|
20
|
+
'Mitre' : '580',
|
|
21
|
+
'Tap' : '103',
|
|
22
|
+
'Hob' : '104',
|
|
23
|
+
'Waterjet' : '645',
|
|
24
|
+
'GlueUp' : '1093',
|
|
25
|
+
'Edge' : '100',
|
|
26
|
+
'Qtd_TI' : '1177',
|
|
27
|
+
'FORM_IDENTIFIER' : 'CUSTOMER AND SITE DETAILS'}
|
|
28
|
+
|
|
29
|
+
CLIENT = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_env_variable(key, default=None):
|
|
33
|
+
return os.environ.get(key, default)
|
|
34
|
+
|
|
35
|
+
# Get the client, default creds and field mappings from the environment variables.
|
|
36
|
+
CLIENT = get_env_variable('MORAWARE_CLIENT')
|
|
37
|
+
JOBCARD_FIELDMAPPING = json.loads(get_env_variable('JOBCARD_FIELDMAPPING', '{}'))
|
|
38
|
+
DEFAULTMORAWARECREDS = json.loads(get_env_variable('DEFAULTMORAWARECREDS', '{}'))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
#############################################################################################################################################################################
|
|
42
|
+
################################################################################# Functions #################################################################################
|
|
43
|
+
#############################################################################################################################################################################
|
|
44
|
+
|
|
45
|
+
############################################################################## General Functions ############################################################################
|
|
46
|
+
|
|
47
|
+
def mw_login_cookie(mw_cookie):
|
|
48
|
+
return mw_cookie.mw_cookie if hasattr(mw_cookie, 'mw_cookie') else mw_cookie
|
|
49
|
+
|
|
50
|
+
def Give_Key():
|
|
51
|
+
'''
|
|
52
|
+
Generates a key for use in encrypting and decrypting strings.
|
|
53
|
+
'''
|
|
54
|
+
return Fernet.generate_key()
|
|
55
|
+
|
|
56
|
+
def encrypt_string(string, key):
|
|
57
|
+
'''
|
|
58
|
+
Encrypts a string using a key.
|
|
59
|
+
Args:
|
|
60
|
+
string (str): The string to encrypt.
|
|
61
|
+
key (str): The key to use for encryption.
|
|
62
|
+
Returns:
|
|
63
|
+
str: The encrypted string.
|
|
64
|
+
'''
|
|
65
|
+
cipher_suite = Fernet(key)
|
|
66
|
+
return cipher_suite.encrypt(string.encode()).decode()
|
|
67
|
+
|
|
68
|
+
def decrypt_string(string, key):
|
|
69
|
+
'''
|
|
70
|
+
Decrypts a string using a key.
|
|
71
|
+
Args:
|
|
72
|
+
string (str): The string to decrypt.
|
|
73
|
+
key (str): The key to use for decryption.
|
|
74
|
+
Returns:
|
|
75
|
+
str: The decrypted string.
|
|
76
|
+
'''
|
|
77
|
+
cipher_suite = Fernet(key)
|
|
78
|
+
return cipher_suite.decrypt(string.encode()).decode()
|
|
79
|
+
|
|
80
|
+
def convert_float(value):
|
|
81
|
+
if isinstance(value, float):
|
|
82
|
+
if np.isnan(value): return ''
|
|
83
|
+
elif np.isinf(value): return True
|
|
84
|
+
if isinstance(value, str) and value.upper() in ('TRUE', 'FALSE'): return value == 'TRUE'
|
|
85
|
+
return value
|
|
86
|
+
|
|
87
|
+
def convert_date(input_string, desired_format):
|
|
88
|
+
if not input_string: return input_string
|
|
89
|
+
for fmt in ('%d/%m/%Y', '%Y-%m-%d', '%d-%m-%Y', '%Y/%m/%d'):
|
|
90
|
+
try:
|
|
91
|
+
dt = datetime.strptime(input_string, fmt)
|
|
92
|
+
return dt.strftime(desired_format)
|
|
93
|
+
except: pass
|
|
94
|
+
print('Date Conversion Failed')
|
|
95
|
+
return input_string
|
|
96
|
+
|
|
97
|
+
def Give_24hrTime(time):
|
|
98
|
+
if not time: return time
|
|
99
|
+
try:
|
|
100
|
+
return datetime.strptime(time, '%I:%M%p').strftime('%H:%M')
|
|
101
|
+
except:
|
|
102
|
+
print('Time Conversion Failed')
|
|
103
|
+
return time
|
|
104
|
+
|
|
105
|
+
def Encode_Dict_to_String(dict_to_encode):
|
|
106
|
+
'''
|
|
107
|
+
Encode a dictionary to a string for use in Moraware updates.
|
|
108
|
+
Gives the P0 field for creating a job from a quote.
|
|
109
|
+
'''
|
|
110
|
+
encoded_items = [f'"{k}":{v}' if isinstance(v, int) else f'"{k}":"{v}"' for k, v in dict_to_encode.items()]
|
|
111
|
+
return '{' + ','.join(encoded_items) + '}'
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
############################################################################### Moraware Classes ###############################################################################
|
|
116
|
+
|
|
117
|
+
class LoggingSession(requests.Session):
|
|
118
|
+
def __init__(self, manager):
|
|
119
|
+
super().__init__()
|
|
120
|
+
self.last_request_time = None
|
|
121
|
+
self.manager = manager
|
|
122
|
+
def request(self, *args, **kwargs):
|
|
123
|
+
if kwargs.get('JS_CORE'): kwargs.pop('JS_CORE') #Can implement other ways to handle different JS_CORE values
|
|
124
|
+
else: self.manager.refresh() #Because JS_Core app is inteded to manage it's own session refreshes.
|
|
125
|
+
self.last_request_time = self.manager.active = datetime.now()
|
|
126
|
+
response = super().request(*args, **kwargs)
|
|
127
|
+
return response
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class MorawareSession:
|
|
131
|
+
'''
|
|
132
|
+
Create a Moraware login session, or fetch login session data from json file in this directory.
|
|
133
|
+
If no session data is found, or the session data is expired, a new session is created.
|
|
134
|
+
Will be defualt use seed data including credentials and decryption key.
|
|
135
|
+
Otherwise will use entered credetntials and look for a json file with the client name and user name.
|
|
136
|
+
Can quickly and easily be converted to and from json for storage - with the key for encryption.
|
|
137
|
+
|
|
138
|
+
Attributes:
|
|
139
|
+
session (requests.Session): the session object for the Moraware login.
|
|
140
|
+
mw_cookie (dict): the cookie data for the Moraware login.
|
|
141
|
+
expiry (datetime): the expiry time for the Moraware login.
|
|
142
|
+
sessionheaders (dict): the headers for the Moraware login
|
|
143
|
+
client (str): the client name for the Moraware login.
|
|
144
|
+
user (str): the user name for the Moraware login.
|
|
145
|
+
active (datetime): the last time the session was used.
|
|
146
|
+
|
|
147
|
+
Methods:
|
|
148
|
+
LoginProcess: logs in to Moraware and updates the session data.
|
|
149
|
+
expired: returns True if the session has expired
|
|
150
|
+
refresh: refreshes the session if it is close to expiry.
|
|
151
|
+
to_json: returns the session data as a json string.
|
|
152
|
+
from_json: creates a MorawareSession object from a json string.
|
|
153
|
+
|
|
154
|
+
'''
|
|
155
|
+
loginform = {'action': 'login',
|
|
156
|
+
'LOGIN': 'Sign In',
|
|
157
|
+
}
|
|
158
|
+
name = 'Moraware'
|
|
159
|
+
user = ''
|
|
160
|
+
|
|
161
|
+
def __init__(self, client=CLIENT, UserCreds={}, Session=None, SeedData=None, key=None, **kwargs):
|
|
162
|
+
if SeedData:
|
|
163
|
+
self.client = SeedData.get('client', CLIENT)
|
|
164
|
+
self.endpoint = f'https://{self.client}.moraware.net/'
|
|
165
|
+
self.user = SeedData.get('user', '')
|
|
166
|
+
self.mw_cookie = SeedData.get('mw_cookie', None)
|
|
167
|
+
self.expiry = SeedData.get('expiry', datetime.now())
|
|
168
|
+
self.active = SeedData.get('active', datetime.now())
|
|
169
|
+
self.sessionheaders = SeedData.get('sessionheaders', {})
|
|
170
|
+
pwd = decrypt_string(SeedData.get('pwd', ''), key) if SeedData.get('pwd') else ''
|
|
171
|
+
self.loginform.update( {'user': SeedData.get('user', ''), 'pwd': pwd} )
|
|
172
|
+
|
|
173
|
+
else:
|
|
174
|
+
self.client = client
|
|
175
|
+
self.endpoint = f'https://{client}.moraware.net/'
|
|
176
|
+
self.loginform.update(UserCreds)
|
|
177
|
+
self.user = UserCreds.get('user', '')
|
|
178
|
+
cookiedat = {}
|
|
179
|
+
if os.path.exists(f'MWS_{client}_{self.user}.json'):
|
|
180
|
+
with open(f'MWS_{client}_{self.user}.json', 'r') as jf: cookiedat = json.load(jf)
|
|
181
|
+
|
|
182
|
+
self.mw_cookie = cookiedat.get('mw_cookie')
|
|
183
|
+
self.expiry = datetime.fromisoformat(cookiedat.get('expiry', '1900-01-01T00:00:00'))
|
|
184
|
+
self.active = datetime.fromisoformat(cookiedat.get('active', '1900-01-01T00:00:00'))
|
|
185
|
+
self.sessionheaders = cookiedat.get('sessionheaders')
|
|
186
|
+
|
|
187
|
+
self.session = Session if Session else LoggingSession(self)
|
|
188
|
+
if self.mw_cookie:
|
|
189
|
+
self.session.cookies.update(self.mw_cookie)
|
|
190
|
+
self.session.headers.update(self.sessionheaders)
|
|
191
|
+
|
|
192
|
+
if not self.mw_cookie or datetime.now() > self.expiry:
|
|
193
|
+
self.LoginProcess()
|
|
194
|
+
|
|
195
|
+
def LoginProcess(self):
|
|
196
|
+
print('Logging in to Moraware...')
|
|
197
|
+
s = self.session
|
|
198
|
+
s.cookies.clear()
|
|
199
|
+
rsp = s.post(self.endpoint, data=self.loginform, JS_CORE='login')
|
|
200
|
+
#read the response page for successful login
|
|
201
|
+
page = BeautifulSoup(rsp.content, 'html.parser')
|
|
202
|
+
if 'sign in' in page.title.text.lower(): raise Exception('Login Failed')
|
|
203
|
+
|
|
204
|
+
self.sessionheaders = dict(s.headers)
|
|
205
|
+
self.expiry = datetime.now() + timedelta(minutes=50)
|
|
206
|
+
self.mw_cookie = s.cookies.get_dict()
|
|
207
|
+
self.active = datetime.now()
|
|
208
|
+
with open(f'MWS_{self.client}_{self.user}.json', 'w') as jsonpointer:
|
|
209
|
+
json.dump({'mw_cookie': self.mw_cookie,
|
|
210
|
+
'expiry' : self.expiry.isoformat(),
|
|
211
|
+
'active' : self.active.isoformat(),
|
|
212
|
+
'user': self.user,
|
|
213
|
+
'sessionheaders': self.sessionheaders}, jsonpointer)
|
|
214
|
+
return self.mw_cookie
|
|
215
|
+
|
|
216
|
+
def refresh(self):
|
|
217
|
+
lr = self.session.last_request_time
|
|
218
|
+
if not lr: lr = self.active
|
|
219
|
+
if datetime.now() - lr > timedelta(minutes=55):
|
|
220
|
+
self.LoginProcess()
|
|
221
|
+
#return (True, 'Initialised')
|
|
222
|
+
return {'active': self.active, 'status': 'Initialised'}
|
|
223
|
+
elif datetime.now() - lr > timedelta(minutes=45):
|
|
224
|
+
self.session.get(self.endpoint, JS_CORE='refresh')
|
|
225
|
+
self.expiry = datetime.now() + timedelta(minutes=55)
|
|
226
|
+
return {'active': self.active, 'status': 'Refreshed'}
|
|
227
|
+
return {'active': self.active, 'status': 'Active'}
|
|
228
|
+
|
|
229
|
+
def expired(self):
|
|
230
|
+
lr = self.session.last_request_time
|
|
231
|
+
if not lr: lr = self.active
|
|
232
|
+
if datetime.now() - lr > timedelta(minutes=55):
|
|
233
|
+
return True
|
|
234
|
+
|
|
235
|
+
def __str__(self):
|
|
236
|
+
return f'Moraware Session: {self.client}/{self.user}/{self.mw_cookie}'
|
|
237
|
+
|
|
238
|
+
def to_json(self, key=None):
|
|
239
|
+
return json.dumps({'mw_cookie': self.mw_cookie,
|
|
240
|
+
'expiry' : self.expiry.isoformat(),
|
|
241
|
+
'active' : self.active.isoformat(),
|
|
242
|
+
'sessionheaders': self.sessionheaders,
|
|
243
|
+
'client': self.client,
|
|
244
|
+
'user': self.user,
|
|
245
|
+
'pwd': encrypt_string(self.loginform['pwd'], key) if key else ''})
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@classmethod
|
|
249
|
+
def from_json(cls, jsondata, key=None):
|
|
250
|
+
data = json.loads(jsondata)
|
|
251
|
+
if data.get('expiry'):
|
|
252
|
+
data['expiry'] = datetime.fromisoformat(data['expiry'])
|
|
253
|
+
if data.get('active'):
|
|
254
|
+
data['active'] = datetime.fromisoformat(data['active'])
|
|
255
|
+
return cls(SeedData=data, key=key)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class MorawareConnection:
|
|
259
|
+
'''
|
|
260
|
+
Class for managing client Moraware data.
|
|
261
|
+
This class stores IDs for various Moraware data, and fetches them if they are not present in the data dictionary, and updates the data dictionary.
|
|
262
|
+
Intended primarily for managing the data dictionary for a single client, and providing that data to other classes.
|
|
263
|
+
Can be converted to and from json for storage.
|
|
264
|
+
Can create a Moraware Session object inside itself to manage the login.
|
|
265
|
+
|
|
266
|
+
Attributes:
|
|
267
|
+
session (MorawareSession): the session object for the Moraware login.
|
|
268
|
+
client (str): the client name for the Moraware login.
|
|
269
|
+
Data (dict): the data dictionary for the client.
|
|
270
|
+
|
|
271
|
+
Methods:
|
|
272
|
+
data_check: checks if a key is in the data dictionary.
|
|
273
|
+
data_update: updates the data dictionary with a key and value.
|
|
274
|
+
data_check_update: checks if a key is in the data dictionary, and if not, fetches the data and updates the dictionary.
|
|
275
|
+
to_json: returns the session data as a json string.
|
|
276
|
+
from_json: creates a MorawareConnection object from a json string.
|
|
277
|
+
check_refresh_session: checks if the session is expired, and refreshes it if necessary.
|
|
278
|
+
GetQuote: fetches a Moraware Quote object.
|
|
279
|
+
|
|
280
|
+
'''
|
|
281
|
+
def __init__(self, Session=None, **kwargs):
|
|
282
|
+
if Session:
|
|
283
|
+
self.session = Session
|
|
284
|
+
client = Session.client
|
|
285
|
+
else:
|
|
286
|
+
client = kwargs.get('client', CLIENT)
|
|
287
|
+
self.session = MorawareSession(**kwargs)
|
|
288
|
+
self.client = client
|
|
289
|
+
|
|
290
|
+
self.Data = {}
|
|
291
|
+
if os.path.exists(f'{client}_MCD.json'):
|
|
292
|
+
with open(f'{client}_MCD.json', 'r') as f:
|
|
293
|
+
self.Data = json.load(f)
|
|
294
|
+
if 'data' in kwargs:
|
|
295
|
+
self.Data.update(kwargs['data'])
|
|
296
|
+
with open(f'{client}_MCD.json', 'w') as f:
|
|
297
|
+
json.dump(self.Data, f)
|
|
298
|
+
self.check_refresh_session()
|
|
299
|
+
|
|
300
|
+
def __str__(self):
|
|
301
|
+
return f'Moraware Connection: {self.client} - {self.session}'
|
|
302
|
+
|
|
303
|
+
@property
|
|
304
|
+
def mw_cookie(self):
|
|
305
|
+
return self.session.mw_cookie
|
|
306
|
+
|
|
307
|
+
def data_check(self, key):
|
|
308
|
+
if key not in self.Data:
|
|
309
|
+
return False
|
|
310
|
+
return True
|
|
311
|
+
|
|
312
|
+
def data_update(self, key, value):
|
|
313
|
+
self.Data[key] = value
|
|
314
|
+
with open(f'{self.client}_MCD.json', 'w') as f:
|
|
315
|
+
json.dump(self.Data, f)
|
|
316
|
+
|
|
317
|
+
def data_check_update(self, keys):
|
|
318
|
+
func = {'StatusIDs': GetActivityStatusIDs,
|
|
319
|
+
'ActivityIDs': GetActivityIDs,
|
|
320
|
+
'ProductIDs': GetALLInventoryIDs,
|
|
321
|
+
#'JobCardFieldMapping':
|
|
322
|
+
}
|
|
323
|
+
if not ( isinstance(keys, list) or isinstance(keys, tuple) ): keys = [keys]
|
|
324
|
+
for key in keys:
|
|
325
|
+
if not self.Data.get(key):
|
|
326
|
+
if key in func: self.data_update(key, func[key](self.session, self.client))
|
|
327
|
+
else: raise Exception(f'Key {key} not found in data dictionary, and no function to fetch it.')
|
|
328
|
+
|
|
329
|
+
def Give(self, key):
|
|
330
|
+
self.data_check_update(key)
|
|
331
|
+
return self.Data[key]
|
|
332
|
+
|
|
333
|
+
def to_json(self, key=None):
|
|
334
|
+
return json.dumps({'client': self.client,
|
|
335
|
+
'data': self.Data,
|
|
336
|
+
'session': self.session.to_json(key)})
|
|
337
|
+
|
|
338
|
+
@classmethod
|
|
339
|
+
def from_json(cls, jsondata, key=None):
|
|
340
|
+
data = json.loads(jsondata)
|
|
341
|
+
return cls(Session=MorawareSession.from_json(data['session'], key), **data)
|
|
342
|
+
|
|
343
|
+
def check_refresh_session(self, force=None):
|
|
344
|
+
if self.session.expired() or force:
|
|
345
|
+
self.session.LoginProcess()
|
|
346
|
+
|
|
347
|
+
def GetQuote(self, QuoteID):
|
|
348
|
+
self.check_refresh_session()
|
|
349
|
+
return MorawareQuote(QuoteID, self)
|
|
350
|
+
|
|
351
|
+
def GetView(self, url, headers=None, IDCol_Index=[0], TableType=None):
|
|
352
|
+
self.check_refresh_session()
|
|
353
|
+
return Fetch_View(url, self.mw_cookie, headers, IDCol_Index, TableType)
|
|
354
|
+
|
|
355
|
+
def GetJob(self, JobID):
|
|
356
|
+
self.check_refresh_session()
|
|
357
|
+
return MorawareJob(self, JobID)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
class MorawareQuote:
|
|
363
|
+
'''
|
|
364
|
+
A Moraware Quote Object.
|
|
365
|
+
'''
|
|
366
|
+
def __init__(self, QuoteID, mw_Connect):
|
|
367
|
+
self.QuoteID = QuoteID
|
|
368
|
+
self.QuoteData = GetQuoteDetails(QuoteID, mw_Connect.mw_cookie, mw_Connect.client)
|
|
369
|
+
self.mw_Connect = mw_Connect
|
|
370
|
+
|
|
371
|
+
def CreateJob(self, JobName):
|
|
372
|
+
'''
|
|
373
|
+
Create a Job from the Quote (conduct normal new-job procedures).
|
|
374
|
+
Send back signal to MorawareConnection object to check to required resources, and call fetching of those resources if necessary.
|
|
375
|
+
'''
|
|
376
|
+
#mw_cookie = self.mw_Connect.mw_cookie
|
|
377
|
+
self.QuoteData = CreateJobFromQuote(JobName, self.QuoteID, self.mw_Connect.mw_cookie, self.QuoteData, self.mw_Connect.client)
|
|
378
|
+
job = MorawareJob(self.mw_Connect, self.QuoteData['Job ID'])
|
|
379
|
+
formfields = self.mw_Connect.Give('JobCardFieldMapping')
|
|
380
|
+
NewJob_Updates(job, self, formfields)
|
|
381
|
+
return job
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
class MorawareJob:
|
|
385
|
+
'''
|
|
386
|
+
A Moraware Job object
|
|
387
|
+
'''
|
|
388
|
+
def __init__(self, mw_Connect, JobID):
|
|
389
|
+
self.JobID = JobID
|
|
390
|
+
self.mw_Connect = mw_Connect
|
|
391
|
+
self.JobData = GetJobDetails(mw_Connect, JobID)
|
|
392
|
+
|
|
393
|
+
def UpdateActivity(self, ActivityType, Updates):
|
|
394
|
+
'''
|
|
395
|
+
Update an activity in the job.
|
|
396
|
+
'''
|
|
397
|
+
return UpdateJobActivity(self.JobID, ActivityType, Updates, self)
|
|
398
|
+
|
|
399
|
+
def UpdateJobForm(self, formid, fields):
|
|
400
|
+
'''
|
|
401
|
+
Update a Job Form on the job.
|
|
402
|
+
'''
|
|
403
|
+
fields.update({'P0': formid})
|
|
404
|
+
return UpdateMoraware(self.JobID, 'Job_UpdateJobForm', 'job', fields, self.mw_Connect.mw_cookie, self.mw_Connect.client)
|
|
405
|
+
|
|
406
|
+
def CreateActivity(self, **Updates):
|
|
407
|
+
'''
|
|
408
|
+
Create an activity in the job.
|
|
409
|
+
'''
|
|
410
|
+
return CreateJobActivity(mw_Connect=self.mw_Connect, JobID=self.JobID, **Updates)
|
|
411
|
+
|
|
412
|
+
def AllocateMaterial(self, ActivityID, MaterialData):
|
|
413
|
+
'''
|
|
414
|
+
Allocate material to the job.
|
|
415
|
+
'''
|
|
416
|
+
return AllocateMaterial(JobID=self.JobID,
|
|
417
|
+
ActivityID=ActivityID,
|
|
418
|
+
MaterialData=MaterialData,
|
|
419
|
+
mw_cookie=self.mw_Connect.mw_cookie,
|
|
420
|
+
client=self.mw_Connect.client)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
############################################################################### Moraware Update Function ###############################################################################
|
|
425
|
+
|
|
426
|
+
def UpdateMoraware(Serial, FormType='Job_UpdateJobActivities', endpoint='job', fields={}, mw_cookie=None, client=CLIENT):
|
|
427
|
+
'''
|
|
428
|
+
Publishes an update form to Moraware.
|
|
429
|
+
Update form is populated with the fields dictionary.
|
|
430
|
+
fields are urlencoded and formatted to match the Moraware form.
|
|
431
|
+
Example:
|
|
432
|
+
P0 : '[str_or_int]'
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
Serial: Serial number of the object to update (job, inventory, quote etc).
|
|
436
|
+
FormType: Type of form to update. This will also set the category for the update.
|
|
437
|
+
endpoint: Endpoint for the update.
|
|
438
|
+
fields: Dictionary containing the fields to update.
|
|
439
|
+
mw_cookie: Login cookie for Moraware.
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
response: Response from the Moraware server.
|
|
443
|
+
'''
|
|
444
|
+
mw_cookie = mw_login_cookie(mw_cookie)
|
|
445
|
+
payload = {
|
|
446
|
+
'C': 'mjtrs1',
|
|
447
|
+
'X': 1,
|
|
448
|
+
'cuid': 24,
|
|
449
|
+
'F': FormType}
|
|
450
|
+
for key, value in fields.items():
|
|
451
|
+
if isinstance(value, str): fields[key] = f'[{urllib.parse.quote(value)}]'
|
|
452
|
+
else: fields[key] = f'[{value}]'
|
|
453
|
+
payload.update(fields)
|
|
454
|
+
ts = int(datetime.now().timestamp() * 1000000)
|
|
455
|
+
response = requests.post(f'https://{client}.moraware.net/sys/{endpoint}/{Serial}', params={'U': ts}, data=payload, cookies=mw_cookie)
|
|
456
|
+
print(response.content)
|
|
457
|
+
if 'MW_RS_ErrorResponse' in response.text: raise Exception(f'Error updating Moraware: {response.text}')
|
|
458
|
+
return response
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
######################################################################### Moraware Connection Data Fetch Functions ######################################################################
|
|
463
|
+
|
|
464
|
+
def GetActivityStatusIDs(mw_cookie, client=CLIENT):
|
|
465
|
+
'''
|
|
466
|
+
Fetches the activity status IDs from Moraware.
|
|
467
|
+
'''
|
|
468
|
+
mw_cookie = mw_login_cookie(mw_cookie)
|
|
469
|
+
url = f'https://{client}.moraware.net/sys/settings/jobactivitystatuses/'
|
|
470
|
+
Activities = {}
|
|
471
|
+
Statuss = requests.get(url, cookies=mw_cookie)
|
|
472
|
+
soup = BeautifulSoup(Statuss.content, 'html.parser')
|
|
473
|
+
table = soup.find('table', {'id': 'ActivityStatusesBody'})
|
|
474
|
+
rows = table.find_all('tr')
|
|
475
|
+
for i,row in enumerate(rows):
|
|
476
|
+
if i == 0: continue
|
|
477
|
+
td_elements = row.find_all('td')
|
|
478
|
+
if td_elements:
|
|
479
|
+
mjtclick_attr = td_elements[0].get('mjtclick', '')
|
|
480
|
+
start = mjtclick_attr.find(',') + 1
|
|
481
|
+
end = mjtclick_attr.find(')', start)
|
|
482
|
+
ActID = mjtclick_attr[start:end].strip()
|
|
483
|
+
ActName = td_elements[0].text.strip()
|
|
484
|
+
Activities[ActName] = ActID
|
|
485
|
+
return Activities
|
|
486
|
+
|
|
487
|
+
def GetActivityIDs(mw_cookie, client=CLIENT):
|
|
488
|
+
'''
|
|
489
|
+
Fetches the activity Type IDs from Moraware.
|
|
490
|
+
'''
|
|
491
|
+
mw_cookie = mw_login_cookie(mw_cookie)
|
|
492
|
+
url = f'https://{client}.moraware.net/sys/settings/jobactivitytypes/'
|
|
493
|
+
Activities = {}
|
|
494
|
+
acts = requests.get(url, cookies=mw_cookie)
|
|
495
|
+
soup = BeautifulSoup(acts.content, 'html.parser')
|
|
496
|
+
table = soup.find('table', {'id': 'ActivityTypesBody'})
|
|
497
|
+
rows = table.find_all('tr')
|
|
498
|
+
for row in rows:
|
|
499
|
+
#if the row isn't of class oddRow or evenRow, then ignore it.
|
|
500
|
+
if not row.get('class') or row.get('class')[0] not in ('oddRow', 'evenRow'): continue
|
|
501
|
+
Act_element = row.find_all('td')[0]
|
|
502
|
+
ActID = Act_element['mjtclick'].split(',')[1].strip()
|
|
503
|
+
ActName = Act_element.text.strip()
|
|
504
|
+
Activities[ActName] = ActID
|
|
505
|
+
return Activities
|
|
506
|
+
|
|
507
|
+
def Get_InvProductNamesID(mw_cookie, client=CLIENT):
|
|
508
|
+
'''
|
|
509
|
+
Fetches the inventory product IDs from Moraware.
|
|
510
|
+
'''
|
|
511
|
+
mw_cookie = mw_login_cookie(mw_cookie)
|
|
512
|
+
url = f'https://{client}.moraware.net/sys/settings/purchaseproducts/'
|
|
513
|
+
inv = requests.get(url, cookies=mw_cookie)
|
|
514
|
+
soup = BeautifulSoup(inv.content, 'html.parser')
|
|
515
|
+
table = soup.find('table', {'id': 'ProductsBody'})
|
|
516
|
+
rows = table.find_all('tr')
|
|
517
|
+
headers = [hd.text.encode('ascii', 'ignore').decode() for hd in rows[0].find_all('td')]
|
|
518
|
+
for q, header in enumerate(headers):
|
|
519
|
+
if 'attribute' in header.lower():
|
|
520
|
+
AttrHeaders = [f'{header} - {i+1}' for i in range(len(rows[1].find_all('td')))]
|
|
521
|
+
break
|
|
522
|
+
headers = headers[:q] + AttrHeaders + headers[q+1:] + ['Product ID']
|
|
523
|
+
prod_data = []
|
|
524
|
+
for row in rows[2:]:
|
|
525
|
+
cells = row.find_all('td')
|
|
526
|
+
if not cells: continue
|
|
527
|
+
line = {headers[i]: cell.text for i, cell in enumerate(cells)}
|
|
528
|
+
prod_page = row.find('a')['href']
|
|
529
|
+
Prod_ID = prod_page.split('/')[-1]
|
|
530
|
+
line['Product ID'] = Prod_ID
|
|
531
|
+
line['url'] = f'https://{client}.moraware.net{prod_page}'
|
|
532
|
+
prod_data.append(line)
|
|
533
|
+
return prod_data
|
|
534
|
+
|
|
535
|
+
def Get_InvProductAttrListIDs(ProductID, mw_cookie, client=CLIENT):
|
|
536
|
+
'''
|
|
537
|
+
Fetches the inventory product attribute list IDs from Moraware.
|
|
538
|
+
'''
|
|
539
|
+
mw_cookie = mw_login_cookie(mw_cookie)
|
|
540
|
+
url = f'https://{client}.moraware.net/sys/settings/purchaseproduct/{ProductID}/'
|
|
541
|
+
AttrList = {}
|
|
542
|
+
inv = requests.get(url, cookies=mw_cookie)
|
|
543
|
+
soup = BeautifulSoup(inv.content, 'html.parser')
|
|
544
|
+
table = soup.find('table', {'id': 'attrScrollBody'})
|
|
545
|
+
if not table: return {}
|
|
546
|
+
rows = table.find_all('tr')
|
|
547
|
+
for row in rows:
|
|
548
|
+
if not row.find('a'): continue
|
|
549
|
+
AttrListID = row.find('a')['href'].split('/')[-1]
|
|
550
|
+
AttrName = row.find('a').text
|
|
551
|
+
AttrList[AttrName] = AttrListID
|
|
552
|
+
return AttrList
|
|
553
|
+
|
|
554
|
+
def Get_InvProductAttrIDs(attrListID, mw_cookie, client=CLIENT):
|
|
555
|
+
'''
|
|
556
|
+
Fetches the inventory product attribute IDs from Moraware.
|
|
557
|
+
'''
|
|
558
|
+
mw_cookie = mw_login_cookie(mw_cookie)
|
|
559
|
+
attributes = []
|
|
560
|
+
i = 1
|
|
561
|
+
PrevTopRow = None
|
|
562
|
+
while True:
|
|
563
|
+
url = f'https://{client}.moraware.net/sys/settings/purchaseproductattr/{attrListID}?&avtpage={i}'
|
|
564
|
+
Products = requests.get(url, cookies=mw_cookie)
|
|
565
|
+
soup = BeautifulSoup(Products.content, 'html.parser')
|
|
566
|
+
table = soup.find('table', {'id': 'avtScrollBody'})
|
|
567
|
+
if not table: return []
|
|
568
|
+
table_rows = table.find_all('tr')[1:]
|
|
569
|
+
if PrevTopRow == table_rows[0]: break
|
|
570
|
+
PrevTopRow = table_rows[0]
|
|
571
|
+
for row in table_rows:
|
|
572
|
+
attrElement = row.find(class_='linkedCell')
|
|
573
|
+
if not attrElement: continue
|
|
574
|
+
attrID = attrElement['mjtclick'].split(',')[1].strip(')')
|
|
575
|
+
attrName = attrElement.text
|
|
576
|
+
attributes.append({'ID': attrID, 'Name': attrName})
|
|
577
|
+
i += 1
|
|
578
|
+
return attributes
|
|
579
|
+
|
|
580
|
+
def Get_InventoryID_Data(ProductID, mw_cookie, client=CLIENT):
|
|
581
|
+
'''
|
|
582
|
+
Fetches Inventory Attribute Data from Moraware for a particular product.
|
|
583
|
+
'''
|
|
584
|
+
mw_cookie = mw_login_cookie(mw_cookie)
|
|
585
|
+
ProductAttributes = Get_InvProductAttrListIDs(ProductID, mw_cookie, client)
|
|
586
|
+
for attrList, ID in ProductAttributes.items():
|
|
587
|
+
ProductAttributes[attrList] = Get_InvProductAttrIDs(ID, mw_cookie, client)
|
|
588
|
+
return ProductAttributes
|
|
589
|
+
|
|
590
|
+
def GetALLInventoryIDs(mw_cookie, client=CLIENT):
|
|
591
|
+
'''
|
|
592
|
+
Fetches all inventory data from Moraware.
|
|
593
|
+
'''
|
|
594
|
+
mw_cookie = mw_login_cookie(mw_cookie)
|
|
595
|
+
result = {}
|
|
596
|
+
products = Get_InvProductNamesID(mw_cookie, client)
|
|
597
|
+
print(f'Found {len(products)} products')
|
|
598
|
+
for i,product in enumerate(products):
|
|
599
|
+
print(f'Fetching {i+1} of {len(products)}')
|
|
600
|
+
namekey = f"{product['Product Family']}{product['Product Line']}{product['Product']}"
|
|
601
|
+
result[namekey] = product
|
|
602
|
+
result[namekey]['_ATTR'] = Get_InventoryID_Data(product['Product ID'], mw_cookie, client)
|
|
603
|
+
return result
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
########################################################################### Report and View Fetch Functions ###########################################################################
|
|
608
|
+
|
|
609
|
+
def Fetch_View(url, mw_cookie, headers=None, IDCol_Index=[0], TableType=None):
|
|
610
|
+
'''
|
|
611
|
+
Fetches a Moraware page and returns it as a list of dictionaries.
|
|
612
|
+
|
|
613
|
+
TODO:
|
|
614
|
+
Complete the type checking for the function.
|
|
615
|
+
Switch to using MorawareSession.session for the requests. Pros: Better session management, less overhead. Cons: Multithreading issues.
|
|
616
|
+
|
|
617
|
+
Args:
|
|
618
|
+
url (str): The url of the Moraware page to fetch.
|
|
619
|
+
LoginSession (MorawareSession): The login session to use for fetching the page. DEPRECATED
|
|
620
|
+
mw_cookie (dict): The cookie data for the Moraware login.
|
|
621
|
+
headers (list): The headers for the table.
|
|
622
|
+
IDCol_Index (list): The index of the ID columns in the table.
|
|
623
|
+
TableType (str): The type of table to fetch.
|
|
624
|
+
|
|
625
|
+
Returns:
|
|
626
|
+
list: A list of dictionaries containing the data from the table.
|
|
627
|
+
'''
|
|
628
|
+
if not headers: print('WARNING: Headers not provided for MWFetch. Parsing headers from the page.')
|
|
629
|
+
views_dict = {'quotes' : 'Quotes',
|
|
630
|
+
'report' : 'Report',
|
|
631
|
+
'jobs' : 'Jobs',
|
|
632
|
+
'pos' : 'POs',
|
|
633
|
+
'inventory' : 'Products',
|
|
634
|
+
'settings' : None,
|
|
635
|
+
'accounts' : 'Customers',
|
|
636
|
+
'jobactivitytypes': 'ActivityTypes',
|
|
637
|
+
'jobactivitystatuses': 'ActivityStatuses',
|
|
638
|
+
'assignees' : 'Assignees',
|
|
639
|
+
'jobfields' : 'JobAttributes',
|
|
640
|
+
'jobissuecategories': 'IssueCategories'}
|
|
641
|
+
if not TableType:
|
|
642
|
+
view_type = url.split('sys/')[1].split('/')[0].split('?')[0]
|
|
643
|
+
if view_type == 'settings':
|
|
644
|
+
view_type = url.split('settings/')[1].split('/')[0].split('?')[0]
|
|
645
|
+
if view_type not in views_dict: raise Exception('View Type Not Found')
|
|
646
|
+
TableType = views_dict[view_type]
|
|
647
|
+
|
|
648
|
+
rsp = requests.get(url, cookies=mw_cookie) #### s.get(url)
|
|
649
|
+
soup = BeautifulSoup(rsp.content, 'html.parser')
|
|
650
|
+
#Check if the login page is in the response.<title>Moraware Sign in - Moraware Systemize</title>
|
|
651
|
+
if 'sign in' in soup.title.text.lower(): raise Exception('Login Failed')
|
|
652
|
+
if "No Data Available. Press the 'Customize..' button above to change the report parameters." in rsp.text: return []
|
|
653
|
+
table = soup.find( 'table', {'id': f'{TableType}Body'} )
|
|
654
|
+
rows = table.find_all('tr')
|
|
655
|
+
if not headers: headers = [th.text.split('\xa0')[0].strip() for th in rows[0].find_all('td')]
|
|
656
|
+
data = []
|
|
657
|
+
for row in rows[1:]:
|
|
658
|
+
cells = row.find_all('td')
|
|
659
|
+
datarow = {headers[i]: td.text.strip() for i, td in enumerate(cells)}
|
|
660
|
+
for i in IDCol_Index:
|
|
661
|
+
#i = idc - 1
|
|
662
|
+
if i > len(cells): continue
|
|
663
|
+
hd = headers[i]
|
|
664
|
+
try: datarow[f'{hd} ID'] = cells[i].find('a').get('href').split('/')[-1]
|
|
665
|
+
except: datarow[f'{hd} ID'] = 'ID Not Found'
|
|
666
|
+
data.append(datarow)
|
|
667
|
+
return data
|
|
668
|
+
|
|
669
|
+
def Fetch_Report(url, mw_cookie=None, headers=None, IDCol_Index=[0]):
|
|
670
|
+
return Fetch_View(url, mw_cookie, headers, IDCol_Index, 'Report')
|
|
671
|
+
|
|
672
|
+
def FetchNewQuotes(mw_cookie, client=CLIENT):
|
|
673
|
+
url = f'https://{client}.moraware.net/sys/quotes?&view=0&sort=d3&cols=QN1,QA39,CN1,QN2,QN7,QN8,QN4,QN5,QN6,QA15&pageSize=9000&filters=2|3:0:j3:9:j3;1,1,2,,,!5|11:0:q3:9:q3;4,1,3,365,,'
|
|
674
|
+
return Fetch_View(url, mw_cookie)
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
############################################################################### Quote Fetch Functions ###############################################################################
|
|
679
|
+
|
|
680
|
+
def GetQuoteDetails(SN, mw_cookie, client=CLIENT):
|
|
681
|
+
mw_cookie = mw_login_cookie(mw_cookie)
|
|
682
|
+
url = f'https://{client}.moraware.net/sys/quote/{SN}'
|
|
683
|
+
QuoteDetails = requests.get(url, cookies=mw_cookie)
|
|
684
|
+
soup = BeautifulSoup(QuoteDetails.content, 'html.parser')
|
|
685
|
+
main_table = soup.find('table', {'class': 'pageInfoCenteredJustifiedTable'})
|
|
686
|
+
rows = main_table.find_all('tr')
|
|
687
|
+
QuoteMainData = {'_QuoteID': SN}
|
|
688
|
+
for row in rows:
|
|
689
|
+
cols = row.find_all('td')
|
|
690
|
+
colsb = ['\n'.join([st for st in ele.stripped_strings]) for ele in cols]
|
|
691
|
+
QuoteMainData[colsb[0].strip(':')] = colsb[1]
|
|
692
|
+
|
|
693
|
+
quotelines_table = soup.find('table', {'class': 'quoteLinesTable'})
|
|
694
|
+
rows = quotelines_table.find_all('tr')
|
|
695
|
+
QuoteLines = []
|
|
696
|
+
for row in rows:
|
|
697
|
+
cols = row.find_all('td')
|
|
698
|
+
rowdata = []
|
|
699
|
+
for ele in cols:
|
|
700
|
+
el = ele.text.strip().replace('\uf111','').replace('×', 'x')
|
|
701
|
+
if el: rowdata.append(el)
|
|
702
|
+
QuoteLines.append(rowdata)
|
|
703
|
+
|
|
704
|
+
QuoteLineData = []
|
|
705
|
+
for i,line in enumerate(QuoteLines):
|
|
706
|
+
if not line: continue
|
|
707
|
+
if i==0: QuoteLineData.append( ['Quoted', ''.join(line)] )
|
|
708
|
+
elif 'Measurement' in line[0].title():
|
|
709
|
+
Pieces = []
|
|
710
|
+
QuoteLineData.append( ['Measurements', ''.join(line).replace('\xa0', ' ')] )
|
|
711
|
+
for piece in line[0].split(','):
|
|
712
|
+
piece = piece.replace('Measurement\xa0- ', '').strip()
|
|
713
|
+
if 'Sq Mtr' in piece:
|
|
714
|
+
piece = piece.split('[')[0].strip()
|
|
715
|
+
if piece: Pieces.append(piece)
|
|
716
|
+
if Pieces: QuoteLineData.append(['Pieces', Pieces])
|
|
717
|
+
elif 'Benchtop' in line[0].title():
|
|
718
|
+
#Get the squaremetre from the next line, eg: '[Qty: 1.0000] [3.5153 Sq Mtr] [$0/Sq Mtr]'
|
|
719
|
+
sqm = QuoteLines[i+1][0].split('[')[2].split(']')[0].split(' ')[0]
|
|
720
|
+
QuoteLineData.append(['Material', line[0], sqm])
|
|
721
|
+
elif 'Includes' in line[0].title():
|
|
722
|
+
#Get the quantity of the item to incliude from next linme, eg: ['[Qty: 1.0000] [$870/Each]']
|
|
723
|
+
qty = QuoteLines[i+1][0].split('[')[1].split(']')[0].split(' ')[1]
|
|
724
|
+
QuoteLineData.append(['Includes', line[0], qty])
|
|
725
|
+
elif 'Edge Finish' in line[0].title():
|
|
726
|
+
#Get the lineal metreage from the following line, eg: ['[Qty: 1.0000] [1500m...5/Lin Mtr]']
|
|
727
|
+
linm = QuoteLines[i+1][0].split('[')[2].split(']')[0].split(' ')[0]
|
|
728
|
+
QuoteLineData.append(['Edge Finish', line[0], linm])
|
|
729
|
+
elif 'Waterjet' in line[0].title():
|
|
730
|
+
QuoteLineData.append(['Waterjet', line[0]])
|
|
731
|
+
QuoteMainData['QuoteLines'] = QuoteLineData
|
|
732
|
+
return QuoteMainData
|
|
733
|
+
|
|
734
|
+
def Give_NewJob_JobCard_Fields(QuoteData, fieldmapping=JOBCARD_FIELDMAPPING):
|
|
735
|
+
'''
|
|
736
|
+
Give the fields for updating a new job jobcard with initial quote data.
|
|
737
|
+
Defaults to field IDs for environment Job Card.
|
|
738
|
+
'''
|
|
739
|
+
fields = []
|
|
740
|
+
edge = ''
|
|
741
|
+
NoteField = ''
|
|
742
|
+
if QuoteData.get('Built Edge'): fields.append( ( fieldmapping['GlueUp'], '1' ) )
|
|
743
|
+
for line in QuoteData['QuoteLines']:
|
|
744
|
+
if line[0] == 'Pieces': NoteField += f'> Pieces: {len(line[1])}\n'
|
|
745
|
+
elif line[0] == 'Quoted': NoteField += ' '.join(line[1:]) + '\n'
|
|
746
|
+
else: NoteField += '> ' + ' '.join(line[1:]) + '\n'
|
|
747
|
+
|
|
748
|
+
if line [0] == 'Includes':
|
|
749
|
+
if 'Tap' in line[1]: fields.append( ( fieldmapping['Tap'], '1' ) )
|
|
750
|
+
if 'Hob' in line[1]: fields.append( ( fieldmapping['Hob'], '1' ) )
|
|
751
|
+
elif line[0] == 'Edge Finish':
|
|
752
|
+
if edge: edge += '\n'
|
|
753
|
+
edge += ' '.join(line[1:])
|
|
754
|
+
if 'Mitre' in line[1]: fields.append( ( fieldmapping['Mitre'], '1' ) )
|
|
755
|
+
elif 'Waterjet' in line[0]: fields.append( ( fieldmapping['Waterjet'], '1' ) )
|
|
756
|
+
elif line[0] == 'Quoted': fields.append( ( fieldmapping['Qtd_TI'], ' '.join(line[1:]) ) )
|
|
757
|
+
fields.append( ( fieldmapping['Notes'], NoteField ) )
|
|
758
|
+
fields.append( ( fieldmapping['Edge'], edge ) )
|
|
759
|
+
|
|
760
|
+
P_fields = list(zip(*fields))
|
|
761
|
+
P1 = '?'.join(P_fields[0])
|
|
762
|
+
P2 = '?'.join(P_fields[1])
|
|
763
|
+
UpdateDict = {}
|
|
764
|
+
for i in range(0, 6): UpdateDict[f'P{i}'] = ''
|
|
765
|
+
UpdateDict.update( {'P1': P1,
|
|
766
|
+
'P2': P2,
|
|
767
|
+
'P4': -1 })
|
|
768
|
+
return UpdateDict
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
############################################################################### Job Fetch Functions ###############################################################################
|
|
773
|
+
|
|
774
|
+
def GetSomeSoup(jid, mw_cookie, client=CLIENT):
|
|
775
|
+
mw_cookie = mw_login_cookie(mw_cookie)
|
|
776
|
+
joburl = f'https://{client}.moraware.net/sys/job/{jid}'
|
|
777
|
+
job = requests.get(joburl, cookies=mw_cookie)
|
|
778
|
+
soup = BeautifulSoup(job.content, 'html.parser')
|
|
779
|
+
return soup
|
|
780
|
+
|
|
781
|
+
def GetJobActivities(JobSoup=None, **args):
|
|
782
|
+
if not JobSoup: JobSoup = GetSomeSoup(args.get('jid'), args.get('mw_cookie'))
|
|
783
|
+
table = JobSoup.find('table', {'id': 'ActivitiesBody'})
|
|
784
|
+
#Give the table headers
|
|
785
|
+
ActivityTableHead = JobSoup.find('table', {'id': 'ActivitiesHead'})
|
|
786
|
+
ActivityTableHeaders = []
|
|
787
|
+
for ath in ActivityTableHead.text.split('\n'):
|
|
788
|
+
if ath:
|
|
789
|
+
cleaned_cell = re.sub(r'[^\x00-\x7F]+', '', ath)
|
|
790
|
+
ActivityTableHeaders.append(cleaned_cell)
|
|
791
|
+
|
|
792
|
+
rows = table.find_all('tr')
|
|
793
|
+
Activities = []
|
|
794
|
+
for i,row in enumerate(rows): #Getting Activity ID and Materials
|
|
795
|
+
if i == 0: continue
|
|
796
|
+
ActID = row['mjtdrop'].split('\'')[1]
|
|
797
|
+
rowdetails = row.find_all('td')
|
|
798
|
+
Actname = rowdetails[0].text
|
|
799
|
+
mci = ActivityTableHeaders.index('Material')
|
|
800
|
+
ActivityMaterials = extract_slab_details( rowdetails[mci] )
|
|
801
|
+
Activities.append({ 'Activity Type': Actname, 'Materials': ActivityMaterials, 'ActivityID': ActID })
|
|
802
|
+
|
|
803
|
+
df = pd.read_html(str(table))[0]
|
|
804
|
+
adata = pd.Series.tolist(df)
|
|
805
|
+
MWJobDat = []
|
|
806
|
+
for i,row in enumerate(adata):
|
|
807
|
+
newrow = []
|
|
808
|
+
for j,cell in enumerate(row):
|
|
809
|
+
cell = convert_float(cell)
|
|
810
|
+
cleaned_cell = re.sub(r'[^\x00-\x7F]+', '', cell)
|
|
811
|
+
if cleaned_cell == 'Add Material': cleaned_cell = None
|
|
812
|
+
if 'date' in ActivityTableHeaders[j].lower() and cleaned_cell:
|
|
813
|
+
try: int(cleaned_cell[0])
|
|
814
|
+
except: cleaned_cell = cleaned_cell.split(' ')[1].strip()
|
|
815
|
+
newrow.append(cleaned_cell)
|
|
816
|
+
MWJobDat.append(newrow)
|
|
817
|
+
|
|
818
|
+
JobData = []
|
|
819
|
+
headers = ActivityTableHeaders
|
|
820
|
+
for i,row in enumerate(MWJobDat):
|
|
821
|
+
Activity = {}
|
|
822
|
+
for j,cell in enumerate(row): Activity[headers[j]] = cell
|
|
823
|
+
Activity.update(Activities[i])
|
|
824
|
+
JobData.append(Activity)
|
|
825
|
+
return JobData
|
|
826
|
+
|
|
827
|
+
def GetJobCard(JobSoup=None, fieldmapping=JOBCARD_FIELDMAPPING, **args):
|
|
828
|
+
if not JobSoup: JobSoup = GetSomeSoup(args.get('jid'), args.get('mw_cookie'))
|
|
829
|
+
JobForms = JobSoup.find_all('table', {'class': 'detail-form'})
|
|
830
|
+
JCID = fieldmapping['FORM_IDENTIFIER']
|
|
831
|
+
JobCard = [form for form in JobForms if JCID in form.text]
|
|
832
|
+
if not JobCard: return None
|
|
833
|
+
JobCard = JobCard[0]
|
|
834
|
+
formid = JobCard['id'].split('tblJobForm')[-1]
|
|
835
|
+
JobData = {}
|
|
836
|
+
header_divs = JobCard.find_all('div', style=re.compile(r';font-size:10pt;font-weight:bold;'))
|
|
837
|
+
for header in header_divs:
|
|
838
|
+
header_text = header.text.strip().strip(':').strip()
|
|
839
|
+
JobData[header_text] = '\n'.join( header.find_next('div').stripped_strings )
|
|
840
|
+
JobData.update({'JobCardID': formid})
|
|
841
|
+
return JobData
|
|
842
|
+
|
|
843
|
+
def extract_slab_details(cell):
|
|
844
|
+
if cell.text.strip('\uf055') == 'Add Material': return []
|
|
845
|
+
Materials = cell.find_all('a')
|
|
846
|
+
slab_details = []
|
|
847
|
+
for material in Materials:
|
|
848
|
+
mt = material.find('div', class_='jobActMaterialCellContent')
|
|
849
|
+
outername = mt.find('span')
|
|
850
|
+
material_type = ''.join([str(content) for content in outername.contents if not content.name])
|
|
851
|
+
serial_number_specs = material.find_all('div', class_='serialNumberSpec')
|
|
852
|
+
for spec in serial_number_specs:
|
|
853
|
+
# Slab Quantity SQM
|
|
854
|
+
square_metre_info = spec.find('span', {'class': 'quantitySpec'}).text
|
|
855
|
+
square_metre_match = re.search(r"(\d+\.\d+)\s*Sq Mtr", square_metre_info)
|
|
856
|
+
quantity = square_metre_match.group(1) if square_metre_match else 0
|
|
857
|
+
# Slab Serial Number
|
|
858
|
+
serial_number = spec.find('span', class_='jobSerialNumber').text.strip()
|
|
859
|
+
# Slab System ID
|
|
860
|
+
if serial_number == '(Unserialized)': system_id = None
|
|
861
|
+
else:
|
|
862
|
+
span_with_onmousedown = spec.find('span', {'onmousedown': True})
|
|
863
|
+
onmousedown_attr = span_with_onmousedown['onmousedown']
|
|
864
|
+
match = re.search(r"JobActivitySerialNumberMouseDown\(event, \d+, \d+, \"(\d+)\"", onmousedown_attr)
|
|
865
|
+
system_id = match.group(1) if match else ''
|
|
866
|
+
|
|
867
|
+
slab_details.append({
|
|
868
|
+
'SQM': quantity,
|
|
869
|
+
'ExtSN': serial_number,
|
|
870
|
+
'MorawareInternalID': system_id,
|
|
871
|
+
'material_type': material_type
|
|
872
|
+
})
|
|
873
|
+
return slab_details
|
|
874
|
+
|
|
875
|
+
def GetJobDetails(mw_Connect, SN):
|
|
876
|
+
jcfm = mw_Connect.Give('JobCardFieldMapping')
|
|
877
|
+
client = mw_Connect.client
|
|
878
|
+
mw_cookie = mw_Connect.mw_cookie
|
|
879
|
+
url = f'https://{client}.moraware.net/sys/job/{SN}'
|
|
880
|
+
JobPage = requests.get(url, cookies=mw_cookie)
|
|
881
|
+
soup = BeautifulSoup(JobPage.content, 'html.parser')
|
|
882
|
+
|
|
883
|
+
JobName = ' - '.join( soup.find('title').text.split(' - ')[:-2] ).strip()
|
|
884
|
+
Job_Card = GetJobCard(soup, fieldmapping=jcfm)
|
|
885
|
+
Activities = GetJobActivities(soup)
|
|
886
|
+
|
|
887
|
+
return {'url' : url,
|
|
888
|
+
'jid': SN,
|
|
889
|
+
'JobPage' : JobPage,
|
|
890
|
+
'soup' : soup,
|
|
891
|
+
'JobName' : JobName,
|
|
892
|
+
'JobCard' : Job_Card,
|
|
893
|
+
'Activities' : Activities}
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
############################################################################### Job Update Functions ###############################################################################
|
|
898
|
+
|
|
899
|
+
def UpdateJobActivity(jid, Activity, Updates, MWJob):#, mw_Connect):
|
|
900
|
+
'''
|
|
901
|
+
Initiate a job update in Moraware.
|
|
902
|
+
Uses a MorawareJob object containing all job data and mw_Connect object containing the Moraware connection objects.
|
|
903
|
+
Activity argument should be the activity to be updated.
|
|
904
|
+
Updates argument should be a dictionary containing the updates to be made.
|
|
905
|
+
ie: 'Install', {'Status': 'Completed', 'Start Date': '2021-06-01', 'Assigned To': 'John Doe', 'Notes': 'Completed the install.'}
|
|
906
|
+
Use the MWSession class to get the StatusIDs and Inventory Locations as class variables for every session.
|
|
907
|
+
Start Date should be in the format 'YYYY-MM-DD' (otherwise will assume US format).
|
|
908
|
+
Names of activities and statuses should be exact and match shown in Moraware.
|
|
909
|
+
|
|
910
|
+
Args:
|
|
911
|
+
jid: Job ID
|
|
912
|
+
Activity: Activity to be updated.
|
|
913
|
+
Updates: Dictionary containing the updates to be made.
|
|
914
|
+
MWJob: MorawareJob object containing all job data.
|
|
915
|
+
mw_Connect: Moraware connection object.
|
|
916
|
+
|
|
917
|
+
Returns:
|
|
918
|
+
rsp: Response from the Moraware server.
|
|
919
|
+
'''
|
|
920
|
+
mw_Connect = MWJob.mw_Connect
|
|
921
|
+
StatusIDs = mw_Connect.Give('StatusIDs')
|
|
922
|
+
mw_cookie = mw_Connect.mw_cookie
|
|
923
|
+
client = mw_Connect.client
|
|
924
|
+
JobData = MWJob.JobData['Activities']
|
|
925
|
+
p_mapping = {
|
|
926
|
+
'ActivityID': 'P0',
|
|
927
|
+
'Status': 'P1',
|
|
928
|
+
'Start Date': 'P2',
|
|
929
|
+
'Sched Time': 'P3',
|
|
930
|
+
'Duration': 'P4',
|
|
931
|
+
'Assigned To': 'P5',
|
|
932
|
+
'Notes': 'P6'}
|
|
933
|
+
ActionedActivity = {act['Activity Type']: act for act in JobData}[Activity]
|
|
934
|
+
ActionedActivity.update(Updates)
|
|
935
|
+
MW_Act_Dat = ActionedActivity.copy()
|
|
936
|
+
if MW_Act_Dat['Status'] == 'Auto-Schedule' and 'Start Date' in Updates and 'Status' not in Updates:
|
|
937
|
+
Updates['Status'] = 'Estimate'
|
|
938
|
+
MW_Act_Dat['Status'] = StatusIDs[MW_Act_Dat['Status']]
|
|
939
|
+
MW_Act_Dat['Start Date'] = convert_date(MW_Act_Dat['Start Date'], '%Y-%m-%d')
|
|
940
|
+
MW_Act_Dat['Sched Time'] = Give_24hrTime(MW_Act_Dat['Sched Time'])
|
|
941
|
+
|
|
942
|
+
#Creating the update dictionary, updating with existing data.
|
|
943
|
+
UpdateDict = {}
|
|
944
|
+
for i in range(0, 11): UpdateDict[f'P{i}'] = ''
|
|
945
|
+
UpdateDict.update( { p_mapping[k]:v for k,v in MW_Act_Dat.items() if k in p_mapping } )
|
|
946
|
+
|
|
947
|
+
#Setting Status update to StatusID, adding the updates to the update dict.
|
|
948
|
+
if 'Status' in Updates: Updates['Status'] = StatusIDs[Updates['Status']]
|
|
949
|
+
UpdateDict.update( { p_mapping[k]:v for k,v in Updates.items() if k in p_mapping } )
|
|
950
|
+
|
|
951
|
+
#This tells Moraware which fields are being modified, even if false information is in other fields, it will only update the fields that are flagged as modified.
|
|
952
|
+
UpdateDict['P9'] = '%2C'.join( [ '1' if key in Updates else '0' for key in p_mapping.keys() ] + ['0'] )
|
|
953
|
+
return UpdateMoraware(Serial=jid,
|
|
954
|
+
FormType='Job_UpdateJobActivities',
|
|
955
|
+
endpoint='job',
|
|
956
|
+
fields=UpdateDict,
|
|
957
|
+
mw_cookie=mw_cookie,
|
|
958
|
+
client=client
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
def CreateJobFromQuote(JobName, QuoteID, mw_cookie, QuoteData, client=CLIENT):
|
|
962
|
+
'''
|
|
963
|
+
Create a job from a quote in Moraware.
|
|
964
|
+
Only creates the job, does not update any fields.
|
|
965
|
+
'''
|
|
966
|
+
mw_cookie = mw_login_cookie(mw_cookie)
|
|
967
|
+
DecodedString = {'quoteId': QuoteID,
|
|
968
|
+
'jobName': JobName,
|
|
969
|
+
'jobTemplateId': '1', #Change this after creating new jobs card template.
|
|
970
|
+
'jobProcessId': 1, #Need a flag in the quote for Commercial, Residential, Remakes etc quotes.
|
|
971
|
+
'salespersonId': '',
|
|
972
|
+
'useDefaultSalespersonId': 1}
|
|
973
|
+
EncodedString = Encode_Dict_to_String(DecodedString)
|
|
974
|
+
rsp = UpdateMoraware(Serial=QuoteID,
|
|
975
|
+
FormType='Quote2_CreateJobFromQuote2',
|
|
976
|
+
endpoint='quote',
|
|
977
|
+
fields={'P0': EncodedString},
|
|
978
|
+
mw_cookie=mw_cookie,
|
|
979
|
+
client=client
|
|
980
|
+
)
|
|
981
|
+
if 'MW_RS_ErrorResponse' in rsp.text: raise Exception(f'Error creating job from quote: {rsp.text}')
|
|
982
|
+
jid = eval(rsp.text)['jobId']
|
|
983
|
+
QuoteData.update({'Job ID': jid})
|
|
984
|
+
return QuoteData
|
|
985
|
+
|
|
986
|
+
def NewJob_Updates(MWJob, MWQuote, FormFields):
|
|
987
|
+
'''
|
|
988
|
+
New version of the JobCreate_update function.
|
|
989
|
+
Utilises the MorawareJob, MorawareQuote and MorawareConnection classes.
|
|
990
|
+
Performs initial job updates for setting up a new job from a quote.
|
|
991
|
+
'''
|
|
992
|
+
QuoteData = MWQuote.QuoteData
|
|
993
|
+
jid = MWJob.JobID
|
|
994
|
+
template = datetime.strptime(QuoteData['Template Date'], '%d/%m/%Y')
|
|
995
|
+
rsp = MWJob.UpdateActivity('Template', {'Start Date': template.strftime('%Y-%m-%d'), 'Status': 'Estimate'})
|
|
996
|
+
rsp = MWJob.UpdateActivity('Deposit Inv Sent', {'Status': 'Estimate'})
|
|
997
|
+
#raise Exception('Test End')
|
|
998
|
+
JobCardFields = Give_NewJob_JobCard_Fields(QuoteData, fieldmapping=FormFields)
|
|
999
|
+
jcid = MWJob.JobData['JobCard']['JobCardID']
|
|
1000
|
+
rsp = MWJob.UpdateJobForm(jcid, JobCardFields)
|
|
1001
|
+
|
|
1002
|
+
def AllocateMaterial(JobID, ActivityID, MaterialData, mw_cookie, client=CLIENT):
|
|
1003
|
+
'''
|
|
1004
|
+
Allocate material to a job.
|
|
1005
|
+
|
|
1006
|
+
Args:
|
|
1007
|
+
JobID: The Job ID to allocate the material to.
|
|
1008
|
+
ActivityID: The Activity ID to allocate the material to.
|
|
1009
|
+
MaterialData: Dictionary containing the material data to allocate.
|
|
1010
|
+
mw_cookie: Login cookie for Moraware.
|
|
1011
|
+
client: Moraware client name.
|
|
1012
|
+
|
|
1013
|
+
Returns:
|
|
1014
|
+
rsp: Response from the Moraware server.
|
|
1015
|
+
'''
|
|
1016
|
+
MaterialData['ActivityID'] = ActivityID
|
|
1017
|
+
if not 'UnserQtyAlloc' in MaterialData: MaterialData['UnserQtyAlloc'] = '0'
|
|
1018
|
+
material_alloc_mapping = {
|
|
1019
|
+
'ActivityID': 'P0',
|
|
1020
|
+
'ProductID': 'P1',
|
|
1021
|
+
'AttrID1': 'P2',
|
|
1022
|
+
'AttrID2': 'P3',
|
|
1023
|
+
'AttrID3': 'P4',
|
|
1024
|
+
'AttrID4': 'P5',
|
|
1025
|
+
'AttrID5': 'P6',
|
|
1026
|
+
'UnserQtyAlloc': 'P7',
|
|
1027
|
+
}
|
|
1028
|
+
form = 'Job_CreateAndAllocateJobActivityProduct'
|
|
1029
|
+
fields = {v:MaterialData.get(k, '') for k,v in material_alloc_mapping.items()}
|
|
1030
|
+
fields['P8'] = f'?|{MaterialData["SN_ID"]}|{MaterialData["SN_Display"]}|{MaterialData["SQM_Allocating"]}|Sq%20Mtr|{MaterialData["SQM_OG"]}'
|
|
1031
|
+
rsp = UpdateMoraware(JobID, form, fields=fields, mw_cookie=mw_cookie, client=client)
|
|
1032
|
+
if 'MW_RS_ErrorResponse' in rsp.text:
|
|
1033
|
+
if 'variant id=' in rsp.text:
|
|
1034
|
+
pass
|
|
1035
|
+
return rsp
|
|
1036
|
+
|
|
1037
|
+
def CreateJob(JobName, mw_cookie, AccountID='', TemplateID='1', ProcessID='1', client=CLIENT):
|
|
1038
|
+
'''
|
|
1039
|
+
Create a job in Moraware.
|
|
1040
|
+
'''
|
|
1041
|
+
mw_cookie = mw_login_cookie(mw_cookie)
|
|
1042
|
+
fields = {
|
|
1043
|
+
'P0': JobName,
|
|
1044
|
+
'P1': AccountID,
|
|
1045
|
+
'P2': TemplateID,
|
|
1046
|
+
'P3': ProcessID,
|
|
1047
|
+
'P4': '',
|
|
1048
|
+
'P5': '1'
|
|
1049
|
+
}
|
|
1050
|
+
rsp = UpdateMoraware(Serial=ProcessID,
|
|
1051
|
+
FormType='Job_CreateJob',
|
|
1052
|
+
endpoint='jobs',
|
|
1053
|
+
fields=fields,
|
|
1054
|
+
mw_cookie=mw_cookie,
|
|
1055
|
+
client=client)
|
|
1056
|
+
return rsp
|
|
1057
|
+
|
|
1058
|
+
def CreateJobActivity(mw_Connect, JobID, Status='Estimate', **Updates):
|
|
1059
|
+
'''
|
|
1060
|
+
Create a job activity in Moraware.
|
|
1061
|
+
|
|
1062
|
+
Args:
|
|
1063
|
+
JobID: The Job ID to create the activity for.
|
|
1064
|
+
mw_Connect: Moraware connection object.
|
|
1065
|
+
ActivityType: The Activity Type by Display Name.
|
|
1066
|
+
ActivityID: The Activity Type by ID.
|
|
1067
|
+
Status: Status of the activity by name (default: Estimate).
|
|
1068
|
+
StatusID: Status of the activity by ID (default: Estimate).
|
|
1069
|
+
StartDate: Start date of the activity (default: unscheduled).
|
|
1070
|
+
ScheduledTime: Scheduled time of the activity (default: unscheduled).
|
|
1071
|
+
Duration: Duration of the activity (default: empty).
|
|
1072
|
+
Assignee: Activity assigned to by name (default: unassigned).
|
|
1073
|
+
Notes: Notes for the activity.
|
|
1074
|
+
|
|
1075
|
+
Returns:
|
|
1076
|
+
rsp: Response from the Moraware server.
|
|
1077
|
+
'''
|
|
1078
|
+
mw_cookie = mw_Connect.mw_cookie
|
|
1079
|
+
client = mw_Connect.client
|
|
1080
|
+
StatusIDs = mw_Connect.Give('StatusIDs')
|
|
1081
|
+
ActivityIDs = mw_Connect.Give('ActivityIDs')
|
|
1082
|
+
Updates['JobID'] = JobID
|
|
1083
|
+
if not 'StatusID' in Updates: Updates['StatusID'] = StatusIDs[Status]
|
|
1084
|
+
if not 'ActivityID' in Updates: Updates['ActivityID'] = ActivityIDs[Updates['ActivityType']]
|
|
1085
|
+
if Updates.get('StartDate'): Updates['StartDate'] = convert_date(Updates['StartDate'], '%Y-%m-%d')
|
|
1086
|
+
|
|
1087
|
+
field_mapping = {
|
|
1088
|
+
'JobID': 'P0',
|
|
1089
|
+
'ActivityID': 'P1',
|
|
1090
|
+
'StatusID': 'P2',
|
|
1091
|
+
'StartDate': 'P3',
|
|
1092
|
+
'ScheduledTime': 'P4',
|
|
1093
|
+
'Duration': 'P5',
|
|
1094
|
+
'Assignee': 'P6',
|
|
1095
|
+
'Notes': 'P7'
|
|
1096
|
+
}
|
|
1097
|
+
fields = {f'P{i}' : '' for i in range(11)}
|
|
1098
|
+
fields.update({ field_mapping[k]:v for k,v in Updates.items() if k in field_mapping })
|
|
1099
|
+
rsp = UpdateMoraware(Serial=JobID,
|
|
1100
|
+
FormType='Job_CreateJobActivity',
|
|
1101
|
+
endpoint='job',
|
|
1102
|
+
fields=fields,
|
|
1103
|
+
mw_cookie=mw_cookie,
|
|
1104
|
+
client=client)
|
|
1105
|
+
return rsp
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
############################################################################### Inventory Materials ###############################################################################
|
|
1110
|
+
|
|
1111
|
+
def Give_BIN(mw_Connect, binconfig, client=CLIENT):
|
|
1112
|
+
mw_cookie = mw_Connect.mw_cookie
|
|
1113
|
+
binsheets = Fetch_View( url=binconfig['binsList'], mw_cookie=mw_cookie, headers=['name', 'account'] )
|
|
1114
|
+
bins = {b['name']:b for b in binsheets}
|
|
1115
|
+
|
|
1116
|
+
binName = binconfig.get('binName', f"{datetime.now().strftime('%B %Y')} BIN Movements")
|
|
1117
|
+
if binName in bins:
|
|
1118
|
+
current_bin = bins[binName]
|
|
1119
|
+
bin_id = current_bin.get('name ID')
|
|
1120
|
+
else:
|
|
1121
|
+
print(f'Creating BIN Movement Job: {binName}')
|
|
1122
|
+
rsp = CreateJob(JobName=binName,
|
|
1123
|
+
mw_cookie=mw_cookie,
|
|
1124
|
+
AccountID=binconfig['accountID'],
|
|
1125
|
+
TemplateID=binconfig['templateID'],
|
|
1126
|
+
ProcessID=binconfig['processID'],
|
|
1127
|
+
client=client)
|
|
1128
|
+
bin_id = rsp.text
|
|
1129
|
+
print(f'Created BIN Movement Job: {binName} with ID {bin_id}')
|
|
1130
|
+
bin_job = MorawareJob(mw_Connect, bin_id)
|
|
1131
|
+
bin_job.total_slabs_allocated = 0
|
|
1132
|
+
for activity in bin_job.JobData['Activities']:
|
|
1133
|
+
bin_job.total_slabs_allocated += len(activity['Materials'])
|
|
1134
|
+
return bin_job
|
|
1135
|
+
|
|
1136
|
+
def slabdata_from_entry(entry, ProductIDs):
|
|
1137
|
+
'''
|
|
1138
|
+
Get the slab data from an inventory list - for example the bin list.
|
|
1139
|
+
|
|
1140
|
+
Args:
|
|
1141
|
+
entry: The entry from the inventory list.
|
|
1142
|
+
ProductIDs: The product IDs from the inventory.
|
|
1143
|
+
|
|
1144
|
+
Returns:
|
|
1145
|
+
SlabData: The slab data for the entry.
|
|
1146
|
+
'''
|
|
1147
|
+
namekey = f"{entry['Product Family']}{entry['Product Line']}{entry['Product']}"
|
|
1148
|
+
if namekey not in ProductIDs:
|
|
1149
|
+
print(f"Product not found in Inventory Data: {namekey}")
|
|
1150
|
+
return None
|
|
1151
|
+
FullStoneName = entry['Product Variant'] # Full name of the stone variant, including all attributes.
|
|
1152
|
+
FullStoneName.replace(entry['Product'], '').strip() # Remove the product from the variant name.
|
|
1153
|
+
VariantData = ProductIDs[namekey] # Attribute lists for this product.
|
|
1154
|
+
AttrIDs = []
|
|
1155
|
+
for attrCat, attrList in VariantData['_ATTR'].items(): # Dict of { attrCategory(colour, thickness, etc): [attr1, attr2, ...] }
|
|
1156
|
+
attrmatchlen = 0 #Give the best match for the attribute.
|
|
1157
|
+
match_attr = None
|
|
1158
|
+
for val in attrList:
|
|
1159
|
+
if val['Name'] in FullStoneName: #Checking if the attribute name is in the variant name.
|
|
1160
|
+
if len(val['Name']) > attrmatchlen: #Checking how much of the attribute name is in the variant name.
|
|
1161
|
+
match_attr = val
|
|
1162
|
+
attrmatchlen = len(val['Name']) #Updating the best match.
|
|
1163
|
+
if match_attr: #If a match is found, add the attribute ID to the list and remove the attribute name from the variant name.
|
|
1164
|
+
AttrIDs.append(match_attr['ID'])
|
|
1165
|
+
FullStoneName = FullStoneName.replace(match_attr['Name'], '').strip()
|
|
1166
|
+
else:
|
|
1167
|
+
print(f'Attribute not found for {namekey}: {attrCat} - {FullStoneName}')
|
|
1168
|
+
|
|
1169
|
+
SlabData = {
|
|
1170
|
+
'ProductID': VariantData['Product ID'],
|
|
1171
|
+
'SN_ID': entry['Serial Number ID'],
|
|
1172
|
+
'SN_Display': entry['Serial Number'],
|
|
1173
|
+
'SQM_Allocating': entry['Available Qty'],
|
|
1174
|
+
'SQM_OG': entry['Available Qty']
|
|
1175
|
+
}
|
|
1176
|
+
for i,attr in enumerate(AttrIDs): SlabData[f'AttrID{i+1}'] = attr
|
|
1177
|
+
return SlabData
|
|
1178
|
+
|
|
1179
|
+
def doBin(mw_Connect, binconfig):
|
|
1180
|
+
client = mw_Connect.client
|
|
1181
|
+
ind = 1
|
|
1182
|
+
binName = binconfig.get('binName', f"{datetime.now().strftime('%B %Y')} BIN Movements")
|
|
1183
|
+
|
|
1184
|
+
def bin_full_check(bin_job): return bin_job.total_slabs_allocated >= 100
|
|
1185
|
+
def next_bin():
|
|
1186
|
+
nonlocal ind
|
|
1187
|
+
ind += 1
|
|
1188
|
+
binconfig['binName'] = f"{binName} - {ind}"
|
|
1189
|
+
return Give_BIN(binconfig=binconfig, mw_Connect=mw_Connect, client=client)
|
|
1190
|
+
def Give_binActivity(bin_job):
|
|
1191
|
+
for activity in bin_job.JobData['Activities']:
|
|
1192
|
+
if activity['Activity Type'] == 'Stone Reservations & Adjustments' and activity['Start Date'] == datetime.now().strftime('%d/%m/%Y'):
|
|
1193
|
+
return activity['ActivityID']
|
|
1194
|
+
datestring = datetime.now().strftime('%Y-%m-%d')
|
|
1195
|
+
print(f'Creating BIN Movement Activity: {datestring}')
|
|
1196
|
+
rsp = bin_job.CreateActivity(ActivityType='Stone Reservations & Adjustments', StartDate=datestring)
|
|
1197
|
+
return rsp.text
|
|
1198
|
+
|
|
1199
|
+
binnableList = binconfig['binnable']
|
|
1200
|
+
InvV = mw_Connect.Give('ProductIDs')
|
|
1201
|
+
binslabs = mw_Connect.GetView(binnableList)
|
|
1202
|
+
binnable = []
|
|
1203
|
+
for binned in binslabs:
|
|
1204
|
+
SlabData = slabdata_from_entry(binned, InvV)
|
|
1205
|
+
if SlabData: binnable.append(SlabData)
|
|
1206
|
+
if not binnable: return None
|
|
1207
|
+
|
|
1208
|
+
bin_job = Give_BIN(binconfig=binconfig, mw_Connect=mw_Connect, client=client)
|
|
1209
|
+
while bin_full_check(bin_job):
|
|
1210
|
+
bin_job = next_bin()
|
|
1211
|
+
actid = Give_binActivity(bin_job)
|
|
1212
|
+
|
|
1213
|
+
print(f'Allocating {len(binnable)} slabs to {bin_job.JobData["JobName"]}')
|
|
1214
|
+
while len(binnable) > 0:
|
|
1215
|
+
print(f' Remaining: {len(binnable)} - Current Bin: {bin_job.JobData["JobName"]}')
|
|
1216
|
+
if bin_full_check(bin_job):
|
|
1217
|
+
while bin_full_check(bin_job):
|
|
1218
|
+
bin_job = next_bin()
|
|
1219
|
+
actid = Give_binActivity(bin_job)
|
|
1220
|
+
else:
|
|
1221
|
+
slab = binnable.pop()
|
|
1222
|
+
bin_job.AllocateMaterial(actid, slab)
|
|
1223
|
+
bin_job.total_slabs_allocated += 1
|
|
1224
|
+
return bin_job
|
|
1225
|
+
|
|
1226
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: js_mw_api
|
|
3
|
+
Version: 2.5.0.3
|
|
4
|
+
Summary: Moraware API Wrapper for Python
|
|
5
|
+
Project-URL: Homepage, https://github.com/JensenSystems/JS_Moraware
|
|
6
|
+
Project-URL: Issues, https://github.com/JensenSystems/JS_Moraware/issues
|
|
7
|
+
Author-email: Tyler Jensen <Tyler@JensenSystems.co.nz>
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# JS_Moraware
|
|
14
|
+
Interaction with Moraware using Python
|
|
15
|
+
|
|
16
|
+
Intended to be the FINAL PUBLIC published repo for JS-related libraries.
|
|
17
|
+
Will include JS_Moraware and optionally other JS code (unless they're published separately).
|
|
18
|
+
|
|
19
|
+
Version no# matches my private library and is an exact clone (copy-paste) besides anonymised.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
js_mw_api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
js_mw_api/mwapi.py,sha256=NlUVVYjq1Qn-U8JDjQ2IZyPwywI_Xkoe_-cH2T6nmRE,54205
|
|
3
|
+
js_mw_api-2.5.0.3.dist-info/METADATA,sha256=rvvBR-vw6SKpijNc3K_FPQzyTO9FMSDwrctjCi5VVx4,768
|
|
4
|
+
js_mw_api-2.5.0.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
5
|
+
js_mw_api-2.5.0.3.dist-info/RECORD,,
|