tdf#62248 fix multiple groupings on same field issue in reportbuilder

In a reportbuilder report, when grouping on the same field multiple times
(with different interval/prefix characters setting), the two groupings interact
and in the end it does not work right; both grouping levels behave like one of
them.

That's because the report function created by Report Builder for each of the
groupings share the same name. Choose unique function name in
ORptExport::exportGroupsExpressionAsFunction and sync OXMLGroup::OXMLGroup to
parse that name. Keep supporting legacy names for backwards compatibility.

Change-Id: Ic91e51743a407225d50b14f85d02c58ca0a243c0
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/193993
Tested-by: Jenkins
Reviewed-by: Hossein <hossein@libreoffice.org>
This commit is contained in:
Vissarion Fisikopoulos
2025-11-24 22:10:29 +01:00
committed by Hossein
parent 76aafa18b7
commit e348d75261
4 changed files with 88 additions and 4 deletions
@@ -21,6 +21,7 @@
#include <com/sun/star/sdb/application/XDatabaseDocumentUI.hpp>
#include <comphelper/namedvaluecollection.hxx>
#include <com/sun/star/text/XTextDocument.hpp>
using namespace ::com::sun::star;
using namespace ::com::sun::star::uno;
@@ -58,6 +59,62 @@ CPPUNIT_TEST_FIXTURE(RptBasicTest, roundTripTest)
testLoadingAndSaving(u"calc8"_ustr, aReportNames[1], xComponentLoader, xActiveConnection);
}
CPPUNIT_TEST_FIXTURE(RptBasicTest, multiGroupingSameFieldIntervals)
{
// ODB fixture must contain a table and a report that groups on the same field twice
// with different settings (e.g., INTERVAL 5 and INTERVAL 10). The report should be
// prepared to reproduce the original failure (Column not found: INT_count_...).
loadURLCopy(u"ReportBuilder_grouping_same_field.odb");
Reference<frame::XModel> xModel(mxComponent, UNO_QUERY_THROW);
Reference<frame::XController> xController(xModel->getCurrentController());
Reference<sdb::application::XDatabaseDocumentUI> xUI(xController, UNO_QUERY_THROW);
xUI->connect();
Reference<XConnection> xActiveConnection = xUI->getActiveConnection();
Reference<XReportDocumentsSupplier> xSupp(xModel, UNO_QUERY_THROW);
Reference<container::XNameAccess> xNameAccess = xSupp->getReportDocuments();
const Sequence<OUString> aReportNames(xNameAccess->getElementNames());
CPPUNIT_ASSERT(aReportNames.getLength() > 0);
Reference<frame::XComponentLoader> xComponentLoader(xNameAccess, UNO_QUERY_THROW);
// Execute all reports via writer export. This will fail the test if any report
// throws ReportExecutionException like "Column not found: INT_count_Number_*".
for (const OUString& rName : aReportNames)
testLoadingAndSaving(u"writer8"_ustr, rName, xComponentLoader, xActiveConnection);
// Additionally, load the textual outputs of all reports and compare their lengths.
// They should be identical.
if (aReportNames.getLength() >= 2)
{
::comphelper::NamedValueCollection aLoadArgs;
aLoadArgs.put(u"ActiveConnection"_ustr, xActiveConnection);
Reference<lang::XComponent> xComp1 = xComponentLoader->loadComponentFromURL(
aReportNames[0], u"_blank"_ustr, 0, aLoadArgs.getPropertyValues());
Reference<lang::XComponent> xComp2 = xComponentLoader->loadComponentFromURL(
aReportNames[1], u"_blank"_ustr, 0, aLoadArgs.getPropertyValues());
Reference<text::XTextDocument> xTextDoc1(xComp1, UNO_QUERY);
Reference<text::XTextDocument> xTextDoc2(xComp2, UNO_QUERY);
if (xTextDoc1.is() && xTextDoc2.is())
{
sal_Int32 nLen1 = xTextDoc1->getText()->getString().getLength();
sal_Int32 nLen2 = xTextDoc2->getText()->getString().getLength();
CPPUNIT_ASSERT_EQUAL_MESSAGE("Report textual output length differs", nLen1, nLen2);
}
Reference<util::XCloseable> xClose1(xComp1, UNO_QUERY);
if (xClose1.is())
xClose1->close(true);
Reference<util::XCloseable> xClose2(xComp2, UNO_QUERY);
if (xClose2.is())
xClose2->close(true);
}
}
void RptBasicTest::testLoadingAndSaving(const OUString& rFilterName, const OUString& rReportName,
Reference<frame::XComponentLoader>& xComponentLoader,
Reference<XConnection>& xActiveConnection)
+3 -1
View File
@@ -1415,7 +1415,9 @@ void ORptExport::exportGroupsExpressionAsFunction(const Reference< XGroups>& _xG
sFunction = "INT";
uno::Reference< XFunction> xCountFunction = xFunctions->createFunction();
xCountFunction->setInitialFormula(beans::Optional< OUString>(true,u"rpt:0"_ustr));
OUString sCountName = sFunction + "_count_" + sExpression;
OUString sCountName = sFunction + "_count_"
+ OUString::number(xGroup->getGroupInterval())
+ "_" + sExpression;
xCountFunction->setName(sCountName);
xCountFunction->setFormula( "rpt:[" + sCountName + "] + 1" );
exportFunction(xCountFunction);
+28 -3
View File
@@ -73,6 +73,14 @@ OXMLGroup::OXMLGroup( ORptFilter& _rImport
case XML_ELEMENT(REPORT, XML_SORT_ASCENDING):
m_xGroup->setSortAscending(IsXMLToken(aIter, XML_TRUE));
break;
case XML_ELEMENT(REPORT, XML_SORT_EXPRESSION):
{
// Use the explicit sort field as the group's expression
OUString sExpr = aIter.toString();
if (sExpr.indexOf("INT_count") == -1)// for backward compatibility
m_xGroup->setExpression(sExpr);
}
break;
case XML_ELEMENT(REPORT, XML_GROUP_EXPRESSION):
{
OUString sValue = aIter.toString();
@@ -138,7 +146,21 @@ OXMLGroup::OXMLGroup( ORptFilter& _rImport
{
nGroupOn = report::GroupOn::INTERVAL;
_rImport.removeFunction(sExpression);
sExpression = sExpression.copy(std::string_view("INT_count_").size());
// sExpression can be either:
// - "INT_count_<digits>_<Field>" (new)
// - "INT_count_<Field>" (legacy)
const OUString aPrefix = u"INT_count_"_ustr;
const sal_Int32 nStart = aPrefix.getLength();
const sal_Int32 nEnd = sExpression.indexOf(u'_', nStart);
const sal_Int32 nDigitsOfInterval = (nEnd >= 0 && nEnd > nStart)
? (nEnd - nStart) : 0;
// Cut after prefix; if digits found, also skip the following '_'
sal_Int32 nCut = nStart;
if (nDigitsOfInterval > 0 && nEnd >= 0)
nCut = nEnd + 1;
if (nCut <= sExpression.getLength())
sExpression = sExpression.copy(nCut);
OUString sInterval = sCompleteFormula.getToken(1,'/');
sInterval = sInterval.getToken(0,')');
m_xGroup->setGroupInterval(sInterval.toInt32());
@@ -146,10 +168,13 @@ OXMLGroup::OXMLGroup( ORptFilter& _rImport
m_xGroup->setGroupOn(nGroupOn);
// Drop only the reference name; keep functions for export
_rImport.removeFunction(sValue);
sValue = sExpression;
// Only set expression from fallback if not already provided via sort-expression
if (m_xGroup->getExpression().isEmpty())
m_xGroup->setExpression(sExpression);
}
m_xGroup->setExpression(sValue);
}
}
break;