【QuotationTool】Model的实现(二),形成价格明细清单.md

项目链接:https://gitee.com/duyang2903/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("缺少字段");

image.png

添加公式

预处理完了就把相应的公式添加上就可以了,对应formulaModelClass.py

处理数量列

从NHCT导出来的文档有个特点,每套设备的配置的第一行一定是主机,也就是说它的数量代表着有多少套设备

image.png

这样其他行只要除以设备数就可以得到单套设备的配置了

如何区分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函数里面有这样一段


    image.png
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));
image.png

重构折扣列

rehandleDisc主要目的是方便我们统一修改折扣。

如下图所示


image.png

在每个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来实现,本质上就是数量行与价格行一一相乘并相加


    image.png
  • 添加设备总单价公式,直接使用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) + ")";

image.png

添加总计

总计的公式等于所有的当前列加起来,除以二。这是因为每套设备的价格明细之和与小计相等,如果把所有的行加起来,说明算了两次,除以二即可。


image.png
    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];
image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容