Generate PDF on submit of sales documents

Great, Ill just create a cron to move them!

When running the install, it all seems well.
erpnext@erp-test:/srv/bench/erpnext$ bench version
erpnext 12.x.x-develop
frappe 12.x.x-develop
pdf_on_submit 0.0.1

However, even after the restart, the search for PDF on Submit Settings does not provide any result. Do I oversee anything? Do I need to enable the app somewhere?

Thanks!

Edit: additionally, in About, I see the following:

Frappe Framework

Open Source Applications for the Web

Website: https://frappe.io

Source: Frappe Ā· GitHub

Installed Apps

ERPNext: v12.x.x-develop () (develop)

Frappe Framework: v12.x.x-develop () (develop)

Edit: Solved. Im a retard. Didnt enable properly

Youā€™re the best, thanks for sharing your plugin.

I solved it as follow with a bash script:

#!/usr/bin/env bash
shopt -s nullglob extglob
dest=/mnt/invoices/
src=/srv/bench/erpnext/sites/YOURSITEHERE/private/files/
now=$(date)

echo "Started script @ $now" 
for item in src*; do
if [[ $item == *YOUR DOCUMENT RANGE BETWEEN THE ASTERIX* ]] 
#SINV, SORD, etc, whatever your name your documents  
  then 
  mv  "$item" "$dest${item##*/}" || exit
  echo "${item##*/} moved"
fi
done

Additionally I created a script which through WEBDAVS has the same location mounted, which allows it to be printed, over an encrypted WEBDAV connection (nextcloud). This way, in my case, when my brother issues an invoice, it is printed at my moms house for her administration.

1 Like

@rmeyer nice App.
Just downloaded and testingā€¦ cool.
Maybe add this to your app if you think is a + pointā€¦ an option where user could select the name of the PDF (name, other custom field, etc)

1 Like

Hi @Helio_Jesus, good idea, iā€™ll consider adding this in a future version.

1 Like

@rmeyer
Very pleased eith your app. Do you think it would be possible to include delivery notes in future as well?

edit: nevermindā€¦ helps if you look into the right environment when testingā€¦ running in test environment checking if it exists in production, doesnā€™t really helpā€¦

@rmeyer

We had some experience with stolen goods during transport. Iā€™ve writter below Python script (please dont be harsh, its my first Python script), to use a Telegram bot to add images to the delivery note folder on a Nextcloud instance. It consists of two scripts; One moving the PDFā€™s to a folder by Customer/Delivery and then the second allowing to add images to this folder through Telegram.

Could probably be done more neat, but works for now, but any Security concerns I would be happy to hear.

import mysql.connector 
import requests
import os
import shutil

def left(s, amount):
    return s[:amount]

folder = "[NEXT CLOUD MOUNT FOLDER]/Deliveries/"

rejected_folder = "[NEXT CLOUD MOUNT FOLDER]/Deliveries/Rejected/"

db = mysql.connector.connect(host="[HOST]", user="[USER]", passwd="[PW]", db="[ERP NEXT DB]")
mycursor = db.cursor()

if not os.path.exists(rejected_folder):
    os.makedirs(rejected_folder)

for file in os.listdir(folder):
    if not file.lower().endswith(".pdf"):
        if os.path.isdir(folder + file):
            print(file + " is a directory")        
        else:       
            print(file + " is not a pdf")
            shutil.move(folder + file, rejected_folder + file)
        continue
    doc = left(file,17)
    try:
        mycursor.execute("""SELECT `customer` from `tabDelivery Note` where name = '%s'""" % doc)
        customer = mycursor.fetchone()
        customer = customer[0].replace(" ","_")
        if not os.path.exists(folder + customer + "/" + doc):
            os.makedirs(folder + customer + "/" + doc)
        print (file + " -> " + doc + " -> " + customer)
        shutil.move(folder + file, folder + customer + "/" + doc + "/" + file)
    except:
        print("Error")

The Telegram Bot script:

#!/usr/bin/env python3
# pylint: disable=W0613, C0116
# type: ignore[union-attr]


import logging
import datetime
from random import randint

from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove, Update
from telegram.ext import (
    Updater,
    CommandHandler,
    MessageHandler,
    Filters,
    ConversationHandler,
    CallbackContext,
)

import mysql.connector 
import requests
from fuzzywuzzy import process 

import os
import shutil

def left(s, amount):
    return s[:amount]

url = "[HERE YOUR NEXTCLOUD URL/FOLDER/ WHERE FILES ARE STORED"

folder = "[MOUNT LOCATION OF MY NEXTCLOUD INSTANCE/Deliveries/"

rejected_folder = "[MOUNT LOCATION OF MY NEXTCLOUD INSTANCE/Deliveries/Rejected/"

# Enable logging
logging.basicConfig(
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
)

logger = logging.getLogger(__name__)

CUSTOMER, MATDOC, PHOTO, PHOTO_CHOICE = range(4)

#Query the database and return the potential customers based on supplied variable
def start(update: Update, context: CallbackContext) -> int: 
    db = mysql.connector.connect(host="[HOST]", user="[USER]", passwd="[PASS]", db="[DB ERPNEXT]")
    mycursor = db.cursor()

    cust = update.message.text.split()
    klant = cust[1]

    mycursor.execute("""SELECT `name` from `tabCustomer`""")
    myresult = list(mycursor.fetchall())
    mycursor.close()
    db.close()
    #put three best matches in a list
    fuzz_cust = process.extract(klant,myresult, limit=3)
    logger.info(fuzz_cust)
    #for score, matchrow in process.extract(row, myresult):
    #    if score >= 60:
    #        logger.info('%d%% partial match: "%s" with "%s" ' % (score, row))
    Digi_Results = [[fuzz_cust[0][0][0]],[fuzz_cust[1][0][0]],[fuzz_cust[2][0][0]], ["/cancel"]]
    
    logger.info(Digi_Results)
    
    update.message.reply_text(
        'Select the customer:',
        reply_markup=ReplyKeyboardMarkup(Digi_Results, one_time_keyboard=True),
    )
    return CUSTOMER



