How to apply extra filter to two Child Tables with same data source?

@Nirmal_Balani I have created the Python and JavaScript codes for you. Before I send the codes to you, I have a suggestion for you but I’m not sure if it is what you want.

My suggestion is:

  1. Change the sales_invoice_number field from Link to Select
  2. On selecting a customer or an item trigger a function
  3. The function checks for the availability of the required field (customer, item), and then:
  • If any is not available, it doesn’t continue.
  • Else:
  1. It gets all the rows from Sales Invoice Item doctype that matches the selected item.
  2. Then it gets all the names from Sales Invoice doctype that matches the selected customer and the rows from the Sales Invoice Item doctype
  3. Finally it populates the Select options

This can be done using JavaScript only.

1 Like

Thank you. Using select instead of link does seem like a feasible option. It does accomplish the same task. I really am not an expert at even the client scripts. I will have to try and look into how to populate the select field, i do recall considering this option, but maybe i didn’t try hard enough. In case you have some experience with doing this, please do share for my use case, i’d be very grateful. Or if you have solved this issue for someone else, please share a refence link.
Since you have already done the python and javascript codes, if it works well, on frappecloud, i can go ahead with that.
Thank you.

How would the select options be populated? Just trying to understand, is it something like
frappe.db.sql(select * from tabSales Invoice Item where tabSales Invoice Item.parent = frm.sales_invoice_number)

And this is somehow used to fill the options of the select field?

@Nirmal_Balani You can try my suggestion and use the following Client Script.

I assumed that:

  1. Doctype name is Goods Return Form
  2. Customer fieldname is customer
  3. Item fieldname is item
frappe.ui.form.on('Goods Return Form', {
    refresh: function(frm) {
        frm.toggle_enable('sales_invoice_number', false);
        frm.trigger('prepare_sales_invoices');
    },
    customer: function(frm) {
        if (frm._old_customer && frm.doc.customer === frm._old_customer) return;
        frm._old_customer = frm.doc.customer;
        frm.trigger('prepare_sales_invoices');
    },
    item: function(frm) {
        if (frm._old_item && frm.doc.item === frm._old_item) return;
        frm._old_item = frm.doc.item;
        frm.trigger('prepare_sales_invoices');
    },
    prepare_sales_invoices: function(frm) {
        var doc = frm.doc;
        if (!doc.customer || !doc.item) return;
        frappe.db.get_list('Sales Invoice Item', {
            fields: ['parent'],
            filters: {
                item_code: doc.item,
                parenttype: 'Sales Invoice',
                parentfield: 'items',
            },
            pluck: 'parent'
        }).then(items_parent => {
            if (!items_parent.length) return;
            frappe.db.get_list('Sales Invoice', {
                fields: ['name'],
                filters: {
                    name: ['in', items_parent],
                    status: ['!=', 'Draft']
                },
                pluck: 'name'
            }).then(invoices => {
                if (!invoices.length) return;
                frm.set_df_property('sales_invoice_number', 'options', invoices);
                frm.get_field('sales_invoice_number').set_options();
                frm.toggle_enable('sales_invoice_number', true);
                if (doc.sales_invoice_number) {
                    frm.get_field('sales_invoice_number').$input.val(doc.sales_invoice_number);
                }
            });
        });
    }
});

What the code will do:

  • On start, it will disable sales_invoice_number field and trigger prepare_sales_invoices
  • On change of customer field, it will check if the value is same as the old value, else it will trigger prepare_sales_invoices
  • On change of item field, it will do the same thing as on change of customer field
  • On trigger of prepare_sales_invoices, it will:
  1. Check the value availability of customer and item fields
  2. It will get all the linked Sales Invoice names from Sales Invoice Item where the item_code equals the value of item field
  3. It will get all the Sales Invoice names where the name is in the list received from Sales Invoice Item and where the customer equals the value of customer field
  4. It will populate the sales_invoice_number options, select the sales invoice option if the sales_invoice_number field has value and then enable the sales_invoice_number field
1 Like

Thank you for your help. I will try this right now and revert with the results

Your code worked flawlessly



There is just one more thing i’d like to ask you. I forgot to mention this, but I’d also like to fetch the sales invoice item Quantity, as we would not want to accept more quantity to be returned than was invoiced, as well as invoice age. Please guide me here, this is the last of what i’m trying to do.
Thank you

