"NightTeam",一个值得加星标的公众号。

在上篇中,我们已经将我们的发票管理工具开发到了能一键导入发票、能看到效果、能仍然不太方便地管理的状态,接下来我们来继续将还没有加进去的那些方便的功能给加上,以将管理发票的方便性提升到一个更高的程度。


先回顾一下上篇的最终状态:


现在我们再来给这个页面加上两个非常重要的功能,一个是设定发票的分类,一个是勾选发票后自动计算已选发票的总金额,这能大幅提高我们处理发票时的效率。

设定发票分类的话,Django Admin有个功能叫action,可以用于在admin页面上添加一个“快捷动作”,我们可以用它来实现。用法很简单,只需要写一个符合Django参数要求的函数并将它扔进一个列表中,再把这个列表设置给admin类的actions配置项即可。

这个有个小问题,action没有办法便捷地处理需要携带参数的情况,官方给出的解决方案是单独写一个中间页,在中间页中进行处理,但我们这种参数可选项不多的情况,如果单独写一个中间页的话好像有点麻烦?而如果复制粘贴直接写一堆函数的话好像又太长了,改起来麻烦、代码也很冗余?

没关系,我们可以用个骚操作来比较方便地解决,毕竟这只是个用来管理发票的工具而已,稍微用用骚操作也不是什么大问题。Python作为一个动态语言,可以很轻松地做到动态创建函数这种骚操作,只需要像这样就可以在代码运行时动态地创建一个函数了:

func = FunctionType(compile(    (f"def replace_category_to_{category}(modeladmin, request, queryset):\n"     f"    queryset.update(category='{category}')\n"     f"    return "),    "",    "exec").co_consts[0], globals())

然后我们就可以基于这个骚操作,把actions自动地创建出来,像这样:


注:这里面的short_description是用来设置它在页面上显示的名称的,如果没有设置的话就会是函数名。

现在我们再刷新页面,就能得到这样的几个按钮了:


注:按钮是simpleui库带来的效果,原始的Django Admin会展示成一个下拉框和一个执行按钮。

接着是勾选发票后自动计算已选发票的总金额,这个要做的话也很简单,我们只需要用JS直接监听复选框勾选后对页面DOM的修改即可。

打开浏览器开发者工具定位到复选框附近的元素并操作几下复选框就会发现:全选时id为action-toggle的那个元素会被修改、单选时class为action-select的那个元素会被修改。所以我们可以基于这两个变化,写一段这样的JS代码:

$("#action-toggle,input[class='action-select']").bind("change", function (e){    let sum_price = 0;    $("#result_list > tbody > tr[class*='selected'] > td[class='field-price']").each(function(){        sum_price += parseFloat($(this).text());    })    console.log(sum_price)})

这段代码用了一个叫jQuery的库,在前端领域里很常见,代码的意思是先对那两个元素设置个监听器,一旦change事件发生时就运行后面那个函数里的代码;后面那个函数里的代码其实就是取出每一个被选中项价格部分的值,转成float之后往sum_price变量里加。

我们可以在浏览器开发者工具的Console中进行测试,结果大致如下,每次点击时都会自动把当前选中的发票计算个总和:


那么我们需要怎么把这段JS代码放进页面里呢?只需要用Django的模板功能就好了。

注:由于我们用了simpleui库,所以有些被simpleui修改过的页面需要以simpleui的页面为基准进行操作,具体请参考simpleui的文档和对应的模板文件源码。

我们直接从admin.py导入django.contrib.admin的地方按鼠标中键点进去,然后在Pycharm上方的路径栏中找到admin目录下的templates/admin目录,在这里我们能看到一堆的.html文件。

接着,我们在浏览器中通过浏览器开发者工具随便选中一个列表中的发票的任意一个DOM元素,并挑一个特征出来(比如前面提到的ID),再用Pycharm的“在文件中查找”功能搜索,就可以找到页面中对应的那部分HTML了。

当然,也可以直接在Django的文档中查看admin的模板部分,里面也有说明每个.html文件分别对应哪个部分。

我这里是想把合计的金额展示在按钮那一排,所以最终找到的是actions.html这个文件。

找到对应的模板文件后,我们在manager目录下新建一个templates目录,然后再在这里面新建一个admin目录,最后再在admin目录里面创建一个manager目录,并新建与前面那个模板文件同名的文件。这么做的目的是为了限定这个修改的适用范围,避免后续我们需要弄别的admin页面或将这个manager应用集成到其他Django项目中时,把修改也带到其他admin页面上。

