Baucore

BauCore V10.06.00 Release Notes

Releaseadminuser

⚠ Upgrading from a previous (1GP) version? A one-time migration from the first-generation package (1GP) to the second-generation package (2GP) is required before installing V10.00.00. Please follow the Baucore Migration Guide: 1GP to 2GP first.

Features:

  • 2nd Generation Managed Package (2GP): Baucore is now delivered as a Second-Generation Managed Package. The ibsbaucore namespace is preserved, so all existing API names, flows, reports, Apex and external integrations keep working. No manual data migration is required. Apex endpoints previously exposed via the webservice keyword are republished as REST/AuraEnabled methods and should be retested.

  • MyProjectSales Hub: New Salesforce Lightning application that connects Baucore to the MyProjectSales project intelligence platform, enabling users to search, review, and synchronize construction project data into Salesforce records.

  • New Quote Line Editor: A single Lightning Web Component that replaces the classic position grid on BoQ and Project Quote records, with inline editing, grouping, drag-and-drop ordering and product lookup. More info: Use Project Quote Position Editor, Configure Project Quote Position Editor, Configure Multi-Instance Setup, Import Data with Project Quote Position Editor.

  • SpecifiQ Integration:

    SpecifiQ helps users analyze specification documents and turn the extracted information into Project Quotes, BOQs, or Quotes.

    • Users can upload and analyze specification documents directly from Project or Inquiry record pages in Salesforce.
    • The embedded SpecifiQ experience connects Salesforce with Baucore’s document analysis tool.
    • Extracted specification information can be converted into a Project Quote, BOQ, or Quote, depending on which options the admin has enabled.
    • A guided flow walks users through account, contact, and project selection before creating the target record.
    • SpecifiQ can be configured to appear on the relevant record pages, depending on the customer setup.

    More info: BC SpecifiQ User Guide / BC SpecifiQ Admin Guide.

  • Baucore Inbox – AI Project Match & Project Watch:

    • AI-powered project matching automatically compares each extracted project against existing Salesforce records and displays the most relevant matches with a confidence score, so users can quickly select and link the correct project or choose to create a new one.
    • Watchlist integration from the Inbox allows users to apply extracted email data directly to existing projects — including updating project details and creating new participants — without navigating away from the Inbox screen.
    • Improved Inbox layout reduces unnecessary whitespace, giving users a more compact view and better use of screen space when reviewing emails and extracted information.
  • Project Watchlist: Scheduler-driven watch process that registers projects with the Baucore API and resynchronises extracted data on every run, with automatic retry and last-response status visibility.

    Key Benefits
    • Stay informed about project changes without manual research
    • Accept or reject suggested field updates directly on the record
    • Ignore field suggestions that should not be proposed again for the same field
    • Discover new participants related to your projects
    • Track project stage progression from external sources
    • Access references, files, and extended attributes from connected providers

    More info: Project Watch Overview / Configure Project Watch.

  • Picklist Field Mapping in Prov Field Mapping: Added picklist and multiselect value mapping to the provider field configuration, enabling automatic provider-to-Salesforce value conversion during data extraction and import. A configurable fallback row handles values with no explicit mapping. Applies across all provider integrations: BaucoreInbox, SpecifiQ, ibau, BarbourABI.

  • Participant Management: The Participant Management component on Account, Contact and Lead pages now shows a visual indicator distinguishing Project from Project Lead records.

Required Steps for This Release

1. Upgrade Order (mandatory)

You cannot upgrade directly to the Second-Generation (2GP) package. A one-time migration from the first-generation package (1GP) to the second-generation package (2GP) is required before installing this release. Please follow the Baucore Migration Guide: 1GP to 2GP, which walks you through installing the final 1GP release, the 2GP bridge version, and the upgrade to this release in the correct order.

Attention: Perform the upgrade in a full-copy sandbox first and run your regression suite (flows, validation rules, integrations, reports) before promoting to production. The ibsbaucore namespace is unchanged, so existing customisations keep working. Retest any external callers of the former webservice endpoints against the new REST equivalents. Review the AI match threshold (Baucore Inbox) and the Project Status setting as needed.

2. Add New Picklist Values

After the package upgrade, the following picklist values must be added in Setup → Object Manager for the listed custom objects and fields. You can add them automatically using the Anonymous Apex script below (recommended), or manually using the tables that follow.

Option A — Automated script (recommended). Run the following Anonymous Apex as a System Administrator: open Setup → Developer Console → Debug → Open Execute Anonymous Window, paste the whole script, tick “Open Log” and click Execute. The script is safe to re-run: existing values are detected and skipped, and nothing is ever removed. It uses the Tooling API via a self-callout, so no Remote Site Setting is required. Read the debug log for a per-field [ADDED n], [OK] or [ERROR] result.

