Cannot Create Multiple Item-variations in Sequence in Custom Script

I’m experiencing a really strange issue when trying to create multiple item variants in sequence, using a custom module for ERPNext.

The function I’m calling is create_variant from erpnext.controllers.item_variant. The first variant is created seamlessly, but the second one refuses to validate the attributes provided. Upon digging into this, it became clear that the second variant is being validated against the attributes of the product specified in the first variant.

Here’s the simplest code I could concoct to demonstrate the issue:

from erpnext.controllers.item_variant import create_variant

@frappe.whitelist()
def item_variant_test():
    # creates two item variants in squence using create_variant from erpnext.controllers.item_variant
    # create the item variant
    # Brace has 3 attributes, Width - in, Height - in, Length - ft
    item_variant_1_template = 'Brace'
    item_variant_1_attributes = {
        'Width - in': 12,
        'Height - in': 12,
        'Length - ft': 48
    }
    item_variant_1 = create_variant(item_variant_1_template, item_variant_1_attributes)
    item_variant_1.insert()

    # create the second item variant
    # Tenon has 2 attributes, Width - in, Length - in
    item_variant_2_template = 'Tenon'
    item_variant_2_attributes = {
        'Width - in': 12,
        'Length - in': 100
    }
    item_variant_2 = create_variant(item_variant_2_template, item_variant_2_attributes)
    item_variant_2.insert()

As you can see, there are a separate list of issues for both items. I call this on the frontend with a button, using this Javascript:

    // button for the item_variant_test function
    var item_variant_test_button = page.add_button('Item Variant Test', function () {
        frappe.call({
            method: 'cadwork_import.cadwork_import.page.bom_import.bom_import.item_variant_test',
            args: {
                data: bom_import_form.get_values(true)
            },
            callback: function (r) {
                if (r.message) {
                    frappe.msgprint(r.message);
                }
            }
        });
    }
    );

As it stands, this produces the error:

100 is not a valid Value for Attribute Length - in of Item Tenon-12-100.

This is not accurate, so I started digging to see where the error was originating and landed on the validate_item_variant_attributes function in apps/erpnext/erpnext/controllers/item_variant.py.

In this case, the “is not a valid Value for Attribute” error is being triggered based on the validate_item_attribute_value function, but that shouldn’t even be running since the Attributes are all numeric and if attribute.lower() in numeric_values should evaluate to true. So I added frappe.msgprint(str(numeric_values)) to see what is present in that variable, as follows:

def validate_item_variant_attributes(item, args=None):
	if isinstance(item, string_types):
		item = frappe.get_doc("Item", item)

	if not args:
		args = {d.attribute.lower(): d.attribute_value for d in item.attributes}

	attribute_values, numeric_values = get_attribute_values(item)
	frappe.msgprint(str(numeric_values))

	for attribute, value in args.items():
		if not value:
			continue

		if attribute.lower() in numeric_values:
			numeric_attribute = numeric_values[attribute.lower()]
			validate_is_incremental(numeric_attribute, attribute, value, item.name)

		else:
			attributes_list = attribute_values.get(attribute.lower(), [])
			validate_item_attribute_value(attributes_list, attribute, value, item.name, from_variant=True)

Here’s where I got the surprise. Both runs of the function are returning the same parent attributes:
image

What’s going on here? Is it simply a bug, or am I supposed to be “unloading” one item/doc before creating a second one?

For whatever it’s worth, here are the versions I’m running:
ERPNext: v13.41.1 (version-13)
Frappe Framework: v13.43.2 (version-13)

Okay, so I found a workaround by using frappe.enqueue to create the items, instead of creating them directly, as follows:

from erpnext.controllers.item_variant import create_variant

def create_variant_job(item_name, item_attributes):
    item = create_variant(item_name, item_attributes)
    item.insert()
    frappe.db.commit()
    return

@frappe.whitelist()
def item_variant_test():
    # creates two item variants in squence using create_variant from erpnext.controllers.item_variant
    # create the item variant
    # Brace has 3 attributes, Width - in, Height - in, Length - ft
    item_variant_1_template = 'Brace'
    item_variant_1_attributes = {
        'Width - in': 12,
        'Height - in': 12,
        'Length - ft': 48
    }
    frappe.enqueue(create_variant_job,
                        queue='default',
                        timeout=10,
                        is_async=True,
                        now=False,
                        enqueue_after_commit=False,
                        job_name='Creating Item Variant of ' + item_variant_1_template,
                        item_name=item_variant_1_template,
                        item_attributes=item_variant_1_attributes)

    # create the second item variant
    # Tenon has 2 attributes, Width - in, Length - in
    item_variant_2_template = 'Tenon'
    item_variant_2_attributes = {
        'Width - in': 12,
        'Length - in': 100
    }
    frappe.enqueue(create_variant_job,
                        queue='default',
                        timeout=10,
                        is_async=True,
                        now=False,
                        enqueue_after_commit=False,
                        job_name='Creating Item Variant of ' + item_variant_1_template,
                        item_name=item_variant_2_template,
                        item_attributes=item_variant_2_attributes)
    return "Item Variants created"

But why? Why should I need to use enqueue rather than just calling this directly? Can someone enlighten me as to what’s going on here?

On quick glance, I suspect the problem is that the get_attribute_values method is using flags (frappe’s version of system globals), which is possibly setting up a race condition for asynchronous insertions. I don’t know enough about the variant controller to understand why it was done that way, but using flags this way is generally speaking not ideal.

Your enqueue solution works because it’s making sure that one doc.insert is finished before the next one begins.

As for numeric/non-numeric, this is a checkbox in the Item Attribute document. You’ve got that checkbox ticked and it’s still parsing as non-numeric?

Thanks for this @peterg.

The flags explanation could make alot of sense here, but is there another way around this? Is there an equivalent to frappe.db.commit() for resetting global flags? My script relies on the sequential creation of Items, as an Item needs to exist before it can be added to a BOM. This workaround leaves me using a loop-based test to wait till the async/background task is finished before it proceeds. That’s not ideal.

As for the numeric/non-numeric issue, that actually has nothing to do with the numeric/non-numeric setting, and everything to do with the fact that the condition in question is testing against the wrong attribute set, thus it’s failing for a non-obvious reason.

Thanks to your lead @peterg, I was able to resolve this issue with a flag reset.

Specifically, frappe.flags.attribute_values is utilized in erpnext/item_variant.py at e54b23d71ba5d9da1aa5c1eee77f983c5852608c · frappe/erpnext · GitHub :

def get_attribute_values(item):
	if not frappe.flags.attribute_values:
		attribute_values = {}
		numeric_values = {}
		for t in frappe.get_all("Item Attribute Value", fields=["parent", "attribute_value"]):
			attribute_values.setdefault(t.parent.lower(), []).append(t.attribute_value)

		for t in frappe.get_all(
			"Item Variant Attribute",
			fields=["attribute", "from_range", "to_range", "increment"],
			filters={"numeric_values": 1, "parent": item.variant_of},
		):
			numeric_values[t.attribute.lower()] = t

		frappe.flags.attribute_values = attribute_values
		frappe.flags.numeric_values = numeric_values

	return frappe.flags.attribute_values, frappe.flags.numeric_values

Therefore, by issuing a frappe.flags.attribute_values = False before creating a variant, my issue is resolved.