diff --git a/app/__init__.py b/app/__init__.py index 2f38180..6238c4e 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -115,6 +115,7 @@ app.register_blueprint(esignature_views.eg043) app.register_blueprint(esignature_views.eg044) app.register_blueprint(esignature_views.eg045) +app.register_blueprint(esignature_views.eg046) app.register_blueprint(connect_views.cneg001) diff --git a/app/eSignature/examples/eg046_multiple_delivery.py b/app/eSignature/examples/eg046_multiple_delivery.py new file mode 100644 index 0000000..74461c3 --- /dev/null +++ b/app/eSignature/examples/eg046_multiple_delivery.py @@ -0,0 +1,253 @@ +import base64 +from datetime import datetime as dt, timezone +from os import path + +from docusign_esign import ( + EnvelopesApi, + EnvelopeDefinition, + Document, + Signer, + CarbonCopy, + SignHere, + Tabs, + Recipients, + RecipientPhoneNumber, + RecipientAdditionalNotification +) + +from flask import session, request + +from ...consts import demo_docs_path, pattern +from ...docusign import create_api_client +from ...ds_config import DS_CONFIG + + +class Eg046MultipleDeliveryController: + @staticmethod + def get_args(): + """Get request and session arguments""" + + # More data validation would be a good idea here + # Strip anything other than characters listed + signer_name = pattern.sub("", request.form.get("signer_name")) + signer_email = pattern.sub("", request.form.get("signer_email")) + cc_name = pattern.sub("", request.form.get("cc_name")) + cc_email = pattern.sub("", request.form.get("cc_email")) + signer_phone_number = request.form.get("signer_phone_number") + signer_country_code = request.form.get("signer_country_code") + cc_phone_number = request.form.get("cc_phone_number") + cc_country_code = request.form.get("cc_country_code") + delivery_method = request.form["delivery_method"] + envelope_args = { + "signer_name": signer_name, + "signer_email": signer_email, + "status": "sent", + "cc_name": cc_name, + "cc_email": cc_email, + "signer_country_code": signer_country_code, + "signer_phone_number": signer_phone_number, + "cc_country_code" :cc_country_code, + "cc_phone_number": cc_phone_number, + "delivery_method": delivery_method + } + args = { + "account_id": session["ds_account_id"], + "base_path": session["ds_base_path"], + "access_token": session["ds_access_token"], + "envelope_args": envelope_args + } + return args + + @classmethod + def worker(cls, args): + """ + 1. Create the envelope request object + 2. Send the envelope + """ + + #ds-snippet-start:eSign46Step3 + envelope_args = args["envelope_args"] + # Create the envelope request object + api_client = create_api_client(base_path=args["base_path"], access_token=args["access_token"]) + envelope_definition = cls.make_envelope(envelope_args) + # Call Envelopes::create API method + # Exceptions will be caught by the calling function + envelopes_api = EnvelopesApi(api_client) + (results, status, headers) = envelopes_api.create_envelope_with_http_info(account_id=args["account_id"], envelope_definition=envelope_definition) + + remaining = headers.get("X-RateLimit-Remaining") + reset = headers.get("X-RateLimit-Reset") + + if remaining is not None and reset is not None: + reset_date = dt.fromtimestamp(int(reset), tz=timezone.utc) + print(f"API calls remaining: {remaining}") + print(f"Next Reset: {reset_date}") + + envelope_id = results.envelope_id + + return {"envelope_id": envelope_id} + #ds-snippet-end:eSign46Step3 + + #ds-snippet-start:eSign46Step2 + @classmethod + def make_envelope(cls, args): + """ + Creates envelope: + document 1 (HTML) has signHere anchor tag: **signature_1** + document 2 (DOCX) has signHere anchor tag: /sn1/ + document 3 (PDF) has signHere anchor tag: /sn1/ + DocuSign will convert all of the documents to the PDF format. + The recipient’s field tags are placed using anchor strings. + The envelope has two recipients: + recipient 1: signer + recipient 2: cc + The envelope will be sent first to the signer via SMS. + After it is signed, a copy is sent to the cc recipient via SMS. + """ + # Create the envelope definition + env = EnvelopeDefinition( + email_subject="Please sign this document set" + ) + doc1_b64 = base64.b64encode(bytes(cls.create_document1(args), "utf-8")).decode("ascii") + # Read files 2 and 3 from a local folder + # The reads could raise an exception if the file is not available! + with open(path.join(demo_docs_path, DS_CONFIG["doc_docx"]), "rb") as file: + doc2_docx_bytes = file.read() + doc2_b64 = base64.b64encode(doc2_docx_bytes).decode("ascii") + with open(path.join(demo_docs_path, DS_CONFIG["doc_pdf"]), "rb") as file: + doc3_pdf_bytes = file.read() + doc3_b64 = base64.b64encode(doc3_pdf_bytes).decode("ascii") + + # Create the document models + document1 = Document( # Create the DocuSign document object + document_base64=doc1_b64, + name="Order acknowledgement", # Can be different from actual file name + file_extension="html", # Many different document types are accepted + document_id="1" # A label used to reference the doc + ) + document2 = Document( # Create the DocuSign document object + document_base64=doc2_b64, + name="Battle Plan", # Can be different from actual file name + file_extension="docx", # Many different document types are accepted + document_id="2" # A label used to reference the doc + ) + document3 = Document( # Create the DocuSign document object + document_base64=doc3_b64, + name="Lorem Ipsum", # Can be different from actual file name + file_extension="pdf", # Many different document types are accepted + document_id="3" # A label used to reference the doc + ) + # The order in the docs array determines the order in the envelope + env.documents = [document1, document2, document3] + + signer_phone_number = RecipientPhoneNumber( + country_code=args["signer_country_code"], + number=args["signer_phone_number"] + ) + signer_additional_notification = RecipientAdditionalNotification( + secondary_delivery_method=args["delivery_method"], + phone_number=signer_phone_number + ) + + # Create the signer recipient model + signer1 = Signer( + name=args["signer_name"], + email=args["signer_email"], + recipient_id="1", + routing_order="1", + delivery_method="Email", + additional_notifications=[signer_additional_notification] + ) + + # Create a RecipientPhoneNumber and add it to the additional SMS notification + cc_phone_number = RecipientPhoneNumber( + country_code=args["cc_country_code"], + number=args["cc_phone_number"] + ) + + cc_additional_notification = RecipientAdditionalNotification( + secondary_delivery_method=args["delivery_method"], + phone_number=cc_phone_number + ) + + # Create a cc recipient to receive a copy of the documents + cc1 = CarbonCopy( + name=args["cc_name"], + email=args["cc_email"], + recipient_id="2", + routing_order="2", + delivery_method="Email", + additional_notifications=[cc_additional_notification] + ) + + # routingOrder (lower means earlier) determines the order of deliveries + # to the recipients. Parallel routing order is supported by using the + # same integer as the order for two or more recipients + + # Create signHere fields (also known as tabs) on the documents + # We're using anchor (autoPlace) positioning + # + # The DocuSign platform searches throughout your envelope"s + # documents for matching anchor strings. So the + # signHere2 tab will be used in both document 2 and 3 since they + # use the same anchor string for their "signer 1" tabs + sign_here1 = SignHere( + anchor_string="**signature_1**", + anchor_units="pixels", + anchor_y_offset="10", + anchor_x_offset="20" + ) + + sign_here2 = SignHere( + anchor_string="/sn1/", + anchor_units="pixels", + anchor_y_offset="10", + anchor_x_offset="20" + ) + + # Add the tabs model (including the SignHere tabs) to the signer + # The Tabs object wants arrays of the different field/tab types + signer1.tabs = Tabs(sign_here_tabs=[sign_here1, sign_here2]) + + # Add the recipients to the envelope object + recipients = Recipients(signers=[signer1], carbon_copies=[cc1]) + env.recipients = recipients + + # Request that the envelope be sent by setting status to "sent" + # To request that the envelope be created as a draft, set to "created" + env.status = args["status"] + + return env + + @classmethod + def create_document1(cls, args): + """ Creates document 1 -- an html document""" + + return f""" + + + + + + +