/*
 * BauCore – Add post-upgrade picklist values (Anonymous Apex)
 * ============================================================================
 * HOW TO RUN
 *   Setup -> Developer Console -> Debug -> Open Execute Anonymous Window
 *   Paste this whole script, tick "Open Log", click Execute.
 *   (CLI alternative: sf apex run -f AddReleasePicklistValues.apex -o <yourOrg>)
 *
 * WHAT IT DOES
 *   Adds the picklist values required after upgrading to the BauCore 2GP package
 *   on the Prov Field Mapping and Prov Picklist Mapping objects, using the
 *   Tooling API via a self-callout. No Remote Site Setting is required.
 *
 * SAFE TO RE-RUN
 *   Values that already exist are detected and skipped. Existing values are
 *   never removed or modified.
 *
 * RESULT
 *   Read the debug log: each field prints [ADDED n], [OK] or [ERROR].
 * ----------------------------------------------------------------------------
 * NOTE: Run as a System Administrator (needs permission to modify metadata).
 * ============================================================================
 */

// Helper: recursively remove null entries so the Tooling API PATCH parser
// does not choke on "VALUE_NULL" complex values returned by the GET.
public class MetaUtil {
    public Object stripNulls(Object node) {
        if (node instanceof Map<String, Object>) {
            Map<String, Object> m = (Map<String, Object>) node;
            Map<String, Object> clean = new Map<String, Object>();
            for (String k : m.keySet()) {
                Object val = m.get(k);
                if (val == null) {
                    continue;
                }
                clean.put(k, stripNulls(val));
            }
            return clean;
        } else if (node instanceof List<Object>) {
            List<Object> src = (List<Object>) node;
            List<Object> out = new List<Object>();
            for (Object item : src) {
                out.add(stripNulls(item));
            }
            return out;
        }
        return node;
    }
}

// ---- Configuration: which values to ensure exist on each field --------------
// Key   = '<ObjectDeveloperName>.<FieldDeveloperName>' (no namespace prefix)
// Value = list of [ valueApiName, label ]
Map<String, List<List<String>>> CONFIG = new Map<String, List<List<String>>>{
    'Prov_Field_Mapping.Type' => new List<List<String>>{
        new List<String>{ 'quote', 'quote' },
        new List<String>{ 'quote_line_item', 'quote_line_item' },
        new List<String>{ 'quote_group', 'quote_group' }
    },
    'Prov_Field_Mapping.Target_Object' => new List<List<String>>{
        new List<String>{ 'ibsbaucore__Beteiligte_Awarded__c', 'Participant Awarded' },
        new List<String>{ 'ibsbaucore__BOQ__c', 'ibsbaucore__BOQ__c' },
        new List<String>{ 'ibsbaucore__BOQ_Line__c', 'ibsbaucore__BOQ_Line__c' },
        new List<String>{ 'ibsbaucore__BOQ_Calculation__c', 'ibsbaucore__BOQ_Calculation__c' },
        new List<String>{ 'ibsbaucore__Project_Quote__c', 'ibsbaucore__Project_Quote__c' },
        new List<String>{ 'ibsbaucore__Project_Quote_Group__c', 'ibsbaucore__Project_Quote_Group__c' },
        new List<String>{ 'ibsbaucore__Project_Quote_Position__c', 'ibsbaucore__Project_Quote_Position__c' },
        new List<String>{ 'Quote', 'Quote' },
        new List<String>{ 'QuoteLineItem', 'QuoteLineItem' }
    },
    'Prov_Field_Mapping.Provider' => new List<List<String>>{
        new List<String>{ 'specifiq', 'specifiq' },
        new List<String>{ 'teleaktiv', 'teleaktiv' }
    },
    'Prov_Picklist_Mapping.Type' => new List<List<String>>{
        new List<String>{ 'Project', 'Project' },
        new List<String>{ 'Company', 'Company' },
        new List<String>{ 'Contact', 'Contact' },
        new List<String>{ 'Person', 'Person' },
        new List<String>{ 'quote', 'quote' },
        new List<String>{ 'quote_line_item', 'quote_line_item' },
        new List<String>{ 'quote_group', 'quote_group' }
    }
};

final String NS = 'ibsbaucore';
final String API = 'v62.0';
final String BASE = Url.getOrgDomainUrl().toExternalForm();
final String SID = UserInfo.getSessionId();

