Building Packaged Customizations

Customization Manager enables many new development and code deployment workflows. In this article we’ll work our way up to a simple workflow that focuses delivered code on customer’s business logic, makes code re-use easier, and backporting fixes to existing customers a breeze.

A simple problem

Let’s start with a simple use case:

My customer needs to set an option field to indicate whether an order needs a compliance check before shipping. The field should be set to true if any of the items in the order is in the account set with code “COMP”.

Extender makes this easy. One solution is to check orders after update or insert and set the optional field accordingly. At a high level:

on insert:
    for each line in the order:
        if the item has an account set code == "COMP":
            set the optional field to True

A simple solution

Extender allows us to create a view script that will trigger on Insert or Update of an Order header. Handling the insert event only, we may end up with something like this attached to the OE0520 view:

from accpac.py import *

def onOpen():
    return Continue

def onAfterInsert(result):
    """Check the account set of all items and set the optional field."""
    # Open the order lines
    oeordd = self.openDataSource("dsOEORDD")

    # Seek to the first line
    oeordd.browse("")

    # Assume no compliance check is required
    compliance_required = False

    # Check all lines for any item requiring a check
    while oeordd.fetch() == 0:
        if oeordd.get("ACCTSET") == "COMP":
            compliance_required = True
            # If any on item matches we can stop looking
            break

    # Set the optional field, if required.
    # Open the header optional fields
    oeordho = self.openDataSource("dsOEORDHO")

    # Try reading the field to see if it already exists.
    oeordho.recordClear()
    oeordho.put("OPTFIELD", OPTFIELD)
    _read = oeordho.read()

    # If it exists, update the field
    if _read == 0:
        if oeordho.read("VALUE") != compliance_required:
            oeordho.put("VALUE", compliance_required)
            _update = oeordho.update()
            if _update != 0:
                showMessageBox("Compliance Optional Field - "
                               "Failed to set the field.")
    # If it does not, insert a new record
    else:
        oeordho.recordGenerate()
        oroedho.put("OPTFIELD", OPTFIELD)
        oeordho.put("VALUE", compliance_required)
        _insert = oeordho.insert()
        if _insert != 0:
            showMessageBox("Compliance Optional Field - "
                           "Failed to set the field.")

Rinse and repeat for the update. This solution isn’t pretty and needs a good refactor but it works. With all the logic required to interact with the views readability is not good - it is hard to tease the business logic out.

Another request

Close on the heels of the first request, another customer requires that an optional field be set on an order if an item begins with a particular string.

Patterns begin to emerge, things that are required to deliver on the customer’s business logic. For example:

  • Open a datasource, clear it, seek to the first record, and iterate. - Check all items in the order.
  • Check for the existence of a record using put and read. - Find whether an optional field already exists.
  • Insert or update an optional field. - If it exists, it needs updating, otherwise inserting.

Before starting the second request, let’s refactor the reusable parts out from the first one into a new file called extools.py

from accpac import *

def all_records_in(datasourceid):
    """Generator that yields all records in a datasource."""
    ds = self.openDataSource(datasourceid)
    ds.browse("")

    while ds.fetch() == 0:
        yield ds

def _optional_field_exists_in(datasource, field):
    """Check if a record with field = value exists."""
    datasource.recordClear()
    datasource.put("FIELD", field)
    if datasource.read() == 0:
        return True
    return False

def insert_or_update_optional_field(datasourceid, field, value):
    """Check if an optional field exists, if so update, otherwise insert"""
    ofds = self.openDataSource(datasourceid)
    ofds.recordClear()

    if _optional_field_exists_in(ofds, field):
        ofds.put("VALUE", value)
        if ofds.update() != 0:
            return false
    else
        ofds.recordGenerate()
        ofds.put("FIELD")
        ofds.put("VALUE", value)
        if ofds.insert() != 0:
            return False

    return True

Now, let’s use our new tools in the solution.

from accpac.py import *
from extools import (insert_or_update_optfield, all_records_in, )

CODE = "HAL"
OPTFIELD = "COMPLIANCE"

def onOpen():
    return Continue

def onAfterInsert(result):
    """Check the first characters of items and set the optional field."""
    # Assume no compliance check is required
    compliance_required = False

    # Check all lines for any item requiring a check
    for line in all_records_in("dsOEORDD"):
        if line.get("ITEM").startswith(CODE):
            compliance_required = True

    result = insert_or_update_optional_field(
        "dsOEORDHO", OPTFIELD, compliance_required)

    if not result:
        showMessageBox(
            "Failed to update {} optional field.".format(OPTFIELD))

    return Continue

That is a lot more readable - imagine trying to discuss what you’ve built with a client. Walking through the refactored version is much easier and reads almost exactly like the pseudo-code. The script is focused entirely on the customer’s business problem.

Packaging it all up

Now we just need a way to distribute the new package to users along with our script.

Creating a python package is easy, we can create one for our extools by creating a new directory in the right format and adding setup.py and empty __init__.py files.

extools/
    |- __init__.py
    |- setup.py
    |- docs/
    |- tests/
    |- extools/
        |- __init__.py
        |- extools.py

The special setup.py file contains instructions on how to install our package and what it depends on. Our simple package has no dependencies [2] but still requires a simple setup.py.

Note that documentation and testing are included directly in the package, along side the code, keeping all the elements of the customization together.

[2]It depends on accpac.py but that isn’t registered in package indexes.
from setuptools import setup

setup(
    name='extools',
    version='0.1',
    author='cbinckly',
    url='https://2665093.ca',
    author_email='cbinckly@gmail.com',
    packages=['extools'],
    description='Tools for Orchid Extender.',
    install_requires=[],
)

That is a minimum viable setup file. The setuptools package provides loads of other options that include advanced metadata, file installation, shortcuts, and more. Check out the Hitchhiker’s Guide to Python Packaging for excellent docs on the topic.

Now that we have our package, we just need to make it publicly available. We can publish on pypi.org or create a VCS repository at Github, bitbucket, or any other VCS server.

Now, the customer can deploy the script and install the extools package in two clicks using Customization Manager.

A bug report

All is well until a report comes in of a bug: the first script doesn’t always check all records. After some debugging, neither of them (or the three sebsequently delivered) always check all records!

It turns out data sources need to be .recordClear() before browsing. It is a simple one line change, before running <datasource>.browse("") we need to add <datasource>.recordClear().

For the first customer, a new script needs to be issued. For the second and all subsequent customers, if the extools package is updated once all they need to do is a two click upgrade. No fiddling with updated files for each customer, the fix is easily backported.

Conclusion

This is just one example of a workflow that is enabled by easy package management for Extender. It helps keep delivered code focused on the business logic, reduces the time to develop by encouraging and making reuse easy, and makes it simple to deploy fixes and backports to existing customers - all of which improve code quality, customer experience, and decrease support engagements.

See what the extools package has become.