diff --git a/base/src/org/compiere/model/CalloutProductCategory.java b/base/src/org/compiere/model/CalloutProductCategory.java new file mode 100644 index 0000000000..f72cbfcf23 --- /dev/null +++ b/base/src/org/compiere/model/CalloutProductCategory.java @@ -0,0 +1,146 @@ +/****************************************************************************** + * Product: Adempiere ERP & CRM Smart Business Solution * + * Copyright (C) 1999-2006 ComPiere, Inc. All Rights Reserved. * + * This program is free software; you can redistribute it and/or modify it * + * under the terms version 2 of the GNU General Public License as published * + * by the Free Software Foundation. This program is distributed in the hope * + * that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * + * See the GNU General Public License for more details. * + * You should have received a copy of the GNU General Public License along * + * with this program; if not, write to the Free Software Foundation, Inc., * + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. * + * For the text or an alternative of this public license, you may reach us * + * ComPiere, Inc., 2620 Augustine Dr. #245, Santa Clara, CA 95054, USA * + * or via info@compiere.org or http://www.compiere.org/license.html * + *****************************************************************************/ +package org.compiere.model; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Iterator; +import java.util.Properties; +import java.util.Vector; +import java.util.logging.Level; + +import org.compiere.util.DB; + + +/** + * Product Category Callouts + * + * @author Karsten Thiemann kthiemann@adempiere.org + * + */ +public class CalloutProductCategory extends CalloutEngine +{ + /** + * Loop detection of product category tree. + * + * @param ctx context + * @param WindowNo current Window No + * @param mTab Grid Tab + * @param mField Grid Field + * @param value New Value + * @return "" or error message + */ + public String testForLoop (Properties ctx, int WindowNo, GridTab mTab, GridField mField, Object value) + { + if (isCalloutActive() || value == null) + return ""; + setCalloutActive(true); + + // get values + Integer newParentCategoryId = (Integer) mTab.getValue(MProductCategory.COLUMNNAME_M_Product_Category_Parent_ID); + Integer productCategoryId = (Integer) mTab.getValue(MProductCategory.COLUMNNAME_M_Product_Category_ID); + + String sql = " SELECT M_Product_Category_ID, M_Product_Category_Parent_ID FROM M_Product_Category"; + final Vector categories = new Vector(100); + try { + Statement stmt = DB.createStatement(); + ResultSet rs = stmt.executeQuery(sql); + while (rs.next()) { + if(rs.getInt(1)==productCategoryId.intValue()) { + categories.add(new SimpleTreeNode(rs.getInt(1), newParentCategoryId)); + } + categories.add(new SimpleTreeNode(rs.getInt(1), rs.getInt(2))); + } + rs.close(); + stmt.close(); + if(hasLoop(newParentCategoryId, categories, productCategoryId)) { + mField.setValue(0, false); + + setCalloutActive(false); + return "ProductCategoryLoopDetected"; + } + } catch (SQLException e) { + log.log(Level.SEVERE, sql, e); + setCalloutActive(false); + return e.getMessage(); + } + + setCalloutActive(false); + return ""; + } // testForLoop + + + /** + * Recursive search for parent nodes - climbes the to the root. + * If there is a circle there is no root but it comes back to the start node. + * @param parentCategoryId + * @param categories + * @param loopIndicatorId + * @return + */ + private boolean hasLoop(int parentCategoryId, Vector categories, int loopIndicatorId) { + final Iterator iter = categories.iterator(); + boolean ret = false; + while (iter.hasNext()) { + SimpleTreeNode node = (SimpleTreeNode) iter.next(); + if(node.getNodeId()==parentCategoryId){ + if (node.getParentId()==0) { + //root node, all fine + return false; + } + if(node.getNodeId()==loopIndicatorId){ + //loop found + return true; + } + ret = hasLoop(node.getParentId(), categories, loopIndicatorId); + } + } + return ret; + } //hasLoop + + /** + * Simple class for tree nodes. + * @author Karsten Thiemann, kthiemann@adempiere.org + * + */ + private class SimpleTreeNode { + /** id of the node */ + private int nodeId; + /** id of the nodes parent */ + private int parentId; + + /** + * Constructor. + * @param nodeId + * @param parentId + */ + public SimpleTreeNode(int nodeId, int parentId) { + this.nodeId = nodeId; + this.parentId = parentId; + } + + public int getNodeId() { + return nodeId; + } + + public int getParentId() { + return parentId; + } + } + +} // CalloutProductCategory diff --git a/client/src/org/compiere/apps/search/Find.java b/client/src/org/compiere/apps/search/Find.java index 2eaec4497e..6e0e5990fa 100644 --- a/client/src/org/compiere/apps/search/Find.java +++ b/client/src/org/compiere/apps/search/Find.java @@ -22,6 +22,7 @@ import java.math.*; import java.sql.*; import java.util.*; import java.util.logging.*; + import javax.swing.*; import javax.swing.event.*; import javax.swing.table.*; @@ -616,9 +617,12 @@ public final class Find extends CDialog // globalqss - Carlos Ruiz - 20060711 // fix a bug with virtualColumn + isSelectionColumn not yielding results GridField field = getTargetMField(ColumnName); + boolean isProductCategoryField = isProductCategoryField(field.getAD_Column_ID()); String ColumnSQL = field.getColumnSQL(false); if (value.toString().indexOf('%') != -1) m_query.addRestriction(ColumnSQL, MQuery.LIKE, value, ColumnName, ved.getDisplay()); + else if (isProductCategoryField && value instanceof Integer) + m_query.addRestriction(getSubCategoryWhereClause(((Integer) value).intValue())); else m_query.addRestriction(ColumnSQL, MQuery.EQUAL, value, ColumnName, ved.getDisplay()); /* @@ -698,6 +702,7 @@ public final class Find extends CDialog String infoName = column.toString(); // GridField field = getTargetMField(ColumnName); + boolean isProductCategoryField = isProductCategoryField(field.getAD_Column_ID()); String ColumnSQL = field.getColumnSQL(false); // Op Object op = advancedTable.getValueAt(row, INDEX_OPERATOR); @@ -730,12 +735,118 @@ public final class Find extends CDialog m_query.addRangeRestriction(ColumnSQL, parsedValue, parsedValue2, infoName, infoDisplay, infoDisplay_to); } + else if (isProductCategoryField && MQuery.OPERATORS[MQuery.EQUAL_INDEX].equals(op)) { + if (!(parsedValue instanceof Integer)) { + continue; + } + m_query + + .addRestriction(getSubCategoryWhereClause(((Integer) parsedValue).intValue())); + } else m_query.addRestriction(ColumnSQL, Operator, parsedValue, infoName, infoDisplay); } } // cmd_save + /** + * Checks the given column. + * @param columnId + * @return true if the column is a product category column + */ + private boolean isProductCategoryField(int columnId) { + X_AD_Column col = new X_AD_Column(Env.getCtx(), columnId, null); + if (col.get_ID() == 0) { + return false; // column not found... + } + return MProduct.COLUMNNAME_M_Product_Category_ID.equals(col.getColumnName()); + } + + /** + * Returns a sql where string with the given category id and all of its subcategory ids. + * It is used as restriction in MQuery. + * @param productCategoryId + * @return + */ + private String getSubCategoryWhereClause(int productCategoryId) { + //if a node with this id is found later in the search we have a loop in the tree + int subTreeRootParentId = 0; + String retString = " M_Product_Category_ID IN ("; + String sql = " SELECT M_Product_Category_ID, M_Product_Category_Parent_ID FROM M_Product_Category"; + final Vector categories = new Vector(100); + try { + Statement stmt = DB.createStatement(); + ResultSet rs = stmt.executeQuery(sql); + while (rs.next()) { + if(rs.getInt(1)==productCategoryId) { + subTreeRootParentId = rs.getInt(2); + } + categories.add(new SimpleTreeNode(rs.getInt(1), rs.getInt(2))); + } + retString += getSubCategoriesString(productCategoryId, categories, subTreeRootParentId); + retString += ") "; + rs.close(); + stmt.close(); + } catch (SQLException e) { + log.log(Level.SEVERE, sql, e); + retString = ""; + } catch (AdempiereSystemError e) { + log.log(Level.SEVERE, sql, e); + retString = ""; + } + return retString; + } + + /** + * Recursive search for subcategories with loop detection. + * @param productCategoryId + * @param categories + * @param loopIndicatorId + * @return comma seperated list of category ids + * @throws AdempiereSystemError if a loop is detected + */ + private String getSubCategoriesString(int productCategoryId, Vector categories, int loopIndicatorId) throws AdempiereSystemError { + String ret = ""; + final Iterator iter = categories.iterator(); + while (iter.hasNext()) { + SimpleTreeNode node = (SimpleTreeNode) iter.next(); + if (node.getParentId() == productCategoryId) { + if (node.getNodeId() == loopIndicatorId) { + throw new AdempiereSystemError("The product category tree contains a loop on categoryId: " + loopIndicatorId); + } + ret = ret + getSubCategoriesString(node.getNodeId(), categories, loopIndicatorId) + ","; + } + } + log.fine(ret); + return ret + productCategoryId; + } + + /** + * Simple tree node class for product category tree search. + * @author Karsten Thiemann, kthiemann@adempiere.org + * + */ + private class SimpleTreeNode { + + private int nodeId; + + private int parentId; + + public SimpleTreeNode(int nodeId, int parentId) { + this.nodeId = nodeId; + this.parentId = parentId; + } + + public int getNodeId() { + return nodeId; + } + + public int getParentId() { + return parentId; + } + } + + /** * Parse Value * @param field column diff --git a/dbPort/src/org/compiere/model/X_M_Product_Category.java b/dbPort/src/org/compiere/model/X_M_Product_Category.java index eac1034857..3ba3219556 100644 --- a/dbPort/src/org/compiere/model/X_M_Product_Category.java +++ b/dbPort/src/org/compiere/model/X_M_Product_Category.java @@ -232,6 +232,27 @@ return ii.intValue(); } /** Column name M_Product_Category_ID */ public static final String COLUMNNAME_M_Product_Category_ID = "M_Product_Category_ID"; + +/** M_Product_Category_Parent_ID AD_Reference_ID=163 */ +public static final int M_PRODUCT_CATEGORY_PARENT_ID_AD_Reference_ID=163; +/** Set Parent Product Category. +@param M_Product_Category_Parent_ID Parent Product Category */ +public void setM_Product_Category_Parent_ID (int M_Product_Category_Parent_ID) +{ +if (M_Product_Category_Parent_ID <= 0) set_Value ("M_Product_Category_Parent_ID", null); + else +set_Value ("M_Product_Category_Parent_ID", Integer.valueOf(M_Product_Category_Parent_ID)); +} +/** Get Parent Product Category. +@return Parent Product Category */ +public int getM_Product_Category_Parent_ID() +{ +Integer ii = (Integer)get_Value("M_Product_Category_Parent_ID"); +if (ii == null) return 0; +return ii.intValue(); +} +/** Column name M_Product_Category_Parent_ID */ +public static final String COLUMNNAME_M_Product_Category_Parent_ID = "M_Product_Category_Parent_ID"; /** Set Name. @param Name Alphanumeric identifier of the entity */ public void setName (String Name) diff --git a/migration/316-trunk/006_add_ProductCategoryParent.sql b/migration/316-trunk/006_add_ProductCategoryParent.sql new file mode 100644 index 0000000000..abcc46f2a8 --- /dev/null +++ b/migration/316-trunk/006_add_ProductCategoryParent.sql @@ -0,0 +1,102 @@ +INSERT INTO ad_element + (ad_element_id, ad_client_id, ad_org_id, isactive, + created, createdby, + updated, updatedby, + columnname, entitytype, NAME, + printname + ) + VALUES (50070, 0, 0, 'Y', + TO_DATE ('04/24/2007 12:30:00', 'MM/DD/YYYY HH24:MI:SS'), 100, + TO_DATE ('04/24/2007 12:30:00', 'MM/DD/YYYY HH24:MI:SS'), 100, + 'M_Product_Category_Parent_ID', 'D', 'Parent Product Category', + 'Parent Product Category' + ); + + +INSERT INTO ad_column + (ad_column_id, ad_client_id, ad_org_id, isactive, + created, + updated, createdby, + updatedby, name, description, help, version, + entitytype, columnname, ad_table_id, ad_reference_id, + ad_reference_value_id, + fieldlength, iskey, isparent, ismandatory, isupdateable, + isidentifier, seqno, istranslated, isencrypted, + isselectioncolumn, ad_element_id, callout, issyncdatabase, + isalwaysupdateable + ) + VALUES (50211, 0, 0, 'Y', + TO_DATE ('04/24/2007 12:30:00', 'MM/DD/YYYY HH24:MI:SS'), + TO_DATE ('04/24/2007 12:30:00', 'MM/DD/YYYY HH24:MI:SS'), 100, + 100, 'Parent Product Category', 'Parent Product Category', 'The parent product category is used to build a category tree.', 1, + 'D', 'M_Product_Category_Parent_ID', 209, 18, + 163, + 22, 'N', 'N', 'N', 'Y', + 'N', 0, 'N', 'N', + 'N', 50070, 'org.compiere.model.CalloutProductCategory.testForLoop', 'N', + 'N' + ); + + +INSERT INTO ad_field + (ad_field_id, ad_client_id, ad_org_id, isactive, + created, createdby, + updated, updatedby, + name, description, iscentrallymaintained, seqno, ad_tab_id, + ad_column_id, isdisplayed, displaylength, isreadonly, + issameline, isheading, isfieldonly, isencrypted, entitytype + ) + VALUES (50181, 0, 0, 'Y', + TO_DATE ('04/24/2007 12:30:00', 'MM/DD/YYYY HH24:MI:SS'), 100, + TO_DATE ('04/24/2007 12:30:00', 'MM/DD/YYYY HH24:MI:SS'), 100, + 'Parent Product Category', 'Parent Product Category', 'Y', 60, 189, + 50211, 'Y', 22, 'N', + 'N', 'N', 'N', 'N', 'D' + ); + +INSERT INTO ad_message + (ad_message_id, ad_client_id, ad_org_id, isactive, + created, createdby, + updated, updatedby, + value, msgtext, msgtype + ) + VALUES (50014, 0, 0, 'Y', + TO_DATE ('04/24/2007 12:30:00', 'MM/DD/YYYY HH24:MI:SS'), 100, + TO_DATE ('04/24/2007 12:30:00', 'MM/DD/YYYY HH24:MI:SS'), 100, + 'ProductCategoryLoopDetected', + 'A loop in the product category tree has been detected - the old value will be restored','E' + ); + +COMMIT ; + +UPDATE ad_sequence + SET currentnextsys = (SELECT MAX (ad_element_id) + 1 + FROM ad_element + WHERE ad_element_id < 1000000) + WHERE NAME = 'AD_Element'; + +UPDATE ad_sequence + SET currentnextsys = (SELECT MAX (ad_column_id) + 1 + FROM ad_column + WHERE ad_column_id < 1000000) + WHERE NAME = 'AD_Column'; + +UPDATE ad_sequence + SET currentnextsys = (SELECT MAX (ad_field_id) + 1 + FROM ad_field + WHERE ad_field_id < 1000000) + WHERE NAME = 'AD_Field'; + + UPDATE ad_sequence + SET currentnextsys = (SELECT MAX (ad_message_id) + 1 + FROM ad_message + WHERE ad_message_id < 1000000) + WHERE NAME = 'AD_Message'; + + +ALTER TABLE M_Product_Category ADD M_Product_Category_Parent_ID NUMBER(10,0); +ALTER TABLE M_Product_Category ADD CONSTRAINT MProductCat_ParentCat FOREIGN KEY (M_Product_Category_Parent_ID) + REFERENCES M_Product_Category (M_Product_Category_ID); +UPDATE AD_Column SET IsSelectionColumn='Y' WHERE AD_Column_ID=2012; + +COMMIT ; \ No newline at end of file diff --git a/migration/316-trunk/postgresql/006_add_ProductCategoryParent.sql b/migration/316-trunk/postgresql/006_add_ProductCategoryParent.sql new file mode 100644 index 0000000000..5d675ca720 --- /dev/null +++ b/migration/316-trunk/postgresql/006_add_ProductCategoryParent.sql @@ -0,0 +1,102 @@ +INSERT INTO ad_element + (ad_element_id, ad_client_id, ad_org_id, isactive, + created, createdby, + updated, updatedby, + columnname, entitytype, NAME, + printname + ) + VALUES (50070, 0, 0, 'Y', + TO_DATE ('04/24/2007 12:30:00', 'MM/DD/YYYY HH24:MI:SS'), 100, + TO_DATE ('04/24/2007 12:30:00', 'MM/DD/YYYY HH24:MI:SS'), 100, + 'M_Product_Category_Parent_ID', 'D', 'Parent Product Category', + 'Parent Product Category' + ); + + +INSERT INTO ad_column + (ad_column_id, ad_client_id, ad_org_id, isactive, + created, + updated, createdby, + updatedby, name, description, help, version, + entitytype, columnname, ad_table_id, ad_reference_id, + ad_reference_value_id, + fieldlength, iskey, isparent, ismandatory, isupdateable, + isidentifier, seqno, istranslated, isencrypted, + isselectioncolumn, ad_element_id, callout, issyncdatabase, + isalwaysupdateable + ) + VALUES (50211, 0, 0, 'Y', + TO_DATE ('04/24/2007 12:30:00', 'MM/DD/YYYY HH24:MI:SS'), + TO_DATE ('04/24/2007 12:30:00', 'MM/DD/YYYY HH24:MI:SS'), 100, + 100, 'Parent Product Category', 'Parent Product Category', 'The parent product category is used to build a category tree.', 1, + 'D', 'M_Product_Category_Parent_ID', 209, 18, + 163, + 22, 'N', 'N', 'N', 'Y', + 'N', 0, 'N', 'N', + 'N', 50070, 'org.compiere.model.CalloutProductCategory.testForLoop', 'N', + 'N' + ); + + +INSERT INTO ad_field + (ad_field_id, ad_client_id, ad_org_id, isactive, + created, createdby, + updated, updatedby, + name, description, iscentrallymaintained, seqno, ad_tab_id, + ad_column_id, isdisplayed, displaylength, isreadonly, + issameline, isheading, isfieldonly, isencrypted, entitytype + ) + VALUES (50181, 0, 0, 'Y', + TO_DATE ('04/24/2007 12:30:00', 'MM/DD/YYYY HH24:MI:SS'), 100, + TO_DATE ('04/24/2007 12:30:00', 'MM/DD/YYYY HH24:MI:SS'), 100, + 'Parent Product Category', 'Parent Product Category', 'Y', 60, 189, + 50211, 'Y', 22, 'N', + 'N', 'N', 'N', 'N', 'D' + ); + +INSERT INTO ad_message + (ad_message_id, ad_client_id, ad_org_id, isactive, + created, createdby, + updated, updatedby, + value, msgtext, msgtype + ) + VALUES (50014, 0, 0, 'Y', + TO_DATE ('04/24/2007 12:30:00', 'MM/DD/YYYY HH24:MI:SS'), 100, + TO_DATE ('04/24/2007 12:30:00', 'MM/DD/YYYY HH24:MI:SS'), 100, + 'ProductCategoryLoopDetected', + 'A loop in the product category tree has been detected - the old value will be restored','E' + ); + +COMMIT ; + +UPDATE ad_sequence + SET currentnextsys = (SELECT MAX (ad_element_id) + 1 + FROM ad_element + WHERE ad_element_id < 1000000) + WHERE NAME = 'AD_Element'; + +UPDATE ad_sequence + SET currentnextsys = (SELECT MAX (ad_column_id) + 1 + FROM ad_column + WHERE ad_column_id < 1000000) + WHERE NAME = 'AD_Column'; + +UPDATE ad_sequence + SET currentnextsys = (SELECT MAX (ad_field_id) + 1 + FROM ad_field + WHERE ad_field_id < 1000000) + WHERE NAME = 'AD_Field'; + + UPDATE ad_sequence + SET currentnextsys = (SELECT MAX (ad_message_id) + 1 + FROM ad_message + WHERE ad_message_id < 1000000) + WHERE NAME = 'AD_Message'; + + +ALTER TABLE M_Product_Category ADD M_Product_Category_Parent_ID NUMERIC(10); +ALTER TABLE M_Product_Category ADD CONSTRAINT MProductCat_ParentCat FOREIGN KEY (M_Product_Category_Parent_ID) + REFERENCES M_Product_Category (M_Product_Category_ID); +UPDATE AD_Column SET IsSelectionColumn='Y' WHERE AD_Column_ID=2012; + +COMMIT ; \ No newline at end of file