if (SID == null) {
    System.debug(LoggingLevel.ERROR, 'No session id. Run from the Developer Console (synchronous context).');
} else {
    Http http = new Http();

    // -- 1) Resolve object Ids ------------------------------------------------
    Set<String> objNames = new Set<String>();
    for (String key : CONFIG.keySet()) {
        objNames.add(key.substringBefore('.'));
    }
    String inObj = '\'' + String.join(new List<String>(objNames), '\',\'') + '\'';
    String objQ = 'SELECT Id, DeveloperName FROM CustomObject WHERE NamespacePrefix = \'' + NS +
                  '\' AND DeveloperName IN (' + inObj + ')';

    HttpRequest objReq = new HttpRequest();
    objReq.setEndpoint(BASE + '/services/data/' + API + '/tooling/query/?q=' + EncodingUtil.urlEncode(objQ, 'UTF-8'));
    objReq.setMethod('GET');
    objReq.setHeader('Authorization', 'Bearer ' + SID);
    HttpResponse objRes = http.send(objReq);

    Map<String, String> objIdByName = new Map<String, String>();   // DeveloperName -> ObjectId
    if (objRes.getStatusCode() == 200) {
        Map<String, Object> body = (Map<String, Object>) JSON.deserializeUntyped(objRes.getBody());
        for (Object rec : (List<Object>) body.get('records')) {
            Map<String, Object> r = (Map<String, Object>) rec;
            objIdByName.put((String) r.get('DeveloperName'), (String) r.get('Id'));
        }
    } else {
        System.debug(LoggingLevel.ERROR, 'Object lookup failed: ' + objRes.getStatusCode() + ' ' + objRes.getBody());
    }

    // -- 2) Resolve field Ids (object + field developer name) -----------------
    Set<String> fieldNames = new Set<String>();
    for (String key : CONFIG.keySet()) {
        fieldNames.add(key.substringAfter('.'));
    }
    String inFld = '\'' + String.join(new List<String>(fieldNames), '\',\'') + '\'';
    String fldQ = 'SELECT Id, DeveloperName, TableEnumOrId FROM CustomField WHERE NamespacePrefix = \'' + NS +
                  '\' AND DeveloperName IN (' + inFld + ')';

    HttpRequest fldReq = new HttpRequest();
    fldReq.setEndpoint(BASE + '/services/data/' + API + '/tooling/query/?q=' + EncodingUtil.urlEncode(fldQ, 'UTF-8'));
    fldReq.setMethod('GET');
    fldReq.setHeader('Authorization', 'Bearer ' + SID);
    HttpResponse fldRes = http.send(fldReq);

    Map<String, String> objNameById = new Map<String, String>();
    for (String n : objIdByName.keySet()) {
        objNameById.put(objIdByName.get(n), n);
    }

    Map<String, String> fieldIdByKey = new Map<String, String>();  // 'Obj.Field' -> CustomFieldId
    if (fldRes.getStatusCode() == 200) {
        Map<String, Object> body = (Map<String, Object>) JSON.deserializeUntyped(fldRes.getBody());
        for (Object rec : (List<Object>) body.get('records')) {
            Map<String, Object> r = (Map<String, Object>) rec;
            String objName = objNameById.get((String) r.get('TableEnumOrId'));
            if (objName == null) {
                continue;
            }
            String key = objName + '.' + (String) r.get('DeveloperName');
            if (CONFIG.containsKey(key) && !fieldIdByKey.containsKey(key)) {
                fieldIdByKey.put(key, (String) r.get('Id'));
            }
        }
    } else {
        System.debug(LoggingLevel.ERROR, 'Field lookup failed: ' + fldRes.getStatusCode() + ' ' + fldRes.getBody());
    }

    // -- 3) For each configured field: GET -> append missing -> PATCH ---------
    Integer grandTotal = 0;
    for (String key : CONFIG.keySet()) {
        String fieldId = fieldIdByKey.get(key);
        if (fieldId == null) {
            System.debug(LoggingLevel.ERROR, '[SKIP] ' + key + ' -> field not found in this org.');
            continue;
        }

        HttpRequest getReq = new HttpRequest();
        getReq.setEndpoint(BASE + '/services/data/' + API + '/tooling/sobjects/CustomField/' + fieldId);
        getReq.setMethod('GET');
        getReq.setHeader('Authorization', 'Bearer ' + SID);
        HttpResponse getRes = http.send(getReq);
        if (getRes.getStatusCode() != 200) {
            System.debug(LoggingLevel.ERROR, '[ERROR] ' + key + ' GET ' + getRes.getStatusCode() + ' ' + getRes.getBody());
            continue;
        }

        Map<String, Object> record = (Map<String, Object>) JSON.deserializeUntyped(getRes.getBody());
        Map<String, Object> meta = (Map<String, Object>) record.get('Metadata');
        Map<String, Object> valueSet = (Map<String, Object>) meta.get('valueSet');
        if (valueSet == null) {
            System.debug(LoggingLevel.ERROR, '[SKIP] ' + key + ' -> not an inline picklist.');
            continue;
        }
        Map<String, Object> vsd = (Map<String, Object>) valueSet.get('valueSetDefinition');
        if (vsd == null) {
            System.debug(LoggingLevel.ERROR, '[SKIP] ' + key + ' -> uses a Global Value Set; edit that value set instead.');
            continue;
        }
        List<Object> values = (List<Object>) vsd.get('value');

        Set<String> existing = new Set<String>();
        for (Object v : values) {
            Map<String, Object> vm = (Map<String, Object>) v;
            existing.add(((String) vm.get('valueName')).toLowerCase());
        }

        Integer added = 0;
        for (List<String> pair : CONFIG.get(key)) {
            if (existing.contains(pair[0].toLowerCase())) {
                continue;
            }
            values.add(new Map<String, Object>{
                'valueName' => pair[0],
                'label' => pair[1],
                'default' => false,
                'isActive' => true
            });
            added++;
        }

        if (added == 0) {
            System.debug(LoggingLevel.INFO, '[OK] ' + key + ' -> already up to date.');
            continue;
        }

        Map<String, Object> cleanMeta = (Map<String, Object>) new MetaUtil().stripNulls(meta);
        Map<String, Object> patchBody = new Map<String, Object>{ 'Metadata' => cleanMeta };
        HttpRequest patchReq = new HttpRequest();
        patchReq.setEndpoint(BASE + '/services/data/' + API + '/tooling/sobjects/CustomField/' + fieldId);
        patchReq.setMethod('PATCH');
        patchReq.setHeader('Authorization', 'Bearer ' + SID);
        patchReq.setHeader('Content-Type', 'application/json');
        patchReq.setBody(JSON.serialize(patchBody));
        HttpResponse patchRes = http.send(patchReq);

        if (patchRes.getStatusCode() == 204 || patchRes.getStatusCode() == 200) {
            grandTotal += added;
            System.debug(LoggingLevel.INFO, '[ADDED ' + added + '] ' + key);
        } else {
            System.debug(LoggingLevel.ERROR, '[ERROR] ' + key + ' PATCH ' + patchRes.getStatusCode() + ' ' + patchRes.getBody());
        }
    }

    System.debug(LoggingLevel.INFO, '==== DONE. Total values added: ' + grandTotal + ' ====');
}