然后我们可以通过Django的模板语法中的include标签来引用原始的actions.html,接着我们在后面写的HTML就会直接在加载页面时被添加到原始模板文件的最后面了。

现在需要的就是写HTML了,我们加个span标签,给它设置个ID和喜欢的样式;

再加个script标签,把前面写好的JS代码放进去,并添加上一行用于修改那个span的text值的代码(比如$("#sum_price_val").text("共计:" + sum_price + "元"));

噢对了,由于这个模板文件的所处位置实际上是在列表页的开头部分,所以其中的JS代码会在列表中的内容加载完成前就执行,所以我们还需要让这段代码能在页面完全加载后才执行,这里可以使用jQuery库的$(document).ready来实现。

最后我们这个actions.html的内容差不多长这样:


然后就可以得到这样的效果:


再回顾一下上篇开头定下的需求列表,功能其实已经完成得差不多了,发票自动识别、分类、总额计算、去重都已经完成,现在剩下的主要功能就是批量打印和导出发票了。

批量打印的话需要先对PDF进行合并,将多个发票PDF合并到一张,这样我们就可以只调用一次打印机但打印多张发票了。

写起来也很简单,直接用一个名为PyPDF4的库来实现即可,使用它只需要像这样就能快速实现合并一个目录下所有发票的效果:

from pathlib import Pathimport PyPDF4 as pdfinvoices_path = Path("./invoices")merger = pdf.merger.PdfFileMerger()for path in invoices_path.iterdir():    if path.suffix != ".pdf":        continue    merger.append(path.open("rb"))merger.write("output.pdf")

导出发票的话就直接用Python自带的zipfile就好了,也很简单,这里就不演示了。

那么这两个功能在admin页面里应该如何实现呢?还是一样,添加一个action即可,只不过这里我们需要让action被触发时先跳到一个中间页面,然后中间页面中通过JS来请求两个接口分别做到返回合并后的PDF文件以及返回打包好的压缩包,接着再自己跳回原页面。

具体实现起来就像这样:

首先,我们先弄这个中间页面,这个中间页面我们可以用到Django的模板渲染功能,这样我们就能方便地将被选中的发票ID传给JS,让JS再来进行后续的步骤。

和前面弄分类设置一样,在manager/templates目录下直接新建一个HTML文件,这里我新建的文件名为use_invoices.html


在模板文件中,我们可以通过{{ invoice_ids }}这样的语法来表示这里需要填充进一个值作为模板中的文本,效果类似于Python的format;还可以通过{% url "manager:merge_invoices" %}这样的语法来表示这里需要填充进这个Django项目下manager应用中名为merge_invoices的View(视图/页面)的URL。

然后我们可以通过用JS的window.open来打开一个新页面(标签)、window.location.href来设置当前页(标签)的URL并刷新、document.referrer来取到上一页的URL。

所以最终这个模板文件的代码会像这样:


这样我们就可以在action函数中调用模板渲染器渲染这个模板页面,并传入选中的发票ID以将发票ID传给JS了。

JS中会把ID赋值给params参数,并做个URL中请求参数部分的拼接,接着分别打开两个页面,再将当前页跳回上一页并刷新。这样我们就可以做到触发action之后自动打开两个新页面并刷新发票列表页的效果了。

然后我们回到admin.py,导入render函数from django.shortcuts import render用来渲染模板。接着再和前面一样,写一个函数用来做action具体的事务。

action函数的queryset参数是可以用来取到被选中对象以及其具体属性的,只需要调用values_list方法并传入属性名就可以直接取到属性列表了,这里我们像这样取出所有被选中的发票的id:queryset.values_list('id', flat=True)

注:flat参数表示返回的结果是单个值,而不是一个元组,设为True之后我们处理起id这种每个对象只有一个值的属性时就会方便些。

所以最终这个action函数会像这样 :


先取到被选中发票对象的ID列表,然后再用Django的ORM一一取到它们的对象实例,然后将used修改为True以标识已使用,并调用save函数保存到数据库中;接着通过str.join来拼接成一个1,2,3这样的字符串,再传给render函数作为invoice_ids参数对前面写好的模板文件进行渲染。

注:还是和之前的action一样,可以通过use_invoices.short_description = "使用这些发票"这样的操作来为这个action命名,在网页上看起来会舒服一些。

写好action函数后记得将它加到Admin类下的actions列表中,接下来我们需要写一下前面在JS中定下的那两个接口的View。