@Nirmal_Balani Use the following modified code.

frappe.ui.form.on('Goods Return Form', {
    refresh: function(frm) {
        frm.toggle_enable('sales_invoice_number', false);
        frm.trigger('prepare_sales_invoices');
    },
    customer: function(frm) {
        if (frm._old_customer && frm.doc.customer === frm._old_customer) return;
        frm._old_customer = frm.doc.customer;
        frm.trigger('prepare_sales_invoices');
    },
    item: function(frm) {
        if (frm._old_item && frm.doc.item === frm._old_item) return;
        frm._old_item = frm.doc.item;
        frm.trigger('prepare_sales_invoices');
    },
    sales_invoice_number: function(frm) {
        if (frm._old_sales_invoice_number
            && frm.doc.sales_invoice_number === frm._old_sales_invoice_number) return;
        frm._old_sales_invoice_number = frm.doc.sales_invoice_number;
        frappe.db.get_value('Sales Invoice Item', {
            parent: frm.doc.sales_invoice_number,
            parenttype: 'Sales Invoice',
            parentfield: 'items',
            item_code: frm.doc.item
        }, 'qty').then(function(ret) {
            frm.set_value('invoiced_quantity', ret.message.qty);
        });
    },
    prepare_sales_invoices: function(frm) {
        var doc = frm.doc;
        if (!doc.customer || !doc.item) return;
        frappe.db.get_list('Sales Invoice Item', {
            fields: ['parent'],
            filters: {
                item_code: doc.item,
                parenttype: 'Sales Invoice',
                parentfield: 'items',
            },
            pluck: 'parent'
        }).then(items_parent => {
            if (!items_parent.length) return;
            frappe.db.get_list('Sales Invoice', {
                fields: ['name'],
                filters: {
                    name: ['in', items_parent],
                    status: ['!=', 'Draft']
                },
                pluck: 'name'
            }).then(invoices => {
                if (!invoices.length) return;
                frm.set_df_property('sales_invoice_number', 'options', invoices);
                frm.get_field('sales_invoice_number').set_options();
                frm.toggle_enable('sales_invoice_number', true);
                if (doc.sales_invoice_number) {
                    frm.get_field('sales_invoice_number').$input.val(doc.sales_invoice_number);
                }
            });
        });
    }
});
1 Like

I tried the modified code
Getting an error:
You do not have enough permissions to access this resource. Please contact your manager to get access.


It occurs when the sales invoice number is selected

I tried doing this as administrator as well, but the same error

@Nirmal_Balani It’s because of accessing a child doctype. Use the following.

frappe.ui.form.on('Goods Return Form', {
    refresh: function(frm) {
        frm.toggle_enable('sales_invoice_number', false);
        frm.trigger('prepare_sales_invoices');
    },
    customer: function(frm) {
        if (frm._old_customer && frm.doc.customer === frm._old_customer) return;
        frm._old_customer = frm.doc.customer;
        frm.trigger('prepare_sales_invoices');
    },
    item: function(frm) {
        if (frm._old_item && frm.doc.item === frm._old_item) return;
        frm._old_item = frm.doc.item;
        frm.trigger('prepare_sales_invoices');
    },
    sales_invoice_number: function(frm) {
        if (frm._old_sales_invoice_number
            && frm.doc.sales_invoice_number === frm._old_sales_invoice_number) return;
        frm._old_sales_invoice_number = frm.doc.sales_invoice_number;
        frappe.db.get_list('Sales Invoice Item', {
            fields: ['qty'],
            filters: {
                parent: frm.doc.sales_invoice_number,
                parenttype: 'Sales Invoice',
                parentfield: 'items',
                item_code: frm.doc.item
            },
            pluck: 'qty'
        }).then(function(ret) {
            frm.set_value('invoiced_quantity', ret[0]);
        });
    },
    prepare_sales_invoices: function(frm) {
        var doc = frm.doc;
        if (!doc.customer || !doc.item) return;
        frappe.db.get_list('Sales Invoice Item', {
            fields: ['parent'],
            filters: {
                item_code: doc.item,
                parenttype: 'Sales Invoice',
                parentfield: 'items',
            },
            pluck: 'parent'
        }).then(items_parent => {
            if (!items_parent.length) return;
            frappe.db.get_list('Sales Invoice', {
                fields: ['name'],
                filters: {
                    name: ['in', items_parent],
                    status: ['!=', 'Draft']
                },
                pluck: 'name'
            }).then(invoices => {
                if (!invoices.length) return;
                frm.set_df_property('sales_invoice_number', 'options', invoices);
                frm.get_field('sales_invoice_number').set_options();
                frm.toggle_enable('sales_invoice_number', true);
                if (doc.sales_invoice_number) {
                    frm._old_sales_invoice_number = doc.sales_invoice_number;
                    frm.get_field('sales_invoice_number').$input.val(doc.sales_invoice_number);
                }
            });
        });
    }
});

