Source code for tendril.sourcing.csil

# Copyright (C) 2015 Chintalagiri Shashank
#
# This file is part of Tendril.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""
CSIL Vendor Module (:mod:`tendril.sourcing.csil`)
=================================================
"""

import time
import locale
import os
from collections import OrderedDict
from future.utils import viewitems

import splinter

import selenium.common.exceptions
from .vendors import VendorBase
from .vendors import VendorPrice
from .vendors import VendorPartBase
from .vendors import SourcingInfo
from .vendors import VendorPartRetrievalError

from tendril.utils import fsutils
from tendril.utils.terminal import TendrilProgressBar

from tendril.utils.config import VENDORS_DATA
from tendril.utils.config import FIREFOX_PROFILE_PATH

from tendril.gedaif import projfile
from tendril.entityhub import projects
from tendril.dox import purchaseorder

from selenium.webdriver.remote.remote_connection import LOGGER
from tendril.utils.files import yml as yaml
from tendril.utils import log
logger = log.get_logger(__name__, log.DEFAULT)
LOGGER.setLevel(log.WARNING)

username = None
password = None


[docs]def get_credentials(): global username global password for vendor in VENDORS_DATA: if vendor['name'] == 'csil': username = vendor['username'] password = vendor['password']
get_credentials() locale.setlocale(locale.LC_ALL, 'en_US.UTF-8') exparams = { 'pcbname': 'QASC-', 'layers': 2, 'dX': '109', 'dY': '151', 'qty': range(70), 'time': 10, # 5, 7, 10, 12, 15, 18, 21, 25, 30 'finish': 'Au', # HAL, Sn, Au, PBFREE, H, NP, I, OC }
[docs]def get_csil_prices(params=exparams, rval=None): if rval is None: rval = {} delivery_codes = { # 3: '3#333', # 5: '5#334', 7: '7#529', 10: '10#1452', 12: '12#7271', 15: '15#1453', 18: '18#7272', 21: '21#7273' } delivery_times = sorted(delivery_codes.keys()) layers_codes = { 1: '2180', 2: '2181', 4: '2183', 6: '2184', } browser = splinter.Browser('firefox', profile=FIREFOX_PROFILE_PATH) url = 'http://login.pcbpower.com/V2/login.aspx' browser.visit(url) values = { 'txtUserName': dvobj.username, 'txtPassword': dvobj.password } browser.fill_form(values) button = browser.find_by_name('btnlogin') button.click() link = browser.find_by_id('ctl00_aPlaceOrder') link.click() values = OrderedDict() values['ctl00$ContentPlaceHolder1$txtPCBName'] = params['pcbname'] values['ctl00$ContentPlaceHolder1$ddlLayers'] = layers_codes[params['layers']] # noqa values['ctl00$ContentPlaceHolder1$txtDimX'] = str(params['dX']) values['ctl00$ContentPlaceHolder1$txtDimY'] = str(params['dY']) if 'qty' in params.keys(): values['ctl00$ContentPlaceHolder1$txtQuantity'] = str(params['qty'][1]) # noqa else: values['ctl00$ContentPlaceHolder1$txtQuantity'] = '1' values['ctl00$ContentPlaceHolder1$DDLsurfacefinish'] = params['finish'] if 'time' in params.keys(): values['ctl00$ContentPlaceHolder1$ddlDelTerms'] = delivery_codes[params['time']] # noqa else: values['ctl00$ContentPlaceHolder1$ddlDelTerms'] = delivery_codes[7] if not browser.is_element_present_by_id('shortNotiText', wait_time=100): raise Exception ready = False timeout = 10 while not ready and timeout: el = browser.find_by_id('shortNotiText') if el[0].text == u"We're online": ready = True timeout -= 1 time.sleep(1) time.sleep(5) browser.fill_form(values) try: oldt = browser.find_by_id('ctl00_ContentPlaceHolder1_lblUnitPrc').text except AttributeError: oldt = '' # qty = str(params['qty'][1]) # oldv = qty time.sleep(2) button = browser.find_by_id('ctl00_ContentPlaceHolder1_btnCalculate') button.click() time.sleep(2) button = browser.find_by_id('ctl00_ContentPlaceHolder1_btnCalculate') button.click() try: newt = browser.find_by_id('ctl00_ContentPlaceHolder1_lblUnitPrc').text except AttributeError: newt = '' try: newtt = browser.find_by_id('ctl00_ContentPlaceHolder1_lblTotalPrice').text # noqa except AttributeError: newtt = '' while oldt == newt: try: newt = browser.find_by_id('ctl00_ContentPlaceHolder1_lblUnitPrc').text # noqa except AttributeError: newt = '' try: newtt = browser.find_by_id('ctl00_ContentPlaceHolder1_lblTotalPrice').text # noqa except AttributeError: newtt = '' time.sleep(0.5) oldt = newt oldtt = newtt pb = TendrilProgressBar(max=len(params['qty'])) for qty in params['qty'][2:]: lined = {} while browser.find_by_name('ctl00$ContentPlaceHolder1$txtQuantity')[0].value != '': # noqa browser.type('ctl00$ContentPlaceHolder1$txtQuantity', '\b') time.sleep(0.1) browser.type('ctl00$ContentPlaceHolder1$txtQuantity', str(qty)) time.sleep(0.1) browser.type('ctl00$ContentPlaceHolder1$txtQuantity', '\t') if qty > 4: loi = [10] else: loi = [10] pb.next(note="{0} {1}".format(qty, loi)) # "\n{0:>7.4f}% {1:<40} Qty: {2:<10} DTS: {3:<4}\nGenerating PCB Pricing".format( # noqa # percentage, params['pcbname'], qty, loi) for dt_s in loi: dt_idx = delivery_times.index(dt_s) dts = delivery_times[dt_idx:dt_idx + 3] browser.select('ctl00$ContentPlaceHolder1$ddlDelTerms', delivery_codes[dt_s]) # noqa time.sleep(1) while True: try: try: newt = browser.find_by_id('ctl00_ContentPlaceHolder1_lblUnitPrc').text # noqa except AttributeError: newt = '' try: # newt1 = '' # newt2 = '' newt1 = browser.find_by_id('ctl00_ContentPlaceHolder1_lblnextunitprc1').text # noqa newt2 = browser.find_by_id('ctl00_ContentPlaceHolder1_lblnextunitprc2').text # noqa except AttributeError: newt1 = '' newt2 = '' break except selenium.common.exceptions.StaleElementReferenceException: # noqa logger.warning("Selenium Exception Caught. Retrying") continue timeout = 25 while oldt == newt and oldtt == newtt and newt is not '' and timeout > 0: # noqa timeout -= 1 time.sleep(1) while True: try: try: newt = browser.find_by_id('ctl00_ContentPlaceHolder1_lblUnitPrc').text # noqa except AttributeError: newt = '' try: # newt1 = '' # newt2 = '' newt1 = browser.find_by_id('ctl00_ContentPlaceHolder1_lblnextunitprc1').text # noqa newt2 = browser.find_by_id('ctl00_ContentPlaceHolder1_lblnextunitprc2').text # noqa except AttributeError: newt1 = '' newt2 = '' break except selenium.common.exceptions.StaleElementReferenceException: # noqa logger.warning("Selenium Exception Caught. Retrying") continue try: newtt = browser.find_by_id('ctl00_ContentPlaceHolder1_lblTotalPrice').text # noqa except AttributeError: newtt = '' try: lined[dts[0]] = locale.atof(newt) if newt1 != '': lined[dts[1]] = locale.atof(newt1) if newt2 != '': lined[dts[2]] = locale.atof(newt2) except ValueError: logger.warning("Caught Exception at CSIL Website. Retrying.") browser.quit() return get_csil_prices(params, rval) oldt = newt oldtt = newtt params['qty'].remove(qty) # print lined rval[qty] = lined browser.quit() return rval
[docs]class CSILPart(VendorPartBase): def __init__(self, vpartno, ident, vendor, max_age=600000): if vendor is None: vendor = dvobj if ident is None: ident = vendor.map.get_canonical(vpartno) self._descriptors = [] super(CSILPart, self).__init__(vpartno, ident, vendor, max_age)
[docs] def _get_data(self): if self.vpno.startswith('PCB'): self._pcbname = self.vpno[4:] else: self._pcbname = self.vpno if self._pcbname not in projects.pcbs.keys(): raise VendorPartRetrievalError("Unrecognized PCB") self._projectfolder = projects.pcbs[self._pcbname] self._load_descriptors() self._manufacturer = self._vendor.name self._load_prices() self._vqtyavail = None
[docs] def _load_descriptors(self): gpf = projfile.GedaProjectFile(self._projectfolder) pricingfp = os.path.join(gpf.configsfile.projectfolder, 'pcb', 'sourcing.yaml') if not os.path.exists(pricingfp): logger.debug( "PCB does not have sourcing file. Not loading prices : " + self._pcbname ) return None with open(pricingfp, 'r') as f: data = yaml.load(f) self._descriptors.append(str(data['params']['dX']) + 'mm x ' + str(data['params']['dY']) + 'mm') # noqa if data["params"]["layers"] == 2: self._descriptors.append("Double Layer") elif data["params"]["layers"] == 4: self._descriptors.append("ML4") # HAL, Sn, Au, PBFREE, H, NP, I, OC if data["params"]["finish"] == 'Au': self._descriptors.append("Immersion Gold/ENIG finish") elif data["params"]["finish"] == 'Sn': self._descriptors.append("Immersion Tin finish") elif data["params"]["finish"] == 'PBFREE': self._descriptors.append("Any Lead Free finish") elif data["params"]["finish"] == 'H': self._descriptors.append("Lead F ree HAL finish") elif data["params"]["finish"] == 'NP': self._descriptors.append("No Copper finish") elif data["params"]["finish"] == 'I': self._descriptors.append("OSP finish") elif data["params"]["finish"] == 'OC': self._descriptors.append("Only Copper finish") else: self._descriptors.append("UNKNOWN FINISH: " + data["params"]["finish"]) # noqa self._descriptors.append("10 Working Days")
[docs] def _load_prices(self): gpf = projfile.GedaProjectFile(self._projectfolder) pricingfp = os.path.join(gpf.configsfile.projectfolder, 'pcb', 'sourcing.yaml') if not os.path.exists(pricingfp): logger.debug( "PCB does not have sourcing file. Not loading prices : " + self._pcbname ) return None with open(pricingfp, 'r') as f: data = yaml.load(f) for qty, prices in viewitems(data['pricing']): if 10 not in prices.keys(): logger.warning( "Default Delivery Time not in prices. Quantity pricing not imported : " + # noqa str([qty, self._pcbname]) ) else: price = VendorPrice( qty, prices[10], self._vendor.currency ) self._prices.append(price)
@property def descriptors(self): return self._descriptors
[docs] def get_price(self, qty): possible_prices = [] base_price, next_base_price = super(CSILPart, self).get_price(qty) for price in self._prices: if price.moq > qty and \ price.extended_price(price.moq).native_value < base_price.extended_price( qty).native_value: # noqa possible_prices.append(price) if len(possible_prices) == 0: return base_price, next_base_price else: mintot = base_price.extended_price(qty).native_value selprice = base_price for price in possible_prices: if price.extended_price(price.moq).native_value < mintot: selprice = price mintot = price.extended_price(price.moq).native_value return selprice, super(CSILPart, self).get_price(selprice.moq + 1)[0]
[docs] def sp_get_price(self, qty): possible_prices = [] base_price, next_base_price = super(CSILPart, self).get_price(qty) for price in self._prices: if price.moq > qty and \ price.extended_price(price.moq).native_value < base_price.extended_price(qty).native_value: # noqa possible_prices.append(price) if len(possible_prices) == 0: return base_price, next_base_price, "GUIDELINE", None else: mintot = base_price.extended_price(qty).native_value selprice = base_price rationale = "GUIDELINE" for price in possible_prices: if price.extended_price(price.moq).native_value < mintot: selprice = price mintot = price.extended_price(price.moq).native_value rationale = "TC Reduction" return selprice, super(CSILPart, self).get_price(selprice.moq + 1)[0], rationale, base_price # noqa
[docs]class VendorCSIL(VendorBase): _partclass = CSILPart _type = 'CSIL' def __init__(self, name, dname, pclass, mappath=None, currency_code='INR', currency_symbol=None, username=None, password=None, **kwargs): self._username = username self._password = password self._devices = ['PCB'] super(VendorCSIL, self).__init__( name, dname, pclass, mappath, currency_code, currency_symbol, **kwargs ) self._vpart_class = CSILPart self.add_order_additional_cost_component("Excise", 12.5) self.add_order_additional_cost_component("CST", 5.625) @property def username(self): return self._username @property def password(self): return self._password
[docs] def search_vpnos(self, ident): if ident not in projects.pcblib: return [], 'PCB_NOT_KNOWN' return [ident], 'CUSTOM'
[docs] def get_optimal_pricing(self, ident, rqty, get_all=False): # return super(VendorCSIL, self).get_optimal_pricing(ident, rqty) # TODO Fix this structure if ident not in projects.pcblib: if get_all: return [] return SourcingInfo(self, None, None, None, None, None, None, None) candidate_names = self.get_vpnos(ident) candidates = [self.get_vpart(x) for x in candidate_names] if len(candidates) == 0: if get_all: return [] return SourcingInfo(self, None, None, None, None, None, None, None) candidate = candidates[0] if len(candidate.prices) == 0: if get_all: return [] return SourcingInfo(self, None, None, None, None, None, None, None) ubprice, nbprice, urationale, olduprice = candidate.sp_get_price(rqty) oqty = ubprice.moq effprice = self.get_effective_price(ubprice) if get_all: return [SourcingInfo(self, candidate, oqty, nbprice, ubprice, effprice, urationale, olduprice)] return SourcingInfo(self, candidate, oqty, nbprice, ubprice, effprice, urationale, olduprice)
[docs] def _generate_purchase_order(self, path): stagebase = super(VendorCSIL, self)._generate_purchase_order(path) if stagebase is not None: stagebase = {} for line in self._order.lines: stage = stagebase stage['intref'] = self._order.orderref stage['username'] = self._username stage['name'] = line[2] stage['qty'] = line[3] stage['unitp'] = line[5].unit_price.source_value totalv = line[5].extended_price(line[3]).source_value stage['totalp'] = totalv addl_costs = self.get_additional_costs(line[5].extended_price(line[3])) # noqa stage['addl_costs'] = [{'desc': x[0], 'cost': x[1]} for x in addl_costs] for desc, cost in addl_costs: totalv += cost stage['total'] = totalv vpart = self.get_vpart(line[2]) stage['description'] = "\\\\".join(vpart.descriptors) purchaseorder.render_po( stage, self._name, os.path.join(path, self._name + '-PO-' + line[2] + '.pdf') )
[docs]def flush_pcb_pricing(projfolder): gpf = projfile.GedaProjectFile(projfolder) pricingfp = os.path.join(gpf.configsfile.projectfolder, 'pcb', 'sourcing.yaml') if os.path.exists(pricingfp): os.remove(pricingfp)
[docs]def generate_pcb_pricing(projfolder, noregen=True, forceregen=False): gpf = projfile.GedaProjectFile(projfolder) try: pcbparams = gpf.configsfile.configdata['pcbdetails']['params'] except KeyError: logger.warning( 'Geda project does not seem have pcb details. ' 'Not generating PCB pricing information : ' + projfolder) return None try: if gpf.configsfile.configdata['pcbdetails']['params']['panelize'] is True: logger.warning( 'Not obtaining pricing for panelized pcb : ' + projfolder ) return None except KeyError: pass try: searchparams = gpf.configsfile.configdata['pcbdetails']['indicativepricing'] except KeyError: searchparams = { 'qty': 20, 'dterm': 7, } pricingfp = os.path.join(gpf.configsfile.projectfolder, 'pcb', 'sourcing.yaml') if noregen is True: if os.path.exists(pricingfp): return pricingfp if forceregen is False: pcb_mtime = fsutils.get_file_mtime(os.path.join(gpf.configsfile.projectfolder, 'pcb', gpf.pcbfile + '.pcb')) # noqa outf_mtime = fsutils.get_file_mtime(pricingfp) if outf_mtime is not None and outf_mtime > pcb_mtime: logger.info('Skipping up-to-date ' + pricingfp) return pricingfp logger.info('Generating PCB Pricing for ' + pricingfp) pcbparams['qty'] = range(searchparams['qty']) sourcingdata = get_csil_prices(pcbparams) dumpdata = {'params': pcbparams, 'pricing': sourcingdata} with open(pricingfp, 'w') as pricingf: pricingf.write(yaml.dump(dumpdata)) return pricingfp
dvobj = VendorCSIL('csil', 'Circuit Systems India Ltd', 'electronics_pcb', currency_code='INR', currency_symbol='INR', username=username, password=password)