View是Django中的视图的概念,它能接收HTTP请求并且将return的内容作为响应返回,只需要创建一个HttpResponse(或其他Response,具体参考Django文档)实例填好对应的参数并return就可以了,搞不明白的话可以简单理解为一个View就是一个页面、返回的Response就是页面的内容。

打开manager/views.py定义两个对应名字的函数,并给它们加入名为request的参数,就创建好了两个View。

在View中,我们可以通过request.GET["ids"]来取到URL中名为ids的参数,然后接下来就是对这些发票进行处理了。

先写合并发票PDF的View,根据前面所说的方法取到id后调用PyPDF4库进行处理即可,差不多像这样:


这里我把之前写的那个导入发票的脚本中用到的INVOICES_PATH移到了invoice_manager/settings.py中,以避免无意义的重复代码;

然后用了io.BytesIO创建了一个虚拟的IO对象,这个IO对象就类似于open(file)后得到的那个一样,只不过它的内容是存在内存中的,这样可以避免不必要的磁盘读写;

接着就是取了id之后用str.split转成了列表,并拼接出对应的发票文件路径,再调用PyPDF4库进行合并;

最后,创建一个HttpResponse,把IO对象中的内容(bytes)放进去,并设置响应头中的content_type为PDF文件对应的那个,这样可以让浏览器打开后直接显示出一个预览页面,而不是作为普通的文件被自动下载。

接着写打包发票的View,和前面这个差不多,只不过是换成了用zipfile库打包,并且把content_type改成了ZIP文件对应的,然后设置了一下返回的文件名以便浏览器在自动下载、保存时能保存成正确的名字。代码差不多像这样:


那么现在我们两个接口的View就都写好了,只需要再做两个简单的配置就可以让它跑起来看效果了。

我们在manager目录下新建一个名为url.py的文件,然后在里面像这样配置一下urlpatterns


上面导入的东西就不用多说了吧?大同小异,直接看下面给path函数提供的参数。

第一个参数的意思是这个View在URL中的路径,这里写的是manager应用下的相对路径,Django会按照层级,依次将每个应用中定义的path一级一级拼接下去;

第二个参数就是这个View的函数了,没什么好说的;

第三个参数是这个View的名字,这个需要与前面在模板文件中使用的那个名字对应上,否则Django会因为找不到对应的View而报错。

接着打开invoice_manager/url.py,在urlpatterns里加上引用manager应用的URL配置,像这样:


注:这里的第一个参数也是相对路径,如果这是最上一级的路径的话,那么最后拼接出来的就是像manager/merge_invoices这样的路径;而如果上面还有层级的话,最后拼接出来的就会是balabala/.../manager/merge_invoices这样的。

现在可以跑起来看一下了,如果不出意外的话你会看到这样的效果:


点击“使用这些发票”按钮后,会瞬间打开两个页面,一个开始了自动下载,另一个则是发票PDF的预览:




在点击预览页右上角的打印机按钮后,就会弹出打印设置了,直接选择打印机打印,就可以一次性打出多张发票了。

而那个压缩包,打开后也可以看到,里面是整整齐齐的发票PDF文件,都是按照指定规范命名好的,直接整个压缩包扔给财务就好了,很方便。


那么最后剩下的抬头和税号检测和自动排除已使用的发票这两个功能,我就不再废话了。毕竟前者直接在导入发票的脚本中判断一下即可,后者则是在选择发票时直接通过Django Admin的筛选功能来筛选未使用发票就可以了,非常简单。如果你不会做的话,可以直接参考我放在GitHub上的源码。


发送消息「发票管理工具」到公众号「NightTeam」即可获取项目开源地址,欢迎提出自己的需求或自行开发自己所需的功能并合并进来,让我们一起将这个工具变得更方便吧~

