本文是一位拥有10年软件开发经验的程序员所写,他经历了从后端到前端再到全栈的开发历程,并感受到行业的兴衰。他在2023年7月开始接触金蝶ERP的开发,通过学习和实践,构建了自己的经验性二开底层。文章主要介绍了他用Python进行金蝶ERP二次开发的过程,包括引入必要的库和文件、解释代码、提供数据库查询和操作的示例、以及控制单据体和服务器请求的代码片段。
我是一个从事软件开发10年程序员,从后端做到前端,再到全栈一站式程序员,见过行业风口,创业开过公司,也经历过低谷,人也到中年,三十五岁,却感受到了这个行业的兴衰,程序开发这东西,建构在中间层东西,总像逆水行舟,稍微不学习就被新的技术开发顶替,也不知道自己还能保持一直学习的状态再坚持几年,总感觉脑子不够用。所以自己写这个文章,也是为了证明这个体系,我来试过。
2023年7月接触金蝶ERP的开发,从什么都不会,到2023年9月份正式上系统跟着公司找的外包项目上线运用小组,经历了不少。大大小小二开做了不少。做了归纳,也构建了自己的经验性的二开底层。不一定适合所有人,如果你一开始就用,那就一定适合。正如标题”用python完成融合二开99%的开发任务“,说到底其实就是自己懒,不想用C#,编译替换dll配置,这个样的开发模式,对一个一直跑的生产环境,一个人开发是灾难性的。
先说我自己的对金蝶python开发的理解,就是BOS开发工具,设置插件配置写入一段python代码保存,按事件函数跑一圈,相关就执行。知道原理,其实事情就好办了,就是去写对应的事件代码。而这些事件,就是程序员二开的主要内容。逛遍社区,其实不难发现,出现最多的就是,函数事件、sql、和数据结果的处理。我本次内容就是针对这部分,做自己的方法的重构。把这个sql和数据结果,变成系统性方法,方便开发单据和简单报表。下面贴代码了,简单做解释:
import clr import sys clr.AddReference('mscorlib') clr.AddReference('System') clr.AddReference('System.Data') clr.AddReference("System.Web.Extensions") clr.AddReference('Kingdee.BOS') clr.AddReference('Kingdee.BOS.Core') clr.AddReference('Kingdee.BOS.App') clr.AddReference('Kingdee.BOS.App.Core') clr.AddReference('Kingdee.BOS.ServiceHelper') clr.AddReference("Kingdee.BOS.ServiceFacade.Common") clr.AddReference('Kingdee.BOS.DataEntity') clr.AddReference("Newtonsoft.Json") from Kingdee.BOS import * from Kingdee.BOS.Util import * from Kingdee.BOS.Core import * from Kingdee.BOS.Core.Bill import * from Kingdee.BOS.Core.List import * from Kingdee.BOS.Core.Permission import * from Kingdee.BOS.Core.DynamicForm import * from Kingdee.BOS.Core.DynamicForm.PlugIn import * from Kingdee.BOS.Core.DynamicForm.PlugIn.ControlModel import * from Kingdee.BOS.Core.DependencyRules import * from Kingdee.BOS.App.Data import * from Kingdee.BOS.Orm.DataEntity import * from Kingdee.BOS.ServiceFacade import * from Kingdee.BOS.KDThread import * from System import * from System.Data import * from System.Threading import * from System.IO import * from System.Net import * from System.Text import * from System.Security.Cryptography import * from System.Web.Script.Serialization import * from System.Collections.Generic import List from System.Collections.Generic import Dictionary from Kingdee.BOS.ServiceHelper import * from Newtonsoft.Json.Linq import * from Kingdee.BOS.Core.Report import * def require(p,root_path = r'D:\files\python'): read=''; pyfile=Path.Combine(r'Z:\python',p); exs=File.Exists(pyfile); if not exs: pyfile=Path.Combine(root_path,p); exs=File.Exists(pyfile); if not exs: return ''; sr = StreamReader(pyfile,Encoding.Default); read=sr.ReadToEnd(); sr.Close(); return read.replace('\r',''); exec(require('odbc.py')); exec(require('common.py'));
引入文件,基本上是必须的,按需求可以去掉部分引入,不懂都是干嘛的,可以直接粘贴。
def require(p,root_path = r'D:\files\python'): read=''; pyfile=Path.Combine(r'Z:\python',p); exs=File.Exists(pyfile); if not exs: pyfile=Path.Combine(root_path,p); exs=File.Exists(pyfile); if not exs: return ''; sr = StreamReader(pyfile,Encoding.Default); read=sr.ReadToEnd(); sr.Close(); return read.replace('\r',''); exec(require('odbc.py')); exec(require('common.py'));
解释一下这一块,就是对自己封装的python类进行引入,具体路径就是Z:\python和D:\files\python,前者是加速读写盘,优先读取,负载繁重的系统才需要。正常python类文件放D:\files\python即可。
obdc.py文件内容见附件:
写不下。本文最重要的内容,精华都在这个文件里。
common.py文件内容见附件:
写不下。
以上两个文件的基本使用方法简单例子,如果有疑问可以留言我在做详细解释。
ls=Db.name('T_PRD_INSTOCKENTRY').noas('t').field('t.FREALQTY,t2.FCONVERT,t2.FNUMBER').join('T_PRD_INSTOCK t1','t.FID = t1.FID').join('T_BD_MATERIAL t2','t.FMATERIALID = t2.FMATERIALID').where(""" AND t2.FNUMBER LIKE '%{2}%' AND t1.FCREATEDATE>'{0}' AND t1.FCREATEDATE<'{1}'""".format(start,end,likeAorB)).where({"t1.FPRDORGID":og.Id,"t1.FDOCUMENTSTATUS":"C","t1.FISREWORK":0}).select(); ls=Db.name('T_PRD_INSTOCKENTRY').noas('t').field('t.FREALQTY,t2.FCONVERT,t2.FNUMBER').join('T_PRD_INSTOCK t1','t.FID = t1.FID').join('T_BD_MATERIAL t2','t.FMATERIALID = t2.FMATERIALID').where(""" AND t2.FNUMBER LIKE '%B%' AND t1.FCREATEDATE>'{0}' AND t1.FCREATEDATE<'{1}'""".format(start,end)).where({"t1.FPRDORGID":og.Id,"t1.FDOCUMENTSTATUS":"C","t1.FISREWORK":0}).select(); def BarItemClick(e): if e.BarItemKey=="debug": Db=ODBC('XY_EWX_USEROPENTRY'); Db.log(this.Context.UserId); Db.rlog(); if e.BarItemKey=="clearcache": Db=ODBC('XY_EWX_USEROPENTRY'); Db.where("AND FEntryID>0 AND FOPTIME<'{0}'".format(DateTime.Now.AddDays(-int(cer)))).delect(); Db.name('XY_EWX_USERENTRY').where("AND FEntryID>0 AND FTIME<'{0}'".format(DateTime.Now.AddYears(-int(ler)))).delect(); this.View.ShowMessage('清除成功');
简单账表的服务器插件例子见附件:
写不下。
再提供几个选择使用的文件:
entry.py控制单据体代码:
def upMoveDown(isMoveUp=True,entryKey='FEntity'): entryEntity = this.View.BusinessInfo.GetEntryEntity(entryKey); entityDataObject = this.View.Model.GetEntityDataObject(entryEntity); rowIndex = this.View.Model.GetEntryCurrentRowIndex(entryKey); length=len(entityDataObject); if isMoveUp: if (rowIndex==0): return; else: movindex=rowIndex-1; else: if (rowIndex==length-1): return; else: movindex=rowIndex+1; this.View.SetEntityFocusRow(entryKey, movindex); this.View.Model.CopyEntryRow(entryKey,rowIndex,-1); this.View.Model.CopyEntryRow(entryKey,movindex,-1); Entity = this.View.BusinessInfo.GetEntryEntity(entryKey); Object = this.View.Model.GetEntityDataObject(Entity); lgt=len(Object); initDir=["Seq","BeginInit","DataEntityState","EndInit","Equals","Events","GetDataEntityType","GetHashCode","GetType","Initialized","IsInitialized","MemberwiseClone","OnInitialized","OnPropertyChanged","OnPropertyChanging","Parent","PropertyChanged","PropertyChanging","ReferenceEquals","ToString","__class__","__delattr__","__doc__","__format__","__getattribute__","__hash__","__init__","__new__","__reduce__","__reduce_ex__","__repr__","__setattr__","__sizeof__","__str__","__subclasshook__"] dirs = dir(Object[lgt-1]); for key in dirs: if key not in initDir: this.View.Model.SetValue(key,Object[lgt-1][key],rowIndex); dirs = dir(Object[lgt-2]); for key in dirs: if key not in initDir: this.View.Model.SetValue(key,Object[lgt-2][key],movindex); this.View.Model.DeleteEntryRow(entryKey,lgt-1); this.View.Model.DeleteEntryRow(entryKey,lgt-2); def createRow(key,lans,bat=True): entity = this.View.BusinessInfo.GetEntity(key); entryDy = this.View.Model.GetEntityDataObject(entity); entryLen=len(entryDy); if entryLen<lans and bat: this.Model.BatchCreateNewEntryRow(key,lans-entryLen); else: lens=int(entryLen/10); lens=lens if lens>10 else 10; for i in range(lens): for j in range(lans,entryLen+1): this.Model.DeleteEntryRow(key, j);
http.py控制服务器发起请求的类:
class HTTP(): version='1.0.5'; headers={}; options={}; def __init__(self,headers={}): self.headers=headers; self.initOptions(); return self; def initOptions(self): self.options={'headers':self.headers,'ContentType':'application/json','Timeout':30000}; def header(self,headers): self.options['headers']=headers; return self; def timeout(self,t): self.options['Timeout']=t; return self; def contype(self,t): self.options['ContentType']=t; return self; def get(self,url): webRequest = WebRequest.Create(url) webRequest.Method = "GET"; webRequest.Timeout = self.options['Timeout']; if self.options['headers']<>{}: for key,val in self.options['headers'].items(): webRequest.Headers.Set(key, val); webResponse = webRequest.GetResponse(); stream = webResponse.GetResponseStream(); streamReader =StreamReader(stream, Encoding.GetEncoding("utf-8")) result = streamReader.ReadToEnd(); if not result: return False; else: res=self.decode(result); if not res: return result; else: return res; def post(self,url,postdata=''): if not isinstance (postdata,str): postdata=self.encode(postdata); webRequest = HttpWebRequest.Create(url); webRequest.Method = "POST" webRequest.ContentType = self.options['ContentType']; webRequest.Timeout = self.options['Timeout']; if self.options['headers']<>{}: for key,val in self.options['headers'].items(): webRequest.Headers.Set(key, val); data=Encoding.UTF8.GetBytes(postdata); webRequest.ContentLength = data.Length; webRequest.GetRequestStream().Write(data, 0, data.Length) webRequest.GetRequestStream().Flush() webRequest.GetRequestStream().Close() webResponse =webRequest.GetResponse() streamReader =StreamReader( webResponse .GetResponseStream(),Encoding.GetEncoding("utf-8") ) result=streamReader.ReadToEnd() if not result: return False; else: res=self.decode(result); if not res: return result; else: return res; def encode(self,obj):#json_encode jsonSerializerProxy=JsonSerializerProxy(Encoding.GetEncoding("utf-8"),False) return jsonSerializerProxy.Serialize(obj) def decode(self,s):#json_decode return JObject.Parse(s); def time(self,time=DateTime.Now): ts = time - DateTime.Parse('1970-01-01 08:00:00'); return Convert.ToInt64(ts.TotalMilliseconds)/1000;
延续的开发模式还可以衍生,金蝶做电脑端,利用webapi,和金蝶的phpapi文件,开发的H5端控,实现手机端直接下推单据,增删改查,不外挂另外的控制后台。这是我开发简单mes系统。上一些相关图,可以看看。
以上是工作需要,开发的内容,只是分享,也希望有人能看到。人生也迷茫的,每个行业都每个行业的规则,有站在转变的档口上了,不知道走向那个方向了。有的人说是Ai,也做过开发,也试过融合,可能是自己的能量太过微小。迷茫归迷茫,对我自己价值来说,总要有个选择,目前选择了手机游戏行业。自己刚刚构建好一套自己的threejs的游戏开发底层框架,内容正在开发,如果能走下去话,我对金蝶,这篇文章是我的留恋。说心里话,金蝶挺好,系统给了足够的生态,足够的自由,也包括外包团队的人,他们都是活力满满的,很积极向上,和他们接触很开心。
Desktop.zip(8.37KB)