PK uNwՀ% % squp/DataConversion.py'''
DataConversion.py
Version: 1.0
Author: Jonathan Hoyle
Last Updated: 11/15/2018
Description: Used to convert exported item jsons from scrapes into more usable dat formats
'''
# ---------------------- BEGIN IMPORTS ----------------------
import json, unicodedata, dataset
# ----------------------- END IMPORTS -----------------------
# ----------------------- BEGIN CLASS -----------------------
# region Base Conversion
# First Conversion class, handles turning the scraped json items into a comprehensive json database
class Conversion(object):
# ---------------- BEGIN SHORTCUT METHODS -------------------
# Method that does absolutely nothing, used to get rid of annoying unused prompts
def no_op(self, anything):
pass
# ----------------- END SHORTCUT METHODS --------------------
# --------------- BEGIN BASIC PARSING METHODS ---------------
# Separating different values by tildes
def tildeSeparate(self, sep):
sepStrings = []
st = sep.split('~')
for s in st:
sepStrings.append(s)
return sepStrings
# Separating different values by commas
def commaSeparate(self, sep):
sepStrings = []
st = sep.split(',')
for s in st:
sepStrings.append(s)
return sepStrings
# ---------------- END BASIC PARSING METHODS ----------------
# ---------------- BEGIN PROGRAM METHODS --------------------
def convert(self, fileName):
# Create the initial json object
jd = None
# Open and parse the json file
with open(fileName) as data_file:
jd = json.loads(data_file.read())
# Create an array to hold the properly formatted json data in
jda = {}
# For each entry in the original data
for j in jd:
# Normalize the name into an ASCII string, not unicode
try:
# name = unicodedata.normalize('NFKD', j['name']).encode('ascii', 'ignore')
name = j['name']
# Set the default entry type
dontPrint = jda.setdefault(name,
{'sku': '', 'url': '', 'price': '', 'description': '', 'image_urls': [],
'colors': [], 'image_paths': [], 'images': [], 'details': [], 'skus': [],
'goto': [], 'origin': '', 'sizes':[], 'color_data':{}})
# Add the information
try:
# Try replacing that weird character set
jda[name]['description'] = j['description'].replace(' ', '').strip()
except:
try:
# Otherwise just try to strip it
jda[name]['description'] = j['description'].strip()
except:
# Or just assign it
jda[name]['description'] = j['description']
try:
# Try stripping the price var
jda[name]['price'] = j['price'] if (j['price'] is not str) else j['price'].strip()
except:
# Otherwise just assign it
jda[name]['price'] = j['price']
jda[name]['image_urls'] = j['image_urls']
jda[name]['colors'] = j['colors']
jda[name]['image_paths'] = j['image_paths']
try:
jda[name]['color_data'] = j['color_data']
except:
self.no_op(None)
try:
jda[name]['origin'] = j['origin']
except:
self.no_op(None)
try:
jda[name]['images'] = j['images']
except:
self.no_op(None)
jda[name]['skus'] = j['skus']
try:
jda[name]['goto'] = j['goto']
except:
self.no_op(None)
jda[name]['sku'] = j['sku'].strip()
jda[name]['url'] = j['url']
jda[name]['details'] = j['details']
jda[name]['sizes'] = j['sizes']
except:
self.no_op(None)
# Return the properly formatted json file
return jda
# ----------------- END PROGRAM METHODS ---------------------
# endregion
# ------------------------ END CLASS ------------------------
# ----------------------- BEGIN CLASS -----------------------
# region SQL Conversion
# Conversion Class that takes a file and uses its data to add/update information in the appropriate database
class SQLConversion(object):
# Connect to the database of items
db = dataset.connect('sqlite:///C:\\Users\\Shipping\\Documents\\Integration Dev\\SquarespaceUploader\\save_data.db')
# Begin referencing the items table
table = db['items']
# Set up the basic json data variable
jd = None
# ---------------- BEGIN PROGRAM METHODS --------------------
# Converts the given file
def convert(self, file_name):
# Open the file
with open(file_name) as data_file:
# And save its converted data
jd = json.loads(data_file.read())
# Loop through the data
for j in jd:
# Get all of the data from the current json item
name = unicodedata.normalize('NFKD', j['name']).encode('ascii', 'ignore')
# Basic assignments
colors = j['colors']
skus = j['skus']
details = j['details']
origin = j['origin']
url = j['url']
image_urls = j['image_urls']
image_paths = j['image_paths']
goto = []
try:
goto = j['goto']
except:
pass
images = []
try:
images = j['images']
except:
pass
while len(images) < len(colors):
images.append('')
while len(goto) < len(colors):
goto.append('')
while len(image_urls) < len(colors):
image_urls.append('')
while len(image_paths) < len(colors):
image_paths.append('')
# Price and description are a bit more complex due to strips
description = ''
try:
# Try replacing that weird character set
description = j['description'].replace(' ', '').strip()
except:
try:
# Otherwise just try to strip it
description = j['description'].strip()
except:
# Or just assign it
description = j['description']
price = ''
try:
# Try stripping the price var
price = j['price'] if (j['price'] is not str) else j['price'].strip()
except:
# Otherwise just assign it
price = j['price']
# Convert the details list into a string so that it can be stored in the SQL DB
if type(details) is list:
details = "\n".join(details)
if type(description) is list:
description = " ".join(description)
assert type(details) is not list
# Create an extra var for the sizes that will be parsed
sizes = []
removes = []
# Go through the list of colors and parse out the sizes
for c in colors:
try:
if (
'1' in c or '2' in c or '3' in c or '4' in c or '5' in c or '6' in c or '7' in c or '8' in c or '9' in c or '0' in c) or len(
c) == 1 or len(c) == 2 or (
len(c) == 3 and (c != 'Sky' and c != 'Red' and c != 'Fig' and c != 'Rio')) or (
len(c) == 4 and 'XXX' in c) or 'Fit' in c:
sizes.append(c)
removes.append(c)
except:
pass
for r in removes:
colors.remove(r)
# Go through each color and size
for c in colors:
for s in sizes:
# Save the current index
ci = colors.index(c)
# And update/insert the current item's information into the table
try:
self.table.upsert(dict(sku=skus[ci] + str(s), name=name, origin=origin, description=description,
image_url=image_urls[ci], color=c, image=images[ci], price=price,
goto=goto[ci], url=url, details=details, size=s, image_oath=image_paths[ci]),
['sku'])
except:
pass
self.db.commit()
# ----------------- END PROGRAM METHODS ---------------------
# endregion
# ------------------------ END CLASS ------------------------
PK %NG G squp/GuiManual.py'''
Gui Manual
Version 0.2
Author: Jonathan Hoyle
Created: 11/1/18
Description: Provides a graphical manual for the interface for the online store
Details: Can be run from the interface or by itself using flexx tech
'''
# Imports
import webview, dominate
from information import Info
from dominate.tags import *
from bottle import route, run, template
# Tricking something for some reason
window = None
# Style import
g_CSS = open('base_bootstrap.css').read()
# Associate the assets appropriately (materia, minty works)
url = 'https://stackpath.bootstrapcdn.com/bootswatch/4.1.2/yeti/bootstrap.min.css'
# Set up dominate documents
home = dominate.document()
# Set up the styling and jquery
with home.head:
link(rel='stylesheet', href=url)
script(src='https://code.jquery.com/jquery-2.1.1.min.js')
# Create the home page
with home:
with div(cls='container', id='home'):
# Page title
with h1(id='home'):
span('Lionel Smith Ltd Custom Interface')
# Welcome section
with div(cls='row'):
p('Welcome to the Lionel Smith Ltd. Custom Interface help page! '
'Please choose a section below to navigate to the proper page\'s help section.')
# Links section
with div(cls='row'):
a('Orders', href='#orders', cls='btn btn-primary col-sm-2')
a('Item Database', href='#database', cls='btn btn-primary col-sm-2')
a('Updates', href='#updates', cls='btn btn-primary col-sm-2')
a('Manage Inventory', href='#manage', cls='btn btn-primary col-sm-2')
a('Create/Edit Item', href='#create_edit', cls='btn btn-primary col-sm-2')
a('FAQ', href='#faq', cls='btn btn-primary col-sm-2')
p()
# Order title
with h1(id='orders'):
span('Orders Page')
# Reference image
with div(cls='row'):
img(src='file:///C:/Users/Shipping/Documents/Integration%20Dev/SquarespaceUploader/imgs/OrdersPage.png')
# Overview
with h3():
span('Overview')
with div(cls='row'):
p('This page provides an interface to see and fulfill orders from the online store. Note that this page '
'has a sort of online equivalent, but this should prove to be faster and more intuitive.')
# Explanations section
with h3():
span('Contents')
with div(cls='row'):
with ol():
li(span('Dropdown list of all orders currently listed online, note that it does not update live, so '
'any time updates are needed the program will need to be restarted.'))
li(span('Shipping tracking information goes here, checks to make sure the tracking number entered is '
'valid and if it\'s not will ask for correction and will not allow saving.'))
li(span('Completes the order in this interface, but does not proceed with fulfilling the order, just '
'marks it as ready.'))
li(span('Saves currently completed orders but does not execute them, just saves the data entered if '
'it\'s valid.'))
li(span('This actually executes the completion of these orders, note that this is where the remote '
'agent comes in on this page and it may take a while.'))
li(span('This will restart the program to update the orders currently online, note that it will ask '
'for permission before doing so.'))
li(span('This button opens the original orders page in Google Chrome.'))
li(span('Customer name appears here.'))
li(span('Customer shipping information appears here.'))
li(span('Corresponding item information appears here, note that each item will be displayed.'))
with div(cls='row'):
a('Return to top', href='#home', cls='btn btn-primary')
# Database Page
with h1(id='database'):
span('Database Page')
# Reference image
with div(cls='row'):
img(src='file:///C:/Users/Shipping/Documents/Integration%20Dev/SquarespaceUploader/imgs/Database.png')
# Overview section
with h3():
span('Overview')
with div(cls='row'):
p('This page allows browsing of all items that are currently being tracked, whether on the store or not.')
# Explanations section
with h3():
span('Contents')
with div(cls='row'):
with ol():
li(span('Tree view of all items in the database, click + button to expand items and double click the '
'item to populate the fields on the left.'))
li(span('Checkboxes for each field, if you don\'t want a particular field to every change, select it '
'here and updates will not be applied to that field until the field is unchecked again.'))
li(span('Item details go in these blanks, one line for the first few and multiple lines for the '
'bottom fields.'))
li(span('If possible, an image of the selected item is shown here for reference, defaults to the image '
'shown.'))
li(span('As the inner text implies, clicking this button will navigate you to the create/edit page of '
'this interface with the current item selected.'))
# Database Page
with h1(id='updates'):
span('Updates Page')
# Reference image
with div(cls='row'):
img(src='file:///C:/Users/Shipping/Documents/Integration%20Dev/SquarespaceUploader/imgs/Updates.png')
# Overview section
with h3():
span('Overview')
with div(cls='row'):
p(
'This page shows the items that have changed between their last edit and the current data, and '
'shows what will be changed')
# Explanations section
with h3():
span('Contents')
with div(cls='row'):
with ol():
li(span(
'Tree view of all items that have changes, double clicking an item will populate the right side.'))
li(span(
'Clicking this completely disables the update for this item, swaps to say "Enable Update" when '
'the item is disabled.'))
li(span(
'Clicking this refreshes the updates needed, note that due to background processes this may take a '
'while and probably will not apply until the program is restarted.'))
li(span(
'Takes all of the currently enabled updates and applies them to the online store.'))
li(span(
'This column shows the item\'s current information on the store.'))
li(span(
'This column shows the item\'s new information from the most recent scrape.'))
with div(cls='row'):
a('Return to top', href='#home', cls='btn btn-primary')
# Inventory Management Page
with h1(id='manage'):
span('Inventory Management Pages')
# Show/Hide Items Page
with h2(id='show_hide'):
span('Show/Hide Items Page')
# Reference image
with div(cls='row'):
img(src='file:///C:/Users/Shipping/Documents/Integration%20Dev/SquarespaceUploader/imgs/InventoryManageShow.png')
# Overview section
with h3():
span('Overview')
with div(cls='row'):
p(
'This page shows the items currently uploaded to the store, whether shown or not, and allows showing '
'and hiding those items.')
# Explanations section
with h3():
span('Contents')
with div(cls='row'):
with ol():
li(span(
'Lists of every item currently uploaded and not shown sorted by origin store. Currently, only one '
'item from each list is selectable at a time, multiple selection coming in a future update.'))
li(span(
'Clicking this button moves the item(s) selected on the left side and moves them to the right. '
'Note that this is only a graphic representation of a possible change, doing this does NOT '
'actually show the item(s) in the store.'))
li(span(
'Clicking this button moves the item(s) selected on the right side and moves them to the left. '
'Note that this is only a graphic representation of a possible change, doing this does NOT '
'actually hide the item(s) in the store.'))
li(span(
'Pressing this button will take the changes made in this interface and apply them to the actual '
'store. Note that this may take a little while.'))
li(span(
'Currently shown items on the store. Selection rules are currently the same as the left side.'))
li(span(
'Sub-tab for uploading items not currently on the store.'))
li(span(
'Sub-tab for managing item stocks.'))
with div(cls='row'):
a('Return to section top', href='#manage', cls='btn btn-primary')
a('Return to top', href='#home', cls='btn btn-primary')
# Upload Items page
with h2(id='upload'):
span('Upload Items Page')
# Reference image
with div(cls='row'):
img(src='file:///C:/Users/Shipping/Documents/Integration%20Dev/SquarespaceUploader/imgs/InventoryManageUpload.png')
# Overview section
with h3():
span('Overview')
with div(cls='row'):
p(
'This page shows all items tracked, whether uploaded to the store or not, and allows for local items '
'to be uploaded to the online store.')
# Explanations section
with h3():
span('Contents')
with div(cls='row'):
with ol():
li(span(
'Lists of every item currently not uploaded to the store. Selection rules are one item from each '
'list, expanded selection coming in future update.'))
li(span(
'Clicking this button moves the item(s) selected on the left side and moves them to the right. '
'Note that this is only a graphic representation of a possible change, doing this does NOT '
'actually upload the item(s) to the store.'))
li(span(
'Pressing this button will take the changes made in this interface and mark them for completion '
'during the next update. In other words, this tells the interface to accomplish these actions '
'THE NEXT TIME THE ITEMS ARE UPDATED, not at the time of the button press.'))
li(span(
'Currently uploaded items on the store. Selection rules are irrelevant here as this only covers '
'uploading, not deletion.'))
with div(cls='row'):
a('Return to section top', href='#manage', cls='btn btn-primary')
a('Return to top', href='#home', cls='btn btn-primary')
# Stock Management Page
with h2(id='stock'):
span('Stock Management Page')
# Reference image
with div(cls='row'):
img(cls='col-sm-6',
src='file:///C:/Users/Shipping/Documents/Integration%20Dev/SquarespaceUploader/imgs/InventoryManageStockTop.png')
img(cls='col-sm-6',
src='file:///C:/Users/Shipping/Documents/Integration%20Dev/SquarespaceUploader/imgs/InventoryManageStockOpened.png')
# Overview section
with h3():
span('Overview')
with div(cls='row'):
p(
'This page shows all online items and allows for their stocks to be managed.')
# Explanations section
with h3():
span('Contents')
with div(cls='row'):
with ol():
li(span(
'Main panel for each item.'))
li(span(
'Clicking this button expands/contracts the attached panel to show/hide the stocks for that item.'))
li(span(
'(a.) Decrements the current stock for the given item/size combo. \n'
'(b.) Current number of items in stock. The amount -1 indicates unlimited stock. \n'
'(c.) Increments the current stock for the given item/size combo, makes unlimited stock convert '
'to limited if the current stock is -1, requires reload to take effect though.'))
li(span(
'Applies current changes to store immediately and will take some time. Make sure '
'to update items before and after this is run to ensure total store accuracy.'))
li(span('Example of item/size combos in action with limited stocks.'))
li(span('An item/size combination with no stock.'))
li(span('An item/size combination with one in stock.'))
with div(cls='row'):
a('Return to section top', href='#manage', cls='btn btn-primary')
a('Return to top', href='#home', cls='btn btn-primary')
# Database Page
with h1(id='create_edit'):
span('Create/Edit Items Page')
# Reference image
with div(cls='row'):
img(cls='col-sm-6', src='file:///C:/Users/Shipping/Documents/Integration%20Dev/SquarespaceUploader/imgs/CreateEditUnsized.png')
img(cls='col-sm-6', src='file:///C:/Users/Shipping/Documents/Integration%20Dev/SquarespaceUploader/imgs/CreateEditSizedScrolled.png')
with div(cls='row'):
img(cls='col-sm-12', src='file:///C:/Users/Shipping/Documents/Integration%20Dev/SquarespaceUploader/imgs/CreateEditSized.png')
# Overview section
with h3():
span('Overview')
with div(cls='row'):
p('This page allows the editing of items in the database, as well as the creation of original items.')
# Explanations section
with h3():
span('Contents')
with div(cls='row'):
with ol():
li(span('Initial view of this page, due to platform restrictions I cannot change this, resizing allows '
'the second option.'))
li(span('Item tree which functions similarly to the others, expand tree and double click an item to '
'populate relevant fields on the right side of the page.'))
li(span('Item fields for item data entry.'))
li(span('Note that for images, a file entry field is used, which will pop up an extra window to allow '
'selection of one or more image files for an item.'))
li(span('This button clears the fields on the left side to allow for new item creation.'))
li(span('This button saves the changed information for pre-existing items or adds a new item to the '
'overall database depending on what has been accomplished here.'))
li(span('The selection of these fields changes entry to allow/show more complex size options, the '
'initial selection being for basic sizes (i.e. S, M, L, XL, etc.) and the alternate being for '
'more complex sizes (i.e. pant sizes, multi-field shoe sizes, etc.)'))
with div(cls='row'):
a('Return to top', href='#home', cls='btn btn-primary')
# FAQ Page
with h1(id='faq'):
span('Frequently Asked Questions')
# Questions
with div(cls='container'):
with h3():
span('I made changes in the interface, but nothing happened on the store.')
p('Depending on what you changed, there may not need to be changes to the store, or maybe the changes '
'weren\'t saved. Make sure that after you are done making changes you hit the corresponding save/apply '
'button. Remember that for convenience and security, all changes made are not uploaded until approved '
'either in this interface or on the store itself.')
with h3():
span('I can\'t select multiple items in the Show/Hide page and/or Upload page.')
p('This issue is known and will be implemented in future releases, but because of the way the code '
'behind it works, this is not yet possible. For now, just choose one item at a time from each brand, as '
'you can use multiple brands at once, just not multiple items from the same brand at once.')
with div(cls='row'):
a('Return to top', href='#home', cls='btn btn-primary')
@route('/')
def root():
return template(str(home))
def main():
import threading
thread = threading.Thread(target=run, kwargs=dict(host='localhost', port=8081), daemon=True)
thread.start()
import time
time.sleep(1)
webview.create_window('Interface Manual', 'http://localhost:8081', width=1000, height=600)
if __name__ == '__main__':
main()PK ,s
OEf} } squp/InventoryGui.pyw'''
Inventory Viewer/Controller
Version 1.0
Author: Jonathan Hoyle
Created: 7/20/17
Description: Creates a GUI for visualization and modification of store item information stored locally on this device
'''
### Imports ###
# noinspection PyUnresolvedReferences
import os, re, time, shelve, webbrowser, json, urllib.request, urllib.parse, urllib.error, subprocess, threading, glob,\
ftfy
from collections import namedtuple
import interface
# noinspection PyPackageRequirements
from splinter import Browser
# noinspection PyPackageRequirements
from win32api import GetSystemMetrics
# noinspection PyPackageRequirements
from PIL import Image
# noinspection PyPackageRequirements
from appJar import gui
# noinspection PyPackageRequirements
from addict import Dict
from DataConversion import Conversion
# noinspection PyPackageRequirements
from selenium.webdriver.chrome.options import Options
# noinspection PyUnresolvedReferences
# noinspection PyPackageRequirements
from selenium.webdriver.common.by import By
# noinspection PyPackageRequirements
# noinspection PyUnresolvedReferences
from selenium.webdriver.support import expected_conditions as EC
# noinspection PyPackageRequirements
# noinspection PyUnresolvedReferences
from selenium.webdriver.support.ui import WebDriverWait
from information import Info
from shortcuts import *
from datetime import date
import SplinterShortcuts as SpS
import SystemShortcuts as SyS
import pyautogui as pa
import GuiManual, UpdateGui
### Global Variables ###
# region Globals
''' JSON And XML Data Variables '''
# Conversion variable
c = Conversion()
# Json Data
jd = c.convert('items.json')
jda = Dict(jd)
# Named tuple for individual items
Item = namedtuple('Item', list(jd[list(jd.keys())[0]].keys()))
jdn = {}
for k in jd.keys():
# noinspection PyUnresolvedReferences
jdn[k] = Item._make([jd[k][k1] for k1 in jd[k]])
# Dict for old json data
jdo = {}
# Current indentation for xml string
current_tabs = 0
# String to store the xml
xml_string = ''
# Number to word conversion
ntw = ['Zero', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine']
''' App Variable '''
# App for the GUI
'''
app = gui()
'''
app = None
# '''
''' Start Progress Variables '''
start_progress = 0.0
stopped = False
''' Orders Variable '''
orders = []
''' Old Item Information Variable '''
old_items = {}
''' Items to Upload Variable '''
upload_these = []
''' Changes Allowed Variable '''
changes_allowed = {}
''' New Items to Add Variable '''
new_item_selection = []
old_item_selection = []
''' Current Store Item Names Variable '''
item_names = []
old_item_names = []
''' Items with Required Updates Variable '''
items_to_be_updated = []
updates_needed = None
''' Items with Cancelled Updates Variable '''
cancelled_updates = {}
''' Time that the online items database was last updated '''
last_updated = 0
''' Variable to indicate the item being edited '''
editing_item = ''
''' All Current Items Online Names Variable '''
all_items_uploaded = []
''' Origin Stores Variable '''
origins = []
''' Browser Variable '''
# Create the browser to be created, closed, and otherwise in the program
browser = None
s_browser = None
''' Update Info Splinter and Browser Variables '''
# u_b = u.browser
u_b = interface.browser.driver
# usb = u.s_browser
usb = interface.browser
''' XPath Selector Variables '''
# Order Page Button
order_page_button = '//div[@class="title" and contains(text(), "Order")]'
# Item information (use with xpath(this).text)
item_info_text = '//table[contains(@class, "order-rows")]/tbody'
# Address/Orderer Information (same as above)
order_info_text = '//table[contains(@class, "order-summary")]/tbody'
# Path for Chrome
chrome_path = 'C:/Program Files/Google/Chrome/Application/chrome.exe %s'
# Path for Firefox
firefox_path = 'C:/Program Files/Mozilla Firefox/firefox.exe %s'
''' Variable for Order Update Thread '''
timer_thread = None
''' Initial and Actual Stock Values '''
initial_stock_values = None
actual_stocks = {}
''' App and Update Threads '''
app_thread = None
loading_thread = None
''' Transfer Var for Drag/Drop '''
transfer = None
''' Var for Entering Brand Information for Email '''
brand_emails = {}
''' Location of scrapers in the system '''
scraper_location = 'C://Users//Shipping//Documents//LiClipse Workspace//ScrapyDerp//LSL_Scrapers//scrapers//scrapers'
''' Brands being managed '''
managed_brands = []
''' Grab the names of all files in the current scraper folder '''
scraper_names = glob.glob(scraper_location + '//spiders//*.py')
# Create a list for the brand names
brand_names = []
# Loop through the spiders
for s in scraper_names:
# Open each spider
with open(s) as temp_file:
# Find the store line and store that line in the names list
try:
brand_names.append(temp_file.readlines()[3].strip().split(': ')[1])
# If that line doesn't exist, don't worry about it
except:
pass
''' List of names of the check boxes for Manage Brands window '''
brand_checks = []
''' Email information variables '''
current_email_row = 0
cer = current_email_row
email_information = dict()
'''Application Information'''
version = '1.0'
# endregion
""" Browser Control Methods """
# region Browser Control
##- Browser Initialization/Creation and Termination -##
# Creates/starts the browser and navigates to the needed page
def start_browser():
# Get the browser
global browser, s_browser
opts = Options()
opts.add_argument("--window-size=1920,1080")
s_browser = Browser('chrome', options=opts)
bad_count = 0
coords = None
while coords is None and bad_count < 50:
try:
# If it has been found
if len(list(pa.locateAllOnScreen('imgs/ChromeOpenedLike.png'))) > 0 or len(
list(pa.locateAllOnScreen('imgs/ChromeOpenedTitle.png'))) > 0:
# Set the coordinates and break the loop
coords = list(pa.locateAllOnScreen('imgs/ChromeOpenedLike.png'))[0] \
if len(list(pa.locateAllOnScreen('imgs/ChromeOpenedLike.png'))) > 0 else \
list(pa.locateAllOnScreen('imgs/ChromeOpenedTitle.png'))[0]
bad_counter = 50
break
# Otherwise
else:
# Python used Rest!
SyS.slp(0.2)
# Increment the counter
bad_count += 1
except:
# Python used Rest!
SyS.slp(0.2)
# Increment the counter
bad_count += 1
# Python used Teleport!
old_mouse = pa.position()
# Python used Pound!
pa.click(coords[0], coords[1])
# Python used Minimize!
pa.keyDown('win')
pa.keyDown('h')
pa.keyUp('h')
pa.keyUp('win')
# Python used Teleport!
pa.moveTo(old_mouse[0], old_mouse[1])
# Python used Rest!
SyS.slp3()
# s_browser = Browser('chrome', options=opts)
# Set the global browser to a new PhantomJS browser
browser = s_browser.driver
set_browser(browser)
# Maximize the browser to ensure its validity
# browser.maximize_window()
# Go to the main page
s_browser.visit(Info.HOME_PAGE)
# Log in to the website
login()
# Rest another second just to be safe
# Python used Rest!
slp(1)
# Python used Future Sight!
try:
# Navigate to the orders page
xpath(order_page_button).click()
# It wasn't very effective...
except:
pass
# Python used Rest!
slp(3)
# Stops the browser
def stop_browser():
# Get the browser
global browser
# Close the browser only if necessary
if browser is not None:
browser.quit()
# Reset the variable entirely
browser = None
##- Browser Navigation Utilities -##
# Logs in to the website
def login():
# Get the browser
global browser, s_browser
try:
# Send the email for the login to the email field
s_browser.fill('email', Info.LOGIN)
# Python used Rest!
slp()
# Send the password for the login to the password field
s_browser.fill('password', Info.PWD)
# Send the Enter key to the password field to submit
s_browser.find_by_text('Log In').click()
# Python used Rest!
slp()
except:
# Python used Rest!
slp(5)
# Reload the page
s_browser.reload()
# Try to login again
login()
# Finds and selects the item with the given name
def select_name(given_name):
SpS.select_item(SpS.item_name(given_name))
# SpS.select_item(SpS.item_name(given_name))
# Unhides the specified item from the store
def unhide_items(item_names, value='Visible'):
# Grab the globals needed
global ntw, app, u, u_b, usb, jd
# usb = u.s_browser
usb = interface.browser
# print item_names
# print visibility_option[0]
# checking = u.urls
# Uncomment/comment the below two lines when multiple pages are implemented
#checking = [Info.STORE_PAGE]
checking = Info.STORE_PAGE
# checking = interface.urls
# Variable for the current page visited
current_page = 0
# Go through each page with item_names still has contents
#while len(item_names) > 0 and current_page < len(checking):
while len(item_names) > 0:
# If there are no more items, break the loop
if len(item_names) <= 0:
break
# Don't navigate to the page again if the browser is already there
#if usb.url != checking[current_page]:
# usb.visit(checking[current_page])
# For each item name (reversed to allow deletion)
for i in reversed(item_names):
# Try to find and click the proper visibility setting
try:
# Python used Pound!
try:
# Tries using the given name with it's origin before, as displayed in-store
select_name(jdn[i].origin + ' ' + i)
except:
# If that fails, it just uses the name given
select_name(i)
element_looking_for = usb.find_by_css('div.clear.field-workflow-wrapper')
# Python used Rest!
slp(1)
while not usb.is_element_present_by_css('div.clear.field-workflow-wrapper'):
slp()
# Python used Pound!
usb.find_by_css('div.clear.field-workflow-wrapper').click()
# Python used Rest!
slp()
# Python used Pound!
try:
try:
usb.find_by_xpath('//*[contains(text(), "' + value + '")]')[0].click()
except:
usb.find_by_xpath('//*[contains(text(), "Visible")]')[0].click()
except:
try:
usb.find_by_css('div.field-workflow-flyout-option-wrapper')[2 if value =='Hidden' else 0].click()
# It wasn't very effective...
except:
usb.find_by_css('div.field-workflow-flyout-option-wrapper')[0].click()
# Python used Rest!
slp(1)
# Python used Pound!
usb.find_by_css('input.saveAndClose').click()
# Python used Rest!
slp(1)
# Remove the item name now that it's been completed
item_names.remove(i)
# It wasn't very effective...
except:
# Do nothing
print('Failed')
pass
# Indicate to go to the next page in the list
current_page += 1
# updates_needed = u.compareBefore()
updates_needed = interface.compare_before()
# endregion
""" Utility Methods """
# region Utilities
## Start Tag Creation ##
# Returns the start tag of the given item/category
def start_tag(tag, just_text=False):
# Grab the current amount of tabs and increment it
global current_tabs
current_tabs += 1
ntag = tag
# Format number(s) out of the string
if len(re.findall('[0-9]', ntag)) > 0:
for n in ntw:
try:
ntag = re.sub(str(ntw.index(n)), n, ntag)
except:
pass
ntag = ftfy.fix_text(ntag)
# Handling a few other cases for errors here
try:
ntag = re.sub('"', '_inch_', ntag)
except:
pass
try:
ntag = re.sub('&', '_and_', ntag)
except:
pass
try:
ntag = re.sub(',', '_comma_', ntag)
except:
pass
try:
ntag = re.sub('\.', '_point_', ntag)
except:
pass
try:
ntag = re.sub("'", '_sq_', ntag)
except:
pass
try:
ntag = re.sub("#", '_hash_', ntag)
except:
pass
try:
ntag = re.sub('\(', '_sp_', ntag)
except:
pass
try:
ntag = re.sub('\)', '_ep_', ntag)
except:
pass
try:
ntag = re.sub('/', '_sl_', ntag)
except:
pass
try:
ntag = re.sub('\$', '_ds_', ntag)
except:
pass
try:
ntag = re.sub(' ', '_', ntag)
except:
pass
try:
ntag = re.sub('\u00ae', '_r_', ntag)
except:
pass
# If the call was just to format text
if just_text:
# Check and ensure dashes aren't in the current info
if '-' in ntag:
ntag = ntag.replace('-', '_')
# Return properly formatted text
return ntag if ntag[0] != '_' else ntag[1:]
# Return the string
return '<{0}>'.format(ntag)
## End Tag Creation ##
# Returns the end tag of the given item/category
def end_tag(tag, just_text=False):
# Grab the current amount of tabs and decrement it
global current_tabs
current_tabs -= 1
ntag = tag
# Format number(s) out of the string
if len(re.findall('[0-9]', ntag)) > 0:
for n in ntw:
try:
ntag = re.sub(str(ntw.index(n)), n, ntag)
except:
pass
ntag = ftfy.fix_text(ntag)
# Handling a few other cases for errors here
try:
ntag = re.sub('"', '_inch_', ntag)
except:
pass
try:
ntag = re.sub('\xa0', ' ', ntag)
except:
pass
try:
ntag = re.sub('&', '_and_', ntag)
except:
pass
try:
ntag = re.sub(',', '_comma_', ntag)
except:
pass
try:
ntag = re.sub('\.', '_point_', ntag)
except:
pass
try:
ntag = re.sub("'", '_sq_', ntag)
except:
pass
try:
ntag = re.sub("#", '_hash_', ntag)
except:
pass
try:
ntag = re.sub('\(', '_sp_', ntag)
except:
pass
try:
ntag = re.sub('\)', '_ep_', ntag)
except:
pass
try:
ntag = re.sub('/', '_sl_', ntag)
except:
pass
try:
ntag = re.sub('\$', '_ds_', ntag)
except:
pass
try:
ntag = re.sub(' ', '_', ntag)
except:
pass
try:
ntag = re.sub('\u00ae', '_r_', ntag)
except:
pass
# If the call was just to format text
if just_text:
# Check and ensure dashes aren't in the current info
if '-' in ntag:
ntag = ntag.replace('-', '_')
# Return properly formatted text
return ntag if ntag[0] != '_' else ntag[1:]
# Return the string
return '{0}>'.format(ntag)
##- Tabifying Function -##
def tabify():
# Grab the current amount of tabs
global current_tabs
# Create the output string
out = ''
# Technically unnecessary, but better safe than sorry
if current_tabs > 0:
# Add as many tabs as necessary
for i in range(0, current_tabs):
out += '\t'
# Return the tabs in str form
return out
##- String Append Function -##
def x(a):
# Grab the global xml string
global xml_string
# Append the string as necessary
try:
xml_string += str(a)
except:
xml_string += a
##- Normalized the string to what it was before formatting -##
def normalize(s):
# Grab the global variable we need
global ntw
# Save to a local var
ns = s
# For each word in the array
for n in ntw:
# If that word is in the string
if n in ns:
# Replace it with the number
ns = re.sub(n, str(ntw.index(n)), ns)
# Replace the inch symbol
try:
ns = re.sub('_inch_', '"', ns)
except:
pass
# Replace the and symbol
try:
ns = re.sub('_and_', '&', ns)
except:
pass
# Replace the comma symbol
try:
ns = re.sub('_comma_', ',', ns)
except:
pass
try:
ns = re.sub('_point_', '.', ns)
except:
pass
# Replace the single quotation symbol
try:
ns = re.sub('_sq_', "'", ns)
except:
pass
try:
ns = re.sub("_hash_", '#', ns)
except:
pass
# Replace the starting parentheses symbol
try:
ns = re.sub('_sp_', '\(', ns)
except:
pass
# Replace the end parentheses symbol
try:
ns = re.sub('_ep_', '\)', ns)
except:
pass
# Replace the forward slash symbol
try:
ns = re.sub('_sl_', '/', ns)
except:
pass
# Replace the dollar symbol
try:
ns = re.sub('_ds_', '\$', ns)
except:
pass
try:
ns = re.sub('_r_', '\u00ae', ns)
except:
pass
# Replace the space symbol
try:
ns = re.sub('_', ' ', ns)
except:
pass
# Return the formatted string
return ns
##- JSON to XML Formatting -##
def format_json():
## Initialization ##
# Grab the globals we need
global jd, jda, jdn, xml_string
# Create the string that will hold the xml data
x(start_tag('Items'))
# Store origin variable
new_dict = {}
nda = Dict()
# Find the length for percentage of completion
o_l = len(jd)
## Sorting ##
# Find the origins of all of the items
for i in jd:
# Using addict as comparison, only step needed for this loop
try:
nda[jda[i].origin][i] = jda[i]
except:
pass
# If the origin company is not already included
if jd[i]['origin'] not in new_dict:
# Initalize that origin company
new_dict[jd[i]['origin']] = {}
# Append the item to the respective origin company's list
new_dict[jd[i]['origin']][i] = jd[i]
## XML String Creation ##
try:
list(new_dict.keys()).sort()
except:
pass
# Now use the new dict to make a properly formatted xml tree
# For each origin store
for key in list(new_dict.keys()):
# Start the origin store's tag
x(start_tag(key))
# print ''
# print key
# Go through it's products
for item in sorted(new_dict[key]):
# Append the item as the first tag
x(start_tag(item))
# Append the end tag for this item
x(end_tag(item))
# End for
# Add the origin store's end tag
x(end_tag(key))
# End for
# Then close the xml document
x(end_tag('Items'))
xml_string = re.sub('\(', '', xml_string)
xml_string = re.sub('\)', '', xml_string)
return xml_string
# Get the data required for the GUI
def obtain_data():
global old_items, item_names, updates_needed
# Grab the updates needed info
# updates_needed = u.compareBefore()
updates_needed = interface.compare_before()
# origins = u.origins
origins = interface.origins
# Create a variable for the grabbed JSON data from the website
items_displayed = None
# Obtain the JSON data from the website
while items_displayed is None:
try:
# items_displayed = json.loads(urllib.urlopen(u.get_json_url('')).read())['items']
items_displayed = json.loads(urllib.request.urlopen(SyS.get_json_url('')).read())['items']
except:
pass
'''
current = 0
while items_displayed is None and current < 20:
try:
#items_displayed = json.loads(urllib.urlopen(u.get_json_url('')).read())['items']
items_displayed = json.loads(urllib.urlopen(SyS.get_json_url('')).read())['items']
except:
current += 1'''
# Find the names and information for each item found
item_names = []
old_items = {}
for i in sorted(items_displayed, key=lambda c: c['title']):
# Name
# temp_name = u.generalize(i['title'])
temp_name = interface.generalize(i['title'])
item_names.append(ftfy.fix_text(temp_name))
# Description
temp_desc = i['excerpt']
temp_desc = re.sub('
', '', temp_desc)
temp_desc = re.sub('
', '', temp_desc)
# Details
temp_body = i['body']
# Get everything needed
temp_dets = re.findall(r'(?P.*?)', temp_body)
# If nothing was found, try a different search
if len(temp_dets) == 0:
temp_dets = re.findall(r'-\s(?P.*?)
', temp_body)
try:
temp_tags = i['tags']
except:
temp_tags = []
# Colors, SKUs, and Price
temp_colors = []
temp_skus = []
temp_price = ''
first = True
# Go through each variant
for v in i['variants']:
# Colors
# If the color isn't included already
### Note that originally 'optionValues' was u'optionValues', putting this here in case something goes wrong later ###
try:
if v['optionValues'][0]['value'] not in temp_colors:
# Add the color to the temp
temp_colors.append(v['optionValues'][0]['value'])
# Add the SKU to the temp
temp_skus.append(v['sku'])
except:
temp_skus.append(v['sku'])
# Price
# Only run for the first time around
if first:
# Append the price
temp_price = float(float(v['price']) / 100.0) # Confirmed
# Indicate not to run this any more
first = False
# Images
temp_images = []
for img in i['items']:
# Take just the file name
temp_images.append(img['assetUrl'].split('/')[-1])
old_items[temp_name] = {'description': temp_desc, 'details': temp_dets, 'tags': temp_tags,
'colors': temp_colors,
'skus': temp_skus, 'price': temp_price, 'images': temp_images}
# Format the obtained data into an xml that the tree can display
xml_string = ''
for n in item_names:
xml_string += start_tag(n) + end_tag(n)
xml_string += ''
return xml_string
# Returns True or False depending on whether the giving tracking number is valid or not
def is_valid(sn):
# Carrier information
carriers = {'UPS': [11], 'FedEx': [12, 15], 'USPS': [20, 30]}
# Create the boolean
valid = False
# Find the length of the shipping number
length = len(sn)
# Check each carrier and see if the information matches
for c in carriers:
if length in carriers[c]:
valid = True
break
return valid
# Remove HTML tags from given text
def remove_html(s):
fs = re.sub(r'<[^>]*>', '', s)
return fs
# Displays about infobox
def aboutMe(button):
global app
app.infoBox("About SqUp",
"---\n" +
"Jonathan Hoyle, 2017-2019\n" +
"---\n\t" +
"Version: " + version + "\n" +
"Python: 3.7.3\n" +
"appJar: 0.94.0\n" +
"splinter: 0.10.0\n" +
"selenium: 3.141.0\n" +
"Pillow: 6.0.0\n" +
"addict: 2.2.1\n" +
"PyAutoGUI: 0.9.44\n" +
"ahk: 0.6.1\n" +
"---\n" +
"Filename: InventoryGui.pyw\n" +
"---")
# endregion
""" Order Information Acquiring Methods """
# region Order Info Acquisition
# Updates the contents of the window when a new order is selected
def change_contents(event):
# Grab all of the globals that we need
global orders, app
# Create an empty variable to hold the information found
item = None
# Save the current selection
co = app.getOptionBox('orders')
# For every order we have
for o in orders:
# If the order number is found
if o['order_number'] == co:
# Save the information, and break
item = o
break
# As long as something was found
if item is not None:
# Create a temporary variable to hold the items found
items_temp = []
# Old shipping number
old_shipping = app.getEntry('shipping_num')
# Get the shipping number
app.setEntry('shipping_num', item['shipping_info'])
# Check if the content is valid
valid = is_valid(app.getEntry('shipping_num'))
# Change the text of the save button depending on content of the shipping information
if not valid:
# Change the text on the button to show that the item has been saved
app.setButton('Save and Continue', "Please Enter a Valid Shipping Number")
elif item['shipping_info'].strip() != '':
app.setButton('Save and Continue', "Saved!")
else:
app.setButton('Save and Continue', "Save and Continue")
# Change the text of the Complete Order button based on the relevant information
if item['completed']:
app.setButton('Complete Order', "Completed!")
# If it's not, reset the window
else:
app.setButton('Complete Order', "Complete Order")
# If this item was saved and the the original and new aren't the same, reset the variable
if event == 'saved' and old_shipping != app.getEntry('shipping_num'):
app.setButton('Complete Order', "Complete Order")
# Get the name of the client
app.setLabel('customer', item['client']['billing'][0])
# Get the address this order is being shipped to
addstring = item['client']['address'][0]
# Construct a string based on that address
for i in item['client']['address'][1:]:
addstring = addstring + '\n' + i
# Set the address to the string constructed
app.setLabel('shipping_addr', addstring)
# Set the item field to the necessary information
items_temp = format_items(item['items'])
# Create the string for the items
items_str = ''
for i in items_temp:
items_str += i + '\n'
# Update the text
app.setLabel('order_items', items_str)
# Finally, enable the Complete Order button if the item has a shipping number
if app.getEntry('shipping_num').strip() != '' and valid:
app.enableButton('Complete Order')
else:
app.disableButton('Complete Order')
# End Change Contents
# Marks the current item for completion
def complete_item(button):
# From StoreGui.py:
global orders, app
# Get the current order number
# co = current_order.get()
# Create a variable for the item
item = None
# Go through each order
for o in orders:
# Once the correct order is found
if o['order_number'] == app.getOptionBox('orders'):
# Set the item variable
item = o
break
# If the item was found
if item is not None:
# Mark it as completed
item['completed'] = True
# Save the orders
main_file = shelve.open('orders_db')
main_file['orders'] = orders
main_file.close()
# Update the contents of the window()
change_contents(None)
# Method for officially completing an item
def complete_items(button):
# Grab the global needed
global orders
# Create arrays for the completed order numbers and shipping information
completed_numbers = []
completed_shipping = []
# For each order
for o in orders:
# If it's been marked as completed
if o['completed']:
# Append this orders information
completed_numbers.append(o['order_number'])
completed_shipping.append(o['shipping_info'])
# If there are in fact completed orders
if len(completed_numbers) != 0:
# Clear the current content of the window
reset_contents()
# Send this information off to the proper method
complete_orders(completed_numbers, completed_shipping)
# Finally, to perform the last update, refresh the orders
refresh_orders(None)
# Takes an array of orders to complete on the website and does so
def complete_orders(order_ids, tracking_ids):
# Get the browser
global browser, app
# Change the window title
app.setTitle('Please wait while your completed items are being processed')
# Start the browser
start_browser()
# Create an array for the newly fulfilled orders
fulfilled_orders = []
# Find the orders we need to complete based on the order ids given
for o in order_ids:
fulfilled_orders.append(xpath(
'//div[contains(@class, "cell") and contains(@class, "order-number") and contains(text(), "' + o + '")]/..'))
# Now, go through and fulfill each order
for f in fulfilled_orders:
# Python used Pound!
f.click()
# Python used Rest!
slp(1)
# Find the button to fulfill the order
fulfill_button = WebDriverWait(browser, 60).until(
EC.presence_of_element_located((By.XPATH, '//input[@type="button" and @value="Mark Fulfilled"]')))
# Python used Pound!
fulfill_button.click()
# Python used Rest!
slp()
# Find the input for the tracking number
tracking_input = name('trackingNumber')
# Send the tracking ID to the input
tracking_input.send_keys(tracking_ids[fulfilled_orders.index(f)])
# Wait on the web page to recognize the carrier of the package
# Python used Rest!
slp(3)
# Find and press the confirm button
confirm = xpath('//input[@value="Confirm"]')
# Python used Pound!
confirm.click()
# Python used Rest!
slp(1)
# Now that everything should be done, stop the browser
stop_browser()
# Reset the title of the window
app.setTitle('Store Orders For Completion')
# Returns a string with the items supplied
def format_items(item_dict):
# Create the string to save everything to
final_string = []
# For each item in the list
for i in item_dict:
# Create a smaller string to construct this item
current_string = 'Item:\t'
# Add the name
current_string = current_string + i + '\n'
# Add the color
current_string = current_string + 'Color:\t' + item_dict[i]['color'] + '\n'
# Add the quantity
current_string = current_string + 'Quantity:\t' + item_dict[i]['quantity'] + '\n'
# Add the size
current_string = current_string + 'Size:\t' + item_dict[i]['size'] + '\n'
# Add the price
current_string = current_string + 'Price:\t' + item_dict[i]['price'] + '\n'
# Add the total cost
current_string = current_string + 'Total:\t' + item_dict[i]['total'] + '\n\n'
# Add the current string to the overall one
final_string.append(current_string)
# Finally, return the constructed string
return final_string
# Gets the information for the orders in the interface
def get_order_info():
# Get the browser sqs-order-list-content
global s_browser, browser, orders
# Get the current url
c_url = interface.browser.url
# Use the interface's browser to do the heavy lifting
interface.browser.visit(Info.ORDER_PAGE)
s_browser = interface.browser
set_browser(s_browser)
# Make sure the browser is running, and if not run it
#if browser is None:
# start_browser()
#while s_browser.url != Info.ORDER_PAGE:
# s_browser.reload()
# s_browser.visit(Info.ORDER_PAGE)
# Do nothing until the wrapper is detected
while not s_browser.is_element_present_by_xpath('//div[contains(@class, "sqs-order-content")]'):
print('Repeating')
# Once it's detected, find the orders
all_orders = s_browser.find_by_xpath(
'//div[contains(@class,"sqs-model-list-content") and contains(@class, "sqs-order-list-content")]//div[contains(@class, "order-summary")]')
# Create a variable for the basic text of each order
all_order_texts = []
# Get the basic text from each order
for a in all_orders:
# Split the text from the parent element by line breaks
all_order_texts.append(a.text.split('\n'))
# Create a variable for the pending orders
orders_pending = []
orders_removed = []
# Figure out which orders are pending
for a in reversed(all_order_texts):
# If it is, add its number to the list
if 'Pending' in [x.title() for x in a]:
orders_pending.append(a)
# Otherwise, remove it from the original array of elements
else:
all_orders.remove(all_orders[all_order_texts.index(a)])
orders_removed.append(all_order_texts.index(a))
# Now remove the remaining orders from the current texts
for o in orders_removed:
all_order_texts.remove(all_order_texts[o])
# Python used Rest!
slp()
# Now go through the remaining orders
for a in all_orders:
# Create a dict to put all of this in
this_order_information = {}
# Python used Pound!
a.click()
# Python used Rest!
slp()
# Loop here until the stuff we need is visible
while not s_browser.is_element_present_by_xpath(item_info_text):
print('Repeating sub')
# Grab the two pieces of information that we need
# Get the item information element
# Item information, parsed and stripped
order_item_text = [x.strip() for x in s_browser.find_by_xpath(item_info_text).text.split('\n')]
# Shorter name and removal of titles for ease of use
oit = order_item_text[4:]
# Find the index where the price information starts
price_index = oit.index('Item Subtotal')
# Create an array for price information
price_info = []
# Add every item after that index to the new array
for i in range(price_index, len(oit)):
price_info.append(oit[i])
# Now remove all of the information added to the price array from the original array
for p in reversed(price_info):
oit.remove(p)
# Create an array to represent the items ordered
ordered_items = []
# Create a variable to indicate which entry of the items we are on
curr = 0
# And an array to hold each item's information in during assembly
temp = []
# Split the remaining entries into individual items
while curr < len(oit):
# Determine if there is a dollar sign in the string
if '$' not in oit[curr]:
# If not, just append the current string to the temporary array
temp.append(oit[curr])
# Increment the current value by 1
curr += 1
# If a dollar sign is found
else:
# Put the current line and the next line in the temporary array
temp.append(oit[curr])
temp.append(oit[curr + 1])
# Increment by 2
curr += 2
# Append the temporary array to the total items
ordered_items.append(temp)
# Reset the temporary array
temp = []
# Create an array for the ordered item attributes
item_names = []
item_colors = []
item_sizes = []
item_number = []
item_prices = []
item_totals = []
item_adds = []
# For each ordered item
for o in ordered_items:
# Append this item's name to the ordered names
item_names.append(o[0])
# Split the item's color and size info
parts = o[2].split('/')
# Save Color and Size as their own variables
ct = parts[0].strip()
try:
st = parts[1].strip()
except:
st = ''
# Check to make sure that the next field is the quantity
try:
temp_int = int(o[3])
# If it isn't, handle the size appropriately
except:
if 'Length' in o[3]:
st = st + ' x ' + str(o[4]) + 'L'
else:
st = st + ', ' + o[3] + ':' + o[4]
# Append this item's color and size
item_colors.append(ct)
item_sizes.append(st)
# Remaining items are located from the bottom to help eliminate possible parsing errors
# Append the quantity of this item in the order
item_number.append(o[len(o) - 3])
# Append the price of this item
item_prices.append(o[len(o) - 2])
# Append the total cost of these items
item_totals.append(o[len(o) - 1])
# Create a dict for the final items
final_items = {}
# Put all of the information into the final dict
for i in range(0, len(item_names)):
final_items.setdefault(item_names[i],
{'color': item_colors[i], 'size': item_sizes[i], 'quantity': item_number[i],
'price': item_prices[i], 'total': item_totals[i]})
# Order/Shipping information
address_info_text = xpath(order_info_text).text.split('\n')
# Shorter name for ease of use
ait = address_info_text
# Get Shipping Information first
# Method of shipment
method_txt = ait[-1]
# Find out how long this thing is, and work accordingly
# If the Billing and Shipping address are the same
if ait[1] == 'SHIPPING TO:':
# Home Address
shipping_txt = ait[2:6]
# Billing Address
billing_txt = shipping_txt
# Email Address
email_txt = ait[7]
# Phone Number (properly formatted for readability)
# Format: Area Code + '-' + First 3 Numbers + '-' + Last 4 Numbers
phone_txt = ait[8][:3] + '-' + ait[8][3:6] + '-' + ait[8][6:]
# Otherwise
else:
# Billing Address
billing_txt = ait[1:5]
# Shipping Address
shipping_txt = ait[6:10]
# Email Address
email_txt = ait[11]
# Phone Number
phone_txt = ait[12][:3] + '-' + ait[12][3:6] + '-' + ait[12][6:]
# Save all of this information in a dict
client_info = {'billing': billing_txt, 'address': shipping_txt, 'email': email_txt, 'phone': phone_txt}
# Now make sure set all of this items information is saved
this_order_information = {'order_number': all_order_texts[all_orders.index(a)][0], 'items': final_items,
'client': client_info, 'shipping_info': '', 'completed': False}
# And add the information to the overall array
orders.append(this_order_information)
# Python used Pound!
css('a.cancel').click()
# Python used Rest!
slp()
# End for loop
# Now that order parsing is complete, close the browser
#stop_browser()
interface.browser.visit(c_url)
# Save the orders to the current file
main_file = shelve.open('orders_db')
try:
old_orders = main_file['orders']
for o in orders:
for l in old_orders:
if (o['order_number'] == l['order_number']) and l['shipping_info'].strip() != '':
o['shipping_info'] = l['shipping_info']
break
except:
main_file['orders'] = orders
main_file.close()
# Obtains backup data
def load_backup_orders():
# Get the global we need
global orders
# Open the main file to save this one too
main_file = shelve.open('orders_db')
# Save the current info
main_file['orders'] = orders
# Close the main file
main_file.close()
# Open the backup file
b_f = shelve.open('save_data.jon')
# Load in the backu orders
orders = b_f['orders']
# Close the backup file
b_f.close()
# Obtains order information from a saved file
def load_order_info(*filename):
# Get the global we need
global orders
# If the orders aren't blank
if orders is not None:
# Open the backup file first
b_f = shelve.open('save_data.jon')
# Save the current orders to the backup file
b_f['orders'] = orders
# Close the backup file
b_f.close()
# Try to open it with a given filename
try:
# Open the file with the information in it
load_file = shelve.open(filename)
# Load the information
orders = load_file['orders']
# Close the opened file
load_file.close()
# If that doesn't work, go with the default database
except:
# Open the file with the information in it
load_file = shelve.open('orders_db')
# Load the information
orders = load_file['orders']
# Close the opened file
load_file.close()
# End lad-order_info
# Refreshes the orders listed
def refresh_orders(button):
global orders, app
# Save the current orders, just in case something goes wrong
# Open a backup file
backup = shelve.open('save_data.jon')
backup['orders'] = orders
lastorders = orders
backup.close()
# Reset the orders variable
orders = []
# Change the title of the window to reflect the status of the update
app.setTitle('Please wait while orders are being refreshed')
# Disable the order selection until it's done
app.disableOptionBox('orders')
# Call the get information method
get_order_info()
# Get all of the order numbers from this new data
new_numbers = [on['order_number'] for on in orders]
# Make sure that if a shipping number has been saved, it's added to the entry
for l in lastorders:
for o in orders:
# If the order numbers are the same and the original is not empty and the new one is
if (l['order_number'] == o['order_number']) and l['shipping_info'] != '' and o['shipping_info'] == '':
o['shipping_info'] = l['shipping_info']
# Reset the title of the window
app.setTitle('Store Orders For Completion')
# Refresh the list's content
try:
app.changeOptionBox('orders', [o['order_number'] for o in orders])
except:
pass
# Re-enable the list
app.enableOptionBox('orders')
# Resets the contents of every element so that
def reset_contents():
# Grab all of the globals that we need
global app
# Get the shipping number
app.setEntry('shipping_num', '')
# Get the name of the client
app.setLabel('customer', '')
# Set the address to the string constructed
app.setLabel('shipping_addr', '')
# Update the text
app.setLabel('order_items', '')
# Disable the item complete button
app.disableButton('Submit All Completed')
# Saves the information currently in the shipping information entry
# to this item's shipping number
def save_item(button):
# From StoreGui.py:
# Get the globals
global orders, app
# Get the shipping information
sn = app.getEntry('shipping_num')
# Check to make sure the shipping number is valid
valid = is_valid(sn)
# If a valid shipping number has been entered
if valid:
# Create a variable for the item
item = None
# For each order
for o in orders:
# Check to see if it's the current one
if o['order_number'] == app.getOptionBox('orders'):
# If it is, set the current order and break
item = o
break
# As long as it was found, which it always should have been
if item is not None:
# Set the item's shipping information to the currently entered entry
item['shipping_info'] = sn
# Open the file holding the information of the orders
save_file = shelve.open('orders_db')
# Save the changes made to the order
save_file['orders'] = orders
# Close the file
save_file.close()
# Update the contents of the window with the new information
change_contents('saved')
# Regularly updates the orders
def order_updates():
global timer_thread
timer_thread = threading.Timer(1800.0, order_updates)
timer_thread.start()
refresh_orders(None)
# endregion
''' Menu Item Functions '''
# region Menu Items
# Opens the Fedex home page in Chrome
def open_fedex(button):
# Get the path for Chrome
global chrome_path
# Navigate to the page
webbrowser.get(chrome_path).open('https://www.fedex.com/us/index.html')
def open_fedex_firefox(button):
# Get the path for Chrome
global firefox_path
# Navigate to the page
webbrowser.get(firefox_path).open('https://www.fedex.com/us/index.html')
# Opens the home page in a window of Chrome
def open_home(button):
# Get the path for Chrome
global chrome_path
# Get the page in Chrome
subprocess.call(chrome_path[:-2] + 'https://brent-cline-7fyn.squarespace.com/config/orders')
# Opens the folder that images are stored in
def open_image_folder(button):
subprocess.call(r'explorer /select,C:\imgJson\full')
# Opens the folder that the JSON for the items is stored in
def open_item_folder(button):
subprocess.call(
r'explorer /select,"C:\Users\Shipping\Documents\Integration Dev\SquarespaceUploader\SquarespaceUploader"')
# Allows user to write feedback for future features/releases
def leave_feedback(button):
app.showSubWindow('leave_feedback')
# Closes the feedback window
def exit_feedback(button):
app.hideSubWindow('leave_feedback')
# Saves the entered feedback and closes the window
def send_feedback(button):
# Get the text input
feedback_text = app.getTextArea('feedback_input')
# Save the current contents of the file
lines = []
with open('feedback.txt') as ff:
lines = ff.readlines()
# Create the final list
final_lines = []
# Ensure that the input is formatted properly
if len(feedback_text.split('\n')) > 1:
final_lines = feedback_text.split('\n')
else:
final_lines.append(feedback_text)
# Append the new lines to the old ones
lines.extend(final_lines)
fl = []
for l in lines:
if l.strip() not in ['', '\n']:
fl.append(l)
lines = fl
# Save the new lines to the file
with open('feedback.txt', 'w') as ff:
ff.write('\n'.join([l.strip() for l in lines]) + '\n')
# Reset the text input and close the window
app.setTextArea('feedback_input', '')
app.hideSubWindow('leave_feedback')
# endregion
""" Program Methods """
''' Content Updating '''
# region Content Update
## Inventory Page Methods ##
# Displays item information on item double-click
def display_info(tree, *args):
# Grab the globals needed
global app, jd, jdn, changes_allowed
# Grab the currently selected item
item = app.getTreeSelected('nav')
# Initialize the current item and its name
ci = None
item_name = ''
# Try to get the item based purely on the selected element
try:
item_name = normalize(item)
ci = jdn[item_name]
# If that fails
except:
# Search through all of the items
for j in jdn:
# And find the one that contains the text of this item
if type(item) is tuple:
item = item[0]
if normalize(item) in j:
item = j
item_name = j
break
# And set the current item appropriately if one is found
if item != app.getTreeSelected('nav'):
try:
ci = jdn[item]
item_name = item
except:
ci = jdn[list(jdn.keys())[0]]
item_name = list(jdn.keys())[0]
# If one isn't found, set the data to be the default item's data
else:
ci = jdn[list(jdn.keys())[0]]
item_name = list(jdn.keys())[0]
# Set the contents appropriately
# Name
last_item = app.getLabel('name')
# Sub functions to get the checkmarks updated
# Handling labels
def update_checks(i, check_text):
changes_allowed[normalize(last_item)][i] = (not app.getCheckBox(check_text))
app.setCheckBox(check_text, ticked=(not changes_allowed[normalize(item)][i]))
app.setLabel(i, ci[i])
# Handling Messages
def update_checks_message(i, check_text):
changes_allowed[normalize(last_item)][i] = (not app.getCheckBox(check_text))
app.setCheckBox(check_text, ticked=(not changes_allowed[normalize(item)][i]))
app.setMessage(i, ci[i])
# Handling ListBoxes
def update_checks_list(i, check_text):
changes_allowed[normalize(last_item)][i] = (not app.getCheckBox(check_text))
app.setCheckBox(check_text, ticked=(not changes_allowed[normalize(item)][i]))
app.updateListBox(i, ci[i])
# Continuing method here
if last_item != '':
update_checks('url', 'URL:')
update_checks('origin', 'Store of origin:')
update_checks('sku', 'SKU:')
update_checks('price', 'Price:')
update_checks_message('description', 'Description:')
update_checks_list('details', 'Details:')
update_checks_list('image_paths', 'Image Paths:')
app.setLabel('name', item_name)
# Image
im = Image.open('C:/imgJson/' + ci.image_paths[0])
im.save('imgs/item.gif', 'GIF')
app.reloadImage('item_image', 'imgs/item.gif')
app.zoomImage('item_image', -3)
# Url
app.setLabel('url', ci.url)
# Origin
app.setLabel('origin', ci.origin)
# SKU
app.setLabel('sku', ci.sku)
# Price
app.setLabel('price', ci.price)
# Description
app.setMessage('description', ci.description)
# Details
app.updateListBox('details', ci.details)
# Image Paths
app.updateListBox('image_paths', ci.image_paths)
## Updates Page Methods ##
# Stops the Add Item Sub-Window and resets the new item selection
def cancel_sub(button):
# Grab the global app and new items
global app, new_item_selection
# Reset the new items array
# new_item_selection = []
# Hide the window
app.hideSubWindow('Add Items')
# Stops the Add Item Sub-Window and applies the selected items to the Compare/Update/Upload database
def confirm_sub(button):
# Grab the global app and new items to be added to the update
global app, old_items, new_item_selection, all_items_uploaded_old, ntw, items_to_be_updated
# Attempt to "un-format" the item name, just as a precaution
for i in range(0, len(new_item_selection)):
new_item_selection[i] = normalize(new_item_selection[i])
print(new_item_selection)
# Create an array of items to upload and unhide
upload_these = []
unhide_these = []
# For each item
for n in new_item_selection:
items_to_be_updated.append(n)
# If it's not already online
if n not in all_items_uploaded_old:
# Put a blank slot where it will be
old_items[n] = {'description': '', 'details': [], 'tags': [], 'colors': [],
'skus': [], 'price': '', 'images': []}
upload_these.append(n)
# Otherwise
else:
unhide_these.append(n)
unhide_items(unhide_these)
# Hide the window
app.hideSubWindow('Add Items')
# Launches the Add Item Sub-Window
def launch_sub(button):
# Grab the global app
global app
# Show the Window
app.showSubWindow('Add Items')
# Modifies list of new items to add based on action in item tree
def modify_new_items(tree):
# Grab the globals
global app, new_item_selection
# Get the currently selected name
new_name = app.getTreeSelected(tree)
# If it hasn't been added, add it to the list
if new_name not in new_item_selection:
new_item_selection.append(new_name)
# If it has, remove it
else:
new_item_selection.remove(new_name)
# Update the list of items selected
app.updateListBox('added_items', new_item_selection)
# Updates the contents in the comparison fields of the Update Item tab
def update_contents(tree, test):
# Grab the globals needed
global app, jd, jdn, old_items, cancelled_updates
# Grab the selected element
selected = app.getTreeSelected(tree)
# Normalize it
selected = normalize(selected)
# If the returned thing is a tuple, make it a string
if type(selected) is tuple:
selected = selected[0]
# Try one more convert, just in case
selected = normalize(selected)
# UpdateItem Contents
# Name
app.setLabel('old_item_name', 'Name: ' + selected)
app.setLabel('new_item_name', 'Name: ' + selected)
# Description
try:
app.setMessage('old_item_desc', old_items[selected]['description'].replace(' ', '').strip())
except:
try:
app.setMessage('old_item_desc', old_items[selected]['description'].strip())
except:
app.setMessage('old_item_desc', '')
try:
app.setMessage('new_item_desc', remove_html(jdn[selected].description.replace(' ', '')).strip())
except:
try:
app.setMessage('new_item_desc', jdn[selected].description.strip())
except:
app.setMessage('new_item_desc', '')
# Details
try:
app.updateListBox('old_item_details', old_items[selected]['details'])
except:
app.updateListBox('old_item_details', [])
try:
app.updateListBox('new_item_details', jdn[selected].details)
except:
app.updateListBox('new_item_details', [])
# Colors
try:
app.updateListBox('old_item_colors', old_items[selected]['colors'])
except:
app.updateListBox('old_item_colors', [])
try:
app.updateListBox('new_item_colors', jdn[selected].colors)
except:
app.updateListBox('new_item_colors', [])
# Button Options
try:
if not cancelled_updates[normalize(selected)]:
app.setButton('Disable Update', 'Disable Update')
else:
app.setButton('Disable Update', 'Enable Update')
except:
cancelled_updates[normalize(selected)] = False
app.setButton('Disable Update', 'Disable Update')
# Updates the content of the first window that appears
def update_meter():
global app, start_progress, stopped
app.setStatusbar('Startup is ' + str(start_progress) + '\% Complete')
if start_progress > 99.8 and not stopped:
app.hideSubWindow('Start Application')
stopped = True
# endregion
''' Update Management Methods '''
# region Update Management
# Diables updates for an item (symbolically, full function TBI)
def disable_update(button):
# Grab the globals needed
global app, items_to_be_updated, cancelled_updates
# Grab the normalized version of the currently selectedd item
selected = normalize(app.getTreeSelected('update_tree'))
# If the update hasn't been cancelled, do so and change the button's text
if not cancelled_updates[selected]:
items_to_be_updated.remove(selected)
app.setButton('Disable Update', 'Enable Update')
# If it has, un-cancel it
else:
items_to_be_updated.append(selected)
app.setButton('Disable Update', 'Disable Update')
cancelled_updates[selected] = not cancelled_updates[selected]
# Hides and unhides the proper items on the online store
def hide_unhide(button):
# Change this back once multiple pages are implemented
# Grab the globals
global new_item_selection, old_item_selection, upload_these, app
# Show the sub window to prevent interaction
app.showSubWindow('apply_wait_indicator')
# Change the sub-window's wait message
app.setLabel('apply_loading_message', SyS.get_loading_message())
# Create the array to unhide and upload
current_page_items = SpS.get_item_names()
unhide_these = []
upload_these = []
# For each item in the new items selected
for n in new_item_selection:
# If it's not already unhidden
# if n in all_items_uploaded:
if n in current_page_items:
# Indicate to unhide it
unhide_these.append(n)
else:
upload_these.append(n)
# print upload_these
# Unhide the indicated items
unhide_items(unhide_these)
# Hide the old items
unhide_items(old_item_selection, 'Hidden')
# Hide the sub-window
app.hideSubWindow('apply_wait_indicator')
# Indicates an item to move from offline to online
def off_to_on(button):
# Grab the globals needed
global app, new_item_selection, old_item_selection, origins
for o in origins:
# Create a string for the prefix of the items selected
off_pref = ''
on_pref = ''
# If it's in the upload page
if 'upload' in button:
off_pref = 'upload_offline_items_'
on_pref = 'upload_online_items_'
# Otherwise
else:
off_pref = 'offline_items_'
on_pref = 'online_items_'
# Grab the selected items from the offline list
selected = app.getListBox(off_pref + o.lower().replace(' ', '_'))
# print 'offline_items_' + o.lower().replace(' ', '_')
# Grab the total online and offline lists
offline = app.getAllListItems(off_pref + o.lower().replace(' ', '_'))
online = app.getAllListItems(on_pref + o.lower().replace(' ', '_'))
if selected is not None:
# For every selected element
for s in selected:
# Remove it from the offline list
offline.remove(s)
# Append it to the online list
online.append(s)
# Work with the global lists as necessary
if s in old_item_selection:
old_item_selection.remove(o + ' ' + s)
else:
new_item_selection.append(o + ' ' + s)
# Update the ListBox contents
app.updateListBox(off_pref + o.lower().replace(' ', '_'), sorted(offline))
app.updateListBox(on_pref + o.lower().replace(' ', '_'), sorted(online))
# Re-format both of the ListBoxes to their original form (See creation comments for details)
alternate = True
for i in range(0, len(app.getAllListItems('offline_items_' + o.lower().replace(' ', '_')))):
if alternate:
pass
else:
app.setListItemAtPosBg(off_pref + o.lower().replace(' ', '_'), i, '#00ffff')
app.setListItemAtPosFg(off_pref + o.lower().replace(' ', '_'), i, '#000000')
alternate = not alternate
alternate = True
for i in range(0, len(app.getAllListItems('online_items_' + o.lower().replace(' ', '_')))):
if alternate:
pass
else:
app.setListItemAtPosBg(on_pref + o.lower().replace(' ', '_'), i, '#00ffff')
app.setListItemAtPosFg(on_pref + o.lower().replace(' ', '_'), i, '#000000')
alternate = not alternate
# Indicates an item to move from online to offline
def on_to_off(button):
# Grab the globals needed
global app, new_item_selection, old_item_selection, origins
for o in origins:
# Create a string for the prefix of the items selected
off_pref = ''
on_pref = ''
# If it's in the upload page
if 'upload' in button:
off_pref = 'upload_offline_items_'
on_pref = 'upload_online_items_'
# Otherwise
else:
off_pref = 'offline_items_'
on_pref = 'online_items_'
# Get the currently selected items from the online list
selected = app.getListBox(on_pref + o.lower().replace(' ', '_'))
# Get the total list for each side
offline = app.getAllListItems(off_pref + o.lower().replace(' ', '_'))
online = app.getAllListItems(on_pref + o.lower().replace(' ', '_'))
if selected is not None:
# For every selected item
for s in selected:
# Append it to offline
offline.append(s)
# Remove it from online
online.remove(s)
# Work with the global lists as necessary
if s in new_item_selection:
new_item_selection.remove(o + ' ' + s)
else:
old_item_selection.append(o + ' ' + s)
# Update the ListBox items
app.updateListBox(off_pref + o.lower().replace(' ', '_'), sorted(offline))
app.updateListBox(on_pref + o.lower().replace(' ', '_'), sorted(online))
# Re-format both of the ListBoxes to their original form (See creation comments for details)
alternate = True
for i in range(0, len(app.getAllListItems(off_pref + o.lower().replace(' ', '_')))):
if alternate:
pass
else:
app.setListItemAtPosBg(off_pref + o.lower().replace(' ', '_'), i, '#00ffff')
app.setListItemAtPosFg(off_pref + o.lower().replace(' ', '_'), i, '#000000')
alternate = not alternate
alternate = True
for i in range(0, len(app.getAllListItems(on_pref + o.lower().replace(' ', '_')))):
if alternate:
pass
else:
app.setListItemAtPosBg(on_pref + o.lower().replace(' ', '_'), i, '#00ffff')
app.setListItemAtPosFg(on_pref + o.lower().replace(' ', '_'), i, '#000000')
alternate = not alternate
# Refreshes the required updates by restarting the app
def refresh_updates(button):
# Grab the globals needed
global app, items_to_be_updated
restart_application()
# Updates indicated current items and uploads indicated new items
def update_items(button):
# Grab the globals needed
global app
# If the overall load is completed
# if start_progress > 99.9:
# Ask to confirm
if app.yesNoBox('Update Database', 'Are you sure you want to update? This may take a while!'):
app.showSubWindow('apply_wait_indicator')
mythread = ItemUpdatesThread()
mythread.start()
# endregion
''' Adding new/Editing items '''
# region Adding/Editing
# Populates and edits items as necessary
def edit_contents(tree, *args):
# Grab the globals we need
global app, editing_item, jdn, jd
# Set the item currently being edited
editing_item = app.getTreeSelected(tree)
# Save the current properly formatted name
cn = normalize(editing_item)
while type(cn) is tuple:
cn = cn[0]
cn = normalize(cn)
# Name
app.setEntry('new_item_name', cn)
# Price
app.setEntry('new_item_price', jd[cn]['price'])
# SKU
app.setEntry('new_item_sku', jd[cn]['sku'])
# Color SKUs
sks = ''
app.clearTextArea('new_item_skus')
for i in jd[cn]['skus']:
sks += i + '\n'
app.setTextArea('new_item_skus', sks)
# Description
app.clearTextArea('new_item_description')
app.setTextArea('new_item_description', jd[cn]['description'])
# Details
ds = ''
app.clearTextArea('new_item_details')
for i in jd[cn]['details']:
ds += i + '\n'
app.setTextArea('new_item_details', ds)
# Colors and Sizes
cs = ''
ss = ''
app.clearTextArea('new_item_colors_new')
app.clearTextArea('new_item_sizes')
if jd[cn]['colors'] is not None:
for i in jd[cn]['colors']:
if i == 'XXS' or i == 'XS' or i == 'S' or i == 'M' or i == 'L' or i == 'XL' or i == 'XXL' or i == 'XXXL' or i == 'XXXXL' or ' x ' in i:
ss += i + '\n'
else:
cs += i + '\n'
app.setTextArea('new_item_colors_new', cs)
app.setTextArea('new_item_sizes', ss)
# Sizes
# Images
iss = ''
app.setEntry('new_item_images', iss)
for i in jd[cn]['image_paths']:
iss += i + ','
app.setEntry('new_item_images', iss)
# Origin
app.setEntry('new_item_origin', jd[cn]['origin'])
# Edits the currently selected item in the original nav tree
def edit_selected(button):
# Grab the globals
global editing_item, app
# Go to the edit tab
app.setTabbedFrameSelectedTab('Store GUI', 'Create New/Edit Item')
# Edit the contents, but get the nav tree and not the edit tree
edit_contents('nav')
# Saves a newly created item
def save_new(button):
# Grab the globals needed
global app, jd, jdn, editing_item, upload_these
# Grab all of the information we want
# Name
t_name = app.getEntry('new_item_name')
# Price
t_price = app.getEntry('new_item_price')
# SKU
t_sku = app.getEntry('new_item_sku')
# Color SKUs
t_skus = [x for x in app.getTextArea('new_item_skus').split('\n')]
# Description
t_desc = app.getTextArea('new_item_description')
# Details
t_details = [x for x in app.getTextArea('new_item_details').split('\n')]
# Colors
t_colors = [x for x in app.getTextArea('new_item_colors_new').split('\n')]
# Check to see which size option is checked
size_type = app.getRadioButton('size_type')
# If it's a basic size type
if size_type == 'Basic':
t_sizes = [x for x in app.getTextArea('new_item_sizes').split('\n')]
# Otherwise
else:
# Get the first column
widths = [x for x in app.getTextArea('advanced_item_size_one').split('\n')]
# Get the second column
lengths = [x for x in app.getTextArea('advanced_item_size_two').split('\n')]
# Put them together to make the final strings
t_sizes = [w + 'x' + l for w in widths for l in lengths]
# Combine the sizes and the colors
for s in t_sizes:
t_colors.append(s)
# Images
t_images = [x for x in app.getEntry('new_item_images').split('\n')]
# Origin
t_origin = app.getEntry('new_item_origin')
# If this isn't editing an old item, add this item to the database
if len(editing_item) == 0:
jd[t_name] = {'description': t_desc, 'colors': t_colors, 'details': t_details, 'goto': [], 'url': '',
'image_paths': t_images, 'image_urls': t_images, 'images': t_images, 'origin': t_origin,
'price': t_price, 'sku': t_sku, 'skus': t_skus, 'sizes': t_sizes}
# Otherwise, just change the current item's information
else:
jd[t_name]['price'] = t_price
jd[t_name]['sku'] = t_sku
jd[t_name]['skus'] = t_skus
jd[t_name]['description'] = t_desc
jd[t_name]['details'] = t_details
jd[t_name]['colors'] = t_colors
jd[t_name]['origin'] = t_origin
jd[t_name]['sizes'] = t_sizes
# Finally, if the button clicked involved uploading, upload the item
if 'Upload' in button:
jd[t_name]['price'] = t_price
jd[t_name]['sku'] = t_sku
jd[t_name]['skus'] = t_skus
jd[t_name]['description'] = t_desc
jd[t_name]['details'] = t_details
jd[t_name]['colors'] = t_colors
jd[t_name]['origin'] = t_origin
jd[t_name]['sizes'] = t_sizes
upload_these.append(t_name)
# noinspection PyUnresolvedReferences
#jdn[t_name] = Item._make([jd[t_name][k] for k in jd[t_name]])
# Resets entry fields and tree for a new item
def start_new(button):
global app, editing_item
editing_item = ''
# Reset each field
# Name
app.setEntry('new_item_name', '')
# Price
app.setEntry('new_item_price', '')
# SKU
app.setEntry('new_item_sku', '')
# Color SKUs
app.clearTextArea('new_item_skus')
# Description
app.clearTextArea('new_item_description')
# Details
app.clearTextArea('new_item_details')
# Colors
app.clearTextArea('new_item_colors_new')
# Sizes
app.clearTextArea('new_item_sizes')
# Images
app.setEntry('new_item_images', '')
# Origin
app.setEntry('new_item_origin', '')
# Method for when different size inputs are selected
def size_option(button):
if app.getRadioButton('size_type') == 'Basic':
app.raiseFrame('Basic Sizes')
else:
app.raiseFrame('Advanced Sizes')
# Method to send changes of stock to the interface
def stock_changes(button):
# Grab the globals necessary
global actual_stocks, initial_stock_values, app
# Show the wait window
app.showSubWindow('apply_wait_indicator')
# Create the dict to send with new information about the products
updates_required = {}
# Create a variable to potentially hold a list of items to convert to unlimited items
make_limited = None
# First, find out what needs to be updated in terms of stocks
for k in actual_stocks:
# If there's no size values for this item
if type(actual_stocks[k]) is not dict:
# Then directly check if they're equal
if actual_stocks[k] != initial_stock_values[k]:
updates_required[k] = actual_stocks[k]
# If there are size values
else:
# Loop through each one
for k2 in actual_stocks[k]:
# If any of them aren't equal
if actual_stocks[k][k2] != initial_stock_values[k][k2]:
# Mark it for updating
updates_required[k] = actual_stocks[k]
# If this item is becoming unlimited
if actual_stocks[k][k2] != -1 and initial_stock_values[k][k2] == -1:
# If it's not already made
if make_limited is None:
# Create the actual list to send over
make_limited = []
# Append this key to the list
make_limited.append(k)
# Call the appropriate method from the interface
interface.updateStock(updates_required, now_limited=make_limited)
print('Done')
# Hide the app wait window
app.hideSubWindow('apply_wait_indicator')
# Method that runs threaded class version of above method, preferred so that the app kills itself during a run, but still in testing
def apply_stock_changes(button):
# Create the thread
stock_thread = ItemStockUpdatesThread()
# And run it
stock_thread.start()
# Method for when a drag/drop operation has begun
def begin_drag(widget):
print('start: ' + widget)
pass
# Method for when a drop/drop operation ends
def end_drag(widget):
print('end: ' + widget)
pass
# endregion
''' Application Functions '''
# region Application Functions
# Check the git repo and pip for updates
def check_for_updates():
global app
app.showSubWindow('update_wait_indicator')
UpdateGui.main()
app.hideSubWindow('update_wait_indicator')
# Loads the jd modified from the last usage (stored in the save_data.jon)
def load_user_data(test):
# Grab the global needed
global jd
# Try to restore the jd var
try:
# Open the file
file_save = shelve.open('save_data.jon')
# Restore the jd
jd = file_save['jd']
# Close the file
file_save.close()
# It wasn't very effective...
except:
# Do nothing, zilch, nada
pass
# Loads the necessary variables into place
def start_application(button):
global all_items_uploaded_old, all_items_uploaded, start_progress, app, u_b
# Get all of the items currently uploaded to the store
try:
sf = shelve.open('save_data.jon')
all_items_uploaded = sf['all_items_uploaded']
sf.close()
except:
all_items_uploaded = interface.get_all_product_names()
# Start the thread to get the application going
# app.setStatusbar('')
refresh_orders(None)
# order_updates()
# Restarts the application (needed to update the Update Information Page)
def restart_application():
# If the user confirms a restart
if app.yesNoBox('Confirm Restart', 'Are you sure you want to restart to update the information?'):
# Stop the application remotely
stop_application_remote()
# Start the application again remotely
start_window()
# Function for what happens when the app stops
def stop_application():
# Grab the globals
global u_b, changes_allowed, cancelled_updates, jd, all_items_uploaded, timer_thread, managed_brands, \
email_information, app
# Ask if the user really wants to exit the app, and if so
if app.yesNoBox('Exit Application', 'Are you sure you want to quit?'):
# Save everything to one file
sft = shelve.open('save_data.jon')
sft['changes_allowed'] = changes_allowed
sft['jd'] = jd
sft['cancelled_updates'] = cancelled_updates
sft['all_items_uploaded'] = all_items_uploaded
sft['managed_brands'] = managed_brands
sft.close()
# Quit the browser in UpdateInfo
u_b.quit()
# Stop the browser in this file
stop_browser()
app.remove('File')
app.remove('Orders')
app.remove('Load')
app.remove('Brands')
app.removeAllWidgets(True)
# If there are emails that need to be sent
if len(email_information) > 0:
# Import the class for emails
import email_script
# Use it to email the proper brands
se = email_script.EmailSending(email_information)
# And send them off
se.send_emails(email_information)
# Stop the timer thread that regularly updates orders
# timer_thread.cancel()
# Indicate stopping the app
return True
# Function for what happens when the app needs to restart
def stop_application_remote():
# Call the stop_application method
stop_application()
# Then actually stop the thing
app.stop()
# Function that updates the loading message
def update_loading_message():
# Update the loading message
app.setLabel('loading_message', SyS.get_loading_message())
# Python used Rest!
SyS.slp(10)
# Shows a sub window with a restart message
def restart_needed(subwindow_name):
test = app.yesNoBox("Confirm Save", "Changes made will only take place after you restart, which may take a while, "
"would you like to do so now?")
if test:
restart_application()
else:
app.hideSubWindow(subwindow_name)
# Function that starts the Manage Brands window
def start_manage_brands():
app.showSubWindow('manage_brands')
# Function for when the user exits manage brands
def exit_manage_brands(button):
# test = app.yesNoBox('Confirm Cancel', 'Are you sure you wish to exit without saving your changes?')
# if test:
app.hideSubWindow('manage_brands')
# Function for when the user saves in manage brands
def save_manage_brands(button):
global managed_brands, app, brand_checks
managed_brands = []
for b in brand_checks:
if app.getCheckBox(b):
managed_brands.append(" ".join(b.split(" ")[1:]))
restart_needed('manage_brands')
# Function to start entering brands to email for new brands
def start_email_brands():
app.showSubWindow('email_new_brands')
# Provides functionality to the button that adds new rows to the email interface
def add_email_brand(button):
# Grab the row that this interface should add the new inputs on
global cer
# Increment that row
cer += 1
# Re-open the email frame
app.openFrame('email_inputs')
# Create an initial row of inputs
app.label('brand_name_' + str(cer), 'Brand Name:', pos=(cer, 0), padding=(30, 15))
app.entry('email_brand_name_' + str(cer), pos=(cer, 1), padding=(30, 15))
app.label('brand_email_' + str(cer), 'Brand Email Address:', pos=(cer, 2), padding=(30, 15))
app.entry('email_brand_email_' + str(cer), pos=(cer, 3), padding=(30, 15))
# Close the frame
app.stopFrame()
# Function for when the user exits emailing brands
def exit_email_brands(button):
# Asks for confirmation since information will actually be lost here
test = app.yesNoBox('Confirm Cancel', 'Are you sure you wish to exit without saving your changes?')
# If yes
if test:
# Hide the window
app.hideSubWindow('email_new_brands')
# Function for saving and sending emails
def save_email_brands(button):
# Grab the globals
global app, email_information
# Ask first
ask = app.yesNoBox('Confirm Send Emails', 'Are you sure you\'d like to send emails to these companies?\n'
'Emails will be sent when the application window is closed.')
# If confirmed
if ask:
# Grab the number of brands that are being contacted
global cer
# Re-initialize the brand names just to be safe
brand_names = {}
# Grab all of the inputs from the entry
for i in range(cer + 1):
# Get the name and email for this column
t_name = app.getEntry('email_brand_name_' + str(i)).title()
t_email = app.getEntry('email_brand_email_' + str(i))
app.setEntry('email_brand_name_' + str(i), '')
app.setEntry('email_brand_email_' + str(i), '')
if i != 0:
app.removeLabel('brand_name_' + str(i))
app.removeEntry('email_brand_name_' + str(i))
app.removeLabel('brand_email_' + str(i))
app.removeEntry('email_brand_email_' + str(i))
# Save it to the dict
brand_names[t_name] = t_email
# Debugging print statement
print(brand_names)
# Update the entries to email when the window is closed
email_information.update(brand_names)
# And finally hide the window
app.hideSubWindow('email_new_brands')
# endregion
''' Window Start Process '''
# Updated start method
def start_window():
## Initialization ##
# Grab the copy module
import copy
# Grab the globals
global app, jd, jdo, new_item_selection, old_items, item_names, all_items_uploaded, u_b, changes_allowed, \
items_to_be_updated, cancelled_updates, old_item_names, origins, actual_stocks, initial_stock_values, \
managed_brands, brand_checks
'''
----- Data -----
'''
# region Data
# Grab all of the item names currently uploaded to the store
# u.get_current_urls()
interface.get_current_urls()
# Get rid of the size information in the colors of the jd
sizes = ['XXS', 'XS', 'S', 'M', 'L', 'XL', 'XXL', 'XXXL']
jdo = {}
jd = c.convert('items.json')
# Try to load the changes allowed variable from the necessary file
try:
file_save = shelve.open('save_data.jon')
changes_allowed = file_save['changes_allowed']
jdo = file_save['jd']
managed_brands = file_save['managed_brands']
file_save.close()
# Add everything that's not in the current data from the previous data
for n in jdo:
if n not in jd:
jd[n] = jdo[n]
# If the changes aren't allowed for an item, reset it's data to what it was in the previous run
for n in changes_allowed:
if n in jd and not changes_allowed[n]:
jd[n] = jdo[n]
# If there is any information missing (mainly description) from this version that wasn't missing from the last
# version, just use the last version of this data
for n in jd:
try:
if jd[n]['description'].strip() == '' and jdo[n]['description'].strip() != '':
jd[n]['description'] = jdo[n]['description']
except:
pass
# If it doesn't work, don't worry about it
except:
pass
# Python used Iterate!
for item in jd:
# Add all items from jd not currently in changes_allowed to it
if item not in changes_allowed:
changes_allowed[item] = {}
# Python used Iterate!
for ib in jd[item]:
exec(('changes_allowed[item][\'' + ib + '\'] = True'), globals(), locals())
# Python used Iterate!
if jd[item]['colors'] is not None:
for co in reversed(jd[item]['colors']):
if co in sizes:
jd[item]['colors'].remove(co)
# endregion
'''
----- Setup -----
'''
# Get the browser to the right place
interface.init()
with gui("Lionel Smith Ltd. Interface") as app:
# region Setup
# Set app window size
app_w = 1000
app_h = 800
# Get Current Screen Size
scr_w = GetSystemMetrics(0)
scr_h = GetSystemMetrics(1)
# Calculate x and y from the previous 4 vars
app_x = (scr_w / 2) - (app_w / 2)
app_y = ((scr_h / 2) - (app_h / 2))
# Set the Title, Font, and other properties
app.setTitle('Lionel Smith Ltd. Online Store Interface')
app.setIcon('imgs/storeicon.ico')
app.setFont(10)
app.setLocation(app_x, app_y)
app.setSize(app_w, app_h)
# app.setResizable(canResize=False)
app.setSticky('news')
app.setExpand('both')
#app.setTtkTheme('arc')
# endregion
'''
----- Sub-Windows -----
'''
# region Sub-Windows
# Wait Indicator Sub-Window
with app.subWindow('apply_wait_indicator', title='One Moment...', modal=True):
# Create the frame to hold the message in
with app.frame('wait_frame', inPadding=(30, 10)):
# Initialize the basic prompt
app.label('wait_message', 'Please wait as changes are applied to your data', 0, 0, inPadding=(30, 10))
# And give a random, completely unnecessary loading message
app.label('apply_loading_message', SyS.get_loading_message(), 1, 0, inPadding=(30, 10))
# JSON Error Update
with app.subWindow('json_update_indicator', title='One Moment...', modal=True):
# Create the frame to hold the message in
with app.frame('json_frame', inPadding=(30, 10)):
# Initialize the basic prompt
app.label('json_message', 'WARNING\nYour database file is not up to date!\n'
'To fix this, please ensure that Kitematic is running (click its icon '
'in the start menu, shown below, and wait to ensure that you do not get a loading screen)\n'
'then click inventoryupdate.bat in the Documents folder and wait for the '
'process to complete.\n\n'
'In the meantime, previous data will be displayed...', 0, 0,
inPadding=(30, 10))
app.addImage("example1", "imgs/KitematicExample.png", 1,0)
app.addImage("example2", "imgs/KitematicOpened.png",1,1)
# And give a random, completely unnecessary loading message
app.label('json_loading_message', SyS.get_loading_message(), 2, 0, inPadding=(30, 10))
# Update Indicator Sub-Window
with app.subWindow('update_wait_indicator', title='One Moment...', modal=True):
# Create the frame to hold the message in
with app.frame('update_wait_frame', inPadding=(30, 10)):
# Initialize the basic prompt
app.label('update_wait_message', 'Please wait as the application and libraries are updated', 0, 0,
inPadding=(30, 10))
# And give a random, completely unnecessary loading message
app.label('update_loading_message', SyS.get_loading_message(), 1, 0, inPadding=(30, 10))
# Start Wait Indicator Sub-Window
with app.subWindow('start_wait_indicator', title='One Moment...', inPadding=(30, 10)):
# Create the frame to hold the message in
with app.frame('hold_on_frame', inPadding=(30, 5)):
# Initialize the basic prompt
app.label('hold_on', 'Please wait as the application starts...', 0, 0, inPadding=(30, 5))
# And give a random, completely unnecessary loading message
app.label('loading_message', SyS.get_loading_message(), 1, 0, inPadding=(30, 5))
# Start Wait Indicator Sub-Window
with app.subWindow('leave_feedback', title='Feedback', inPadding=(30, 10)):
# Create the frame to hold the message in
with app.frame('leave_feedback_frame', inPadding=(30, 5)):
# Initialize the basic prompt
app.label('enter_feedback_indicator', 'Enter feedback here:', 0, 0, inPadding=(30, 5))
# Create the area for the text input
app.addTextArea('feedback_input', 0, 1, colspan=2)
# Create the buttons to save/send the feedback
app.button('Cancel Feedback', exit_feedback, pos=(1, 0))
app.button('Save Feedback', send_feedback, pos=(1, 2))
# Manage Brands Sub-Window
with app.subWindow('manage_brands', title='Manage Brands', modal=True):
# Create the frame to hold the info in
with app.frame('manage_brands_frame', inPadding=(10, 30)):
# Add the label for this window
app.label('pick_one_already', 'Check the items you would like to manage:', pos=(0, 0), colspan=3)
# Create the positional variables for the loop
x = 0
y = 1
# For every brand found in the scrapers folder
for b in brand_names:
# Create the checkbox for the current brand
app.checkBox('Show ' + b, checked=b in managed_brands, pos=(y, x), padding=(30, 15))
# Check it if it's currently managed
app.setCheckBox('Show ' + b, b in managed_brands)
# Append the name of this checkbox to the list
brand_checks.append('Show ' + b)
# Increment the x value
x += 1
# If the table has reached the third column
if x == 3:
# Reset x
x = 0
# Increment y
y += 1
# Increment y by 2
y += 2
# Add a horizontal line as a separator
app.addHorizontalSeparator(y - 1, 0, colour="red", colspan=3)
# Add the buttons needed
app.button('Cancel Brand Management', exit_manage_brands, pos=(y, 0), padding=(30, 15))
app.button('Save Brand Changes', save_manage_brands, pos=(y, 2), padding=(30, 15))
# Email New Brands Sub-Window
with app.subWindow('email_new_brands', title='Enter Brands', modal=True):
# Create the frame to hold the info in
with app.frame('new_email_frame', padding=(30, 15)):
# Create an indicator label
app.label('Enter the name of the company and email in the fields below:', pos=(0, 0), colspan=4,
padding=(30, 15))
# Create a frame for the inputs to email new brands
with app.frame('email_inputs', pos=(1, 0), colspan=4, padding=(30, 15)):
# Create an initial row of inputs
app.label('brand_name_0', 'Brand Name:', pos=(0, 0), padding=(30, 15))
app.entry('email_brand_name_0', pos=(0, 1), padding=(30, 15))
app.label('brand_email_0', 'Brand Email Address:', pos=(0, 2), padding=(30, 15))
app.entry('email_brand_email_0', pos=(0, 3), padding=(30, 15))
# Create a button that would add another row
app.button('Add Another Brand', add_email_brand, pos=(2, 1), colspan=2, padding=(30, 15))
# Create the cancel button
app.button('Cancel Email', exit_email_brands, pos=(3, 0), colspan=2, padding=(30, 15))
# Create the submit/send button
app.button('Submit/Send Emails', save_email_brands, pos=(3, 2), colspan=2, padding=(30, 15))
# Register the string changing method
# app.registerEvent(update_loading_message())
date_str = 'items' + \
(('0' + str(date.today().month)) if date.today().month < 10 else str(date.today().month)) + \
('0' + str(date.today().day) if date.today().day < 10 else str(date.today().day)) + \
str(date.today().year)[2:] + '.json'
if date_str in glob.glob('*.json'):
# Start the startup sub-window
app.showSubWindow('start_wait_indicator')
else:
app.showSubWindow('json_update_indicator')
# Start the thread that reloads the message in the sub window
# loading_thread = threading.Thread(target=update_loading_message())
# loading_thread.start()
# endregion
'''
----- Menus -----
'''
# region Menus
# Create the file menu
file_menu = ['Open in Chrome...', 'Open Item Source Folder...', 'Open Images Folder...']
file_functions = [open_home, open_item_folder, open_image_folder]
app.addMenuList('File', file_menu, file_functions)
# Create the orders menu
order_menu = ['Refresh Orders', 'Open Fedex in Chrome...', 'Open Fedex in Firefox...']
order_functions = [refresh_orders, open_fedex, open_fedex_firefox]
app.addMenuList('Orders', order_menu, order_functions)
# Data Restoration Menu
load_menu = ['Load Previous Data', 'Upload Changes']
load_functions = [load_user_data, update_items]
app.addMenuList('Load', load_menu, load_functions)
# Brands Menu
brands_menu = ['Configure Brands...', 'Email New Brands...']
brands_functions = [start_manage_brands, start_email_brands]
app.addMenuList('Brands', brands_menu, brands_functions)
# Help Menu
help_menu = ['Open Manual...', 'Check for updates...', 'Leave Feedback...', 'Update Driver...', 'About...']
help_functions = [GuiManual.main, check_for_updates, leave_feedback, SpS.update_driver, aboutMe]
app.addMenuList('Help', help_menu, help_functions)
# Create the Splash Screen
# app.showSplash('Lionel Smith Ltd. Store Manager', fill='teal', stripe='white', fg='blue', font=44)
# endregion
'''
----- Main Frame -----
'''
## Begin Tabbed Frame ##
with app.tabbedFrame('Store GUI'):
'''
----- Orders -----
'''
# region Orders
## Begin Order Management Tab ##
with app.tab('Orders'):
## Start Order Selection/Information Editing Pane ##
with app.panedFrame('Order Management', width=app_w / 3):
# Order Choice Label and Input
app.label('oc', 'Choose an order:', 0, 0)
app.option('orders', ['', ''], 1, 0, colspan=2)
app.setOptionBoxChangeFunction('orders', change_contents)
# Shipping Label and Input
app.label('si', 'Shipping #:', 2, 0)
app.entry('shipping_num', pos=(3, 0), colspan=2)
# Complete Order Button
app.button('Complete Order', complete_item, 4, 0, colspan=2)
# Save and Continue Button
app.button('Save and Continue', save_item, 5, 0, colspan=2)
# Complete Button
app.button('Submit All Completed', complete_items, 6, 0, colspan=2)
## Start Order View/Refresh Pane ##
with app.panedFrame('Order Misc',vertical=True):
# Refresh Orders Button
app.button('Refresh Orders', refresh_orders, 0, 0)
# Open in Chrome Button
app.button('Open in Chrome', open_home, 0, 1)
## Start Order Information Pane ##
with app.panedFrame('Order Information', width=app_w * 2 / 3):
# Name of Customer Label and Information
app.label('cn', 'Name:', 0, 0)
app.label('customer', '', 0, 1)
# app.addImage('product_image', 'icon.jpg')
# Shipping Address Label and Information
app.label('sa', 'Shipping Address:', 1, 0)
app.label('shipping_addr', '', 1, 1)
# Items Label and Information
app.label('il', 'Items:', 2, 0, rowspan=2)
app.label('order_items', '', 2, 1, rowspan=2)
## Stop Order View/Refresh Pane ##
## Stop Order Information Pane ##
## Stop Order Selection/Information Editing Pane ##
## Stop Orders Tab ##
# endregion
'''
----- Inventory -----
'''
# region Stock
## Begin Inventory Checker Tab ##
with app.tab('Items Database'):
## Begin Tree Frame ##
# Begin by creating the first paned frame
with app.panedFrame('items'):
# Then put the item tree based on the formatted string given
tree_xml = format_json()
app.addTree('nav', tree_xml)
app.setTreeColours('nav', 'black', 'white', 'blue', 'yellow')
app.setTreeEditable('nav', False)
# Add the tree-clicking functionality
app.setTreeDoubleClickFunction('nav', display_info)
## Begin Item Contents Pane ##
# Create the second paned frame
with app.panedFrame('info'):
with app.scrollPane('infoScrollPane'):
# Name label and info
app.checkBox('Item Name:', pos=(0, 0))
app.label('name', '', pos=(0, 1))
# Image and info
app.image('item_image', 'imgs/icon.gif', pos=(0, 2), rowspan=2)
app.zoomImage('item_image', -5)
# Url label and info
app.checkBox('URL:', pos=(1, 0))
app.label('url', '', pos=(1, 1))
# Origin label and info
app.checkBox('Store of origin:', pos=(2, 0))
app.label('origin', '', pos=(2, 1), colspan=2)
# Sku label and info
app.checkBox('SKU:', pos=(3, 0))
app.label('sku', '', pos=(3, 1), colspan=2)
# Price label and info
app.checkBox('Price:', pos=(4, 0))
app.label('price', '', pos=(4, 1), colspan=2)
# Description label and info
app.checkBox('Description:', pos=(5, 0))
app.message('description', '', pos=(5, 1), colspan=2)
# Details label and info
app.checkBox('Details:', pos=(6, 0))
app.listBox('details', [], pos=(6, 1), colspan=2)
# Image path label and info
app.checkBox('Image Paths:', pos=(7, 0))
app.listBox('image_paths', [], pos=(7, 1), colspan=2)
app.button('Edit Item', edit_selected, pos=(8, 0), colspan=3)
# Update the shown image if that image is clicked in its list
def update_image(img_list):
# Grab the global/local app (either one works, but using global just in case)
global app
# Get the selected image in the list
sel = app.getListItems('image_paths')
# As long as an image was found
if sel is not None:
if len(sel) != 0:
# Open the image given
im = Image.open('C:/imgJson/' + sel[0])
# Save it as a gif to allow for easier opening
im.save('imgs/item.gif', 'GIF')
# Reload the image in the GUI
app.reloadImage('item_image', 'imgs/item.gif')
# Scale the image to make it an appropriate size
app.zoomImage('item_image', -3)
# If that doesn't work
else:
# Do nothing
pass
# Set the list item click function to the one above
app.setListBoxChangeFunction('image_paths', update_image)
## Stop Item Info Pane ##
## Stop Item Select pane ##
## Stop Inventory Tab ##
# endregion
'''
----- Updates -----
'''
# region Updates
## Begin Update Tab ##
with app.tab('Updates'):
## Begin Item Update Selection Pane ##
with app.panedFrame('Item Updates'):
## Begin Data Obtaining Stage ##
xml_string = obtain_data()
## End Data Obtaining Stage ##
# Set the current names to be uploaded to the item names referenced by the changes needed
items_to_be_updated = item_names
# Try to load the previous cancelled updates
try:
s_f = shelve.open('save_data.jon')
cancelled_updates = s_f['cancelled_updates']
s_f.close()
# If there aren't any, create a dict for the cancelled updates
except:
# Python used Iterate!
for i in items_to_be_updated:
cancelled_updates[i] = False
## Item Selection Contents ##
#app.setSticky('news')
# Updated Items Tree
app.addTree('update_tree', xml_string)
app.setTreeColours('update_tree', 'black', 'white', 'blue', 'yellow')
app.setTreeEditable('update_tree', False)
# Add the tree-clicking functionality
app.setTreeDoubleClickFunction('update_tree', update_contents)
## Start Update Button Pane ##
with app.panedFrame('Update Buttons',vertical=True):
# Disable Update Button (singular)
app.addButton('Disable Update', disable_update, 0, 0, colspan=2)
# Refresh Updates Button
app.addButton('Refresh Updates', refresh_updates, 0, 2, colspan=2)
# Separator for visual clarity
app.addHorizontalSeparator(1, 0, 4, colour='lightblue')
# Submit/Update Store Button
app.addButton('Update Store', update_items, 2, 0, colspan=4)
## Start Other Update Information/Operations Pane ##
with app.panedFrame('Update Misc'):
# Label for clarity
app.addLabel('item_prompt', 'Comparing Item Data', 0, 0, colspan=2)
# Separator for clarity
app.addHorizontalSeparator(1, 0, 2, colour='lightblue')
# Start the Old Item Information
with app.labelFrame('Old Item', 2, 0):
# Display Old Name
app.addLabel('old_item_name', 'Name: ' + item_names[0], 0, 0)
# Separator for clarity
app.addHorizontalSeparator(1, 0, colour='lightblue')
# Display Old Description Label
app.addLabel('old_item_desc_label', 'Description:', 2, 0)
# Display Old Description
app.addMessage('old_item_desc',
remove_html(
old_items[item_names[0]]['description'].replace(' ', '')), 3, 0)
# Separator for clarity
app.addHorizontalSeparator(4, 0, colour='lightblue')
# Display Old Details Label
app.addLabel('old_item_dets_label', 'Details:', 5, 0)
# Display Old Details
app.addListBox('old_item_details', old_items[item_names[0]]['details'], 6, 0)
# Separator for clarity
app.addHorizontalSeparator(7, 0, colour='lightblue')
# Display Old Details Label
app.addLabel('old_item_colors_label', 'Colors:', 8, 0)
# Display Old Details
app.addListBox('old_item_colors', sorted(old_items[item_names[0]]['colors']), 9, 0)
# Stop Old Item Information
# Start New Item Information
with app.labelFrame('New Item:', 2, 1):
# Display New Item Name
app.addLabel('new_item_name', 'Name: ' + normalize(item_names[0]), 0, 0)
# Separator for clarity
app.addHorizontalSeparator(1, 0, colour='lightblue')
# Display New Description Label
app.addLabel('new_item_desc_label', 'Description:', 2, 0)
# Display New Description
app.addMessage('new_item_desc', jd[item_names[0]]['description'], 3, 0)
# Separator for clarity
app.addHorizontalSeparator(4, 0, colour='lightblue')
# Display New Details Label
app.addLabel('new_item_dets_label', 'Details:', 5, 0)
# Display New Details
app.addListBox('new_item_details',
[s.replace('\xa0', ' ').replace('\xe9', 'e') for s in
jd[item_names[0]]['details']], 6, 0)
# Separator for clarity
app.addHorizontalSeparator(7, 0, colour='lightblue')
# Display Old Details Label
app.addLabel('new_item_colors_label', 'Colors:', 8, 0)
# Display Old Details
app.addListBox('new_item_colors', sorted(jd[item_names[0]]['colors']), 9, 0)
# Stop New Item Information
## Stop Update Information/Operations Pane ##
## Stop Update Buttons Frame ##
## Stop Item Update Selection Pane ##
## End Update Tab ##
# endregion'''
'''
----- Inventory Management -----
'''
# region Inventory Management
## Begin Store Inventory Management Tab ##
with app.tab('Inventory Management'):
# region Data Setup
# Create an array to store items not currently online
not_online_items = []
# Create a list to store currently uploaded items
uploaded_items = []
# Set up dicts to hold the items sorted by origin store
origin_items_online = {}
origin_items_offline = {}
origin_items_uploaded = {}
origin_items_not_uploaded = {}
shown = []
# Set the origin to UpdateInfo's origins variable
# origins = u.origins
origins = interface.origins
# Get the list of items currently uploaded to the store
current_store_items = SpS.get_item_names()
try:
assert (len(current_store_items) > 0)
except:
print('No store items obtained')
# Python used Iterate!
for o in origins:
# Create the arrays within each dict
origin_items_online[o] = []
origin_items_offline[o] = []
origin_items_not_uploaded[o] = []
# Python used Iterate!
for i in jd:
# Populate the arrays
if i not in item_names:
# If it's actually online
if i in current_store_items or jd[i]['origin'] + ' ' + i in current_store_items:
# Append it to the total uploaded items
uploaded_items.append(jd[i]['origin'] + ' - ' + i)
# And to the dict of items by origin
try:
origin_items_offline[jd[i]['origin']].append(i)
except:
origin_items_offline[jd[i]['origin']] = []
origin_items_offline[jd[i]['origin']].append(i)
# If it's not online
else:
# Add it to the proper dict
not_online_items.append(jd[i]['origin'] + ' - ' + i)
# And to the dict sorted by origin
try:
origin_items_not_uploaded[jd[i]['origin']].append(i)
except:
origin_items_not_uploaded[jd[i]['origin']] = []
origin_items_not_uploaded[jd[i]['origin']].append(i)
# If it's uploaded and not hidden
else:
# Add it to the list of appended items
uploaded_items.append(jd[i]['origin'] + ' - ' + i)
shown.append(jd[i]['origin'] + ' - ' + i)
# And to the dict of uploaded items by origin
try:
origin_items_online[jd[i]['origin']].append(i)
except:
origin_items_online[jd[i]['origin']] = []
origin_items_online[jd[i]['origin']].append(i)
# Find all of the currently uploaded out of season items
out_of_season = []
# Python used Iterate!
for a in shown:
try:
if a.split(' - ')[1] not in list(jd.keys()):
out_of_season.append(a)
except:
if a not in list(jd.keys()):
out_of_season.append(a)
# Find all of the not currently uploaded in-season items
in_season = []
# Python used Iterate!
for j in jd:
if j not in all_items_uploaded:
in_season.append(j)
# Set the old item names (backup variable) to item names
old_item_names = item_names
# Variable to keep track of which row we're on
current_row = 0
# endregion
# Start the internal tabbed frame
with app.tabbedFrame('Inventory Management Tabs'):
# region Show/Hide Tab
with app.tab('Show/Hide Online Items'):
# Start the first Paned Frame
with app.panedFrame('In-Season Offline Item Boxes', sash=10):
# Start the scroll pane for this section
with app.scrollPane('In-Season Offline Item Scroll Pane'):
# Set the sticky value
app.setSticky('news')
# Python used Iterate!
for o in origins:
# Set the stretch and sticky attributes
# app.setStretch('row')
hname = 'Hidden ' + o.title() + ' Items:'
space = '' * int((60-len(hname))/2)
hname = space + hname + space
# Add the offline labels
app.label('offline_label_' + o.lower().replace(' ', '_'),
hname, current_row, 0)
# Set stretch
app.setStretch('both')
# Add the offline items ListBox
app.listbox('offline_items_' + o.lower().replace(' ', '_'),
sorted(origin_items_offline[o]),
current_row + 1, 0, drag=[begin_drag, end_drag])
# Add a horizontal separator if it's not the last box
if origins.index(o) != len(origins) - 1:
app.addHorizontalSeparator(current_row + 2, 0, colour="red")
# Increment the current row
current_row += 3
# Start the second Paned Frame for the transfer arrows
#app.setSticky('news')
with app.panedFrame('Arrows', vertical=True, width=app_w/2): #, sash=67, stretch='ew'
# Set sticky
app.setSticky('nesw')
# Add offline to online button label
app.label('offtoon', 'Show', 0, 0)
# Set sticky
app.setSticky('')
# Set padding inside the widget
app.setInPadding([30, 10])
# Add offline to online button
app.button('->', off_to_on, 1, 0)
# Set sticky
app.setSticky('s')
# Set padding
app.setInPadding([0, 0])
# Add online to offline move button label
app.label('ontooff', 'Hide', 2, 0)
# Set sticky
app.setSticky('')
# Set padding
app.setInPadding([30, 10]) #
# Add online to offline move button
app.button('<-', on_to_off, 3, 0)
# Set padding
app.setInPadding([0, 0])
# Set sticky to nothing
app.setSticky('')
# Set padding
app.setPadding([20, 20])
# app.setPadding([150, 0])
# Set inside padding
# app.setInPadding([10, 10])
app.setInPadding([30, 20])
# Add apply button
app.button('Apply', hide_unhide, 6, 0)
# Set padding
app.setInPadding([0, 0])
# Set sticky
app.setSticky('nesw')
# End Buttons Panel
# Add the list of out of season items if necessary
if len(out_of_season) != 0:
# Set the stretch to both
app.setStretch('both')
# Start the out of season items frame
app.startPanedFrame('Out of Season Items')
# Set the stretch
# app.setStretch('row')
# Add the label for the items
app.addLabel('oos', 'Out of Season Items:', 0, 0)
# Add the list
app.addListBox('oosi', sorted(out_of_season), 1, 0)
# Stop the out of season items frame
app.stopPanedFrame()
# Stop the arrow Paned Frame
# Reset the current row variable
current_row = 0
app.setPaneSashPosition(67, 'Arrows')
app.setSticky('nes')
# Start the Online Items Paned Frame
with app.panedFrame('Online Item Boxes'):
# Start the ScrollPane for this section
with app.scrollPane('In-Season Online Item Scroll Pane'):
# Reset the sticky
app.setSticky('news')
# Python used Iterate!
for o in origins:
# Set the stretch and sticky attributes
# app.setStretch('row')
sname = 'Shown/Live ' + o.title() + ' Items:'
space = '' * int((60-len(sname))/2)
sname = space + sname + space
# Add the online labels
app.label('online_label_' + o.lower().replace(' ', '_'),
sname, current_row)
# Set the stretch
app.setStretch('both')
# Add online items ListBox
app.listbox('online_items_' + o.lower().replace(' ', '_'),
sorted(origin_items_online[o]),
current_row + 1, drag=[begin_drag, end_drag])
# Add a horizontal separator if it's not the last box
if origins.index(o) != len(origins) - 1:
app.addHorizontalSeparator(current_row + 2, colour="red")
# Increment the current row
current_row += 3
# - Begin formatting ListBoxes -#
# Create variable to indicate alternating rows
alternate = True
# Python used Iterate!
for o in origins:
# Reset the alternating variable
alternate = True
# Python used Iterate!
for i in range(0, len(
app.getAllListItems('offline_items_' + o.lower().replace(' ', '_')))):
# If it's not the alternated row
if alternate:
# Do nothing
pass
# If it is
else:
# Reformat this row
app.setListItemAtPosBg('offline_items_' + o.lower().replace(' ', '_'),
i,
'#00ffff')
app.setListItemAtPosFg('offline_items_' + o.lower().replace(' ', '_'),
i,
'#000000')
# Toogle the alternating variable
alternate = not alternate
# Reset alternating variable
alternate = True
# Python used Iterate!
for o in origins:
# Rest the alternating variable
alternate = True
# Python used Iterate!
for i in range(0,
len(app.getAllListItems(
'online_items_' + o.lower().replace(' ', '_')))):
# If it's not the alternated row
if alternate:
# Do nothing
pass
# If it is
else:
# Reformat this row
app.setListItemAtPosBg('online_items_' + o.lower().replace(' ', '_'), i,
'#00ffff')
app.setListItemAtPosFg('online_items_' + o.lower().replace(' ', '_'), i,
'#000000')
# Toogle the alternating variable
alternate = not alternate
# - End formatting ListBoxes -#
# End ScrollPane
# End Online Panel
# End Offline Panel
# endregion
# region Upload Tab
# Start offline item management tab
with app.tab('Upload Offline Items'):
# Start the first Paned Frame
with app.panedFrame('Upload In-Season Offline Item Boxes', width=app_w / 3):
with app.scrollPane('Upload In-Season Offline Item Scroll', sticky='nesw'):
# Set the sticky value
app.setSticky('nesw')
# Python used Iterate!
for o in origins:
# Set the stretch and sticky attributes
# app.setStretch('row')
# Add the offline labels
app.label('upload_offline_label_' + o.lower().replace(' ', '_'),
'Offline ' + o.title() + ' Items:',
current_row, 0)
# Set stretch
app.setStretch('both')
# Add the offline items ListBox
app.listbox('upload_offline_items_' + o.lower().replace(' ', '_'),
sorted(origin_items_not_uploaded[o]),
current_row + 1, 0, drag=[begin_drag, end_drag])
# Add a horizontal separator if it's not the last box
if origins.index(o) != len(origins) - 1:
app.addHorizontalSeparator(current_row + 2, 0, colour="red")
# Increment the current row
current_row += 3
# End Scroll Pane
# Start the second Paned Frame for the transfer arrows
with app.panedFrame('Upload Arrows', width=app_w / 3, vertical=True):
# Set sticky
app.setSticky('s')
# Add offline to online button label
app.label('upload_offtoon', 'Upload', 0, 0)
# Set sticky
app.setSticky('')
# Set padding inside the widget
app.setInPadding([30, 10])
# Add offline to online button
app.addNamedButton('->', 'upload', off_to_on, 1, 0)
'''
# Set sticky
app.setSticky('s')
# Set padding
app.setInPadding([0, 0])
# Add online to offline move button label
app.addLabel('upload_ontooff', 'Remove from Store', 2, 0)
# Set sticky
app.setSticky('')
# Set padding
app.setInPadding([30, 10]) #
# Add online to offline move button
app.addNamedButton('<-', 'unupload', on_to_off, 3, 0)
#'''
# Set padding
app.setInPadding([0, 0])
# Set sticky
app.setSticky('nesw')
# Set padding
app.setPadding([20, 20])
# Set inside padding
app.setInPadding([30, 20])
app.setSticky('')
# Add apply button
app.addNamedButton('Apply', 'apply_upload', hide_unhide, 6, 0)
# Set padding
app.setInPadding([0, 0])
# Set sticky
app.setSticky('nesw')
# End Buttons Panel
# Add the list of out of season items if necessary
if len(out_of_season) != 0:
# Set the stretch to both
app.setStretch('both')
# Start the out of season items frame
app.startPanedFrame('Uploaded Out of Season Items')
# Set the stretch
# app.setStretch('row')
# Add the label for the items
app.addLabel('uoos', 'Out of Season Items:', 0, 0)
# Add the list
app.addListBox('uoosi', sorted(out_of_season), 1, 0)
# Stop the out of season items frame
app.stopPanedFrame()
# Stop the arrow Paned Frame
# Reset the current row variable
current_row = 0
# Start the Online Items Paned Frame
with app.panedFrame('Upload Online Item Boxes', width=app_w / 3):
# Start the scroll pane for these items
with app.scrollPane('Upload Online Item Boxes Scroll', sticky='nesw'):
# Reset the sticky
app.setSticky('nesw')
# Python used Iterate!
for o in origins:
# Set the stretch and sticky attributes
# app.setStretch('row')
# Add the online labels
app.label('upload_online_label_' + o.lower().replace(' ', '_'),
'Uploaded ' + o.title() + ' Items:',
current_row, 0)
# Set the stretch
app.setStretch('both')
# Create a temporary list to hold all uploaded items
temp_online = []
# Populate said list
for o0 in origin_items_online[o]:
temp_online.append(o0)
for o0 in origin_items_offline[o]:
temp_online.append(o0)
# Add online items ListBox
app.listbox('upload_online_items_' + o.lower().replace(' ', '_'),
sorted(temp_online),
current_row + 1, 0, drag=[begin_drag, end_drag])
# Add a horizontal separator if it's not the last box
if origins.index(o) != len(origins) - 1:
app.addHorizontalSeparator(current_row + 2, 0, colour="red")
# Increment the current row
current_row += 3
# - Begin formatting ListBoxes -#
# Create variable to indicate alternating rows
alternate = True
# Python used Iterate!
for o in origins:
# Reset the alternating variable
alternate = True
# Python used Iterate!
for i in range(0, len(
app.getAllListItems(
'upload_offline_items_' + o.lower().replace(' ', '_')))):
# If it's not the alternated row
if alternate:
# Do nothing
pass
# If it is
else:
# Reformat this row
app.setListItemAtPosBg(
'upload_offline_items_' + o.lower().replace(' ', '_'), i,
'#00ffff')
app.setListItemAtPosFg(
'upload_offline_items_' + o.lower().replace(' ', '_'), i,
'#000000')
# Toogle the alternating variable
alternate = not alternate
# Reset alternating variable
alternate = True
# Python used Iterate!
for o in origins:
# Rest the alternating variable
alternate = True
# Python used Iterate!
for i in range(0, len(
app.getAllListItems(
'upload_online_items_' + o.lower().replace(' ', '_')))):
# If it's not the alternated row
if alternate:
# Do nothing
pass
# If it is
else:
# Reformat this row
app.setListItemAtPosBg(
'upload_online_items_' + o.lower().replace(' ', '_'), i,
'#00ffff')
app.setListItemAtPosFg(
'upload_online_items_' + o.lower().replace(' ', '_'), i,
'#000000')
# Toogle the alternating variable
alternate = not alternate
# - End formatting ListBoxes -#
# End Scroll Pane
# End Online Panel
# End Offline Panel
# End offline item management tab
# endregion
# region Stock Management tab
# Start stock management tab
with app.tab('Stock Management'):
#app.setSticky('NESW')
#app.setStretch('BOTH')
# Start the Scroll Pane for the items
with app.scrollPane('Item Stock Management'):
#app.setScrollPaneWidth('Item Stock Management', app_w)
# Create the function for stock handling
def change_in_stock(button):
# Get the name of the entry to change
entry_name = 'stock_' + '_'.join(button.split('_')[1:])
# If its a minus button
if 'minus_' in button:
# Decrement the value
app.setEntry(entry_name, int(app.getEntry(entry_name)) - 1 if int(
app.getEntry(entry_name)) >= -1 else int(
app.getEntry(entry_name)))
# Otherwise
else:
# Increment the value
app.setEntry(entry_name, int(app.getEntry(entry_name)) + 1)
# Modify the value of the stock that's been changed
try:
actual_stocks[' '.join(entry_name.split('_')[1:-1])][
entry_name.replace('_', ' ').split(' ')[-1]] = app.getEntry(entry_name)
except:
actual_stocks[' '.join(entry_name.split('_')[1:])] = app.getEntry(entry_name)
# Get the stocks by name
items_stock_dict = SpS.get_item_names(get_stocks=True)
# Grab the actual item stocks from the new method
actual_stocks = SpS.get_item_stocks()
initial_stock_values = copy.deepcopy(actual_stocks)
# For every uploaded item (since they're the only ones relevant)
for k in sorted(list(actual_stocks.keys())):
# Create the value for the total stock for this item
current_item_stock = -1
# Loop through the items list
for k2 in items_stock_dict:
# And find the stock value for the current item
try:
if k.split(' - ')[1] in k2 or (' - ' not in k and k in k2):
current_item_stock = items_stock_dict[k]
break
except:
current_item_stock = -1
sil = (170-len(k)) / 2
empty_string = ' ' * int(sil)
# Create the toggle frame for this item
with app.toggleFrame(empty_string + k + ':' + empty_string, list(actual_stocks.keys()).index(k), width=app_w):
try:
current_row = 0
for k2 in actual_stocks[k].keys():
# Add the label for that item
app.label(
'stock_' + k.replace(' ', '_') + '_' + k2.replace(' ', '_'),
k + ' ' + k2 + ':', current_row, 0)
# Add the minus button
app.addNamedButton('-', 'minus_' + k.replace(' ', '_') + '_' +
k2.replace(' ', '_'), change_in_stock, current_row, 1)
# Add and set the stock entry
app.entry('stock_' + k.replace(' ', '_') + '_' + k2.replace(' ', '_'),
pos=(current_row, 2), kind="numeric")
try:
app.setEntry(
'stock_' + k.replace(' ', '_') + '_' + k2.replace(' ', '_'),
int(actual_stocks[k][k2]))
except:
try:
app.setEntry(
'stock_' + k.replace(' ', '_') + '_' + k2.replace(' ', '_'),
int(actual_stocks[k]))
except:
app.setEntry(
'stock_' + k.replace(' ', '_') + '_' + k2.replace(' ', '_'), -1)
# Add the plus button
app.addNamedButton('+', 'plus_' + k.replace(' ', '_') + '_' +
k2.replace(' ', '_'), change_in_stock, current_row, 3)
current_row += 1
except:
try:
# Add the label for that item
app.label(
'stock_' + k.replace(' ', '_'), k + ':', 0, 0)
# Add the minus button
app.addNamedButton('-', 'minus_' + k.replace(' ', '_'), change_in_stock, 0,
1)
# Add and set the stock entry
app.entry('stock_' + k.replace(' ', '_'), pos=(0, 2), kind="numeric")
app.setEntry('stock_' + k.replace(' ', '_'), int(actual_stocks[k]))
# Add the plus button
app.addNamedButton('+', 'plus_' + k.replace(' ', '_'), change_in_stock, 0,
3)
except:
# Add the label for that item
app.label('stock_' + k.replace(' ', '_'), k + ':', 0, 0)
# Add the minus button
app.addNamedButton('-', 'minus_' + k.replace(' ', '_'), change_in_stock, 0,
1)
# Add and set the stock entry
app.entry('stock_' + k.replace(' ', '_'), pos=(0, 2), kind="numeric")
try:
app.setEntry('stock_' + k.replace(' ', '_'), int(current_item_stock))
except:
app.setEntry('stock_' + k.replace(' ', '_'), -1)
# Add the plus button
app.addNamedButton('+', 'plus_' + k.replace(' ', '_'), change_in_stock, 0,
3)
# End current toggle pane
# End the scroll pane
# Start the frame for the apply button
with app.frame('Apply Stock', height=150):
app.setSticky('')
app.setInPadding(['30', '15'])
# Add the apply button
app.addNamedButton('Apply', 'apply_stock', stock_changes)
app.setSticky('nesw')
# End the frame
# End stock management tab
# endregion
# End internal tab frame
## End Store Inventory Management Tab ##
# endregion
'''
----- Adding/Editing -----
'''
# region Adding/Editing
## Begin Item Creation Tab ##
with app.tab('Create New/Edit Item'):
# - Start the Item Select Pane -#
with app.panedFrame('Item_Select'):
# Item Select Tree #
app.tree('item_edit_tree', tree_xml)
# Tree Properties #
app.setTreeColours('item_edit_tree', 'black', 'white', 'blue', 'yellow')
app.setTreeEditable('item_edit_tree', False)
# Add the tree-clicking functionality
app.setTreeDoubleClickFunction('item_edit_tree', edit_contents)
def generate_sku():
from faker import Faker
fake = Faker()
app.setEntry('new_item_sku', str(fake.random_int(1000, 10000)))
# Start the Item Information Entry #
with app.panedFrame('Item_Edit', vertical=True):
app.setSticky('')
# Start the Buttons Paned Frame #
with app.frame('Buttons'):
# Start a New Item #
app.button('Start New Item', start_new, 0, 0)
# Save the Current Item #
app.button('Save Changes', save_new, 0, 1)
# Save and upload the item#
# app.addButton('Save and Upload', save_new, 0, 2)
# End Button Pane #
app.setSticky('nesw')
with app.panedFrame('ItemEditPane'):
# Start the scroll pane for the edit items thing
with app.scrollPane('ItemEditScroll', sticky='nesw'):
# Name
app.label('new_item_name_label', "Item Name:", pos=(0, 0))
app.entry('new_item_name', pos=(0, 1))
# Price
app.label('new_item_price_label', 'Item Price:', pos=(0, 2))
app.entry('new_item_price', pos=(0, 3))
# SKU
app.button('Item SKU:', generate_sku, pos=(1, 0))
app.entry('new_item_sku', pos=(1, 1))
# Color SKUs
app.label('new_item_skus_label', 'Color SKUs:', pos=(1, 2))
app.addScrolledTextArea('new_item_skus', 1, 3)
# Origin
app.label('new_item_origin_label', 'Item Origin Store:', pos=(2, 0))
app.entry('new_item_origin', pos=(2, 1))
# Images
app.label('new_item_images_label', 'Item Images:', pos=(2, 2))
app.addFileEntry('new_item_images', 2, 3)
# Description
app.label('new_item_description_label', 'Item Description:', pos=(3, 0), colspan=2)
app.addScrolledTextArea('new_item_description', 3, 2, colspan=2)
# Details
app.label('new_item_details_label', 'Item Details:', pos=(4, 0), colspan=2)
app.addScrolledTextArea('new_item_details', 4, 2, colspan=2)
# Colors
app.label('new_item_colors_label_new', 'Item Colors:', pos=(5, 0), colspan=2)
app.addScrolledTextArea('new_item_colors_new', 5, 2, colspan=2)
# Sizes
app.label('new_item_sizes_label', 'Item Sizes:', pos=(6, 0), rowspan=2, colspan=2)
# Button to choose size type
app.addRadioButton('size_type', 'Basic', 6, 2)
app.addRadioButton('size_type', 'Advanced', 7, 2)
app.setRadioButtonChangeFunction('size_type', size_option)
# Create a frame for Advanced sizes
with app.frame('Advanced Sizes', 6, 3, colspan=2, rowspan=2):
app.addScrolledTextArea('advanced_item_size_one', 0, 0)
app.addScrolledTextArea('advanced_item_size_two', 0, 1)
# Stop Advanced Size frame
# And a frame for Basic sizes
with app.frame('Basic Sizes', 6, 3, colspan=2, rowspan=2):
app.addScrolledTextArea('new_item_sizes')
# Stop Basic Size frame
# Stop the Scroll Pane
# End Entry Pane #
# End Tree Pane #
## End Item Creation Tab ##
# endregion
## End Tabbed Frame ##
# endregion
# Check for the appropriate file again
if date_str in glob.glob('*.json'):
# Hide the startup sub-window
app.hideSubWindow('start_wait_indicator')
else:
app.hideSubWindow('json_update_indicator')
# Set what happend after the app closes
app.setStopFunction(stop_application)
# Start the app
start_application(None)
app.go()
# loading_thread.join()
# u_b.quit()
# Thread to get all product names once the application starts
class ItemNameThread(threading.Thread):
# Runs the algorithm
def run(self):
global start_progress, all_items_uploaded, app, u, u_b
# Start the process by getting the first url
# u_b.get(u.urls[0])
if u_b.current_url != interface.urls[0]:
u_b.visit(interface.urls[0])
# Make sure to be logged in as well, won't do any good if it's not
# if not u.logged_in: #
if not interface.logged_in:
interface.login()
# u.login()
# Python used Rest!
slp(3)
# Save the current url
cu = u_b.current_url
# For every url
for url in interface.urls: # for url in u.urls:
# Create a temporary array of items to use
temp_items = []
if u_b.current_url != url:
# Go to the current url
u_b.get(url)
# Python used Rest!
slp(3)
# Grab the item names from this page
# temp_items = u.get_item_names()
temp_items = SpS.get_item_names()
# Append each item found to the total item array
for t in temp_items:
if t not in all_items_uploaded:
all_items_uploaded.append(t)
# start_progress += (100.0 / float(len(u.urls)))
start_progress += (100.0 / float(len(interface.urls)))
app.setStatusbar('Startup is {:.2f}'.format(start_progress) + '% Complete')
print(str(start_progress) + '% Complete')
# Return to the original url, just to be safe
# u_b.get(cu)
if usb.url != cu:
usb.visit(cu)
# all_items_uploaded = u.get_all_product_names()
# app.hideSubWindow('Start Application')
# Thread to apply the updates needed
class ItemUpdatesThread(threading.Thread):
# Thread running method
def run(self):
global app, items_to_be_updated, updates_needed, \
upload_these, all_items_uploaded, u, jd, start_progress
# Create a dict to the put the items that need updating in
item_updates = {}
# Loop through the list of items
for i in items_to_be_updated:
# And append that item to the dict
try:
item_updates.setdefault(i, updates_needed[i])
except:
pass
# Comment to put break_point on
break_point = None
# Update the status bar as appropriate
app.setStatusbar('Store Update in Progress...')
print('Store Update in Progress...')
# Then add items as necessary
# u.addNewItemsGivenWithData(upload_these, jd)
interface.add_new_items(upload_these, jd)
for i in upload_these:
all_items_uploaded.append(i)
# And update the store appropriately
interface.update_store(item_updates, jd)
# Hide and unhide all appropriate items
hide_unhide(None)
# Update the status bar as appropriate
app.setStatusbar('Store Update is complete')
app.hideSubWindow('apply_wait_indicator')
# Thread to run stock updates without killing the app thread
class ItemStockUpdatesThread(threading.Thread):
# Thread running method
def run(self):
# Grab the globals necessary
global actual_stocks, initial_stock_values, app
# Show the wait window
# app.showSubWindow('apply_wait_indicator')
# Change the sub-window's wait message
app.setLabel('apply_loading_message', SyS.get_loading_message())
# Create the dict to send with new information about the products
updates_required = {}
# Create a variable to potentially hold a list of items to convert to unlimited items
make_limited = None
# First, find out what needs to be updated in terms of stocks
for k in actual_stocks:
# If there's no size values for this item
if type(actual_stocks) is not dict:
# Then directly check if they're equal
if actual_stocks[k] != initial_stock_values[k]:
updates_required[k] = actual_stocks[k]
# If there are size values
else:
# Loop through each one
for k2 in actual_stocks[k]:
# If any of them aren't equal
if actual_stocks[k][k2] != initial_stock_values[k][k2]:
# Mark it for updating
updates_required[k] = actual_stocks[k]
# If this item is becoming unlimited
if actual_stocks[k][k2] != -1 and initial_stock_values[k][k2] == -1:
# Create the actual list to send over if it's not already made
if make_limited is None:
make_limited = []
# Append this key to the list
make_limited.append(k)
# Call the appropriate method from the interface
interface.updateStock(updates_required, now_limited=make_limited)
# Hide the app wait window
# app.hideSubWindow('apply_wait_indicator')
def main():
# Check the overall program for updates
# check_for_updates()
# Import the classes to do some cleanup
import datetime, glob
# Get the current date
current = datetime.datetime.now()
# And save each field
day = current.strftime('%d')
month = current.strftime('%m')
year = current.strftime('%y')
# Find all of the logs for this month
this_months_logs = glob.glob(month + '_*_' + year + '*.log')
# Create the string for the last month
lm = str(int(month) - 1)
if int(lm) < 10:
lm = '0' + lm
if int(lm) == 0:
lm = '12'
# Create the string for the current month
cm = str(int(month))
if int(cm) < 10:
cm = '0' + cm
# Get the last month's logs
last_months_logs = glob.glob(lm + '_*_' + year + '*.log')
# If there are remaining logs from last month
if len(last_months_logs) > 1:
# Add all except one to this months (for legacy checking)
for i in range(len(last_months_logs) - 1):
this_months_logs.append(last_months_logs[i])
# Find all of the jsons for this month
this_months_jsons = glob.glob('items' + cm + '*' + year + '.json')
# And the remaining jsons from last month
last_months_jsons = glob.glob(
'items' + lm + '*' + year + '*.log')
# If there are remaining jsons from last month
if len(last_months_jsons) > 1:
# Add all except one to this months (for legacy checking)
for i in range(len(last_months_jsons) - 1):
this_months_jsons.append(last_months_jsons[i])
# If there are sufficient logs and jsons to warrant deleting
if len(this_months_jsons) > 7 and int(day) > 27:
# For all except the latest log
for i in range(len(this_months_logs) - 1):
# Delete the log
os.remove(this_months_logs[i])
# For all except the latest json
for i in range(len(this_months_jsons) - 1):
# Delete the json
os.remove(this_months_jsons[i])
# If this goes through, save the current work directory
current_dir = os.getcwd()
# Navigate to the logs for the scrapers
os.chdir('C://Users//Shipping//Documents//LiClipse Workspace//ScrapyDerp//LSL_Scrapers//scrapers//scrapers')
# Find the scraper logs
scraper_logs = glob.glob(month + '*.log')
# Loop through all but one for this month
for i in range(len(scraper_logs) - 1):
os.remove(scraper_logs[i])
# Then move back to the original directory
os.chdir(current_dir)
# Create the start thread
# start_thread = threading.Thread(target=start_window())
# start_thread.start()
# Start the GUI
start_window()
# Lets this program run by itself
if __name__ == '__main__':
main()PK {N&{iB B squp/LICENSE.txtMozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.PK v
Ogcv v squp/MANIFEST.ininclude README.md
include chromedriver.exe
include companyemail.txt
include example_items.json
include LICENSE.txtPK :n
OqkB B squp/README.md
SquarespaceUploader
===================
Takes data from a json file (formatted as indicated) and uploads the item to Squarespace
Language: Python 3.7
This program, as the description states, takes a specifically formatted python file database and uploads each entry to the Squarespace interface through the selenium library with Python. As of right now, its most useful purpose is defining the interface for entering data on Squarespace, and using that to input data. An example python file for the data is included as items.json
This program has been designed to do the following:
- Take the data from the json and use it directly
- Waits for inputs to appear or dialogs to dissappear before entering data
- Has the capability to upload text and images stored on the local machine
- Puts functionality for managing orders, inventory, and item updates into an easy to use GUI interface that can be used outside of the browser
## Current Features:
- Updates existing items if and only if new information is found
- Adds new items if and only if they are not already in the database
- Moves items onto the store page/into storage based on the season (since this is meant for a clothing store)
- Item capability: Nearly all Men's clothing types, including pants, shirts, blazers, shorts, etc.
## Usage:
1. To start, download this repository, and make sure that your proper login information and store page(s) information is put into information.py
2. Next, create your database file for accessing, see example_items.json for formatting details
3. Finally, if you need to add new fields, modify the interface.py file between the necessary comments to rename the fields to what you need them to be
*Note: This program was originally designed to upload clothing items, so some fields may not be applicable to your item(s) or store(s).*
*I've documented the code as well as I can, and have put small notes where personal modification would be necessary if your store is not currently compatible with this layout.*
*This includes the automated email sending, see companyemail.txt for the base template I used and make changes as needed.*
*If you have any questions or would like some better explanation/assistance on the process of adapting this program to your store, feel free to contact me either through GitHub or through my email.*
PK sNM3Z 3Z squp/SplinterShortcuts.py# -*- coding: utf-8 -*-
'''
SplinterShortcuts.py
Provides all of the shortcut methods used throughout the classes of the Squarespace Uploader (interface.py) program
'''
# ---------------------- BEGIN IMPORTS ----------------------
import re
from splinter import Browser
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
import SystemShortcuts as SyS
from information import Info
# ----------------------- END IMPORTS -----------------------
# -------------- BEGIN VARIABLE DECLARATION -----------------
# region Globals
# Declare the browser that will be used, set as nothing until initialized
browser = None
# Method that will set the browser that will be known globally throughout this class
def set_browser(b):
global browser
browser = b
# Declare the browser that will be used, set as nothing until initialized
logged_in = False
# Method that will set the browser that will be known globally throughout this class
def set_logged_in(l):
global logged_in
loggedIn = l
# endregion
# --------------- END VARIABLE DECLARATION ------------------
# ---------------- BEGIN SHORTCUT METHODS -------------------
# region Shortcuts
### Literal Keyboard Shortcut methods
# Sends the CTRL + A shortcut to the designated element
def send_select_all(element):
global browser
selectAll = ActionChains(browser.driver)
selectAll.send_keys(Keys.CONTROL + 'a')
element.click()
selectAll.perform()
# Sends the CTRL + A shortcut to the designated element, and then hits backspace to delete everything
def select_all_and_erase(element):
global browser
# Select everything
# element.click()
selectAll = ActionChains(browser.driver)
selectAll.send_keys(Keys.CONTROL + 'a')
selectAll.perform()
# Delete everything
try:
# Try using the splinter implementation
element.type(Keys.BACK_SPACE)
# It wasn't very effective...
except:
# If that doesn't work, use the selenium implementation, or nothing at all
try:
element.send_keys(Keys.BACK_SPACE)
# It wasn't very effective
except:
pass
# Saves and closes the current element
def save_and_close():
move_and_click(css('input.saveAndClose'))
# Python used Rest!
SyS.slp(1.5)
# Runs a script inside the browser, given as a string (kept camelCase to avoid possible conflicts)
def runScript(script, element):
global browser
browser.driver.execute_script(script, element)
# Simple log in method for when the browser is already on the login page
def simple_login():
global browser
browser.fill('email', Info.LOGIN)
browser.fill('password', Info.PWD)
browser.find_by_text('Log In').click()
# Log in if the browser is not yet at the login page
def complete_login():
global browser
browser.visit(Info.STORE_PAGE)
simple_login()
# Method to find and return all names of the items on the current page (kept camelCase to avoid possible conflicts)
def get_item_names(get_stocks=False):
global browser
### Name Parsing ###
# Keeping the section following this one for now, but only for backup purposes
# First, we will obtain the name of each product currently on the store
productItemCSS = 'div.yui3-widget.sqs-widget.sqs-content-item-image.sqs-content-item-product.yui3-dd-drop div.title'
productItemStockCSS = 'div.yui3-widget.sqs-widget.sqs-content-item-image.sqs-content-item-product.yui3-dd-drop div.stock'
productNames = None
product_stocks = None
if get_stocks:
productNames = {}
product_stocks = csss_b(productItemStockCSS)
else:
productNames = []
products = csss_b(productItemCSS)
while len(products) == 0:
SyS.slp(.5)
products = csss_b(productItemCSS)
# Obtain each product's name to use for a reference point
for pi in range(len(products)):
p = products[pi]
# Try to get the name without scrolling first
try:
# Executing script
runScript("return arguments[0].scrollIntoView();", p)
WebDriverWait(browser, 3).until(EC.visibility_of_element_located(p))
if product_stocks == None:
productNames.append(p.text)
else:
productNames[p.text] = product_stocks[pi].text.split(' ')[0]
# If that doesn't work, throw a script onto the page to scroll that name into view
except:
# Try executing the script without having to re-declare anything
try:
# Executing script
runScript("return arguments[0].scrollIntoView();", p)
# While any of the texts are blank, keep on re-creating the objects
while p.text == '':
products2 = csss_b(productItemCSS)
p = products2[pi]
# Append the found text
if product_stocks == None:
productNames.append(p.text)
else:
productNames[p.text] = product_stocks[pi].text.split(' ')[0]
continue
# If the product has gone stale, re-create the currently referenced element and go again
except:
# Re-creating element
products2 = csss_b(productItemCSS)
p2 = products2[pi]
# Executing script
runScript("return arguments[0].scrollIntoView();", p2)
# While any of the texts are blank, keep on re-creating the objects
# while p.text == '':
# products2 = csss_b(productItemCSS)
# p = products2[current]
# Append the found text
if product_stocks == None:
productNames.append(p2.text.replace('Southern Tide ', '').replace('Copy of ', ''))
else:
try:
productNames[p2.text] = product_stocks[pi].text.split(' ')[0]
except:
products = csss_b(productItemCSS)
p = products[pi]
try:
productNames[p.text] = product_stocks[pi].text.split(' ')[0]
except:
try:
product_stocks = csss_b(productItemStockCSS)
productNames[p.text] = product_stocks[pi].text.split(' ')[0]
except:
products = csss_b(productItemCSS)
product_stocks = csss_b(productItemStockCSS)
p = products[pi]
productNames[p.text] = product_stocks[pi].text.split(' ')[0]
# productNames.append(p.text.replace('Southern Tide ', '').replace('Copy of ', ''))
# Increment the current value
continue
# Remove any and all empty strings obtained from the above
if type(productNames) is list:
for n in productNames:
if len(re.findall('[^ ]+', n)) == 0:
productNames.remove(n)
return productNames
# Gets detailed item stocks from all relevant items
def get_item_stocks():
# Grab the global browser
global browser
# Save the CSS value of the overall stocks of items first
productItemStockCSS = 'div.yui3-widget.sqs-widget.sqs-content-item-image.sqs-content-item-product.yui3-dd-drop div.stock'
# Save the CSS value of the names of each item
productItemCSS = 'div.yui3-widget.sqs-widget.sqs-content-item-image.sqs-content-item-product.yui3-dd-drop div.title'
# Create a dict to store the item's individual stocks in
product_stocks = {}
# Save where each product is
products = csss_b(productItemCSS)
# List to store the overall stocks in
product_overall_stocks = csss_b(productItemStockCSS)
repeats = len(products)
# Loop through every product
for i in range(repeats):
# If the stock is unlimited
if product_overall_stocks[i].text == '∞':
# Set the overall stock to -1 to indicate such
product_stocks[products[i].text] = -1
# print(products[i].text)
# Otherwise
else:
# Get the current name/key
ckey = products[i].text
# print(products[i].text)
product_stocks[ckey] = {}
# Open the item
try:
WebDriverWait(browser, 3).until(EC.visibility_of_element_located(products[i]))
select_item(products[i])
except:
# Executing script
runScript("return arguments[0].scrollIntoView();", products[i])
# Python used Rest!
SyS.slp(1)
# Try to select it again
select_item(item_name(products[i].text))
# Get the variants tab
variants_tab = []
# Create a repetitions count var
current = 0
# While either there are still repetitions to go or the tab hasn't been found yet
while len(variants_tab) == 0 and current < 60:
# Try getting the tab
try:
variants_tab = xpath('//div[contains(text(), "Variants")]')
# Otherwise just forget about it
except:
pass
# Python used Rest!
SyS.slp(.1)
# Increment the repetitions counter
current += 1
# Python used Pound!
move_and_click(variants_tab)
# Python used Rest!
SyS.slp(.5)
# Grab all of the rows of variants
rows = xpath('//div[@class="attributes"]')
# Check to see if the complex Size fields exist
# (checked first because if not it will give a false basic positive too)
complex_size = len(xpath('//input[@name="Size W"]')) > 0
# Check to see if the basic Size field exists
basic_size = len(xpath('//input[@name="Size"]')) > 0
# Check to see if the Color field exists
color_exists = len(xpath('//input[@name="Color"]')) > 0
# If there's more than one row to loop through
if len(rows) > 1:
# Loop through every row
for j in range(len(rows)):
# If there's not a complex size, there is a basic size, and there are colors
if not complex_size and basic_size and color_exists:
# Size value
sv = xpath('//input[@name="Size"]')[j].value
# Color value
cv = xpath('//input[@name="Color"]')[j].value
# Create the key for the dict
key = cv + '-' + sv
# Get the combo's stock
stv = xpath('//div[@class="sqs-stock-input-content"]')[j].value
# Save the stock value to the given key
product_stocks[ckey][key] = stv
# If there's not a complex size, there is a basic size, and there are no colors
elif not complex_size and basic_size and not color_exists:
# Size value
key = xpath('//input[@name="Size"]')[j].value
# Grab the current rows stock input
stv = xpath('//div[@class="sqs-stock-input-content"]')[j].value
# Set the current size's stock value
product_stocks[ckey][key] = stv
# If there is a complex size and there are no colors
elif complex_size and not color_exists:
# Get the first size key
s1 = xpath('//input[@name="Size W"]')[j].value
# Get the second size key
s2 = xpath('//input[@name="Size L"]')[j].value
# Construct the final key
key = s1 + 'x' + s2
# Grab the current rows stock input
stv = xpath('//div[@class="sqs-stock-input-content"]')[j].value
# Set the current size's stock value
product_stocks[ckey][key] = stv
elif complex_size and color_exists:
# Get the first size key
s1 = xpath('//input[@name="Size W"]')[j].value
# Get the second size key
s2 = xpath('//input[@name="Size L"]')[j].value
# Get the Color value
cv = xpath('//input[@name="Color"]')[j].value
# Construct the final key
key = s1 + 'x' + s2 + '-' + cv
# Grab the current rows stock input
stv = xpath('//div[@class="sqs-stock-input-content"]')[j].value
# Set the current size's stock value
product_stocks[ckey][key] = stv
# If there's only one thing here
elif len(rows) == 1:
# Just set the overall stock to the row found
product_stocks[ckey] = xpath('//div[@class="sqs-stock-input-content"]')[0].value
# Python used Pound!
xpath('//a[@class="cancel"]')[0].click()
# Python used Rest!
SyS.slp2()
# Check to see if the extra pop-up appeared
if len(txt('Discard')) > 0:
# Python used Pound!
txt('Discard')[0].click()
# Python used Rest!
SyS.slp2()
# Save where each product is
products = csss_b(productItemCSS)
# Return the final stocks
return product_stocks
# endregion
# ----------------- END SHORTCUT METHODS --------------------
# ----------------- BEGIN SELECTION METHODS -----------------
# region Selection
### CSS Selector: Document Base ###
# Methods for easy execution of finding an element by CSS selector
# With these retrieval methods, a lowercase means no base (document-based), while a capital means element-based
# Document Base
# Known that only one css element will be returned
def css(css_selector):
global browser
return browser.find_by_css(css_selector)
# Multiple css elements will be returned
def csss(csss_selector):
global browser
return browser.find_by_css(csss_selector)
# Uses selenium to get the css elements
def csss_b(csss_sel):
global browser
return browser.driver.find_elements_by_css_selector(csss_sel)
# Finds element based on name
def name(name_selector):
global browser
return browser.find_by_name(name_selector)
# Finds element based on class name
def clas(clas_selector):
global browser
return browser.driver.find_element_by_class_name(clas_selector)
# Finds link based on part of the link text
def partLink(linkpart):
global browser
return browser.find_link_by_partial_text(linkpart)
# Finds element by text value
def txt(txt_in):
global browser
return browser.find_by_text(txt_in)
# Finds element by value property
def val(value):
global browser
return browser.find_by_value(value)
### CSS Selector: Element Base ###
# Specific Bases
# Known that only one css element will be returned
def Css(base, css_selector):
try:
return base.find_by_css(css_selector)
except:
try:
return base.first.find_by_css(css_selector)
except:
return base.find_element_by_css_selector(css_selector)
# Multiple css elements will be returned
def Csss(base, csss_selector):
try:
return base.find_by_css(csss_selector)
except:
try:
return base.first.find_by_css(csss_selector)
except:
return base.find_elements_by_css_selector(csss_selector)
# Finds element based on name
def Name(base, name_selector):
try:
return base.find_by_name(name_selector)
except:
try:
return base.first.find_by_name(name_selector)
except:
return base.find_element_by_name(name_selector)
# Finds element based on class name
def Clas(base, clas_selector):
try:
return base.find_by_css('.' + clas_selector)
except:
try:
return base.first.find_by_css('.' + clas_selector)
except:
return base.find_element_by_class_name(clas_selector)
### XPath Selector: Document Base ###
# Methods for easy execution of finding an element by XPATH selector
# Document base
# Returns the single result of the query
def xpath(xpath_selector):
global browser
return browser.find_by_xpath(xpath_selector)
# Returns all results of the query
def xpaths(xpath_selector):
global browser
return browser.find_by_xpath(xpath_selector)
### XPath Selector: Element Base ###
# Specific Bases
# Returns the single result of the query
def Xpath(base, xpath_selector):
try:
return base.find_by_xpath(xpath_selector)
except:
try:
return base.first.find_by_xpath(xpath_selector)
except:
try:
return base.find_element_by_xpath(xpath_selector)
except:
return base.find_element_by_xpath('.' + (xpath_selector))
# Returns all results of the query
def Xpaths(base, xpath_selector):
try:
return base.find_by_xpath(xpath_selector)
except:
try:
return base.first.find_by_xpath(xpath_selector)
except:
return base.find_element_by_xpath('.' + (xpath_selector))
### More specific, this method finds an element based on an item name, not the element name itself ###
### In other words, the input here is plain English, not a query ###
# Returns an item based on that item's name
def item_name(iname):
return xpath('//div[contains(text(), "' + iname + '")]/../..')
# endregion
# ------------------ END SELECTION METHODS ------------------
# ----------------- BEGIN MOVEMENT METHODS ------------------
# region Movement
# Moves the cursor to the specified element
def move_to(element):
try:
element.mouse_over()
except:
try:
element[0].mouse_over()
except:
pass
# Moves the cursor to the specified element and clicks it
def move_and_click(element):
try:
element.mouse_over()
SyS.slp(.5)
element.click()
except:
try:
element[0].mouse_over()
element[0].click()
except:
print(element, ' Failed')
# Moves the cursor to the specified element and clicks it
def move_and_double_click(element):
try:
element.mouse_over()
SyS.slp(.5)
element.double_click()
except:
try:
element[0].mouse_over()
element[0].double_click()
except:
pass
# Easier form of calling the above
def select_item(element):
try:
element.mouse_over()
SyS.slp(.5)
element.double_click()
except:
try:
element[0].mouse_over()
element[0].double_click()
except:
pass
# endregion
# ------------------ END MOVEMENT METHODS -------------------
# ------------------- BEGIN CHECK METHODS -------------------
# region Check
# Returns whether an elements is visible by checking xpath
def epx(x):
global browser
return browser.is_element_present_by_xpath(x)
# endregion
# -------------------- END CHECK METHODS --------------------
# ------------------- BEGIN UPDATE METHODS -------------------
# Updates chromedriver.exe in case of Browser startup failure
def update_driver():
# Create a temporary browser using Firefox
tb = Browser()
# Note that this does not make use of splinter since it can't
base_url = 'https://chromedriver.storage.googleapis.com/index.html'
# Navigate to the url where the chromedrivers are
tb.visit(base_url)
# Grab all the links here
links = tb.find_by_xpath('//tr/td/a')
# Find the folder that goes after the last relevant one
current_link = ''
for i in range(len(links)):
try:
if links[i].html == 'icons':
current_link = links[i-1].outer_html
break
except:
pass
# Parse the url out of this
current_link = current_link.split('"')[1]
tb.visit(base_url + current_link)
# Click the driver to download it
tb.find_by_xpath('//a[contains(text(), "win32")]')[0].click()
# Close the browser, we're done with it
tb.quit()
# Import the libraries necessary for the following code
import os, glob, zipfile, shutil
# Save the current directory to easily get back to it
od = os.getcwd()
# Navigate to the Downloads folder
os.chdir(Info.DOWNLOADS_DIR)
# Find all of the relevant zip files located in this folder
zips = glob.glob('chromedriver*.zip')
# Save the first found as a default latest file and time created
latest = zips[0]
last_date = os.path.getctime(latest)
# Loop through the zips and find the actual latest files
for i in range(1, len(zips)):
if os.path.getctime(zips[i]) > last_date:
last_date = os.path.getctime(zips[i])
latest = zips[i]
# Unzip the latest downloaded driver file
with zipfile.ZipFile(latest) as z:
z.extractall(latest.split('.')[0])
# Navigate into this folder
os.chdir(latest.split('.')[0])
# Copy this into each of the relevant folder as necessary
for d in Info.PYTHON_DIRS:
if os.path.getctime(d + '//chromedriver.exe') < last_date:
try:
shutil.copy('chromedriver.exe', d + '//chromedriver.exe')
except:
print('Copy failed due to admin, hopefully copying elsewhere works better...')
os.chdir(od)
# -------------------- END UPDATE METHODS --------------------PK mN:Bp p squp/SystemShortcuts.py'''
SystemShortcuts.py
Provides all of the system shortcuts used throughout the classes of the Squarespace Uploader program
'''
# ---------------------- BEGIN IMPORTS ----------------------
from information import Info
import time, re, json, urllib.request, urllib.parse, urllib.error, unicodedata, random
# ----------------------- END IMPORTS -----------------------
# ---------------- BEGIN SHORTCUT METHODS -------------------
### Enables for shortening of making the program sleep
# Tells the program to sleep for the specified amount of time
# Python used Rest!
def slp(tim=0.5):
time.sleep(tim)
# Sleeps for 0.5 seconds
def slp2():
time.sleep(.5)
# Sleeps for 0.3 seconds
def slp3():
time.sleep(.3)
# Returns whether the string is numeric or not
def isNumber(s):
try:
float(s)
return True
except ValueError:
return False
# Method that determines a url based on keywords
def get_admin_url(keywords):
# If no keyword is given at all, go to store page
if len(keywords) == 0:
return Info.STORE_PAGE
# However, if something is given, figure out what to give back
# Basic Filename (keyword) input
elif 'Inventory2' or 'inventory2' or 'Inventory 2' or 'inventory 2' in [x for x in [y for y in keywords]]:
return Info.INVENTORY_PAGE_Spring_2017
elif 'Inventory' or 'inventory' in [x for x in [y for y in keywords]]:
return Info.INVENTORY_PAGE
# Direct URL input, assuming only one given
elif 'http' in keywords[0]:
return keywords[0]
# Default all other cases (errors) to less important page
else:
return Info.TESTING_PAGE
# Similar to above, returns the json format of the page indicated by the keyword
def get_json_url(keywords):
# If no keyword is given at all, go to store page
if len(keywords) == 0:
return 'https://www.lionelsmithltd.com/shop?format=json-pretty'
# However, if something is given, figure out what to give back
# Basic Filename (keyword) input
elif 'Inventory' or 'inventory' in [x for x in [y for y in keywords]]:
return 'https://www.lionelsmithltd.com/shop-more?format=json-pretty'
# If it's a direct url input, get it from the last parameter in the given keys
elif 'http' in keywords[-1]:
return keywords[-1]
# Default all other cases (errors) to main page since it's good data that can't be modified
else:
return 'https://www.lionelsmithltd.com/shop?format=json-pretty'
# Returns a random entry from the funny loading quotes
def get_loading_message():
# Define the messages
messages = Info.messages
# Return the message at that index
return random.choice(messages)
# ----------------- END SHORTCUT METHODS --------------------
# ----------------- BEGIN COMPARE METHODS -------------------
# A very basic comparison of two lists
def compare(list1, list2):
# Removes empty items from the first list
for l in reversed(list1):
if len(l) == 0 or l is None or l == '':
list1.remove(l)
# Removes empty items from the second list
for l in reversed(list2):
if len(l) == 0 or l is None or l == '':
list2.remove(l)
# Returns the comparison of the new lists
return sorted(list1) == sorted(list2) and len(list1) == len(list2)
# List 1 is hopefully the smaller of the two
def compare_imgs(list1, list2):
# Duplicate list variables for temporary use
d1 = []
d2 = []
update = False
# Copy over the relevant parts of each filename from both lists
for l in list1:
d1.append(l[0:13].lower())
for l in list2:
d2.append(l.split('/')[-1][0:13].lower())
# No need to check the first list here, it's assumed that it's the list from the current database, and must have at least one file (for now)
if len(d2) != 0:
for l in d1:
# If it's not in the other list, mark it as such and move on
if l not in d2:
print('Update Needed')
update = True
# Python used Brick Break!
break
return update
# Replaces first-person pronouns to accredit sellers properly
def other_people(replaced, origin='Southern Tide'):
if replaced is not None:
r = " ".join(replaced) if type(replaced) is list else replaced
r.encode('ascii', 'xmlcharrefreplace')
o = str(origin)
if "(u'" in o:
o = o.split("'")[1]
woST = re.sub(r"\b", "", r)
woST = re.sub(r"\.\b", "", woST)
woST = re.sub(r"(?i)^We.*ve\sgot", o + " has", woST)
woST = re.sub(r"(?i)\sWe.*ve\sgot", " " + o + " has", woST)
woST = re.sub(r"(?i)^we.*ve", o + " has", woST)
woST = re.sub(r"(?i)\swe.*ve", " " + o + " has", woST)
woST = re.sub(r"(?i)^we\s", o + " has ", woST)
woST = re.sub(r"(?i)\swe\s", " " + o + " has ", woST)
woST = re.sub(r"(?i)^our\s", o + "'s ", woST)
woST = re.sub(r"(?i)\sour\s", " " + o + "'s ", woST)
return woST
else:
return replacedPK AoM squp/UpdateGui.py'''
Gui Software Updater
Version 0.1
Author: Jonathan Hoyle
Created: 11/8/18
Description: Provides functionality for applying updates to the interface and corresponding libraries
'''
# Imports
from git import Repo
import os
# Main method for update procedures
def main():
# Initialize a repo object from the current path (which is the base of the repo anyway)
gui_repo = Repo(os.getcwd())
# Grab the origin for the repo
origin = gui_repo.remotes[0]
# And pull to update the interface
origin.pull()
# Then check for outdated pip packages by creating the command
check_outdated_string = 'pip list --outdated --no-cache-dir'
# And running it and saving the results
outdated = os.popen(check_outdated_string).read()
# Indicate which results to not update
dont_update = ['docker', 'spynner']
# Save the necessary lines from the above output
needed_updates = outdated.split('\n')[2:]
# Create a list for the packages to update
packs = []
# Loop through the lines obtained
for n in needed_updates:
# Save the package in that line
pack = n.split(' ')[0]
# If it's not to be ignored
if pack not in dont_update:
# Append it to the list
packs.append(pack)
# Create the string template for updating
update_string = 'pip install --upgrade --no-cache-dir %s'
# Consolidate all packages that need updating to a single string
packs_str = ' '.join(packs)
# Execute the command created by the above two fields
update_output = os.popen(update_string % packs_str).read()
if __name__ == '__main__':
main()PK k{Ou8 8 squp/__init__.py'''
File: __init__.py
Description: Creates initial setup for the squp package
Created: 7/27/19
Version: 0.0.1
'''
__version__ = "0.1.7"
# Starts the environment, import must happen within this method to prevent execution on import
def start():
from . import InventoryGui
InventoryGui.main()PK kN&^ squp/chromedriver.exeMZx @ x !L!This program cannot be run in DOS mode.$ PE L
P\ " X 2* U @ @ ~ Y Q~ , X $~ `} | ~ 0 .text XX X `.rdata % X % X @ @.data ~ v ~ @ .00cfg < @ @.tls > @ .voltbl @ CPADinfo( D @ prot F @ @.rsrc H @ @.reloc X @ B USWV
p E 11MuP@
|H+
B
ƉlpDžx tE E E h F
uh E
u~}F 4 4 h Wk E}vUPR P0 r}vEPE0h r #0 1M0 lD2 |*
M1\ T İ ^_[]Éh5 2E
uh8 "E
thM
h7 땉hH D
f+%tW]hP SFE
C{v]MQPS/ M. & M]/ MfRhX D
tI]hd SD
C{vMxRPQ وT. T hb 3D
tO]hl SD
M. CKE؉C C @H. Et]؋SGـ;/ ]1@K{P/ 1@Pht ]ع C9KvU؍MPR $. EUM1 EЉMԍMEEMU- UM؋EUFʀ|/t~]1@K{Ps. E9Mv}؉PW" 1@Pht M- EUM1 EЉMԉوEEMUW- h B
}hx WB
Gv}1ɍ`Aj QQh PWRm O1H9ˉD L{vXK\r8[u|]u@X\u{ XP- tLluV:/ t`uVuVh lV/ 8 LMj{LrPh m 1DžT MG@MW t#uVuV lV. 7 LMT7 T 9D]S+ lSV. E7 S lS:. )7 ]S S]Sc lS. 6 M6 *M7+ h b DžH 'h L 1ۍ`8+ M+ hl @@
}h W@
h `/ * RPhM
h H t=l;pt6h W-@
}vEPh [* h- hQ 1W h! 1W jBU PW F tLA$
jh M RHE؍lxPV~ Rh +1W 1GUSWVp }]1E]jU Sj WVp0IU M1T ^_[]USWV L1ۡp 1E ^F Ph }Vx ) Sj V L< 0S' S MtPR Wa E>FEFLR: ܉1
7P@ uWVh!@ \ MtVPDž Dž PP獍0V* @ QVPh-@ i j m T Ɖhp-@ hP-@ +0
F@ ^w FfFhp h h PR 7蒂 0
tHuPw tHuPX : MtRPDž Dž jcS Ɖh A@ hA@ !/
F @ whz h h Pj 7誁 ܋tHuP蜂 /
L' 8 0) M赓 M1S e^_[]jJU U øU USWV p MU1艆 y9t"u u 9uEU;XtMRuO 1S j t]~ jh h F(h P~ M^S| C{vNPQ6`
%
j(S Fh GZ N 1ۉAYj
h- / ~ G_jh8 ^F PW:Z $ N $ &PE WPP?tY W~S 1S &e^_[]USWV(e̡p 1EE EPPe( uP.@ jS ÃE؉ي $E|$t$D$/@ D$ j|mS ljh /@ h@/@ h.@ V,
Ou;-
_]O uV uԉ~h h h }WV MЉ>~ ẻ,
M1S e^_[]USWVujS Hu,
1ۍO_G W E։USEP ƅtmuh$ U ' WO u} tۉub|jBS Hu ,
1OGG W. tjUjuk t=mt,tu+j V W ^_[]h h )W jCsV UWV V Q J V > 1Wd tjU U W3 tj^_]USWV
p }E1MN9sML$D$<$5 r~WPSU ; M1S ^_[] USWVE QMUxف9v\# P|S x#GuEpXVuWU E7 r@P3Z ;^_[] 9C؍K r1S$HF뎅t
QS 11K Uh T UEM= rQ)у s#PQDS ] SV UEHuuQp P]USWV}]j$}S Ɖhp!@ S?(
NFu) E F ^_[]UMt! ]S ]USWVPu1ωAAF+iP9 t(v}?9tSE 9uExlj^_[] UWVM1FFtMsKρ r!1H#BQS @#H W|S Fǰ~^_] Uh
T USWVp u1Eu6W EI\ |$1ٹ 9)ø ʾ MEEEu 1U 8B E @#L@tYUу |WuL8D@P tDE @|1L8D@P u U}]
E rމU}ML8j WRP$1 uH |51E OE @L8D@P@ 9t }E E uU1ɋ@L$L }1@9t8DэTVR M EIL8tPM1AS ^_[]UV1tAR VS ^]USWVp uM1E@\ D$ | 9F)1ۍ}V @#L@t"tL8D@P t1K@u1ۋL8j URuP$1 u21u* @L8D@P 1I9tKu 1ɋ@L$L 1ҋ@9T8D|RWM Mw EIL8tPM1S ^_[]UWV}Ή9PL8t
PPDuL<1t9tR @DF^_] UVQ E: tQ02~N2I r1 PR^] UWVp 1E@|8 tX}VE t*@L8P4u@Dj P1 M[ EIL8tPM1S ^_]USWVEp 1UA#AtKL uw a DU Sh'@ hU ZS uWSj M1HS ^_[] UVT ΅tVЃP1PPPPPV ̋D$ @ 1@ UV} t VS ^] ̸ USWVEu u^F jh %P|T lj^F Pd1W PWf^_[] ̋D$T$H D$I@3H3T$ USWVp uE}1UPWRGN@;Au
E;1ۋM1S ^_[] USWVp ]}؉1EG G S0W PSWuuP M1jS ^_[] ̋I EU1PPPPPV USWV eԋ}Ρp 1EEP 1҉SKS P E{}؉C;J $eԃvE؉F e EM FNM1S e^_[] USWVp Mu1E~ tjh E}؋HrWP G9OvU؉PR\ 1M CCFCNKAQ A 7M1S ^_[]USWV
p E1M^V)9Ms UT$L$D$$: <~rPQRU ; M1qS ^_[] USWVQu)9 y։UM}xځ9 # P9S Mx#GquYr>]ESPWU MQuPU ]MF Vu*]SQWeU MQuPRU ] >^_[] 9C؍{ r1S$HFDtWwS A11K7jUSWV] K9Cv9wK NZϸM9C rG$PS @#HGPS uuNQSPdU uN~^_[] UV@| u.Dt%L8P4u@Dj P^]UVEPpHQRVP^]UMt
]S ]UEHPuuQRP]USWV}]jS Ɖh .@ S
EN Fu
F ^_[]UMt ]+S ]UVA΅tHuP)o ^]J
USWVe؍up 1EF EPPVvh00@ C}h h h W_
M܉;m e؉M1S e^_[]UV
p Eu1PMH@ @RPQQ
M1}S ^]̋L$|$ >
)
UWVut$NE ~/
(
^_]S ^_]UEHP@uRPQ ]USWVP=p uEMU1}6P _GփM1S e^_[]USWV]}h .@ u
NFS
E Ft@^_[] UEHPQM ]USWV}]jaS Ɖh /@ h 1@ SN
Nu3
F^_[]USWVp EM1UP1ۉuWQQ
7tE V`S M1S e^_[]UVMtqDs
^]S ^]USWV Up 1艆 NQj$dS ^ljE ~Sj k } t0S( WO F } Wf *S W } tw Wb S ~= dž dž Ɔ GO~ NjRWP j=h ]
Nj h h
Fh P} ERPh P SW Gv PQ6, N
NjT~Gj S ÉfNىx$( fS
^ OS 1S ؍e^_[]UWV9w6Q
t%1~FFPB Vh@@ WV
>^_]UV I,
N N
} t V@S ^] USWV}I SWJ, NSW$, ^_[] USWV` p } 1艄$X $y \$ 1\sC jhi $ |$SV $@ 1ɉNN(P` F |$4D$0 D$ P Phn $P 9$T v$@ PVD$09|$4vT$ $ PR V! $ \$ {1ى{ jh L$S$ P $ t$~~(Pg F]t8|$vt$$ PV W $ w L$ $@ } L$ 1ۉqYjhX N$ qYj
h^ 1D$ PV? $Ít$zJV $@ WV j$S h@@ h@@ h?@
EG8@ _O$@ GSG
C>G $@VuPP
@ $ "
L$
$X 1nS e^_[] jl
] t$ jjTh
D$(hv P$P $T vN$@ Lj
ttt$ jj\h
D$(h PL$|$v-T$+$@ QRP
$ +T$QRP}
$ p L$ $@ $|$ p G G j?h ?hD6 WS& UV1tk$ VS ^]USWV8p uύU1E~ ubM1ۉYE AjhX u F^j
h |M]SVY> u UOhD6 r2% M1;S 8^_[] USWVp ]1ES ;tكP x
E0@ &EU]RPh VS! uFM1S ^_[] USWVu]j@S 1҉HHf@ A@ IH@QYX