js制定一个单选按钮_【下】每个月整理发票太头疼?手把手教你快速开发一个工具解决!...相关推荐

  1. 手把手教你快速搭建一个代码在线编辑预览工具

    简介 大家好,今天我跟大家分享的是一个代码在线编辑预览工具的实现教程,手把手教你完成这样一个项目. 目前这类工具使用很广泛,常见于各种文档网站及代码分享场景,相关工具也比较多,如codepen.jsr ...

  2. 手把手教你快速构建一个企业自有“微信”

    超链接实验室,是融云策划推出的 IT 系列直播课,携手行业专家,一起聊聊 IT 国产化.协同办公通信.通信中台.企业数字化的那些事儿.关注[融云 RongCloud],了解协同办公平台更多干货. 后疫 ...

  3. 教你快速开发一个微信小游戏好友排行榜

    概述 ​ 要做这个好友排行榜.必然要有好友的战绩比分,然后再做排序,最后将数据呈现在UI上 , 可以分为下面几个步骤: 保存每个用户的分数 获取好友列表,并获取好友的分数 渲染排行榜 保存每个用户的分 ...

  4. 手把手教你快速打造一个AI识物点读机

    0 项目背景 "六·一"儿童节到了,献上一个识物读英文的AI点读机作为一个节日礼物. 在完成前面几个"点读"相关项目后,我们会发现,其实从pipeline上看, ...

  5. 手把手教你快速创建一个超高性价比弹性云服务器

    近些年来,我们能明显的感受到物价.尤其是房价上涨的迹象.本来就不容易鼓起来的钱袋子,在高昂的房价压力下显得更加瘪了.但你听过网络的"房屋空间"吗?即云服务器,近些年却在不断降低成本 ...

  6. 教你快速开发一个 狼人杀微信小程序(附源码)

    点击关注公众号,利用碎片时间学习 一.项目展示 狼人杀是一款多人参与的,通过语言描述推动.较量口才和分析判断能力的策略类桌面游戏 玩家人数适于4-18人参与 主要角色有:狼人 .预言家 .平民 .女巫 ...

  7. 微信小程序傻瓜制作_微信小程序模板制作:手把手教你做一个生鲜小程序

    传统线下生鲜水果类商家如今正面临诸多问题,包括服务范围有限.客户源不稳定.缺少订单导致新鲜食材过期等等.新零售概念的提出,很多商家虽然懂得要打通线上渠道,但通常都是仅仅局限于普通O2O外卖平台,其实这 ...

  8. 设计模式之美-11| 实战一(下):如何利用基于充血模型的DDD开发一个虚拟钱包系统?

    十一.设计模式之美-11| 实战一(下):如何利用基于充血模型的DDD开发一个虚拟钱包系统? 一.钱包业务背景介绍 一个简单的钱包业务功能如下 1.充值 用户通过三方支付渠道,把自己银行卡账户内的钱, ...

  9. 智能机器人c语言编程,【图片】教你快速制作一个简单的人工智能机器人(懂编程的人进来看)【科幻吧】_百度贴吧...

    该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 教你快速制作一个简单的人工智能机器人(懂编程的人进来看) 本文我首发于"人工智能吧",叫做<机器人理解人类语言>和< ...

最新文章

  1. RDKit | 基于RDKit探索ChEMBL数据库中合成药物历史
  2. MOSS SDK学习笔记系列文章
  3. java学习(108):StringBuilder连接字符串和删除操作
  4. 亲身经历之微信支付沙箱环境扫码支付遇到的那些坑
  5. miniui 查询_JQueryMiniUI按照时间进行查询的实现方法
  6. Fiddler设置断点(一)
  7. 【matplotlib笔记】柱形图、直方图、散点图、饼图以及叠加图绘制
  8. Memcached的几种Java客户端(待实践)
  9. LAMP架构之编译安装MySQL和PHP
  10. python token flask_flask 实现token机制
  11. 父与子一起学python3_父与子的编程之旅(与小卡特一起学Python第3版全彩印刷)/图灵程序设计丛书...
  12. 用外挂只为“吃鸡”成功?为什么不试试正当手段!
  13. MMDetection2.XX-Backbone之ResNet源码最全解析
  14. Halcon算子实现——Texture_Laws
  15. Meatycake,51nod2117,树状数组
  16. RN综合演练,仿美团电商(谢谢你的STAR)
  17. 【单片机毕业设计】【mcuclub-304】智能保温杯 | 语音智能水杯 | 恒温杯 | 多功能水杯
  18. Android参考之代号、标签和版本号
  19. PAT甲级英文单词整理
  20. linux原生安装postgresgl

热门文章

  1. 移动推送消息送达常见问题与解决办法
  2. 【 CDN 最佳实践】CDN 命中率优化思路
  3. Fortinet SD-Branch保障医疗服务机构安全组网
  4. 都道业务提升坑大事儿多,但英特尔云方案却说“简单”
  5. 将信息系学生的计算机文化学,计算机等级考试(国家)-关系数据库操作语言sql(四)...
  6. python读取oracle数据到hvie parquet_创建Hive表来从parquet / avro模式读取parquet文件
  7. Linux centos7 安装 MySQL5.7.x
  8. SpringBoot2 集成 xxl-job任务调度中心_路由策略
  9. zookeeper 单机和集群搭建(windows环境+linux环境)
  10. RabbitMQ非root用户安装(Linux环境)