【QuotationTool】Model的实现(二),形成价格明细清单.md
项目链接: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];