How to properly use play_success_sound and play_fail_sound in BarcodeScanner?

Hi all,

I’m trying to use the optional sound settings in the BarcodeScanner class of ERPNext, as mentioned in the code comment:

// optional sound name to play when scan either fails or passes.
// see https://frappeframework.com/docs/v14/user/en/python-api/hooks#sounds
this.success_sound = opts.play_success_sound;
this.fail_sound = opts.play_fail_sound;

I would like to activate these sounds during barcode scanning (e.g. in Pick List or Stock Entry), so that:

  • :white_check_mark: a sound like "submit" is played on successful scans
  • :x: a sound like "error" is played when the item is not found

However, most doctypes initialize BarcodeScanner internally, and I’m not sure how or where to pass these options (play_success_sound, play_fail_sound) during that process.

:point_right: My question is:

  • How can I activate these sounds via Custom Script or config, without fully reinitializing the BarcodeScanner?
  • Is there a recommended way to inject these options globally or after scanner init?

Hi @maydo7777
Kindly review this post — it may be helpful to you.

Thank You!

Thanks for the explanation on how to add new sounds via hooks.py – that part is clear and already working.

However, my original question is more specific:

:point_right: How do I actually activate the play_success_sound and play_fail_sound options for the barcode scanner, e.g. in Pick List or Stock Entry?

I can see in the BarcodeScanner class that these options are supported:

this.success_sound = opts.play_success_sound;
this.fail_sound = opts.play_fail_sound;

…and that the methods play_success_sound() and play_fail_sound() are called inside process_scan().

But most doctypes (like Pick List) initialize the scanner internally, and there is no documented way to pass those options into the constructor from a Custom Script.

frappe.ui.form.on('Pick List', {
    onload: function(frm) {
        if (frm.barcode_scanner) {
            frm.barcode_scanner.success_sound = "submit"; 
            frm.barcode_scanner.fail_sound = "error"; 
        }
    }
});

this is not working, normally it should work ?

In this PR #32245, the play_success_sound and play_fail_sound options were added to the BarcodeScanner class — which is great!

But:
It’s still not clear how to properly use them in practice.

It’s quite frustrating to constantly look at the screen while scanning – especially in warehouse (Pick List) or production workflows – and sound feedback would solve that perfectly.

Can anyone help clarify

Would love some guidance or a best-practice way to get instant audio feedback when scanning – it’s a small thing but super important in daily use!

Thanks a lot :pray:

finally i was able to figure it out,
this code works, if anyone else is looking for scanner sound solution

if (erpnext && erpnext.utils && erpnext.utils.BarcodeScanner) {
    const OriginalBarcodeScanner = erpnext.utils.BarcodeScanner;

    erpnext.utils.BarcodeScanner = class CustomBarcodeScanner extends OriginalBarcodeScanner {
        constructor(opts) {
            let erweiterte_optionen = Object.assign({}, opts);

            if (!erweiterte_optionen.play_success_sound) {
                erweiterte_optionen.play_success_sound = "submit";
            }
            if (!erweiterte_optionen.play_fail_sound) {
                erweiterte_optionen.play_fail_sound = "error";
            }

            super(erweiterte_optionen);
        }
    };
}
1 Like

if anyone need, this code, is reading pick list positions (text2speech)

so u can pick with scanner without watching displays

ps: its in german, u can easily translate with chatgpt

// Globale Variable, in der wir unsere beste gefundene Stimme speichern
let besteStimme = null;

// Diese Funktion sucht und speichert die beste verfügbare Stimme
function findeUndLadeBesteStimme() {
    const wunschStimmenListe = ["Katja", "Anna", "Google Deutsch", "Samantha", "Karen", "Daniel"];
    const alleStimmen = window.speechSynthesis.getVoices();
    if (alleStimmen.length === 0) { return; }

    for (const wunschName of wunschStimmenListe) {
        const gefundeneStimme = alleStimmen.find(stimme => stimme.name.toLowerCase().includes(wunschName.toLowerCase()));
        if (gefundeneStimme) {
            besteStimme = gefundeneStimme;
            return;
        }
    }
}

window.speechSynthesis.onvoiceschanged = findeUndLadeBesteStimme;
findeUndLadeBesteStimme();

// Haupt-Sprachausgabefunktion
function speak(text_to_speak) {
    window.speechSynthesis.cancel();
    const utterance = new SpeechSynthesisUtterance(text_to_speak);
    utterance.lang = 'de-DE';
    if (besteStimme) {
        utterance.voice = besteStimme;
    }
    window.speechSynthesis.speak(utterance);
}

// --- NEU: Hilfsfunktion zum Kürzen von Text ---
function getShortText(fullText, maxLength = 35) {
    // Prüft, ob der Text existiert und länger als die maximale Länge ist
    if (fullText && fullText.length > maxLength) {
        // Gibt nur die ersten 'maxLength' Zeichen zurück
        return fullText.substring(0, maxLength);
    }
    // Gibt den vollen Text zurück, wenn er kürzer oder nicht vorhanden ist
    return fullText || "";
}


// --- Trigger für die Pick-Liste (mit Anpassung für gekürzten Namen) ---
frappe.ui.form.on('Pick List', {
    refresh: function(frm) {
        setTimeout(() => {
            const first_item = frm.doc.locations.find(row => row.picked_qty < row.qty);
            if (first_item) {
                // ANPASSUNG: Wir verwenden die neue Kürzungs-Funktion
                const itemName = getShortText(first_item.item_name);
                const text = `Starte Kommissionierung. Nächster Artikel: ${itemName}, ${first_item.qty} Stück, von Lagerplatz ${first_item.warehouse}.`;
                speak(text);
            }
        }, 500);
    }
});

frappe.ui.form.on('Pick List Item', 'picked_qty', function(frm, cdt, cdn) {
    const current_row = locals[cdt][cdn];
    const current_index = current_row.idx;
    const next_item = frm.doc.locations.find(row => row.idx > current_index && row.picked_qty < row.qty);

    if (next_item) {
        // ANPASSUNG: Wir verwenden die neue Kürzungs-Funktion
        const itemName = getShortText(next_item.item_name);
        const text = `Nächster Artikel: ${itemName}, ${next_item.qty} Stück, von Lagerplatz ${next_item.warehouse}.`;
        speak(text);
    } else {
        speak("Alle Artikel kommissioniert. Pick-Liste abgeschlossen.");
    }
});