Sorry to keep bothering you. The last field to be filled was the selected invoice date. Please help with fetching that. Invoiced quantity worked perfectly

@Nirmal_Balani Not bothered at all. The following code will make multiple database queries, although I can reduce these queries but there will be several JS codes for filtering.

frappe.ui.form.on('Goods Return Form', {
    refresh: function(frm) {
        frm.toggle_enable('sales_invoice_number', false);
        frm.trigger('prepare_sales_invoices');
    },
    customer: function(frm) {
        if (frm._old_customer && frm.doc.customer === frm._old_customer) return;
        frm._old_customer = frm.doc.customer;
        frm.trigger('prepare_sales_invoices');
    },
    item: function(frm) {
        if (frm._old_item && frm.doc.item === frm._old_item) return;
        frm._old_item = frm.doc.item;
        frm.trigger('prepare_sales_invoices');
    },
    sales_invoice_number: function(frm) {
        if (frm._old_sales_invoice_number
            && frm.doc.sales_invoice_number === frm._old_sales_invoice_number) return;
        frm._old_sales_invoice_number = frm.doc.sales_invoice_number;
        frappe.db.get_value('Sales Invoice', frm.doc.sales_invoice_number, 'posting_date')
        .then(function(ret) {
            frm.set_value('invoice_date', ret.message.posting_date);
        });
        frappe.db.get_list('Sales Invoice Item', {
            fields: ['qty'],
            filters: {
                parent: frm.doc.sales_invoice_number,
                parenttype: 'Sales Invoice',
                parentfield: 'items',
                item_code: frm.doc.item
            },
            pluck: 'qty'
        }).then(function(ret) {
            frm.set_value('invoiced_quantity', ret[0]);
        });
    },
    prepare_sales_invoices: function(frm) {
        var doc = frm.doc;
        if (!doc.customer || !doc.item) return;
        frappe.db.get_list('Sales Invoice Item', {
            fields: ['parent'],
            filters: {
                item_code: doc.item,
                parenttype: 'Sales Invoice',
                parentfield: 'items',
            },
            pluck: 'parent'
        }).then(items_parent => {
            if (!items_parent.length) return;
            frappe.db.get_list('Sales Invoice', {
                fields: ['name'],
                filters: {
                    name: ['in', items_parent],
                    status: ['!=', 'Draft']
                },
                pluck: 'name'
            }).then(invoices => {
                if (!invoices.length) return;
                frm.set_df_property('sales_invoice_number', 'options', invoices);
                frm.get_field('sales_invoice_number').set_options();
                frm.toggle_enable('sales_invoice_number', true);
                if (doc.sales_invoice_number) {
                    frm._old_sales_invoice_number = doc.sales_invoice_number;
                    frm.get_field('sales_invoice_number').$input.val(doc.sales_invoice_number);
                }
            });
        });
    }
});
2 Likes

Works flawlessly!
I thought I could handle figuring out the invoice date - today’s date for the invoice age, But since you’re so well versed with erpnext, I thought i’d ask you. :sweat_smile:

@Nirmal_Balani The invoice age field must be of type Int so you can set the number of days.

