Pages

Friday, September 7, 2018

D365FO: Import from Excel Template

Noted:  This content is based on Dynamics 365 for Finance and Operations version 8.0 Platform Update 15 



Scenario: 

Sometime consultants don't want to use the Data Management in D365 due to lack of data structure knowledge,  short timeline or many table relations.  They want to import from an Excel to all related tables in one shot.      


Solution: 

Prepare a batch class for importing master data.  I normally use this template and just change the part of create() method for each data.   


using System.IO;
using OfficeOpenXml;
using OfficeOpenXml.ExcelPackage;
using OfficeOpenXml.ExcelRange;

class AeyImportTemplate extends RunBaseBatch
{
    Filename        ItemFileName;
    Filename        filename;
    DialogField     dialogFilename;

    System.IO.Stream    stream;
    FileUploadTemporaryStorageResult fileUploadResult;
    OfficeOpenXml.ExcelRange    range;
    CommaTextStreamIo   commaIo;
    RecordInsertList    rsl;
    boolean             newIteration;
    int                 line,
                        lineimported,lineerror, lineskipped;
    Counter             loop;
    FileUploadBuild     dialogFileUpload;
    FileUpload          fileUpload;
    DialogRunbase       dialog;



    #define.CurrentVersion(1)
    #define.Version1(1)
    #localmacro.CurrentList
        fileName
    #endmacro

    void create(int row)
    {
        str                         itemId;
        price                       inventPrice;

        InventTableModule           inventTableModule;

        itemId = range.get_Item(row, 1).Value;
        inventPrice = range.get_Item(row, 2).Value;

        try
        {
            ttsbegin;
            select forUpdate inventTableModule
                where inventTableModule.ItemId  == ItemId
                && inventTableModule.ModuleType == ModuleInventPurchSales::Invent;

            if (!inventTableModule)
            {
                inventTableModule.clear();
            }
            inventTableModule.Price             = inventPrice;
            if (!inventTableModule)
            {
                inventTableModule.insert();
            }
            else
            {
                inventTableModule.update();
            }
            lineimported ++;
            ttscommit;
        }
        catch
        {
            lineerror ++;
            ttsAbort;
        }
    }

    void import()
    {
        Container               con;
        InventTable             inventTable;
        
        ;
        setPrefix("Import master data");
        
        if (this.openFile())
        {
            using (ExcelPackage Package = new ExcelPackage(stream))
            {
                int                         rowCount, i, endRow, startRow;
                Package.Load(stream);
                ExcelWorksheet  worksheet   = package.get_Workbook().get_Worksheets().get_Item(1);
                range       = worksheet.Cells;
                endRow      = worksheet.Dimension.End.Row;
                startRow    = worksheet.Dimension.Start.Row;
                rowCount    = endRow - startRow + 1;

                try
                {
                    for (i = 2; i<= rowCount; i++)
                    {
                        setPrefix(strFmt("Line %1", i));
                        this.create(i);
                    }
                }
                catch
                {
                    throw error("Error - Import template");
                }
            }
        }

        info(strfmt("%1 record(s) imported", lineimported));
        info(strfmt("%1 record(s) fail", lineerror));
    }

    boolean openFile()
    {
        boolean ret = false;

        if (fileUploadResult != null && fileUploadResult.getUploadStatus())
        {
            stream = fileUploadResult.openResult();
            ret = true;
        }
        else
        {
            throw error(strfmt("@SYS62207",filename));
        }

        return ret;
    }

    public Object dialog()
    {
        dialog = super();
        
        DialogGroup dialogGroup = dialog.addGroup('Upload file');

        FormBuildControl formBuildControl = dialog.formBuildDesign().control(dialogGroup.name());
        dialogFileUpload = formBuildControl.addControlEx(classstr(FileUpload), 'FileUpload');
        dialogFileUpload.style(FileUploadStyle::MinimalWithFilename);
        dialogFileUpload.fileNameLabel("@SYS308842");
        dialogFileUpload.fileTypesAccepted(".xlsx");
        
        return dialog;
    }

    /// <summary>
    /// Disables the dialog Ok button until the file upload is complete.
    /// </summary>
    /// <param name="_dialog">The <c>Runbase</c> dialog object.</param>
    public void dialogPostRun(DialogRunbase _dialog)
    {
        fileUpload = _dialog.formRun().control(_dialog.formRun().controlId('FileUpload'));
        fileUpload.notifyUploadCompleted += eventhandler(this.uploadCompleted);
        this.setDialogOkButtonEnabled(_dialog, false);
    }

