From b6e809682be8bab783a4b76a356a20cb3add080a Mon Sep 17 00:00:00 2001 From: bcomsugi Date: Mon, 10 Jun 2024 08:46:51 +0700 Subject: [PATCH] 20240610 --- .gitignore | 172 ++++++++++++++++++++++++++ QBClasses.py | 75 +++++++++++ server.py | 343 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 590 insertions(+) create mode 100644 .gitignore create mode 100644 QBClasses.py create mode 100644 server.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8a14d77 --- /dev/null +++ b/.gitignore @@ -0,0 +1,172 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal +*.pot +*.pyc +__pycache__/ +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +testvenv/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ +DN_Excel_files/ +ItemInventory/ +QBbackup/ +.xlsx +.pdf +QBbackup/ +test_folder_source_DeliveryNote/ \ No newline at end of file diff --git a/QBClasses.py b/QBClasses.py new file mode 100644 index 0000000..7ccc519 --- /dev/null +++ b/QBClasses.py @@ -0,0 +1,75 @@ +from server import baseQBQuery + +class ItemInventoryQuery(baseQBQuery): + def __init__(self, *args, **kwargs): + print(f'{args = }') + print(f'{kwargs = }') + super().__init__(*args, **kwargs) + self.QBDict[self.__class__.__name__ + "Rq"]={} + print(self.QBDict) + if 'ListID' in kwargs: + self.QBDict[self.__class__.__name__ + "Rq"]["ListID"]=kwargs['ListID'] + elif 'FullName' in kwargs: + self.QBDict[self.__class__.__name__ + "Rq"]["FullName"]=kwargs['FullName'] + else: + if 'MaxReturned' in kwargs: + self.QBDict[self.__class__.__name__ + "Rq"]["MaxReturned"]=kwargs['MaxReturned'] + if 'ActiveStatus' in kwargs: + self.QBDict[self.__class__.__name__ + "Rq"]["ActiveStatus"]=kwargs['ActiveStatus'] + if 'FromModifiedDate' in kwargs: + self.QBDict[self.__class__.__name__ + "Rq"]["FromModifiedDate"]=kwargs['FromModifiedDate'] + if 'ToModifiedDate' in kwargs: + self.QBDict[self.__class__.__name__ + "Rq"]["ToModifiedDate"]=kwargs['ToModifiedDate'] + if 'MatchCriterion' in kwargs and 'Name' in kwargs: + self.QBDict[self.__class__.__name__ + "Rq"]["NameFilter"]={'MatchCriterion':kwargs['MatchCriterion', 'Name':kwargs['Name']]} + if 'FromName' in kwargs or 'ToName' in kwargs: + self.QBDict[self.__class__.__name__ + "Rq"]["NameRangeFilter"]={'FromName':kwargs.get('FromName', ""), 'ToName':kwargs.get('ToName', "")} + if 'IncludeRetElement' in kwargs: + self.QBDict[self.__class__.__name__ + "Rq"]["IncludeRetElement"]=kwargs['IncludeRetElement'] + if 'OwnerID' in kwargs: + self.QBDict[self.__class__.__name__ + "Rq"]["OwnerID"]=kwargs['OwnerID'] + + print(self.__class__.__name__ + "Rq") + print(self.QBDict) + +class GeneralSummaryReportQuery(baseQBQuery): + def __init__(self, *args, **kwargs): + print(f'{args = }') + print(f'{kwargs = }') + super().__init__(*args, **kwargs) + self.QBDict[self.__class__.__name__ + "Rq"]={} + print(self.QBDict) + self.QBDict[self.__class__.__name__ + "Rq"]["GeneralSummaryReportType"]="InventoryStockStatusByItem" + if 'GeneralSummaryReportType' in kwargs: + self.QBDict[self.__class__.__name__ + "Rq"]["GeneralSummaryReportType"]=kwargs['GeneralSummaryReportType'] + if 'FromReportDate' in kwargs or 'ToReportDate' in kwargs: + self.QBDict[self.__class__.__name__ + "Rq"]["ReportPeriod"]={'FromReportDate':kwargs.get('FromReportDate', ""), 'ToReportDate':kwargs.get('ToReportDate', "")} + elif 'ReportDateMacro' in kwargs: + self.QBDict[self.__class__.__name__ + "Rq"]["ReportDateMacro"]=kwargs.get('FromReportDate', "") + + elif 'FullName' in kwargs: + self.QBDict[self.__class__.__name__ + "Rq"]["FullName"]=kwargs['FullName'] + else: + if 'MaxReturned' in kwargs: + self.QBDict[self.__class__.__name__ + "Rq"]["MaxReturned"]=kwargs['MaxReturned'] + if 'ActiveStatus' in kwargs: + self.QBDict[self.__class__.__name__ + "Rq"]["ActiveStatus"]=kwargs['ActiveStatus'] + if 'FromModifiedDate' in kwargs: + self.QBDict[self.__class__.__name__ + "Rq"]["FromModifiedDate"]=kwargs['FromModifiedDate'] + if 'ToModifiedDate' in kwargs: + self.QBDict[self.__class__.__name__ + "Rq"]["ToModifiedDate"]=kwargs['ToModifiedDate'] + if 'MatchCriterion' in kwargs and 'Name' in kwargs: + self.QBDict[self.__class__.__name__ + "Rq"]["NameFilter"]={'MatchCriterion':kwargs['MatchCriterion', 'Name':kwargs['Name']]} + if 'FromName' in kwargs or 'ToName' in kwargs: + self.QBDict[self.__class__.__name__ + "Rq"]["NameRangeFilter"]={'FromName':kwargs.get('FromName', ""), 'ToName':kwargs.get('ToName', "")} + if 'IncludeRetElement' in kwargs: + self.QBDict[self.__class__.__name__ + "Rq"]["IncludeRetElement"]=kwargs['IncludeRetElement'] + if 'OwnerID' in kwargs: + self.QBDict[self.__class__.__name__ + "Rq"]["OwnerID"]=kwargs['OwnerID'] + + print(self.__class__.__name__ + "Rq") + print(self.QBDict) + +x=ItemInventoryQuery('bagus', 'kedua', key5=5, key2="hore", FullName1='hooooo', FromName1="sg", ToName1="sugi", IncludeRetElement=['Name', 'FullName'], MaxReturned="2") +x.create_QBXML() +x.connect_to_quickbooks() \ No newline at end of file diff --git a/server.py b/server.py new file mode 100644 index 0000000..01b7156 --- /dev/null +++ b/server.py @@ -0,0 +1,343 @@ +import pprint +import xmltodict +import win32com.client +import xml.etree.ElementTree as ET + +class baseQBQuery: + def __init__(self, *args, **kwargs) -> None: + # print(f'kwargs:{kwargs}') + # print(args) + self.onError = "continueOnError" + self.GeneralSummaryReportType = kwargs['GeneralSummaryReportType'] if 'GeneralSummaryReportType' in kwargs else 'SalesByCustomerSummary' + self.ReportPeriod = kwargs['ReportPeriod'] if 'ReportPeriod' in kwargs else None + self.ReportDateMacro = None + if 'ReportDateMacro' in kwargs: + if kwargs['ReportDateMacro'] in ['All', 'Today', 'ThisWeek', 'ThisWeekToDate', 'ThisMonth', 'ThisMonthToDate', 'ThisQuarter', 'ThisQuarterToDate', 'ThisYear', 'ThisYearToDate', 'Yesterday', 'LastWeek', 'LastWeekToDate', 'LastMonth', 'LastMonthToDate', 'LastQuarter', 'LastQuarterToDate', 'LastYear', 'LastYearToDate', 'NextWeek', 'NextFourWeeks', 'NextMonth', 'NextQuarter', 'NextYear']: + self.ReportDateMacro = kwargs['ReportDateMacro'] + self.FromReportDate = self.validate_date(kwargs['FromReportDate']) if 'FromReportDate' in kwargs else None + self.ToReportDate = self.validate_date(kwargs['ToReportDate']) if 'ToReportDate' in kwargs else None + self.ReportEntityFilter = kwargs['ReportEntityFilter'] if 'ReportEntityFilter' in kwargs else None + self.FullName = kwargs['FullName'] if 'FullName' in kwargs else None + # print(self.ReportDateMacro, self.ReportPeriod, self.FromReportDate, self.ToReportDate) + self.QBXML = None + self.QBDict = {} + self.response_string = None + + # def create_sub_element(self, ET, parentNode, thisNode, text="\n", whiteSpace = 0, attrib =None): + # if type(attrib) is not dict: + # attrib = {} + # ele = ET.SubElement(parentNode, thisNode) + # for x in attrib: + # ele.set(x, attrib[x]) + # ele.text = text + # tail = "\n" + # for x in range(whiteSpace): + # tail = tail + " " + # ele.tail = tail + # return ele + + def create_QBXML(self): + dataDict = { ### Header for qmxml with version attribute + "?qbxml": { + "@version": "13.0", + } + } + # dataDict["?qbxml"]["QBXML"] = {"QBXMLMsgsRq": { ### Simple Example ### + # "@onError": "continueOnError", + # "GeneralSummaryReportQueryRq": { + # "GeneralSummaryReportType": self.GeneralSummaryReportType, + # } + # } + # } + + # dataDict["?qbxml"]["QBXML"] = {"QBXMLMsgsRq": { ### Example with multiple FullName Item ### + # "@onError": "continueOnError", + # "ItemInventoryQueryRq": { + # "FullName": ["TACO:AA:TH-003AA", + # "TACO:AA:TH-010AA"] + # }, + # } + # } + firstKey = str(list(self.QBDict.keys())[0]) + dataDict["?qbxml"]["QBXML"] = {"QBXMLMsgsRq": { ### Example with multiple FullName Item ### + "@onError": self.onError, + # self.__class__.__name__+"Rq": self.QBDict[self.__class__.__name__+"Rq"]}} + firstKey: self.QBDict[firstKey]}} + print(dataDict) + # print(xmltodict.unparse(dataDict, pretty=True)) +# # QBXML = '' + xmltodict.unparse(dataDict, pretty=True) + self.QBXML = xmltodict.unparse(dataDict, pretty=True).replace("", "") + print(self.QBXML) + return self.QBXML + # def create_QBXML(self): + # root = ET.Element("QBXML") + # root.tail = "\n" + # root.text = "\n " + # QBXMLMsgsRq = ET.SubElement(root, "QBXMLMsgsRq") + # QBXMLMsgsRq.set("onError", "continueOnError") + # QBXMLMsgsRq.tail = "\n" + # QBXMLMsgsRq.text = "\n " + # GeneralSummaryReportQueryRq = self.create_sub_element(ET, QBXMLMsgsRq, "GeneralSummaryReportQueryRq","\n " ) + # GeneralSummaryReportType = self.create_sub_element(ET, GeneralSummaryReportQueryRq, 'GeneralSummaryReportType', self.GeneralSummaryReportType) + # if self.ReportDateMacro: + # GeneralSummaryReportType = self.create_sub_element(ET, GeneralSummaryReportQueryRq, "ReportDateMacro", self.ReportDateMacro) + # elif type(self.FromReportDate) is datetime.date or type(self.ToReportDate) is datetime.date: + # ReportPeriod = self.create_sub_element(ET, GeneralSummaryReportQueryRq, "ReportPeriod", "\n ",) + # if type(self.FromReportDate) is datetime.date: + # FromReportDate = self.create_sub_element(ET, ReportPeriod, "FromReportDate", self.FromReportDate.strftime('%Y-%m-%d'),4) + # if type(self.ToReportDate) is datetime.date: + # ToReportDate = self.create_sub_element(ET, ReportPeriod, "ToReportDate", self.ToReportDate.strftime('%Y-%m-%d')) + + # mydata = ET.tostring(root, encoding = "unicode") + + # qbxml_query = """\n""" + # qbxml_query = qbxml_query + """""" + # qbxml_query = qbxml_query + "\n" + mydata + + # return qbxml_query + + def connect_to_quickbooks(self, qbxml_query=None): + # Connect to Quickbooks + # sessionManager = win32com.client.Dispatch("QBXMLRP2.RequestProcessor") + # sessionManager.OpenConnection('', 'DASA') + # enumfodnc= win32com.client.Dispatch('QBXMLRP2.RequestProcessor') + # print(enumfodnc) + # print(enumfodnc.qbFileOpenDoNotCare) + sessionManager = win32com.client.Dispatch("QBXMLRP2.RequestProcessor") + sessionManager.OpenConnection('', 'DASA2') + # ticket = sessionManager.BeginSession("z:\\DBW Bogor.qbw", 2) + ticket = sessionManager.BeginSession("", 2) + + # Send query and receive response + # response_string = sessionManager.ProcessRequest(ticket, qbxml_query) + self.response_string = sessionManager.ProcessRequest(ticket, self.QBXML) + + # Disconnect from Quickbooks + sessionManager.EndSession(ticket) # Close the company file + sessionManager.CloseConnection() # Close the connection + print(self.response_string) + self.isDataOK() + return self.response_string + + def isDataOK(self): + print("isdataok") + # QBXML = ET.fromstring(self.response_string) + # print(xmltodict.parse(self.response_string)) + varDict = xmltodict.parse(self.response_string) + pprint.pprint(varDict) + for _ in self.gen_dict_extract("@statusMessage", varDict): + print(_) + if 'Status OK'.lower()==_.lower(): + print(_) + isStatusOK = True + break + else: + isStatusOK=False + if isStatusOK: + print(self.returnRet(varDict)) + + def returnRet(self, varDict): + print("returnRet") + pprint.pprint(varDict) + stillNoRetFound = True + counter = 0 + print(f'{varDict = }') + varDict = varDict['QBXML']['QBXMLMsgsRs'][self.__class__.__name__+"Rs"] + print(f'{varDict = }') + + for idx, key in enumerate(varDict): + # print(idx, key, len(varDict)) + if "Ret" in key: + return varDict[key] + return None + + + def gen_dict_extract(self, key, var:dict=None): ### Utils + if var==None: + var=self.response_string + # print("var") + if hasattr(var,'items'): # hasattr(var,'items') for python 3, hasattr(var,'iteritems') for python 2 + # print("hassattr") + for k, v in var.items(): # var.items() for python 3, var.iteritems() for python 2 + # print(k,v) + if k == key: + yield v + if isinstance(v, dict): + for result in self.gen_dict_extract(key, v): + yield result + elif isinstance(v, list): + for d in v: + for result in self.gen_dict_extract(key, d): + yield result + + def __str__(self, *args) -> str: + # return str(self._get_datarow(self.connect_to_quickbooks(self.create_QBXML()))) + # print("__str__") + return str(self.get_datarow()) + # return "hello" + + + # def get_datarow(self, *args): + # return self._get_datarow(self.connect_to_quickbooks(self.create_QBXML())) + + # def get_dict(self, *args): + # return pd.DataFrame(self._get_datarow(self.connect_to_quickbooks(self.create_QBXML()))) + + # def get_total(self): + # return self._get_total(self.connect_to_quickbooks(self.create_QBXML())) + + def status_ok(self, QBXML): + GSRQRs=QBXML.find('.//GeneralSummaryReportQueryRs') + status_code = GSRQRs.attrib #.get('statusCode') + # print(GSRQRs.attrib) + # print(GSRQRs.attrib['statusCode']) + status=GSRQRs.attrib.get('statusMessage') + + print(f'status={status}') + if 'OK' in status: + return True, status_code + else: + return False, status_code + + # def get_coldesc(self, QBXML): + # coldescs = QBXML.findall('.//ColDesc') + # coldesclist=[] + # firstcol=[] + # allcols=[] + # for idx, coldesc in enumerate(coldescs): + # coltitles = coldesc.findall('ColTitle') + # # coldesclist.append(coltitles[1].attrib.get('value')) + # firstcol.append(coltitles[0].attrib.get('value')) + # cols=[] + # for idy, coltitle in enumerate(coltitles): + # cols.append(coltitle.attrib.get('value')) + # # allcols[idy].append(coltitle.attrib.get('value')) + # # print(idx, coltitle.tag) + # # print(idx, coltitle.attrib.get('value')) + # allcols.append(cols) + # print(f'getcoldesc:{firstcol}; coldesclist:{coldesclist}; allcols:{allcols}') + # # print(allcols) + # return allcols + + # def _get_total(self, response_string): + # # print(response_string) + # # Parse the response into an Element Tree and peel away the layers of response + # QBXML = ET.fromstring(response_string) + # if self.status_ok(QBXML): + # print("") + # # print(QBXML) + # # QBXMLMsgsRs = QBXML.find('QBXMLMsgsRs') #this is a must have. dont CHANGE. this is the root + # total = QBXML.findall('.//TotalRow') + # return {'Total':total[0][1].attrib.get('value')} + # else: + # return () + + # def get_reportsubtitle(self, QBXML): + # reportsubtitle=QBXML.find('.//ReportSubtitle').text + # return reportsubtitle + + # def _get_datarow(self, response_string): + # # print(response_string) + # print('get_datarow') + # # Parse the response into an Element Tree and peel away the layers of response + # QBXML = ET.fromstring(response_string) + # datadict={} + # reportsubtitle=None + # total = float(QBXML.find('.//TotalRow')[1].attrib.get('value')) + # status, statusdict= self.status_ok(QBXML) + # print(status, statusdict, total) + # if status and total > 0: + # # print("get_datarow status ok") + # reportsubtitle=self.get_reportsubtitle(QBXML) + # DataRowRs = QBXML.iter("DataRow") + # col_desc=self.get_coldesc(QBXML) + # # print(f'getdatarow coldesc:{col_desc}') + # # print(f'Datarow length:{len(list(DataRowRs))}') + # temp=[] + # rowvalue=[] + # amount=[] + # rowType=[] + # idxerrorcount=0 + # # print(DataRowRs) + # for idx_datarow, DataRow in enumerate(DataRowRs): + # # print(DataRow.attrib.get('rowNumber')) + # RowData=DataRow.find('RowData') + # if RowData is None: + # print(f'{idx_datarow} none continue') + # idxerrorcount+=1 + # continue + # if 'value' not in DataRow.find('RowData').attrib: + # print(f'{idx_datarow} value continue') + # idxerrorcount+=1 + # continue + # RowData_value=DataRow.find('RowData').attrib.get('value') + # rowType.append(RowData_value) + # ColDatas = DataRow.findall("ColData") + # cols=[] + # for idx_coldata, coldata in enumerate(ColDatas): + # if idx_coldata==0: + # cols.append(rowType[idx_datarow-idxerrorcount]) + # else: + # cols.append(coldata.attrib.get('value')) + # rowvalue.append(cols) + # temp.append(ColDatas[0].attrib.get('value')) + # # datadict[ColDatas[0].attrib.get('colID')]=temp + # amount.append(ColDatas[1].attrib.get('value')) + # # datadict[ColDatas[1].attrib.get('colID')]=amount + # # print(rowvalue) + # datadict['rowvalue']=rowvalue + # datadict['RowType']=rowType + # datadict['CustomerFullName']=temp #ganti dgn rowType + # datadict['Sales']=amount + # # print(f"lendatadict {len(datadict['CustomerFullName'])}, {len(datadict['Sales'])}, {len(datadict['RowType'])}, {len(datadict['rowvalue'])}") + + # df=pd.DataFrame(rowvalue) + # col_desc= [cdesc[-1] for cdesc in col_desc] + # print(f'coldesc getdatarow:{col_desc}') + + # # print(df) + # df.columns=col_desc + # # print(df) + # # print('lendatadict') + # print(f"lendatadict {len(datadict['CustomerFullName'])}, {len(datadict['Sales'])}, {len(datadict['RowType'])}, {len(datadict['rowvalue'])}") + # datadict=df.to_dict('list') + # # print(datadict, type(datadict)) + # return datadict, reportsubtitle + # else: + # return datadict, reportsubtitle + + # def validate_date(self, date_text): + # if date_text is None: + # return None + # try: + # return datetime.datetime.strptime(date_text, '%Y-%m-%d').date() + # except ValueError: + # return None + # # raise ValueError("Incorrect data format, should be YYYY-MM-DD") +print('### GeneralSummaryReport ###') +if __name__ == '__main__': + ini=GeneralSummaryReportQuery(ReportDateMacro='LastYear') + # ini=GeneralSummaryReportQuery(FromReportDate='2023-01-11', ToReportDate='2023-01-12') + ini=GeneralSummaryReportQuery(GeneralSummaryReportType='SalesByItemSummary') + # ini=GeneralSummaryReportQuery(GeneralSummaryReportType='SalesByRepSummary') + # ini=GeneralSummaryReportQuery(GeneralSummaryReportType='PurchaseByVendorSummary') + # ini=GeneralSummaryReportQuery(GeneralSummaryReportType='ProfitAndLossStandard') + # ini=GeneralSummaryReportQuery(GeneralSummaryReportType='PhysicalInventoryWorksheet') + # ini=GeneralSummaryReportQuery(GeneralSummaryReportType='InventoryStockStatusByItem') + print(ini.create_QBXML()) + # print(f'print ini:{ini}') + # print(type(ini.get_datarow())) + # print(ini.get_total()) + # print(f'ini.getdatarow:{ini.get_datarow()}') + ini.connect_to_quickbooks() + df=pd.DataFrame(ini.get_datarow()[0]) + headers=list(df.columns) + print(df.tail(10)) + df.columns=['CustomerFullName', 'TotalSales'] + # df['TotalSales']=df['TotalSales'].astype(float) + df['TotalSales']=pd.to_numeric(df['TotalSales']) + # df['TotalSales']=df['TotalSales'].astype('Int64') + print(df.loc[df['TotalSales']>0]) + print(df.tail(10)) + print(headers) + print(list(df.keys().values)) \ No newline at end of file