frappe.ui.form.on('Goods Return Form', {
    refresh: function(frm) {
        frm.toggle_enable('sales_invoice_number', false);
        frm.trigger('prepare_sales_invoices');
    },
    customer: function(frm) {
        if (frm._old_customer && frm.doc.customer === frm._old_customer) return;
        frm._old_customer = frm.doc.customer;
        frm.trigger('prepare_sales_invoices');
    },
    item: function(frm) {
        if (frm._old_item && frm.doc.item === frm._old_item) return;
        frm._old_item = frm.doc.item;
        frm.trigger('prepare_sales_invoices');
    },
    sales_invoice_number: function(frm) {
        if (frm._old_sales_invoice_number
            && frm.doc.sales_invoice_number === frm._old_sales_invoice_number) return;
        frm._old_sales_invoice_number = frm.doc.sales_invoice_number;
        frappe.db.get_value('Sales Invoice', frm.doc.sales_invoice_number, 'posting_date')
        .then(function(ret) {
            frm.set_value('invoice_date', ret.message.posting_date);
            let now_dt = moment(),
            invoice_dt = moment(ret.message.posting_date, frappe.defaultDateFormat);
            frm.set_value('invoice_age', now_dt.diff(invoice_dt, 'days'));
        });
        frappe.db.get_list('Sales Invoice Item', {
            fields: ['qty'],
            filters: {
                parent: frm.doc.sales_invoice_number,
                parenttype: 'Sales Invoice',
                parentfield: 'items',
                item_code: frm.doc.item
            },
            pluck: 'qty'
        }).then(function(ret) {
            frm.set_value('invoiced_quantity', ret[0]);
        });
    },
    prepare_sales_invoices: function(frm) {
        var doc = frm.doc;
        if (!doc.customer || !doc.item) return;
        frappe.db.get_list('Sales Invoice Item', {
            fields: ['parent'],
            filters: {
                item_code: doc.item,
                parenttype: 'Sales Invoice',
                parentfield: 'items',
            },
            pluck: 'parent'
        }).then(items_parent => {
            if (!items_parent.length) return;
            frappe.db.get_list('Sales Invoice', {
                fields: ['name'],
                filters: {
                    name: ['in', items_parent],
                    status: ['!=', 'Draft']
                },
                pluck: 'name'
            }).then(invoices => {
                if (!invoices.length) return;
                frm.set_df_property('sales_invoice_number', 'options', invoices);
                frm.get_field('sales_invoice_number').set_options();
                frm.toggle_enable('sales_invoice_number', true);
                if (doc.sales_invoice_number) {
                    frm._old_sales_invoice_number = doc.sales_invoice_number;
                    frm.get_field('sales_invoice_number').$input.val(doc.sales_invoice_number);
                }
            });
        });
    }
});
2 Likes

Thank you for all your help. Just curious, I have never heard of ‘pluck’, amongst other things. Is there any frappe specific documentation that you can share that you have learned such advanced things from? Or is it more javascript?
Thank you

@Nirmal_Balani The pluck key wasn’t mentioned in any ERPNext documentation. I think it was mentioned in Frappe Framework > Python > Database Api documentation, but I’m not sure. I learned about it from browsing the source code of Frappe Framework and tracing the work of the code from JavaScript to Python I found out that it is possible to use the pluck key.

The complete workflow I am envisioning is that the sales rep does all the work for registering the customer’s return. Once validated, and submitted from the office, the Sales rep bring the goods to the warehouse. Upon Receiving the goods, the person in charge here, should be able to place the final approval, wherein the credit note is created for the invoice for that particular item. Since the Goods Return Form accepts only one item at a time, it may get cumbersome to create a credit note for each item on the invoice, for certain scenarios, as i realize now. Maybe some way to select multiple Goods Return for the same invoice? Perhaps. That would be the ideal Solution

So what we just did was the Initiation of the Goods Return Form, done by the rep. The form is saved as a Draft. The Returns Clerk will be notified of this new goods return, and will do the first approval, conveying to the rep to pick up the goods. This second state can be ‘Approval 1’. Once it is received in the warehouse, the Returns Manager will look at the state of the items and decide what must be done with each. Upon his input, Maybe a resolution for the item(Missing parts sent, item damaged, etc.), As far as i know, there is no in build handler for exchange of goods in erpnext. Maybe the item can be tagged as damaged, and the full issue recorded.
Once the goods have been assessed, either a credit note can be created, from the same goods return doctype, which should change its status to closed, and customer notified, or for an exchanged item, it is tied into the delivery trip, wherein I plan to also incorporate payment collections

1 Like

@Nirmal_Balani Regarding the returned goods, you can do it for multiple items but it depends if the request will be for items from one sales invoice or multiple. Either way it can be done easily.

Regarding the return process, all I can say is that it is very well planned. And whatever you find missing in ERPNext, it can be implemented.

1 Like