    /// <summary>
    /// After the file has been uploaded, the Ok button is enabled.
    /// </summary>
    public void uploadCompleted()
    {
        filename = fileUpload.fileName();

        this.setDialogOkButtonEnabled(dialog, true);
        
        fileUploadResult = fileUpload.getFileUploadResult();

        fileUpload.notifyUploadCompleted -= eventhandler(this.UploadCompleted);
    }

    /// <summary>
    /// Enables or disables the dialog Ok button.
    /// </summary>
    /// <param name = "_dialog">The <c>Runbase</c> dialog object.</param>
    /// <param name = "_isEnabled">Indicates to enable or disable the Ok button.</param>
    protected void setDialogOkButtonEnabled(DialogRunbase _dialog, boolean _isEnabled)
    {
        FormControl okButtonControl = _dialog.formRun().control(_dialog.formRun().controlId('OkButton'));

        if (okButtonControl)
        {
            okButtonControl.enabled(_isEnabled);
        }
    }

    public container pack()
    {
        return [#CurrentVersion,#CurrentList];
    }

    public boolean unpack(container packedClass)
    {
        Version version = runbase::getVersion(packedClass);
        ;
        switch (version)
        {
            case #CurrentVersion:
                [version,#CurrentList] = packedClass;
                break;
            default:
                return false;
        }
        return true;
    }

    public void run()
    {
        this.import();
        super();
    }

    static void main(Args _args)
    {
        AeyImportTemplate   import;
        FormRun formRun;
        Args    args;


        ;
        import = new AeyImportTemplate();
        if (import.prompt())
        {
            import.run();
        }
    }

    public ClassDescription caption()
    {
        return 'Import template';
    }

}

Tuesday, September 4, 2018

D365FO: Renaming the primary key through coding

Noted:  This content is based on Dynamics 365 for Finance and Operations version 8.0 Platform Update 15 



Scenario: 

Rename many customer names. 


Solution: 

If the table has many relations, it will take a long time to rename through the record information one by one.  We can use the table method renamePrimaryKey() in a batch class and run in batch mode.   


     CustTable custTable;
     ttsBegin;
     select firstOnly custTable
         where custTable.AccountNum == 'XXXX-WC-00002';
     if (custTable)
     {
         custTable.AccountNum = 'XXXX-WC-00001';
         custTable.renamePrimaryKey();
     }    
     ttsCommit;





Saturday, April 29, 2017

Dynamics 365 for Operations: Extend String Size

Noted:  This content is based on Dynamics 365 for Operations Platform Update 5 


In previous AX version, once we want to change the string field size, we just only edit its String Size easily.  But it's funny that there are more steps in Dynamics 365.    

Scenario: 

Extend invoice field to 30 characters.  


Overlaying or extension: 

Not matter I try both overlaying and extension.  The String Size properties is always disabled.  Only using EDT overlaying, that allow to change the Extends properties.  

EDT overlaying is available, only when the EDT isn't in Application Platform package.   As below, the InvoiceId is in Application Suite, it's allowed to adjust Extends properties.        



The reason why we can't edit EDT in Application Platform package, e.g. Num.  Bececause we can't create the model under that package.        




So, your project must be in the model that under ApplicationSuite package for overlaying the EDT.  



Then, I create a new EDT with String Size 30 and replace it in InvoiceId's Extends properties.  Now the String Size is changed to 30.    



  
Later that, don't forget to do full synchronize database (Go to Dynamics 365 > Synchronized database) to check if there is an affect with some object.  If there is an error, you must adjust that object too.  








   

Thursday, April 27, 2017

Dynamics 365 for Operations: Customize Existing Report

Noted:  This content is based on Dynamics 365 for Operations Platform Update 5 


Yesterday, I tried my first customization for existing Report.  I can't find any training resource on Microsoft Learning Portal.  So, this is the way I tried by myself. 

Scenario: 

Customize an existing report on Dynamics 365.   For example, Dimension Statement in General Ledger module.   


Create Model:

Once you know which report will be customized and search for all related object.  Notice that those objects are under "Application Suite".    



When create a model about report, we must select the option "Select existing package" as a part of "ApplicationSuite" 







Add report the project:

Once create a new project after creating model.   Go to the report in AOT > right click Customize.   When finish to modify the report layout, right click at the report > Deploy Reports.       




  



Modify Class:

If you have to modify the DataProvider class, right click > Customize.




Build & Deploy:

Once finish customization, we can build the project and deploy the report together.  Select the project > right click > Deploy Reports







Wednesday, April 26, 2017

Dynamics 365 for Operations: Override Form Control Lookup


Noted:  This content is based on Dynamics 365 for Operations Platform Update 5 


It's been really long time, right?   I changed job 2 years ago and was too busy with projects.  Since right now, we are moving to Dynamics 365.  While writing this blog, I'm doing self-learning (started 3 days ago) and work with a project together.  So, I want to make a note for later reference. 


Scenario: 

In FreeText invoice form, create a lookup invoice list of current customer in the freeText invoice line.

I'll skip how to create new field in Dynamics 365 since there are many blogs mentioned already.  This will show the event handler class for the field in form only.  

Referenced packages:

Because we are going to write code about Query and static class.  We need to reference some packages.  I prepared a model referencing below packages.   

  • ApplicationFoundation 
  • ApplicationPlatform
  • ApplicatonSuite
  • Directory
  • SourceDocumentation
  • SourceDocumentationTypes

How do you know which package we should reference?  Just notice from the model of class we want to call.  For example as below, I'm going to use SysQuery class belonging to "Application Foundation".   



Or when I want to create a static class, there are 3 packages are needed; Directory, SourceDocumentation, SourceDocumentationTypes.  Once the project needs more packages, it will show some errors when we build the project to inform you.   


Copy Event Handler method: 

I created a string control for the new field and want to override OnLookup event.  Under the form control, go to Events > OnLookup > right click > Copy event handler method.   


      


Event Handler Class: 

Create a class to project and paste code.  It will create a method with FormControlEventType::Loopup.  This lookup will be used in another form.  So, I create another extension class sending the FormControl, its EventAgrs and InvoiceAccount as parameters.  Notice the red text blow to retrieve a field value (InvoiceId) from form datasource.     

/// <summary>
/// Handles events raised by <c>CustFreeInvoice</c> form.
/// </summary>
[ExtensionOf(FormStr(CustFreeInvoice))]
final public class PKA_CustFreeInvoiceEventHandler_Extension
{
    

    /// <summary>
    /// Adds a lookup to the <c>RefInvId</c> control on <c>CustInvoiceLine</c> form.
    /// </summary>
    /// <param name="_sender">The source of the event.</param>
    /// <param name="_e">Arguments of the OnLookup event.</param>
    [FormControlEventHandler(formControlStr(CustFreeInvoice, CustInvoiceLine_RefInvId), FormControlEventType::Lookup)]
    public static void CustInvoiceLine_PKA_RefInvId_OnLookup(FormControl _sender, FormControlEventArgs _e)
    {
        FormRun                 form = _sender.formRun();
        FormDataSource          custInvoiceTable_ds = form.dataSource(formDataSourceStr(CustFreeInvoice, CustInvoiceTable)) as FormDataSource;
        CustInvoiceTable        custInvoiceTable = custInvoiceTable_ds.cursor(); 

        PKA_CustInvoiceTable_Extension::lookup_RefInvId(_sender, _e, custInvoiceTable.InvoiceAccount);
    }


}


/// <summary>
/// Method extension for <c>CustInvoiceTable</c> table.
/// </summary>
public static class PKA_CustInvoiceTable_Extension
{
    /// <summary>
    /// Adds an InvoiceId lookup from CustInvoiceJour.
    /// </summary>
    /// <param name="_sender">The source of the event.</param>
    /// <param name="_e">Arguments of the OnLookup event.</param>
    [SysClientCacheDataMethodAttribute(true)]
    public static void lookup_RefInvId(FormControl _sender, FormControlEventArgs _e, CustInvoiceAccount _invoiceAccount)
    {
        SysTableLookup sysTableLookup = SysTableLookup::newParameters(tablenum(CustInvoiceJour), _sender);
        Query query = new Query();

        sysTableLookup.addLookupfield(fieldnum(CustInvoiceJour, InvoiceId), true);
        sysTableLookup.addLookupfield(fieldnum(CustInvoiceJour, InvoiceAccount));
        sysTableLookup.addLookupfield(fieldnum(CustInvoiceJour, InvoiceDate));

        QueryBuildDataSource qbds = query.addDataSource(tablenum(CustInvoiceJour));
        QueryBuildRange qdr = qbds.addRange(fieldnum(CustInvoiceJour, InvoiceAccount));
        qdr.value(queryValue(_invoiceAccount));
        qdr = qbds.addRange(fieldnum(CustInvoiceJour, InvoiceId));
        qdr.value(sysQuery::valueNotEmptyString());
        
        sysTableLookup.parmQuery(query);

        sysTableLookup.performFormLookup();

        FormControlCancelableSuperEventArgs ce = _e as FormControlCancelableSuperEventArgs;

        //cancel super() to prevent error.
        ce.CancelSuperCall();
    }

}
  

Thursday, June 4, 2015

AX 2012 Enterprise Portal: Create SSRS Report for EP

Noted:  This content is based on AX 2012 R2 + CU7 


It's been really long time, right?   I was so busy with company's projects and focused on other modules instead of Retail.  These days, I have to do Enterprise Portal with showing SSRS report.  Someone asks me to prepare guideline for colleagues.  So, just have idea to write in here too.    

Scenario: 

Create new SSRS Report to show on Enterprise Portal.  

Pages: 

Payroll Pay Statement is simplest example to start with.   

  • Go to:  Employee Services > Personal infomation > Pay stetements

To have SSRS on EP, there are 2 main pages

  • List page for sending report parameters



  • SSRS Viewer page  



Prepare SSRS Report: 


  • Temporary table 
    • TableType = InMemory
  • Report Data Provider Class (DP) 
    • Extends SRSReportDataProviderBase
  • Output MenuItem
    • RunOn = Server




Report Viewer: 

Web Control to support Report Viewer must extend AxBaseUserControl.  Set Output MenuItems in AxReportViewer.    

      <dynamics:AxReportViewer runat="server" ID="MyReport" MenuItemName="PayrollEPWorkerPayStatement" /> 

In code behind, page_init method.   Send parameters to report.  Below is example when parameter is from RecId. 

        protected void Page_Init(object sender, EventArgs e)
    {
        Dictionary<string, object> parms = new Dictionary<string,object>();

        int tableId = AxQueryString.GetCurrent(Page).WTID;
        KeyValuePair<int, string> kv1 = ControlHelper.DecryptWKEY(AxQueryString.GetCurrent(Page));

        Int64 payStatementRecId = 0;

        if (kv1.Value != null)
        {
            payStatementRecId = Int64.Parse(kv1.Value, CultureInfo.InvariantCulture);
        }

        // Add params required for report when accessed from EP
        parms.Add("PayrollPayStatementReport_PayStatement", payStatementRecId);

        //
        // Must add blank versions of the other variables in the data contract
        // even though they are not used when report is accessed from EP.
        //
        parms.Add("PayrollPayStatementReport_PayCycle", 0);
        parms.Add("PayrollPayStatementReport_PayPeriod", 0);
        parms.Add("Disbursement", "blank");
        parms.Add("PrintContributions", true);

        // Add the parameters to the report control 
        this.MyReport.AddParameters(parms);
    } 

But if parameter is from a field in dataset.  Need to get record from AxQueryString first.   Add below functions to get current record.  

       /// <summary>
    /// Get AxQueryString for the current page
    /// </summary>
    private AxQueryString AxQueryString
    {
        get
        {
            return AxQueryString.GetCurrent(this.Page);
        }
    }

    private ISession AxSession
    {
        get
        {
            AxBaseWebPart webpart = AxBaseWebPart.GetWebpart(this);
            return webpart == null ? null : webpart.Session;
        }

    }

Then in page_init, use IAxaptaRecordAdapter to get the record entity from AxQueryString.   

    Dictionary<string, object> parms = new Dictionary<string, object>();

    string itemId = ""; 

    if (this.AxQueryString != null && this.AxQueryString.RecordContext != null)
    {
        IAxaptaRecordAdapter entityRecord = null;
        entityRecord = this.AxQueryString.RecordContext.DataKey.GetRecord(this.AxSession);

        if (entityRecord != null)
        {
            itemId = (string)entityRecord.GetField("ItemId");
        }

    }

Noted:  Add reference as below for IAxaptaRecordAdapter. 

using Microsoft.Dynamics.AX.Framework.Services.Client;
using Microsoft.Dynamics.Framework.BusinessConnector.Session;
using Microsoft.Dynamics.Framework.BusinessConnector.Adapter;

Once a Web Control is created, there will be a Managed Web Content with same name.   You can deploy it here (right click > Deploy to EP).   



New Web URL MenuItem and Page Definitions will be created in AOT and deployed to EP.  


MenuItems: 

The list page can be Web Control or AX List Page form.  The most important is MenuItems.  There are 2 main MenuItems: -

  • Web URL MenuItem to open report viewer page 
  • Output MenuItem to call the report.



If the list page is Web Control, add AxHyperLinkBoundField for the Web URL MenuItem.   

      <dynamics:AxHyperLinkBoundField DataField="PaymentDate" DataSet="HcmEPWorkerPayStatementList" DataSetView="PayrollPayStatement"
                SortExpression="PaymentDate" MenuItem="PayrollEPWorkerPayStatement">                     
      </dynamics:AxHyperLinkBoundField>

If the list page is AX ListPage, set the Web URL MenuItem in HyperLinkMenuItem and HyperLinkDataSource (example from SalesTableListPage).





Reference: 

If you are beginner for Enterprise Portal, you may start it from Microsoft Dynamics AX 2012 White Paper: Enterprise Portal Development Cookbook.   There is everything in the white paper already.   I just gather about SSRS here.  Hope it helps you.  


   



       

Monday, December 16, 2013

AX Retail 2012 R2: Query product availability in POS

Noted:  This customization is based on AX 2012 R2 + CU6 

Scenario: 

Retail POS still allow to sell unavailable items.  How to check item availability and prevent selling unavailable one.  


Inventory On-hand & Item Bar code

When I have 2 physical inventory quantity of Item No. 81137 (with variant): -      



I also create item bar codes matched all variants:- 




Calculate inventory 

After that we need to calculate inventory from Retail > Period > Calculate inventory.  And then run job "Products, prices, and bar codes"  



Sale item 

Scan bar code 81183736BR with 2 quantities and pay cash. 



But when I re-scan the same bar code, POS still allow me to sell it.  It shouldn't do, right? 


Creates SQL Views 

After inventory calculation, the table RetailInventAvailability manages available quantities based on inventory dimension. But because of I have various items (both product / product master subtype), so I have to create views that cover all products.  

There are 3 main SQL views: -

  • PKARETAILSALESTYPE queries retail transaction ID for sales type
  • PKARETAILINVENTVARIANT queries inventory dimension based on table RetailInventAvailability  
  • PKARETAILINVENTSTOCK  queries all available items based on table RetailInventAvailability and quantity in stock  

CREATE VIEW [dbo].[PKARETAILSALESTYPE]
AS
SELECT        TRANSACTIONID, SALEISRETURNSALE
FROM            dbo.RETAILTRANSACTIONTABLE AS TRANS
WHERE        (TYPE = 2) AND (ENTRYSTATUS IN (0, 2))
AND EXISTS 
(SELECT 1 FROM dbo.RETAILTRANSACTIONSALESTRANS SALES 
WHERE TRANS.TRANSACTIONID = SALES.TRANSACTIONID 

AND   SALES.TRANSACTIONSTATUS IN (0, 2))


CREATE VIEW [dbo].[PKARETAILINVENTVARIANT]
AS
SELECT DISTINCT    
CASE 
WHEN ISNULL(SR.INVENTSERIALID, '') = '' THEN DIMV.INVENTDIMID
ELSE DIM.INVENTDIMID 
END
AS INVENTDIMAVAIL

CASE 
WHEN ISNULL(DIMC.ITEMID, '') = '' THEN SR.ITEMID 
ELSE DIMC.ITEMID 
END 
AS ITEMID
, ISNULL(DIMC.RETAILVARIANTID,'') AS RETAILVARIANTID
, DIM.INVENTDIMID, DIM.INVENTBATCHID, DIM.WMSLOCATIONID, 
                         DIM.WMSPALLETID, DIM.INVENTSERIALID, DIM.INVENTLOCATIONID, DIM.CONFIGID, DIM.INVENTSIZEID, DIM.INVENTCOLORID, DIM.INVENTSTYLEID, 
                         DIM.INVENTSITEID, DIM.DATAAREAID, DIM.RECVERSION, DIM.RECID
FROM            dbo.INVENTDIM AS DIM INNER JOIN
                         dbo.INVENTDIMCOMBINATION AS DIMC ON DIM.INVENTDIMID = DIMC.INVENTDIMID 
INNER JOIN
                         dbo.INVENTDIM AS DIMV ON 
DIM.INVENTBATCHID = DIMV.INVENTBATCHID AND DIM.WMSLOCATIONID = DIMV.WMSLOCATIONID AND 
                         DIM.WMSPALLETID = DIMV.WMSPALLETID AND DIM.CONFIGID = DIMV.CONFIGID AND 
                         DIM.INVENTSIZEID = DIMV.INVENTSIZEID AND DIM.INVENTCOLORID = DIMV.INVENTCOLORID AND DIM.INVENTSTYLEID = DIMV.INVENTSTYLEID AND 
                         DIM.INVENTSITEID = DIMV.INVENTSITEID 
AND DIM.INVENTLOCATIONID <> DIMV.INVENTLOCATIONID
 
LEFT OUTER JOIN INVENTSERIAL SR ON 

   DIM.INVENTSERIALID = SR.INVENTSERIALID AND DIMC.ITEMID = SR.ITEMID


CREATE VIEW [dbo].[PKARETAILINVENTSTOCK]
AS
SELECT        CHANNEL.RECID AS STORERECID
, INVENT.ITEMID
, DIM.INVENTDIMID
, DIMC.RETAILVARIANTID
, DIM.INVENTLOCATIONID
,CASE 
WHEN ISNULL(SALES.INVENTSERIALID,'') <> '' THEN SALES.INVENTSERIALID
WHEN ISNULL(SR.INVENTSERIALID,'') <> '' THEN SR.INVENTSERIALID  
ELSE ISNULL(DIM.INVENTSERIALID,'') 
END AS INVENTSERIALID
, DIM.INVENTCOLORID
, DIM.CONFIGID
, DIM.INVENTSIZEID
, DIM.INVENTSTYLEID
, ISNULL(BAR.ITEMBARCODE, N'') AS ITEMBARCODE
, AVAIL.AVAILPHYSICAL
,ISNULL(SALES.QTY, 0) AS SALESQTY
,ISNULL((AVAIL.AVAILPHYSICAL + ISNULL(SALES.QTY, 0)),0) AS QTYINSTOCK

FROM            dbo.RETAILINVENTAVAILABILITY AS AVAIL INNER JOIN
                         dbo.INVENTDIM AS DIM ON AVAIL.INVENTDIMRECID = DIM.RECID INNER JOIN
                         dbo.RETAILCHANNELTABLE AS CHANNEL ON DIM.INVENTLOCATIONID = CHANNEL.INVENTLOCATION INNER JOIN
                         dbo.INVENTTABLE AS INVENT ON AVAIL.ITEMRECID = INVENT.RECID AND DIM.DATAAREAID = INVENT.DATAAREAID 
INNER JOIN
                         dbo.PKARETAILINVENTVARIANT AS DIMC ON DIM.INVENTDIMID = DIMC.INVENTDIMAVAIL AND DIMC.ITEMID = INVENT.ITEMID AND 
                         DIMC.DATAAREAID = INVENT.DATAAREAID 
LEFT OUTER JOIN
                         dbo.INVENTITEMBARCODE AS BAR ON DIMC.INVENTDIMID = BAR.INVENTDIMID
LEFT OUTER JOIN
dbo.INVENTSERIAL AS SR ON BAR.ITEMBARCODE = SR.INVENTSERIALID AND SR.ITEMID = INVENT.ITEMID
LEFT OUTER JOIN ( 
SELECT  SALESTRANS.ITEMID, SALESTRANS.VARIANTID, INVENTSERIALID, SUM(SALESTRANS.QTY) AS QTY
FROM dbo.RETAILTRANSACTIONSALESTRANS AS SALESTRANS INNER JOIN
dbo.PKARETAILSALESTYPE AS TRANS ON SALESTRANS.TRANSACTIONID = TRANS.TRANSACTIONID
GROUP BY SALESTRANS.ITEMID, SALESTRANS.VARIANTID, SALESTRANS.INVENTSERIALID, INVENTLOCATIONID
HAVING (SUM(SALESTRANS.QTY) <> 0)
) AS SALES 
ON INVENT.ITEMID = SALES.ITEMID AND DIMC.RETAILVARIANTID = SALES.VARIANTID AND 
(DIM.INVENTSERIALID = SALES.INVENTSERIALID OR SR.INVENTSERIALID = SALES.INVENTSERIALID)
WHERE AVAIL.AVAILPHYSICAL > 0
   



OK, now we can see available / unavailable items.  I will explain in next blog how to use them to prevent selling unavailable items.