Option B — Manual. Alternatively, add the values by hand in Setup → Object Manager using the tables below.

A. Prov Field Mapping (ibsbaucore__Prov_Field_Mapping__c)

1. ibsbaucore__Type__c — Add Picklist Values

LabelAPI Name
quotequote
quote_line_itemquote_line_item
quote_groupquote_group

2. ibsbaucore__Target_Object__c — Add Picklist Values

LabelAPI Name
Participant Awardedibsbaucore__Beteiligte_Awarded__c
ibsbaucore__BOQ__cibsbaucore__BOQ__c
ibsbaucore__BOQ_Line__cibsbaucore__BOQ_Line__c
ibsbaucore__BOQ_Calculation__cibsbaucore__BOQ_Calculation__c
ibsbaucore__Project_Quote__cibsbaucore__Project_Quote__c
ibsbaucore__Project_Quote_Group__cibsbaucore__Project_Quote_Group__c
ibsbaucore__Project_Quote_Position__cibsbaucore__Project_Quote_Position__c
QuoteQuote
QuoteLineItemQuoteLineItem

3. ibsbaucore__Provider__c — Add Picklist Values

LabelAPI Name
specifiqspecifiq
teleaktivteleaktiv

B. Prov Picklist Mapping (ibsbaucore__Prov_Picklist_Mapping__c)

1. ibsbaucore__Type__c — Add Picklist Values

LabelAPI Name
ProjectProject
CompanyCompany
ContactContact
PersonPerson
quotequote
quote_line_itemquote_line_item
quote_groupquote_group

Other new objects and fields in this release (Project Watchlist, Watchlist Suggestion, Project File Category, Quote Position Source Data) are delivered with the package and require no manual picklist changes.

Page Layout Considerations

The BC – Quote Line Editor component is not added to page layouts automatically. Add it manually to the BoQ and Project Quote Lightning Record Pages where you previously used the classic position grid. Legacy “z deprecated” components remain where previously placed and should be swapped for their modern equivalents at your convenience.

Still need help?

Our support team is happy to help with anything not covered here.

Contact support