项目链接:https://gitee.com/xyjtysk/quotationTools
在【QuotationTool】Model的实现(一),获得Excel路径以及Excel输出格式里面我们已经获得了Excel的路径,已经规定好了输出和输出有哪些列,下面就可以开始正式转换了。
预处理
由Controller进行调度
首先自然是读取Excel,我们在Controller里面调用XlrdTool中的getAssociativeArray
lists = XlrdTool().getAssociativeArray(inputPath, sheetName, inputParam.keys())
我们知道Controller其实是数据的中转站,所以其他Model处理以后的lists都要发到Controller中。
接下来就是调度rehandleModelClass.py进行预处理了
rehandleInstance = M("rehandle");
rehandleInstance.assign(lists);
lists = rehandleInstance.doRehandel(diffList);
那么我们来看一下rehandleModelClass.py是怎么实现的。
rehandleModelClass
这个Model主要是对读入的数组进行预处理,主要是
# 删除含有#的行
self.removeRows();
# 加行
self.addRow();
# 加colorTag
self.addColorTag();
# 加列
self.addColumns(diffList);
因为读入的Excel可能不规范,比如没有总计行或者小计行等,所以我们需要把这些行加上。
然后加上ColorTag
最后按照输出的keys把不存在的列加上。
removeRows
功能:删除不需要的行
我们在分析需求的时候就说过,官方的报价清单里面冗余太多,需要删除
那怎么删除呢?当然是遍历数组,对符合条件的删除呗。
这就有一个问题,删除以后iterator就改变了,所以最后的结果会乱七八糟
有什么办法可以解决吗?可以参考Python的list循环遍历中,删除数据的正确方法
def removeRows(self):
# 逆序遍历,否者一边删除一边iterator就改变了
for aList in self.lists[::-1]:
try:
if set([\'BOM\',\'typeID\',\'ID\']) < set(aList.keys()) and str (aList[\'BOM\']).find("#") != -1 and aList[\'ID\'] == "" and aList[\'typeID\'] == "":
self.lists.remove(aList);
info("删除了含有#的行");
elif \'description\' in aList.keys() and str(aList[\'description\']).find(\'Factory integrated\') != -1:
self.lists.remove(aList);
info("删除了含有Factory integrated行");
# 单独删除NHCT导出模板中的含截止日期行
elif \'unitsNetPrice\' in aList.keys() and str(aList[\'unitsNetPrice\']).find(u\'截止日期\') != -1:
self.lists.remove(aList);
info("删除了含有截止日期的行");
elif \'ID\' in aList.keys() and str(aList[\'ID\']).find(u\'价格明细清单\') != -1:
self.lists.remove(aList);
info("删除了含有价格明细清单的行");
# 删除空行,取出所有的values,通过map全部变为str类型,然后转换为list,最后串接在一起。
elif len("".join(list(map(str,aList.values())))) == 0 :
self.lists.remove(aList);
info("删除空行");
else:
continue;
except Exception as data:
error("删除空行时,超出表格范围"+str(data));
加行
因为输入的Excel可能不含有小计行等,我们需要再进行一次遍历,把该加上行的地方加上行。
def getRow (self , key , value):
# 先全部填上空白
row = {};
for k in self.lists[0].keys():
row[k] = "";
row[key] = value;
return row;
# **************加上小计行、总计行**************
def addRow(self):
# 遍历lists,插入小计行、总计行
try:
aDiff = [i for i in [\'BOM\',\'typeID\',\'description\'] if i in self.lists[0].keys()];
colTag = aDiff[0];
for i in range(len(self.lists) - 1 , 1 , -1):
list = self.lists[i];
if list[\'ID\'] != "" and self.lists[i-1][colTag] != \'小计\':
self.lists.insert(i,self.getRow(colTag,\'小计\'));
info (\'在第\'+str(i)+\'行增加了小计行\')
if self.lists[-1][colTag] != \'总计\':
self.lists.append(self.getRow(colTag,\'总计\'));
info (\'在最后一行增加了总计行\')
if self.lists[-2][colTag] != \'小计\':
self.lists.insert(len(self.lists)-1 , self.getRow(colTag,\'小计\'));
info (\'在倒数第二行增加了小计行\')
except Exception as data:
error(data);
error ("addRow函数中")
加列
我们把要加的列放到inputVariable.py中的diff数组中
比如
diff = {
\'totalNum\':\'0\',
"unit":"个",
"billType":"增值税",
"taxRate":"17%"
};
再动态的从inputVariable.py里面读取diff数组
# **************加列 **************
def addColumns (self , diffList):
var = __import__("libs.inputVariable");
inputvar = getattr(var , "inputVariable");
diff = getattr(inputvar , "diff");
for arr in self.lists:
if arr[\'colorTag\'] == "general":
for d in diffList:
# 查找到相应的字段则直接复制,没查找到的则为空
arr[d] = diff.get(d) if diff.get(d) != None else "";
else:
for d in diffList:
arr[d] = "";
这样就可以灵活的扩展输出的列了。
加颜色标签
遍历数组加上colorTag,用于区别不同的行的角色,主要有
- header:标题
- site:设备的标题
- subtotal:小计
- total:总计
- general:其他
# **************加颜色标签**************
def addColorTag (self) :
try:
aDiff = [i for i in [\'BOM\',\'typeID\',\'description\'] if i in self.lists[0].keys()];
colTag = aDiff[0];
for aList in self.lists:
if aList[colTag] == "小计":
aList[\'colorTag\'] = "subtotal";
elif aList[colTag] == "总计":
aList[\'colorTag\'] = "total";
elif aList[\'ID\'] != "":
aList[\'colorTag\'] = \'site\';
else:
aList[\'colorTag\'] = "general";
self.lists[0][\'colorTag\'] = "header"
except Exception as data:
error(data)
error("缺少字段");
添加公式
预处理完了就把相应的公式添加上就可以了,对应formulaModelClass.py
处理数量列
从NHCT导出来的文档有个特点,每套设备的配置的第一行一定是主机,也就是说它的数量代表着有多少套设备
这样其他行只要除以设备数就可以得到单套设备的配置了
如何区分site
那么就有个问题了,怎么区分不同的设备呢?
我们可以使用
-
self.aSite:数组,存放site行的序号
-
self.aSubtotal:数组,存放小计行的序号
-
self.aTotal :存放标题的序号
这样就知道每套设备从那里开始呢
那怎么获得这些数组呢?
遍历一下即可。
def getSubtotalIndex (self):
self.aSite = [];
self.aSubtotal = [];
self.aTotal = 0;
# 遍历数组,根据colorTag来进行判断
for i , arr in enumerate (self.lists):
if arr[\'colorTag\'] == \'site\':
self.aSite.append(i);
elif arr[\'colorTag\'] == \'subtotal\':
self.aSubtotal.append(i);
elif arr[\'colorTag\'] == \'total\':
self.aTotal = i;
else:
continue;
self.aHeader = 0;
添加“单套数量”列
关键代码如下:
# 从aSite数组里面取出site所在行的行号
for i , s in enumerate(self.aSite):
# 如果site标题所在行的quantity为空,同时在\'BOM\'那一列没有BTO的字样时
if self.lists[s][\'quantity\'] == "" and self.lists[s][tag].find("BTO") == -1:
# 获得到了套数
Qty = int (self.lists[s+1][\'quantity\']);
# 配置开始行均为主机,所以他的quantity实际就是套数
self.lists[s][\'quantity\'] = Qty
# 将剩下的都除以套数
for j in range(s + 1 , self.aSubtotal[i]):
self.lists[j][\'quantity\']= int(self.lists[j][\'quantity\'])/Qty;
首先从Site序号数组中获得site在那里,它的下一行即为主机
取出主机的数量,即为设备实际的套数
剩下的行都除以套数即可得到单套配置
添加总数量列
总数量列需要添加公式
关键代码如下:
for i , s in enumerate(self.aSite):
# siteInitial代表表格中显示的site起始行(表格是从1开始)
siteInitial = str(s + 1);
for j in range(s + 1 , self.aSubtotal[i]-1 + 1):
self.lists[j][\'totalQuantity\'] = \'=$\' + self.dCol[\'quantity\'] + "$" + siteInitial + "*" + self.dCol[\'quantity\'] + str (j + 1);
i表示site在aSite数组里面的序号,s表示每个site的序号
需要注意的是Excel是从1开始的,而数组一般是从0开始的,所以在Excel里面site的序号 = s + 1
最后一行我们可以详细的说一下:
\'=$\' + self.dCol[\'quantity\'] + "$" + siteInitial + "*" + self.dCol[\'quantity\'] + str (j + 1);
- 在看self.dCol[\'quantity\']表示什么意思之前,我们可以看一下assign函数里面有这样一段
colOrdinal = [\'A\', \'B\', \'C\', \'D\', \'E\', \'F\', \'G\', \'H\', \'I\', \'J\', \'K\', \'L\', \'M\',
\'N\', \'O\', \'P\', \'Q\', \'R\', \'S\', \'T\', \'U\', \'V\', \'W\', \'X\', \'Y\', \'Z\'];
# 先组合成为dict
self.dCol = dict(zip(self.outputKeys, colOrdinal));
colOrdinal其实就是A~Z,它与outputKeys一起组成一个dict,这样的好处在于,我们可以通过self.dCol[\'quantity\']
获得数量列所在的下标。在上图中就是"E"
-
那么"j"从那里来?
for j in range(s + 1 , self.aSubtotal[i]-1 + 1):
也就是说j表示每套设备的配置细节行。
self.aSubtotal[i]-1表示小计行前一行,然后+1就可以得到在Excel里面的行号
所以这段表示对每套的配置进行遍历,加上这个总数量行的公式即可。
总结一下,主要过程是
-
对设备site数组进行遍历,可以得到每套设备的起始行号,
-
然后对每套设备的详细配置项进行遍历,在每一行加上总数量的公式
-
注意Excel的行号与python 的数组的行号不同。
把这一小节理解了,后面其他的函数基本上都是沿着这个思路来写的
比如说重构单价列
# # **************重构单价列**************
def rehandleUnitPrice(self):
try :
for i , s in enumerate(self.aSite):
siteInitial = str(s + 1 );
for j in range(s + 1 , self.aSubtotal[i] - 1+ 1 ):
self.lists[j][\'unitsNetPrice\'] = \'=\' + self.dCol[\'unitsNetListPrice\'] + str(j+1) + "*" + self.dCol[\'discount\'] + str(j + 1 );
except Exception as data:
error(\'缺少price字段\' + str(data));
添加总价列
def addTotalPrice (self):
try :
for i , s in enumerate(self.aSite):
siteInitial = str(s + 1 );
for j in range(s + 1 , self.aSubtotal[i] - 1 + 1):
self.lists[j][\'totalPrice\'] = \'=\' + self.dCol[\'unitsNetPrice\'] + str(j+1) + "*" + self.dCol[\'totalQuantity\'] + str(j+1);
except Exception as data:
error(\'缺少price字段\' + str(data));
重构折扣列
rehandleDisc主要目的是方便我们统一修改折扣。
如下图所示
在每个site里面加一个折扣,它等于总计栏里面的折扣。
而配置细项里面的折扣又等于对应site里面的折扣。
这样只需要修改总计栏里面的折扣,就可以把全局的折扣改变了。
然后再修改每套设备里面的折扣就可以了。
缺点就是没有办法针对某些单板、模块进行折扣的修改。
具体代码如下:
# **************重构折扣列#######################
def rehandleDisc(self):
# 若输出含有折扣
if \'discount\' in self.outputKeys:
# 在总计行上填上100%
self.lists[-1][\'discount\'] = 1;
for i , s in enumerate(self.aSite):
# 所有的site上的off与总计行的off相等
self.lists[s][\'discount\'] = \'=\' + self.dCol[\'discount\'] + str(self.aTotal + 1);
# 详细配置的disc列与site行的相等
siteInitial = str(s + 1 );
for j in range(s + 1 , self.aSubtotal[i] - 1+ 1 ):
self.lists[j][\'discount\'] = \'=\' + self.dCol[\'discount\'] + siteInitial;
添加小计和总计行的公式
小计行公式
小计行要做的主要有三件事:
-
添加上“小计”字样,有些输出的表格里面可能不含有BOM或者description,我们要做一下判断
-
添加单套设备的小计,用SUMPRODUCT来实现,本质上就是数量行与价格行一一相乘并相加
-
添加设备总单价公式,直接使用SUM就可以了。
核心代码为:
# 看typeID或者description谁在输入的列中
aDiff = [i for i in [\'typeID\', \'description\'] if i in self.outputKeys];
tag = aDiff[0];
# 在小计行的typeID或者description位处加上配置主机的型号
for i,sub in enumerate(self.aSubtotal):
siteInitial = self.aSite[i] + 1;
siteEnd = sub - 1;
self.lists[sub][tag] = \'\';
if \'typeID\' in self.outputKeys and \'BOM\' not in self.outputKeys:
self.lists[sub][\'typeID\'] = \'小计\';
self.lists[sub][\'totalPrice\'] = \'=SUM(\' + self.dCol[\'totalPrice\'] + str(siteInitial + 1) + ":" + self.dCol[\'totalPrice\'] + str(siteEnd + 1) + ")";
# 单套总价格
if getParser(\'inOutmode\',\'outputMode\') in ["internal",\'HPE\']:
self.lists[sub][\'unitsNetPrice\'] = \'=SUMPRODUCT(\' + self.dCol[\'unitsNetPrice\'] + str(siteInitial + 1) + ":" + self.dCol[\'unitsNetPrice\'] + str(siteEnd + 1) + "," + self.dCol[\'quantity\'] + str(siteInitial + 1 ) + ":" + self.dCol[\'quantity\'] + str(siteEnd + 1) + ")";
添加总计
总计的公式等于所有的当前列加起来,除以二。这是因为每套设备的价格明细之和与小计相等,如果把所有的行加起来,说明算了两次,除以二即可。
self.lists[-1][\'totalPrice\'] = \'=SUM(\' + self.dCol[\'totalPrice\'] + \'2:\' + self.dCol[\'totalPrice\'] + str(self.aTotal) + \')/2\';
controller进行调度
最后我们来看一下controller是如何调度上述的代码的
# —————————————————————————参数准备—————————————————————————
# 分别获取输入和输出文件的名称
var = __import__("libs.inputVariable")
inputvar = getattr(var,"inputVariable")
inputFile = M("file").getProjectName();
outputFile = M("outputfile").getOutputFile(inputFile);
info("打开的文件是" + inputFile);
# 获得输入和输出的keys
[inputParam , outputParam] = M("parameter").getParameter(inputFile);
# 以quotationTools的根目录作为基准
basepath = os.path.dirname(os.path.dirname(os.path.dirname(__file__)));
inputPath = os.path.join(basepath,getParser(\'path\',\'inputfilePath\'),inputFile);
outputPath = os.path.join(basepath,getParser(\'path\',\'outputfilePath\'),outputFile);
# 主sheetName
sheetName = \'价格明细清单\';
# 添加公式
iFormula = M("formula");
iFormula.assign(lists, list(outputParam.keys()));
lists = iFormula.addFormula();
# 替换首行为想让他输出的模式
for k in outputParam.keys():
lists[0][k] = outputParam[k];