Generate PDF on submit of sales documents

V13 beta is pretty usable already, but still you must expect a lot of changes and errors. I would recommend to only use the stable version in production.

I have a question regarding permissions with submitted pdfs.

Once a pdf is attached, the Read permissions are virtually set for every system user, isn’t it. As from my experience, I cannot limit read access to files attached based on user roles.

Please correct me if I’m wrong…

File is just a reguler doctype. You can restrict it just like sale orders, invoices etc.

yes right, but in this case, the permissions should not depend on the doctype “File”, but the doctype which the pdf represents, e.g. Sales Invoice or Quotation.

I see here a basic lack of permission control on ERPNext side, which is essenial.
Do I miss something?

For this use case it would be nice to have an option to choose either

  • Role based permission management for a particular document based on its doctype (default)
  • inherit permissions from another document (e.g. document where a file is attached to)

Does this app work in version 12?

Yes, but only for sales documents.

1 Like

New to how to include this, but going to test this. Looks promising. I was working around in python to achieve this in a different way, but your solution makes more sense. Thanks.

Can you define where it stores, e.g. NFS mount?

The app uses the builtin attachment feature of Frappe / ERPNext. So all files are stored on the server in ~/frappe-bench/sites/[YOUR SITE]/private/files. Maybe you can mount some other file system at this location?

1 Like

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 !