Difference between revisions of "ZH/Case-Study-01-Journal-31"
From ADempiere
This Wiki is read-only for reference purposes to avoid broken links.
(→2011-01-04) |
(→Step 2.写 Process 脚本) |
||
(15 intermediate revisions by 3 users not shown) | |||
Line 134: | Line 134: | ||
==2011-01-03== | ==2011-01-03== | ||
− | + | ===讨论-窗体保存新增记录会调用什么方法=== | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
* 向大家请教一个问题:Requisition窗体保存新增记录时,会调用什么方法? | * 向大家请教一个问题:Requisition窗体保存新增记录时,会调用什么方法? | ||
:-- Peanut Blake, Jan 03, 2010. | :-- Peanut Blake, Jan 03, 2010. | ||
Line 173: | Line 147: | ||
===实现从RfQ生成Requistion=== | ===实现从RfQ生成Requistion=== | ||
* 已经测试通过。 | * 已经测试通过。 | ||
+ | * 去掉原方案 ''Step 2.新建一个中间表 Assist_Target_ID'',改为从 ad_sequence 取ID值。 --Peanut Blake. 07 January 2011. | ||
====Step 1.新建 Process ==== | ====Step 1.新建 Process ==== | ||
* 新建 Process: | * 新建 Process: | ||
Line 180: | Line 155: | ||
* 实现如下界面: | * 实现如下界面: | ||
[[Image:Script_Process_M_RfQ_Requisition_Create_03_Window.png]] | [[Image:Script_Process_M_RfQ_Requisition_Create_03_Window.png]] | ||
− | ====Step 2 | + | ====Step 2.写 Process 脚本==== |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
* 通过Rule引擎写脚本。 | * 通过Rule引擎写脚本。 | ||
* Search Key: beanshell:M_RfQ_Requisition_Create | * Search Key: beanshell:M_RfQ_Requisition_Create | ||
Line 203: | Line 163: | ||
* beanshell:M_RfQ_Requisition_Create | * beanshell:M_RfQ_Requisition_Create | ||
* Create Requisition from RfQ | * Create Requisition from RfQ | ||
− | * Last update: Peanut Blake. Jan | + | * Last update: Peanut Blake. Jan 07, 2010. |
* Created: Peanut Blake. DEC 30, 2010. | * Created: Peanut Blake. DEC 30, 2010. | ||
* */ | * */ | ||
− | /* Version | + | /* Version 2.0*/ |
import org.compiere.util.*; | import org.compiere.util.*; | ||
− | |||
− | |||
String result_msg; | String result_msg; | ||
Line 216: | Line 174: | ||
/* Check conditions */ | /* Check conditions */ | ||
if( P_C_RfQ_ID==void || P_M_PriceList_ID == void || (P_Requisition_No_A == void && P_Requisition_No_B ==void)) { | if( P_C_RfQ_ID==void || P_M_PriceList_ID == void || (P_Requisition_No_A == void && P_Requisition_No_B ==void)) { | ||
− | result_msg="Please fill the required fields!"; | + | result_msg="请您填写相关字段! Please fill the required fields!"; |
javax.swing.JOptionPane.showMessageDialog(null,result_msg); | javax.swing.JOptionPane.showMessageDialog(null,result_msg); | ||
return ""; | return ""; | ||
Line 225: | Line 183: | ||
String brand=""; | String brand=""; | ||
String vendor=""; | String vendor=""; | ||
+ | String vendorname1=""; | ||
+ | String vendorname2=""; | ||
+ | String price1=""; | ||
+ | String price2=""; | ||
Integer assist_mid_id_head, assist_mid_id_line; | Integer assist_mid_id_head, assist_mid_id_line; | ||
for ( int i=1; i<=2 ; i++ ){ | for ( int i=1; i<=2 ; i++ ){ | ||
Line 232: | Line 194: | ||
brand="brand"; | brand="brand"; | ||
vendor="A"; | vendor="A"; | ||
+ | vendorname1="c_bpartner_id"; | ||
+ | vendorname2="c_bpartner_id_2"; | ||
+ | price1="price"; | ||
+ | price2="price2"; | ||
} | } | ||
else | else | ||
Line 241: | Line 207: | ||
brand="brand2"; | brand="brand2"; | ||
vendor="B"; | vendor="B"; | ||
+ | vendorname1="c_bpartner_id_2"; | ||
+ | vendorname2="c_bpartner_id"; | ||
+ | price1="price2"; | ||
+ | price2="price"; | ||
} | } | ||
else | else | ||
continue; | continue; | ||
− | } | + | } |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
/* INSERT m_requisition */ | /* INSERT m_requisition */ | ||
Line 258: | Line 221: | ||
+ " m_warehouse_id, purchasingstaff_id, priorityrule, daterequired, docaction,docstatus,c_doctype_id, isapproved, " | + " m_warehouse_id, purchasingstaff_id, priorityrule, daterequired, docaction,docstatus,c_doctype_id, isapproved, " | ||
+ " processing, processedon, c_bpartner_id_a,c_bpartner_id_b ) " | + " processing, processedon, c_bpartner_id_a,c_bpartner_id_b ) " | ||
− | + " SELECT ( SELECT | + | + " SELECT (SELECT currentnext FROM ad_sequence WHERE name='M_Requisition') ," |
+ A_AD_Client_ID + "," + G_AD_Org_ID + "," + A_AD_User_ID + "," + A_AD_User_ID + ",'" + req_no + "',"+P_C_RfQ_ID | + A_AD_Client_ID + "," + G_AD_Org_ID + "," + A_AD_User_ID + "," + A_AD_User_ID + ",'" + req_no + "',"+P_C_RfQ_ID | ||
+", (c_rfq.Name || c_rfq.Description) , c_rfq.Help ," + P_M_PriceList_ID + "," | +", (c_rfq.Name || c_rfq.Description) , c_rfq.Help ," + P_M_PriceList_ID + "," | ||
+ G_M_Warehouse_ID +","+ A_AD_User_ID + ", 5, now(), 'CO','DR',127, 'N'," | + G_M_Warehouse_ID +","+ A_AD_User_ID + ", 5, now(), 'CO','DR',127, 'N'," | ||
− | +"'N',0,c_rfq. | + | +" 'N',0,c_rfq." + vendorname1 + ",c_rfq."+ vendorname2 |
− | + "FROM c_rfq WHERE c_rfq_id=" + P_C_RfQ_ID + " AND ad_client_id=" + A_AD_Client_ID + ";"; | + | + " FROM c_rfq WHERE c_rfq_id=" + P_C_RfQ_ID + " AND ad_client_id=" + A_AD_Client_ID + ";"; |
System.out.println("sql_head: "+sql_head); | System.out.println("sql_head: "+sql_head); | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
/* INSERT m_requisitionline*/ | /* INSERT m_requisitionline*/ | ||
sql_line="INSERT INTO m_requisitionline (m_requisitionline_id,ad_client_id,ad_org_id,createdby,updatedby,line, " | sql_line="INSERT INTO m_requisitionline (m_requisitionline_id,ad_client_id,ad_org_id,createdby,updatedby,line, " | ||
− | + " m_requisition_id,qty,m_product_id,description | + | + " m_requisition_id,qty,m_product_id,description,c_uom_id,brand,usage, " |
− | + " SELECT ( ( SELECT | + | + " priceactual,price2,linenetamt )" |
+ | + " SELECT (( SELECT currentnext FROM ad_sequence WHERE name='M_RequisitionLine') + ceil(rl.line / 10))," | ||
+ A_AD_Client_ID + "," + G_AD_Org_ID + "," + A_AD_User_ID + "," + A_AD_User_ID + ", rl.line, " | + A_AD_Client_ID + "," + G_AD_Org_ID + "," + A_AD_User_ID + "," + A_AD_User_ID + ", rl.line, " | ||
− | + " ( SELECT | + | + " (SELECT currentnext FROM ad_sequence WHERE name='M_Requisition')," |
− | + "rl.quantity, rl.m_product_id, rl.description | + | + " rl.quantity, rl.m_product_id, rl.description, rl.c_uom_id, rl." + brand + ", rl.purpose, " |
− | + "FROM c_rfqline rl " | + | + " rl." + price1 + ",rl." + price2 + ", round(rl.quantity * rl." + price1 + ", 3) " |
− | + "WHERE (rl.c_rfq_id=" + P_C_RfQ_ID + " AND ad_client_id=" + A_AD_Client_ID + " AND rl.choosequotation='" + vendor + "');"; | + | + " FROM c_rfqline rl " |
+ | + " WHERE (rl.c_rfq_id=" + P_C_RfQ_ID + " AND ad_client_id=" + A_AD_Client_ID + " AND rl.choosequotation='" + vendor + "');"; | ||
System.out.println("sql_line: "+sql_line); | System.out.println("sql_line: "+sql_line); | ||
+ | |||
+ | /* insert assist_target_id for head*/ | ||
+ | sql_assist_head="UPDATE ad_sequence SET updated=now(), currentnext=(SELECT MAX(m_requisition_id) FROM M_Requisition) +1 " | ||
+ | + " WHERE name='M_Requisition';"; | ||
+ | System.out.println("sql_assist_head: "+sql_assist_head); | ||
+ | |||
+ | /* insert assist_target_id for line*/ | ||
+ | sql_assist_line="UPDATE ad_sequence SET updated=now(), currentnext=(SELECT MAX(m_requisitionline_id) FROM M_Requisitionline) +1 " | ||
+ | +" WHERE name='M_RequisitionLine';"; | ||
+ | System.out.println("sql_assist_line: "+sql_assist_line); | ||
− | |||
int no_head = DB.executeUpdate(sql_head, A_TrxName); | int no_head = DB.executeUpdate(sql_head, A_TrxName); | ||
− | |||
int no_line = DB.executeUpdate(sql_line, A_TrxName); | int no_line = DB.executeUpdate(sql_line, A_TrxName); | ||
+ | int no_assist_head = DB.executeUpdate(sql_assist_head, A_TrxName); | ||
+ | int no_assist_line = DB.executeUpdate(sql_assist_line, A_TrxName); | ||
} | } | ||
Line 313: | Line 281: | ||
====二楼-Peanut==== | ====二楼-Peanut==== | ||
* Albert你好!感谢你的关心! | * Albert你好!感谢你的关心! | ||
− | * | + | * 这家公司的采购流程是先询价,将询价结果汇总后,形成采购申请单(列出两家报价对比),由主管批准后,方可下达采购确认单。 |
* 由于我对ADempiere的代码尚不了解,对SQL比较熟悉。为了降低程序调试难度,让项目进度更为可控,因此我目前采用 SQL 来实现。 | * 由于我对ADempiere的代码尚不了解,对SQL比较熟悉。为了降低程序调试难度,让项目进度更为可控,因此我目前采用 SQL 来实现。 | ||
* 将来对 Rule 引擎的脚本编写非常熟悉之后,我将会更多地使用 ADempiere 已经有的 Java 类。 | * 将来对 Rule 引擎的脚本编写非常熟悉之后,我将会更多地使用 ADempiere 已经有的 Java 类。 | ||
Line 328: | Line 296: | ||
===修改Rule引擎脚本默认字符长度限制=== | ===修改Rule引擎脚本默认字符长度限制=== | ||
− | * Rule Engine | + | * Rule Engine 系统默认限制2000字符长度,现改为4500字符。先执行SQL语句,然后进入Table窗口对Script字段进行设置。 |
* 经测试4000字符的脚本,可以正常执行。 | * 经测试4000字符的脚本,可以正常执行。 | ||
* SQL语句如下: | * SQL语句如下: | ||
Line 363: | Line 331: | ||
My Name is (outside if):ADempiere | My Name is (outside if):ADempiere | ||
</pre> | </pre> | ||
+ | ==2011-01-07== | ||
+ | ===冲突:脚本不完善导致 Requisition 窗体新增记录无法保存=== | ||
+ | ====Requisition 页签新增记录保存报错==== | ||
+ | * 01月03日写脚本从 RfQ 询价单自动生成 Requisition 采购申请单,通过 SQL 实现,初步测试通过。 | ||
+ | * 但是进一步测试发现一个问题,通过脚本生成新的采购申请单后,在 Requisition 窗体 Requisition 页签上新建申请单保存失败,错误提示如下: | ||
+ | <pre> | ||
+ | ===========> DB.executeUpdate: INSERT INTO M_Requisition (AD_Client_ID,AD_Org_ID,C_DocType_ID,Created,CreatedBy,DateDoc, | ||
+ | DateRequired,DocAction,DocStatus,DocumentNo,IsActive,IsApproved,M_PriceList_ID,M_Requisition_ID,M_Warehouse_ID,Posted,PriorityRule, | ||
+ | Processed,ProcessedOn,Processing,purchasingstaff_id,TotalLines,Updated,UpdatedBy) VALUES (11,50001,127, | ||
+ | TO_TIMESTAMP('2011-01-03 13:16:25','YYYY-MM-DD HH24:MI:SS'),100,TO_TIMESTAMP('2011-01-03','YYYY-MM-DD'), | ||
+ | TO_TIMESTAMP('2011-01-06','YYYY-MM-DD'),'CO','DR','TJ005','Y','N',101,1000002,50002,'N','5','N',0,'N',1000020,0, | ||
+ | TO_TIMESTAMP('2011-01-03 13:16:25','YYYY-MM-DD HH24:MI:SS'),100) [POSave_a2f6503a-8143-410e-92b3-979f8c5f5b9e] [11] | ||
+ | org.postgresql.util.PSQLException: ERROR: duplicate key value violates unique constraint "m_requisition_pkey"; State=23505; ErrorCode=0 | ||
+ | at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2062) | ||
+ | ... ... | ||
+ | |||
+ | ===========> DB.saveError: DBExecuteError - ERROR: duplicate key value violates unique constraint "m_requisition_pkey" [11] | ||
+ | -----------> MRequisition.saveNew: [POSave_a2f6503a-8143-410e-92b3-979f8c5f5b9e]Not inserted - M_Requisition [11] | ||
+ | -----------> GridTable.saveWarning: SaveErrorNotUnique - ERROR: duplicate key value violates unique constraint "m_requisition_pkey" [11] | ||
+ | </pre> | ||
+ | * '''01月03日的错误分析:''' | ||
+ | <pre> | ||
+ | * 检查后初步认定与 Requisition 页签保存新增记录时 m_requisition_id 的确认方法有关。于是去看窗体保存的代码,我找到 org.compiere.model.I_M_Requisition.java 与 | ||
+ | org.compiere.model.MRequisition.java 。 但是我没有找到有价值的信息,不知道是不是自己没有找对地方。 | ||
+ | |||
+ | * 于是折回去看 m_requisition 表中的数据,对比脚本生成的记录与窗体生成的记录的区别。 | ||
+ | |||
+ | * 结果发现有两个字段存在差异,窗体生成的记录当中 Processing = 'N', ProcessedOn=0,而我在脚本SQL中把这两个字段忽略了(留空)。 | ||
+ | |||
+ | * 于是将所有记录设置为 Processing = 'N'。然后再次测试,OK,问题解决。 | ||
+ | |||
+ | * 看来ADempiere里面暗藏的机关不少呀,还是得小心测试、反复验证为妙。 | ||
+ | </pre> | ||
+ | |||
+ | ====Requisition Line 页签新增记录保存报错==== | ||
+ | |||
+ | * 通过脚本执行 SQL 生成新的 Requisition Line (采购申请单行)后,Requisition 窗体 Requisition Line 页签新增记录无法保存,错误提示如下: | ||
+ | <pre> | ||
+ | ===========> DB.saveError: DBExecuteError - ERROR: duplicate key value violates unique constraint "m_requisitionline_pkey" [11] | ||
+ | -----------> MRequisitionLine.saveNew: [POSave_ea719be5-80db-4f3e-9d45-f0eb9700dac9]Not inserted - M_RequisitionLine [11] | ||
+ | </pre> | ||
+ | |||
+ | * '''01月05日的错误分析:''' | ||
+ | <pre> | ||
+ | * 经检查初步认定与 linenetamt 字段有关。我在脚本SQL中没有设置 linenetamt 字段,数据库采用默认值 0 。于是执行SQL语句: | ||
+ | |||
+ | UPDATE m_requisitionline SET linenetamt = qty * PriceActual; | ||
+ | |||
+ | * 然后再次测试,OK,问题解决。 | ||
+ | </pre> | ||
+ | |||
+ | ====01月07日的重新分析==== | ||
+ | * '''前面01月07日、01月07日这两段分析是错误的,现在发现与数据库的一致性有关。脚本执行SQL语句后再进行 Requisition 窗体新建记录操作时,窗体未能收到有效的数据库更新信息。''' | ||
+ | * 再次查看代码 PO.get_ID(),但是没有查出个来龙去脉。后来上sf.net论坛搜索到相关讨论,发现保存新建记录时是从 AD_Sequence 表的 currentnext 字段来获取,与之前分析的 Processing = 'N'或 linenetamt = 0 没有任何关系,之前测试通过是存在巧合因素。 在脚本中加下两项SQL语句: | ||
+ | <pre> | ||
+ | UPDATE ad_sequence SET updated=now(), currentnext=(SELECT MAX(m_requisition_id) FROM M_Requisition) +1 WHERE "name"='M_Requisition'; | ||
+ | |||
+ | UPDATE ad_sequence SET updated=now(), currentnext=(SELECT MAX(m_requisitionline_id) FROM M_Requisitionline ) +1 WHERE "name"='M_RequisitionLine'; | ||
+ | </pre> | ||
+ | * 再次测试,问题终于解决。 | ||
+ | |||
+ | ====遗留问题==== | ||
+ | * UPDATE 之后 linenetamt 字段出现了一长串的 0,例如 13134.000000000000000000000000,如何才能去掉多余的0? | ||
+ | |||
+ | ===讨论=== | ||
+ | ====一楼-Albert==== | ||
+ | * 台灣技術支持團隊 | ||
+ | * Skype: Adempiere/Compiere 技術轉移顧問 Albert | ||
+ | * | ||
+ | * 向祖國團隊建議 : | ||
+ | * | ||
+ | * Postgres DB/Oracle DB 四捨五入到第二位 | ||
+ | * 但是不建議寫 SQL Insert Statement | ||
+ | |||
+ | UPDATE m_requisitionline SET linenetamt = round(qty * PriceActual, 2); | ||
+ | |||
+ | * 為何要用物件.,因為 MRequisitionLine寫入前會 setLineNetAmt | ||
+ | |||
+ | <pre> | ||
+ | protected boolean beforeSave (boolean newRecord) | ||
+ | { | ||
+ | if (newRecord && getParent().isComplete()) | ||
+ | { | ||
+ | log.saveError("ParentComplete", Msg.translate(getCtx(), "M_RequisitionLine")); | ||
+ | return false; | ||
+ | } | ||
+ | if (getLine() == 0) | ||
+ | 中間刪除不表示 | ||
+ | if (getPriceActual().signum() == 0) | ||
+ | setPrice(); | ||
+ | setLineNetAmt(); | ||
+ | return true; | ||
+ | } | ||
+ | </pre> | ||
+ | |||
+ | <pre> | ||
+ | public void setLineNetAmt () | ||
+ | { | ||
+ | BigDecimal lineNetAmt = getQty().multiply(getPriceActual()); | ||
+ | super.setLineNetAmt (lineNetAmt); | ||
+ | } | ||
+ | </pre> | ||
+ | |||
+ | * 建議事項 : 要記得小數點長度 | ||
+ | |||
+ | <pre> | ||
+ | public void setLineNetAmt () | ||
+ | { | ||
+ | BigDecimal lineNetAmt = getQty().multiply(getPriceActual()); | ||
+ | if (lineNetAmt .scale() > getPrecision()) | ||
+ | lineNetAmt = lineNetAmt.setScale(getPrecision(), BigDecimal.ROUND_HALF_UP); | ||
+ | super.setLineNetAmt (lineNetAmt); | ||
+ | } | ||
+ | </pre> | ||
+ | |||
+ | * 為何要用物件.,因為 MRequisitionLine寫入後會 updateHeader | ||
+ | |||
+ | <pre> | ||
+ | protected boolean afterSave (boolean newRecord, boolean success) | ||
+ | { | ||
+ | if (!success) | ||
+ | return success; | ||
+ | return updateHeader(); | ||
+ | } | ||
+ | </pre> | ||
+ | |||
+ | <pre> | ||
+ | private boolean updateHeader() | ||
+ | { | ||
+ | log.fine(""); | ||
+ | String sql = "UPDATE M_Requisition r" | ||
+ | + " SET TotalLines=" | ||
+ | + "(SELECT COALESCE(SUM(LineNetAmt),0) FROM M_RequisitionLine rl " | ||
+ | + "WHERE r.M_Requisition_ID=rl.M_Requisition_ID) " | ||
+ | + "WHERE M_Requisition_ID=?"; | ||
+ | int no = DB.executeUpdateEx(sql, new Object[]{getM_Requisition_ID()}, get_TrxName()); | ||
+ | if (no != 1) | ||
+ | log.log(Level.SEVERE, "Header update #" + no); | ||
+ | m_parent = null; | ||
+ | return no == 1; | ||
+ | </pre> | ||
+ | ====二楼-Peanut==== | ||
+ | * 非常感谢 Albert 大人耐心详细的解答! 用SQL这个方法的确是很折腾,等采购这块上线试运行了,我就立即着手学习 Callout 。 --Peanut Blake. 07 January 2011. | ||
+ | |||
=链接= | =链接= | ||
[[Category:Chinese]] | [[Category:Chinese]] |
Latest revision as of 20:10, 6 January 2011
Contents
实施日志-31-脚本编写与Rule引擎
- 本文属于机械装备制造业进销存实施案例——ADempiere ERP案例研究之一。
- 本文记录ADempiere ERP项目实施的过程
2010-12-16
Rule Engine与脚本
- 今天开始学习和探索Rule Engine(引擎)的Script(脚本编程)。
- 主要目的:从于从RfQ(询价单)创建Purchase Request(采购申请)。
- 介绍:ADempiere的Rule窗口可以写脚本,集成了BeanShell。
- ADempiere的Rule Engine要用到BeanShell,于是了解了一下BeanShell。
- 相关链接:
讨论栏
2010-12-17
Rule Engine下写脚本
- 今天把Rule Engine(引擎)的Script弄清楚了。
- Script Callout主要用于窗体,Script Process主要用于Process。
- 对于从 询价单 生成 采购申请单,可以利用Script Process执行SQL语句来实现。
org.compiere.util.DB.executeUpdate(sql, A_TrxName);
- 学习了HumanFlash的动画视频,做了两篇笔记。见ADempiere教学动画学习笔记-脚本编写
- 相关链接:
脚本使用体会
- 优点:脚本的功能很强大。同时脚本既不需要重新编译ADempiere源代码,也不需要重新布署ADempiere服务器,因为脚本规则都存储在数据库中了。
- 缺点:不容易进行错误调试。我今天测试的Script当中有个SQL语句语法错误,居然没有任何报错。
对Rule Engine的疑问
- Rule Engine为什么要区分成不同Event Type?比如Callout与Process,为什么不合并在一起?
- Rule Engine的Rule Type为什么是JSR 223 Scripting APIs,而不是BeanShell官方网站上JSR 274?
- A_TrxName的TrxName是什么意思?
- Hengsin的解答:
- TrxName = Transaction Name. ADempiere transaction manager managed transaction via named transaction instead of the thread local transaction approach that J2EE and hibernate transaction manager used.Hengsin 09:09, 28 December 2010 (UTC)
弹出对话框
- Rule Engine中弹出对话框,找到一段代码:
javax.swing.JOptionPane.showMessageDialog(null,"Doing something in Callout!");
- 2010年12月28日测试了一下,在C/S下可以正常弹出,但是在B/S下会报错。
- 能否实现在C/S和B/S都可以弹出对话框?
- Hengsin的解答:
- At the moment, you have to check Ini.isClient() and use reflection to call zkwebui code. Hengsin 09:04, 28 December 2010 (UTC)
2010-12-28
自动选择最低报价
界面
- 今天用Rule引擎编写脚本,实现了一个功能——通过比较价格1与价格2,自动选择最低报价。
- 自动选择最低报价-询价单明细-网格界面:
- 自动选择最低报价-询价单明细-窗体界面
具体实现
- Rule引擎:
- Search Key: beanshell:C_RfQ_AutoChooseLowerPrice
- Name: Auto Choose Lower Price in RfQ
- Description: Auto choose lower price in RfQ Line, by comparing Price 1 with Price 2.
- Comment/Help: Last update: Dec 28, 2010. Ver 1.1. Peanut
- Event Type: Callout
- Rule Type: JSR 223 Scripting APIs
- Script:
int rowCount = A_Tab.getRowCount(); //统计行数 for(int i = 0; i< rowCount; i++){ A_Tab.setCurrentRow(i); //设置为当前行 int iCompare=A_Tab.getValue("Price").compareTo(A_Tab.getValue("Price2")); //比较价格 if (iCompare==-1) A_Tab.setValue("ChooseQuotation", "A"); // Price 1 < Price 2,则选择供应商A else if (iCompare==1) A_Tab.setValue("ChooseQuotation", "B"); // Price 1 > Price 2,则选择供应商B } result = "";
脚本使用问题小结
Yes/No字段
- Yes/No字段不能直接赋值给字符字段,要转为String才行,可以使用方法 Boolean.toString() 。示例:
A_Tab.setValue("Purpose",A_Tab.getValue("SelectBPartnerA").toString());
Number对象的处理
- 对于Number、Cost+Price、Quantity等对象,ADempiere是作为 java.math.BigDecimal 对象处理的。
- BigDecimal 对象不能像 float 一样 BigDecimal_1 >= BigDecimal_2 来进行比较,而是要通过 BigDecimal.compareTo(BigDecimal val) 方法来比较。
- 我一开始是直接比较:
if (A_Tab.getValue("Price") > A_Tab.getValue("Price2") ) A_Tab.setValue("ChooseQuotation", "B");
- 于是得到错误代码:
===========> GridTab.processCallout: [12] javax.script.ScriptException: Sourced file: inline evaluation of: ``int rowCount = A_Tab.getRowCount(); float priceA; float priceB; for(int i = 0; i . . . '' : Invalid types in binary operator : at Line: 8 : in file: inline evaluation of: ``int rowCount = A_Tab.getRowCount(); float priceA; float priceB; for(int i = 0; i . . . '' : ) { in inline evaluation of: ``int rowCount = A_Tab.getRowCount(); float priceA; float priceB; for(int i = 0; i . . . '' at line number 8 at bsh.engine.BshScriptEngine.evalSource(BshScriptEngine.java:92)
- 后来尝试直接转为float:
float priceA=(float) A_Tab.getValue("Price"); float priceB=(float) A_Tab.getValue("Price2");
- 于是得到错误代码:
java.awt.LightweightDispatcher.dispatchEvent(Container.java:4168) caused by: java.lang.ClassCastException: Cannot cast java.math.BigDecimal to float
- 这时才明白ADempiere将 Number 作为 BigDecimal 对象处理的。要想转为 float 的话可以用 BigDecimal.floatValue() 方法。
脚本使用体会
- Script响应速度明显要比编译好的Callout慢。
- Script的测试和排错的确不方便。
2011-01-03
讨论-窗体保存新增记录会调用什么方法
- 向大家请教一个问题:Requisition窗体保存新增记录时,会调用什么方法?
- -- Peanut Blake, Jan 03, 2010.
- 调用 org.compiere.apps.APanel.cmd_save(boolean)
- 在client下面。如果你用Eclipse的话,Ctrl+Shift+T,然后输入类名就可以找到了。
- 而且这个是只在swing客户端下才用的,web client应该是另外一个类。
- 所有的save都从这里过去的。
- 然后实际的save过程,你可以看看PO.save:org.compiere.model.PO.saveNew()
- -- 徐浣泽, Jan 03, 2010.
实现从RfQ生成Requistion
- 已经测试通过。
- 去掉原方案 Step 2.新建一个中间表 Assist_Target_ID,改为从 ad_sequence 取ID值。 --Peanut Blake. 07 January 2011.
Step 1.新建 Process
- 新建 Process:
- 参数设置:
- 实现如下界面:
Step 2.写 Process 脚本
- 通过Rule引擎写脚本。
- Search Key: beanshell:M_RfQ_Requisition_Create
- 脚本代码如下:
/* ADempiere ERP 3.6.0 * beanshell:M_RfQ_Requisition_Create * Create Requisition from RfQ * Last update: Peanut Blake. Jan 07, 2010. * Created: Peanut Blake. DEC 30, 2010. * */ /* Version 2.0*/ import org.compiere.util.*; String result_msg; System.out.println("Parameter:" + P_C_RfQ_ID + P_M_PriceList_ID+ P_Requisition_No_A+ P_Requisition_No_B ); /* Check conditions */ if( P_C_RfQ_ID==void || P_M_PriceList_ID == void || (P_Requisition_No_A == void && P_Requisition_No_B ==void)) { result_msg="请您填写相关字段! Please fill the required fields!"; javax.swing.JOptionPane.showMessageDialog(null,result_msg); return ""; } String sql_assist_head, sql_head, sql_assist_line, sql_line; String req_no=""; String brand=""; String vendor=""; String vendorname1=""; String vendorname2=""; String price1=""; String price2=""; Integer assist_mid_id_head, assist_mid_id_line; for ( int i=1; i<=2 ; i++ ){ if (i==1 ) { if ( P_Requisition_No_A !=void ){ req_no=P_Requisition_No_A; brand="brand"; vendor="A"; vendorname1="c_bpartner_id"; vendorname2="c_bpartner_id_2"; price1="price"; price2="price2"; } else continue; } else if ( i==2) { if ( P_Requisition_No_B !=void ){ req_no=P_Requisition_No_B; brand="brand2"; vendor="B"; vendorname1="c_bpartner_id_2"; vendorname2="c_bpartner_id"; price1="price2"; price2="price"; } else continue; } /* INSERT m_requisition */ sql_head="INSERT INTO m_requisition " + "(m_requisition_id,AD_Client_ID,AD_Org_ID,createdby,updatedby,documentno, c_rfq_id, Description,Help,m_pricelist_id," + " m_warehouse_id, purchasingstaff_id, priorityrule, daterequired, docaction,docstatus,c_doctype_id, isapproved, " + " processing, processedon, c_bpartner_id_a,c_bpartner_id_b ) " + " SELECT (SELECT currentnext FROM ad_sequence WHERE name='M_Requisition') ," + A_AD_Client_ID + "," + G_AD_Org_ID + "," + A_AD_User_ID + "," + A_AD_User_ID + ",'" + req_no + "',"+P_C_RfQ_ID +", (c_rfq.Name || c_rfq.Description) , c_rfq.Help ," + P_M_PriceList_ID + "," + G_M_Warehouse_ID +","+ A_AD_User_ID + ", 5, now(), 'CO','DR',127, 'N'," +" 'N',0,c_rfq." + vendorname1 + ",c_rfq."+ vendorname2 + " FROM c_rfq WHERE c_rfq_id=" + P_C_RfQ_ID + " AND ad_client_id=" + A_AD_Client_ID + ";"; System.out.println("sql_head: "+sql_head); /* INSERT m_requisitionline*/ sql_line="INSERT INTO m_requisitionline (m_requisitionline_id,ad_client_id,ad_org_id,createdby,updatedby,line, " + " m_requisition_id,qty,m_product_id,description,c_uom_id,brand,usage, " + " priceactual,price2,linenetamt )" + " SELECT (( SELECT currentnext FROM ad_sequence WHERE name='M_RequisitionLine') + ceil(rl.line / 10))," + A_AD_Client_ID + "," + G_AD_Org_ID + "," + A_AD_User_ID + "," + A_AD_User_ID + ", rl.line, " + " (SELECT currentnext FROM ad_sequence WHERE name='M_Requisition')," + " rl.quantity, rl.m_product_id, rl.description, rl.c_uom_id, rl." + brand + ", rl.purpose, " + " rl." + price1 + ",rl." + price2 + ", round(rl.quantity * rl." + price1 + ", 3) " + " FROM c_rfqline rl " + " WHERE (rl.c_rfq_id=" + P_C_RfQ_ID + " AND ad_client_id=" + A_AD_Client_ID + " AND rl.choosequotation='" + vendor + "');"; System.out.println("sql_line: "+sql_line); /* insert assist_target_id for head*/ sql_assist_head="UPDATE ad_sequence SET updated=now(), currentnext=(SELECT MAX(m_requisition_id) FROM M_Requisition) +1 " + " WHERE name='M_Requisition';"; System.out.println("sql_assist_head: "+sql_assist_head); /* insert assist_target_id for line*/ sql_assist_line="UPDATE ad_sequence SET updated=now(), currentnext=(SELECT MAX(m_requisitionline_id) FROM M_Requisitionline) +1 " +" WHERE name='M_RequisitionLine';"; System.out.println("sql_assist_line: "+sql_assist_line); int no_head = DB.executeUpdate(sql_head, A_TrxName); int no_line = DB.executeUpdate(sql_line, A_TrxName); int no_assist_head = DB.executeUpdate(sql_assist_head, A_TrxName); int no_assist_line = DB.executeUpdate(sql_assist_line, A_TrxName); } System.out.println("beanshell:M_RfQ_Requisition_Create - Finished!"); result = "OK";
讨论-关于"实现从RfQ生成Requistion"
一楼-Albert
- Albert:
- 管理需求探討:
- 不知用途為何需要直接拋轉 Rfq(詢價/報價)產生 Requisition ?
- Rfq(詢價/報價)可以產生 Order(受訂單/採購單)底稿
- Requisition(需求)可以產生 Order(採購單)底稿
- MRP(材料需求計畫)可以產生 Requisition(需求)底稿;Requisition(需求)可以產生 Order(採購單)
- 以下在管理上的應用待研究 :
- Rfq(詢價/報價)產生 Requisition(需求)底稿? 報價會有需求 ?
- Requisition(需求)產生 Rfq(詢價/報價)底稿? 需求要有報價 ?
- 技術性探討 :
- 為何不用物件產生 MRequisistion
- MRfq m_Rfq = new MRfq (ctx, p_MRfq_ID, getTrx() );
- MRequisistion m_Requisition = m_Rfq.createRequisition();
- ..
二楼-Peanut
- Albert你好!感谢你的关心!
- 这家公司的采购流程是先询价,将询价结果汇总后,形成采购申请单(列出两家报价对比),由主管批准后,方可下达采购确认单。
- 由于我对ADempiere的代码尚不了解,对SQL比较熟悉。为了降低程序调试难度,让项目进度更为可控,因此我目前采用 SQL 来实现。
- 将来对 Rule 引擎的脚本编写非常熟悉之后,我将会更多地使用 ADempiere 已经有的 Java 类。
- Peanut Blake. 04 January 2011.
2011-01-04
实现在新建记录时不执行某段脚本
- 我通过 Rule 引擎写了一个 Callout 脚本 beanshell:C_RfQ_Unselect ,用在 RfQLine 表的 UnselectAll 字段上。
- 我在RfQLine上新建记录时(尚未保存),ADempiere 就直接触发了Callout。
- 那么我应当如何在代码进行区分,实现在新建记录时不执行某段脚本?
- 豪客和风筝建议了一个解决方法——检测当前记录,如果是新建记录就直接跳出。在脚本最开头加上这段代码即可:
if ( A_Tab.getRecord_ID()==-1 ) return "";
修改Rule引擎脚本默认字符长度限制
- Rule Engine 系统默认限制2000字符长度,现改为4500字符。先执行SQL语句,然后进入Table窗口对Script字段进行设置。
- 经测试4000字符的脚本,可以正常执行。
- SQL语句如下:
ALTER TABLE ad_rule ALTER script TYPE character varying(4500);
2011-01-05
定义变量未赋初值,导致默认按局部变量使用
- 今天用Rule引擎写Process脚本(Beanshell)遇到一个问题,字符串变量如果在定义时未赋初值,就会作为局部变量使用,导致在其他地方以空值形式出现。不知道这种情况对于其他变量是否也同样存在?
- 举例代码:
String myname; for ( int i=1; i<=1 ; i++ ){ if (i==1 ) { myname="ADempiere"; System.out.println("My Name is (inside if): " + myname ); } System.out.println("My Name is (outside if): "+myname); } result="";
- 运行结果为:
My Name is (inside if):ADempiere My Name is (outside if):void
- 将 String myname; 改为 String myname=""; ,运行结果为:
My Name is (inside if):ADempiere My Name is (outside if):ADempiere
2011-01-07
冲突:脚本不完善导致 Requisition 窗体新增记录无法保存
Requisition 页签新增记录保存报错
- 01月03日写脚本从 RfQ 询价单自动生成 Requisition 采购申请单,通过 SQL 实现,初步测试通过。
- 但是进一步测试发现一个问题,通过脚本生成新的采购申请单后,在 Requisition 窗体 Requisition 页签上新建申请单保存失败,错误提示如下:
===========> DB.executeUpdate: INSERT INTO M_Requisition (AD_Client_ID,AD_Org_ID,C_DocType_ID,Created,CreatedBy,DateDoc, DateRequired,DocAction,DocStatus,DocumentNo,IsActive,IsApproved,M_PriceList_ID,M_Requisition_ID,M_Warehouse_ID,Posted,PriorityRule, Processed,ProcessedOn,Processing,purchasingstaff_id,TotalLines,Updated,UpdatedBy) VALUES (11,50001,127, TO_TIMESTAMP('2011-01-03 13:16:25','YYYY-MM-DD HH24:MI:SS'),100,TO_TIMESTAMP('2011-01-03','YYYY-MM-DD'), TO_TIMESTAMP('2011-01-06','YYYY-MM-DD'),'CO','DR','TJ005','Y','N',101,1000002,50002,'N','5','N',0,'N',1000020,0, TO_TIMESTAMP('2011-01-03 13:16:25','YYYY-MM-DD HH24:MI:SS'),100) [POSave_a2f6503a-8143-410e-92b3-979f8c5f5b9e] [11] org.postgresql.util.PSQLException: ERROR: duplicate key value violates unique constraint "m_requisition_pkey"; State=23505; ErrorCode=0 at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2062) ... ... ===========> DB.saveError: DBExecuteError - ERROR: duplicate key value violates unique constraint "m_requisition_pkey" [11] -----------> MRequisition.saveNew: [POSave_a2f6503a-8143-410e-92b3-979f8c5f5b9e]Not inserted - M_Requisition [11] -----------> GridTable.saveWarning: SaveErrorNotUnique - ERROR: duplicate key value violates unique constraint "m_requisition_pkey" [11]
- 01月03日的错误分析:
* 检查后初步认定与 Requisition 页签保存新增记录时 m_requisition_id 的确认方法有关。于是去看窗体保存的代码,我找到 org.compiere.model.I_M_Requisition.java 与 org.compiere.model.MRequisition.java 。 但是我没有找到有价值的信息,不知道是不是自己没有找对地方。 * 于是折回去看 m_requisition 表中的数据,对比脚本生成的记录与窗体生成的记录的区别。 * 结果发现有两个字段存在差异,窗体生成的记录当中 Processing = 'N', ProcessedOn=0,而我在脚本SQL中把这两个字段忽略了(留空)。 * 于是将所有记录设置为 Processing = 'N'。然后再次测试,OK,问题解决。 * 看来ADempiere里面暗藏的机关不少呀,还是得小心测试、反复验证为妙。
Requisition Line 页签新增记录保存报错
- 通过脚本执行 SQL 生成新的 Requisition Line (采购申请单行)后,Requisition 窗体 Requisition Line 页签新增记录无法保存,错误提示如下:
===========> DB.saveError: DBExecuteError - ERROR: duplicate key value violates unique constraint "m_requisitionline_pkey" [11] -----------> MRequisitionLine.saveNew: [POSave_ea719be5-80db-4f3e-9d45-f0eb9700dac9]Not inserted - M_RequisitionLine [11]
- 01月05日的错误分析:
* 经检查初步认定与 linenetamt 字段有关。我在脚本SQL中没有设置 linenetamt 字段,数据库采用默认值 0 。于是执行SQL语句: UPDATE m_requisitionline SET linenetamt = qty * PriceActual; * 然后再次测试,OK,问题解决。
01月07日的重新分析
- 前面01月07日、01月07日这两段分析是错误的,现在发现与数据库的一致性有关。脚本执行SQL语句后再进行 Requisition 窗体新建记录操作时,窗体未能收到有效的数据库更新信息。
- 再次查看代码 PO.get_ID(),但是没有查出个来龙去脉。后来上sf.net论坛搜索到相关讨论,发现保存新建记录时是从 AD_Sequence 表的 currentnext 字段来获取,与之前分析的 Processing = 'N'或 linenetamt = 0 没有任何关系,之前测试通过是存在巧合因素。 在脚本中加下两项SQL语句:
UPDATE ad_sequence SET updated=now(), currentnext=(SELECT MAX(m_requisition_id) FROM M_Requisition) +1 WHERE "name"='M_Requisition'; UPDATE ad_sequence SET updated=now(), currentnext=(SELECT MAX(m_requisitionline_id) FROM M_Requisitionline ) +1 WHERE "name"='M_RequisitionLine';
- 再次测试,问题终于解决。
遗留问题
- UPDATE 之后 linenetamt 字段出现了一长串的 0,例如 13134.000000000000000000000000,如何才能去掉多余的0?
讨论
一楼-Albert
- 台灣技術支持團隊
- Skype: Adempiere/Compiere 技術轉移顧問 Albert
- 向祖國團隊建議 :
- Postgres DB/Oracle DB 四捨五入到第二位
- 但是不建議寫 SQL Insert Statement
UPDATE m_requisitionline SET linenetamt = round(qty * PriceActual, 2);
- 為何要用物件.,因為 MRequisitionLine寫入前會 setLineNetAmt
protected boolean beforeSave (boolean newRecord) { if (newRecord && getParent().isComplete()) { log.saveError("ParentComplete", Msg.translate(getCtx(), "M_RequisitionLine")); return false; } if (getLine() == 0) 中間刪除不表示 if (getPriceActual().signum() == 0) setPrice(); setLineNetAmt(); return true; }
public void setLineNetAmt () { BigDecimal lineNetAmt = getQty().multiply(getPriceActual()); super.setLineNetAmt (lineNetAmt); }
- 建議事項 : 要記得小數點長度
public void setLineNetAmt () { BigDecimal lineNetAmt = getQty().multiply(getPriceActual()); if (lineNetAmt .scale() > getPrecision()) lineNetAmt = lineNetAmt.setScale(getPrecision(), BigDecimal.ROUND_HALF_UP); super.setLineNetAmt (lineNetAmt); }
- 為何要用物件.,因為 MRequisitionLine寫入後會 updateHeader
protected boolean afterSave (boolean newRecord, boolean success) { if (!success) return success; return updateHeader(); }
private boolean updateHeader() { log.fine(""); String sql = "UPDATE M_Requisition r" + " SET TotalLines=" + "(SELECT COALESCE(SUM(LineNetAmt),0) FROM M_RequisitionLine rl " + "WHERE r.M_Requisition_ID=rl.M_Requisition_ID) " + "WHERE M_Requisition_ID=?"; int no = DB.executeUpdateEx(sql, new Object[]{getM_Requisition_ID()}, get_TrxName()); if (no != 1) log.log(Level.SEVERE, "Header update #" + no); m_parent = null; return no == 1;
二楼-Peanut
- 非常感谢 Albert 大人耐心详细的解答! 用SQL这个方法的确是很折腾,等采购这块上线试运行了,我就立即着手学习 Callout 。 --Peanut Blake. 07 January 2011.