def customer(update: Update, context: CallbackContext) -> int:
    matdocs = [["Yes"], ["No"], ["/cancel"]]
    user = update.message.from_user
    logger.info("Selected customer %s", update.message.text)
    global cust 
    cust = update.message.text
    db = mysql.connector.connect(host="[HOST]", user="[USER]", passwd="[PASS]", db="[DB ERPNEXT]")
    mycursor = db.cursor()
    mycursor.execute("""SELECT `name` from `tabDelivery Note` WHERE `customer` = '%s' AND `status` <> 'Cancelled' ORDER BY `creation` DESC LIMIT 3""" % cust)
    myresult = list(mycursor.fetchall())
    myresult.append(["/cancel"])

    update.message.reply_text(
        update.message.text +  " selected.\n\n"
        "Select Delivery Note or Cancel",
        reply_markup=ReplyKeyboardMarkup(myresult, one_time_keyboard=True),
    )
    mycursor.close()
    db.close()
    return MATDOC


def matdoc(update: Update, context: CallbackContext) -> int:
    global matdc
    matdc = update.message.text

    global store
    store = ""

    update.message.reply_text(
        update.message.text +  " selected.\n\n"
        "Send in photos. One at a time."
    )
    return PHOTO

def photo(update: Update, context: CallbackContext) -> int:
    user = update.message.from_user
    photo_file = update.message.photo[-1].get_file()
    #photo_file.download('img/' + str(datetime.date.today()) + '_' + str(randint(0, 10000)) + '.jpg')

    doc = left(matdc,17)
    customer = cust.replace(" ","_")
    photo_ref = str(datetime.date.today()) + '_' + str(randint(0, 10000)) + '.jpg'
    if not os.path.exists(folder + customer + "/" + doc):
        os.makedirs(folder + customer + "/" + doc)
    global loc
    loc = (url + customer + "/" + doc)
    photo_file.download(folder + customer + "/" + doc + "/" + photo_ref)

    re = [["Yes"], ["No"], ["/cancel"]]
    update.message.reply_text(
        "Your image is stored with customer: " + cust + ", delivery: "+left(matdc,17)+ ", want to add another?",
        reply_markup=ReplyKeyboardMarkup(re, one_time_keyboard=True),
    )
    return PHOTO_CHOICE


def photo_choice(update: Update, context: CallbackContext) -> int:
    user = update.message.from_user
    choice = update.message.text
    if choice == "Yes":
        update.message.reply_text(
            'Ok. Send a new picture.'
        )
        return PHOTO
    else:
        user = update.message.from_user
        logger.info("User is done.")
        update.message.reply_text("Your images are stored on " + loc + ". You can access them on Nextcloud.\n\nYou can start a new session by sending /photo + {customer}. This session will be ended. ")
        return ConversationHandler.END


def cancel(update: Update, context: CallbackContext) -> int:
    user = update.message.from_user
    logger.info("User %s canceled the conversation.", user.first_name)
    update.message.reply_text(
        'Transaction closed. Use /photo + {customer} to start a new session.', reply_markup=ReplyKeyboardRemove()
    )
    return ConversationHandler.END


def main() -> None:
    # Create the Updater and pass it your bot's token.
    updater = Updater("YOUR BOTS TOKEN")

    # Get the dispatcher to register handlers
    dispatcher = updater.dispatcher

    # Conversation handler
    conv_handler = ConversationHandler(
        entry_points=[CommandHandler('photo', start)],
        states={
            CUSTOMER: [MessageHandler(Filters.text & ~Filters.command, customer)],
            MATDOC: [MessageHandler(Filters.text & ~Filters.command, matdoc)],
            PHOTO: [MessageHandler(Filters.photo, photo)],
            PHOTO_CHOICE: [MessageHandler(Filters.text & ~Filters.command, photo_choice)]
            #SUMMARY: [MessageHandler(Filters.text & ~Filters.command, summary)]
        },
        fallbacks=[CommandHandler('cancel', cancel)],
    )

    dispatcher.add_handler(conv_handler)

    # Start the Bot
    updater.start_polling()

    # Run the bot until you press Ctrl-C or the process receives SIGINT,
    # SIGTERM or SIGABRT. This should be used most of the time, since
    # start_polling() is non-blocking and will stop the bot gracefully.
    updater.idle()


if __name__ == '__main__':
    main()

Edit:
So with /photo {customername} the bot will send three best matching customers to choose from. Based on this choice it shows latest three Material documents, you can select. From there you can submit photoā€™s which will be stored in the same folder as the material document.

When a customer has question about the state it received the goods, I have an image of how it looked when it was shipped.

2 Likes

Interesting feature.
This way the company can ā€œprintā€ the pdf on supplier or buyerā€™s office.

1 Like

Hi I use ERPNext v13 hosted on frappe cloud. I tried uninstalling and reinstalling PDF on Submit to go from v13 to v14 for this app but Iā€™m only showed v13 on Frappe ā€œAdd Appā€.
Is it because Iā€™m using ERPNext v13 ? Should I upgrade ?

Thanks

Yes, major versions are intended to match.

You need to use PDF on Submit v13 on ERPNext v13, just like you need Frappe v13 to run ERPNext v13.

Ok, thanks I get it, that makes sense !