World Wide Corp

+

Order Processing Division

+

Ordered by {args["signer_name"]}

+

Phone Number: {args["signer_phone_number"]}

+

Copy to: {args["cc_name"]}

+

+ Candy bonbon pastry jujubes lollipop wafer biscuit biscuit. Topping brownie sesame snaps sweet roll pie. + Croissant danish biscuit soufflé caramels jujubes jelly. Dragée danish caramels lemon drops dragée. + Gummi bears cupcake biscuit tiramisu sugar plum pastry. Dragée gummies applicake pudding liquorice. + Donut jujubes oat cake jelly-o. + Dessert bear claw chocolate cake gummies lollipop sugar plum ice cream gummies cheesecake. +

+ +

Agreed: **signature_1**/

+ + + """ +#ds-snippet-end:eSign46Step2 diff --git a/app/eSignature/views/__init__.py b/app/eSignature/views/__init__.py index 00c5cc9..3a66bfa 100644 --- a/app/eSignature/views/__init__.py +++ b/app/eSignature/views/__init__.py @@ -42,3 +42,4 @@ from .eg043_shared_access import eg043 from .eg044_focused_view import eg044 from .eg045_delete_restore_envelope import eg045 +from .eg046_multiple_delivery import eg046 diff --git a/app/eSignature/views/eg046_multiple_delivery.py b/app/eSignature/views/eg046_multiple_delivery.py new file mode 100644 index 0000000..bc26a66 --- /dev/null +++ b/app/eSignature/views/eg046_multiple_delivery.py @@ -0,0 +1,92 @@ +""" Example 046: Request a signature bt multiple delivery channels """ + +import json +from os import path + +from docusign_esign.client.api_exception import ApiException +from flask import redirect, render_template, session, Blueprint, url_for + +from ..examples.eg046_multiple_delivery import Eg046MultipleDeliveryController +from ...docusign import authenticate, ensure_manifest, get_example_by_number +from ...docusign.utils import is_cfr +from ...ds_config import DS_CONFIG +from ...error_handlers import process_error +from ...consts import API_TYPE + +example_number = 46 +api = API_TYPE["ESIGNATURE"] +eg = f"eg0{example_number}" # reference (and url) for this example +eg046 = Blueprint(eg, __name__) + + +@eg046.route(f"/{eg}", methods=["POST"]) +@ensure_manifest(manifest_url=DS_CONFIG["example_manifest_url"]) +@authenticate(eg=eg, api=api) +def send_by_multiple_channels(): + """ + 1. Get required arguments + 2. Call the worker method + 3. Render success response with envelopeId + """ + example = get_example_by_number(session["manifest"], example_number, api) + + # 1. Get required arguments + args = Eg046MultipleDeliveryController.get_args() + try: + # 1. Call the worker method + results = Eg046MultipleDeliveryController.worker(args) + except ApiException as err: + error_body_json = err and hasattr(err, "body") and err.body + # we can pull the DocuSign error code and message from the response body + try: + error_body = json.loads(error_body_json) + except json.decoder.JSONDecodeError: + error_body = {} + error_code = error_body and "errorCode" in error_body and error_body["errorCode"] + + # check for specific error + if "ACCOUNT_LACKS_PERMISSIONS" in error_code: + error_message = example["CustomErrorTexts"][0]["ErrorMessage"] + return render_template( + "error.html", + error_code=error_code, + error_message=error_message + ) + + return process_error(err) + + session["envelope_id"] = results["envelope_id"] # Save for use by other examples which need an envelopeId + + # 2. Render success response with envelopeId + return render_template( + "example_done.html", + title=example["ExampleName"], + message=f"The envelope has been created and sent!
Envelope ID {results['envelope_id']}." + ) + + +@eg046.route(f"/{eg}", methods=["GET"]) +@ensure_manifest(manifest_url=DS_CONFIG["example_manifest_url"]) +@authenticate(eg=eg, api=api) +def get_view(): + """responds with the form for the example""" + example = get_example_by_number(session["manifest"], example_number, api) + + cfr_status = is_cfr(session["ds_access_token"], session["ds_account_id"], session["ds_base_path"]) + if cfr_status == "enabled": + if DS_CONFIG["quickstart"] == "true": + return redirect(url_for("eg041.get_view")) + else: + return render_template("cfr_error.html", title="Error") + + return render_template( + "eSignature/eg046_multiple_delivery.html", + title=example["ExampleName"], + example=example, + source_file= "eg046_multiple_delivery.py", + source_url=DS_CONFIG["github_example_url"] + "eg046_multiple_delivery.py", + documentation=DS_CONFIG["documentation"] + eg, + show_doc=DS_CONFIG["documentation"], + signer_name=DS_CONFIG["signer_name"], + signer_email=DS_CONFIG["signer_email"] + ) diff --git a/app/templates/eSignature/eg046_multiple_delivery.html b/app/templates/eSignature/eg046_multiple_delivery.html new file mode 100644 index 0000000..474743d --- /dev/null +++ b/app/templates/eSignature/eg046_multiple_delivery.html @@ -0,0 +1,107 @@ + {% extends "base.html" %} {% block content %} + +{% include 'example_info.html' %} + +{% set recipient_form_index = 0 %} +{% set delivery_method_index = 0 %} +{% set sms_delivery_method_index = 1 %} +{% set whatsapp_delivery_method_index = 2 %} +{% set signer_name_index = 3 %} +{% set signer_email_index = 4 %} +{% set signer_country_code_index = 5 %} +{% set signer_phone_number_index = 6 %} +{% set cc_name_index = 7 %} +{% set cc_email_index = 8 %} +{% set cc_country_code_index = 9 %} +{% set cc_phone_number_index = 10 %} + +
+ {% if 'FormName' in example['Forms'][recipient_form_index] %} +

{{ example['Forms'][recipient_form_index]['FormName'] | safe }}

+ {% endif %} + +
+
+ + + + + + +
+ +
+ + +
+
+ + + + {{ session['manifest']['SupportingTexts']['HelpingTexts']['EmailWontBeShared'] | safe}} + +
+ +
+ + + + {{ session['manifest']['SupportingTexts']['HelpingTexts']['CountryCodeText'] | safe}} + +
+
+ + + + {{ session['manifest']['SupportingTexts']['HelpingTexts']['PhoneNumberWontBeShared'] | safe}} + +
+ +
+ + +
+
+ + + + {{ session['manifest']['SupportingTexts']['HelpingTexts']['EmailWontBeShared'] | safe}} + +
+ +
+ + + + {{ session['manifest']['SupportingTexts']['HelpingTexts']['CountryCodeText'] | safe}} + +
+
+ + + + {{ session['manifest']['SupportingTexts']['HelpingTexts']['PhoneNumberWontBeShared'] | safe}} + +
+ + + {% include 'submit_button.html' %} +
+ +{% endblock %} diff --git a/requirements.txt b/requirements.txt index 9ec341d..3d18fea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ astroid==3.3.10 certifi==2025.4.26 -cffi==1.17.1 +cffi==2.0.0 chardet==5.2.0 Click cryptography==45.0.3 -docusign-esign==5.3.0 +docusign-esign==5.4.0 docusign-rooms==1.3.0 docusign-monitor==1.2.0 docusign-click==1.4.0