update Stripe Settings
This commit is contained in:
parent
7b40c86b40
commit
afcd9a3488
@ -33,20 +33,35 @@ def stripe_webhook():
|
||||
payload = frappe.request.get_data()
|
||||
sig_header = frappe.request.headers.get('Stripe-Signature')
|
||||
session, metadata = StripeService.get_session_and_metadata(payload, sig_header)
|
||||
required_keys = ["order_num", "company", "payment_type"]
|
||||
for key in required_keys:
|
||||
if not metadata.get(key):
|
||||
raise frappe.ValidationError(f"Missing required metadata key: {key}")
|
||||
|
||||
# Validate required metadata
|
||||
if not metadata.get("company"):
|
||||
raise frappe.ValidationError("Missing required metadata key: company")
|
||||
if not metadata.get("payment_type"):
|
||||
raise frappe.ValidationError("Missing required metadata key: payment_type")
|
||||
|
||||
# Determine reference document based on payment type
|
||||
payment_type = metadata.get("payment_type")
|
||||
reference_doctype = None
|
||||
reference_doc_name = None
|
||||
|
||||
if payment_type == "advance":
|
||||
reference_doctype = "Sales Order"
|
||||
reference_doc_name = metadata.get("sales_order")
|
||||
if not reference_doc_name:
|
||||
raise frappe.ValidationError("Missing sales_order in metadata for advance payment")
|
||||
elif payment_type == "full":
|
||||
reference_doctype = "Sales Invoice"
|
||||
reference_doc_name = metadata.get("sales_invoice")
|
||||
if not reference_doc_name:
|
||||
raise frappe.ValidationError("Missing sales_invoice in metadata for full payment")
|
||||
else:
|
||||
raise frappe.ValidationError(f"Invalid payment type in metadata: {payment_type}")
|
||||
|
||||
# Check if payment already exists
|
||||
if DbService.exists("Payment Entry", {"reference_no": session.id}):
|
||||
raise frappe.ValidationError("Payment Entry already exists for this session.")
|
||||
|
||||
reference_doctype = "Sales Invoice"
|
||||
if metadata.get("payment_type") == "advance":
|
||||
reference_doctype = "Sales Order"
|
||||
elif metadata.get("payment_type") != "full":
|
||||
raise frappe.ValidationError("Invalid payment type in metadata.")
|
||||
|
||||
amount_paid = flt(session.amount_total) / 100
|
||||
|
||||
# Convert Unix timestamp to date string (YYYY-MM-DD)
|
||||
@ -63,7 +78,7 @@ def stripe_webhook():
|
||||
reference_date=reference_date,
|
||||
received_amount=amount_paid,
|
||||
company=metadata.get("company"),
|
||||
reference_doc_name=metadata.get("order_num")
|
||||
reference_doc_name=reference_doc_name
|
||||
)
|
||||
)
|
||||
pe.flags.ignore_permissions = True
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import frappe
|
||||
from custom_ui.services import AddressService, ClientService, ServiceAppointmentService, TaskService
|
||||
from custom_ui.services import SalesOrderService, AddressService, ClientService, ServiceAppointmentService, TaskService
|
||||
from datetime import timedelta
|
||||
import traceback
|
||||
|
||||
@ -83,7 +83,9 @@ def after_save(doc, method):
|
||||
print("DEBUG: After Save Triggered for Project:", doc.name)
|
||||
if doc.status == "Completed":
|
||||
print("DEBUG: Project marked as Completed. Generating and sending final invoice.")
|
||||
|
||||
sales_order_status = frappe.get_value("Sales Order", doc.sales_order, "billing_status")
|
||||
if sales_order_status == "Not Billed":
|
||||
SalesOrderService.create_sales_invoice_from_sales_order(doc.sales_order)
|
||||
if doc.ready_to_schedule:
|
||||
service_apt_ready_to_schedule = frappe.get_value("Service Address 2", doc.service_appointment, "ready_to_schedule")
|
||||
if not service_apt_ready_to_schedule:
|
||||
|
||||
16
custom_ui/events/sales_invoice.py
Normal file
16
custom_ui/events/sales_invoice.py
Normal file
@ -0,0 +1,16 @@
|
||||
import frappe
|
||||
from custom_ui.services.email_service import EmailService
|
||||
|
||||
def on_submit(doc, method):
|
||||
print("DEBUG: On Submit Triggered for Sales Invoice:", doc.name)
|
||||
|
||||
# Send invoice email to customer
|
||||
try:
|
||||
print("DEBUG: Preparing to send invoice email for", doc.name)
|
||||
EmailService.send_invoice_email(doc.name)
|
||||
print("DEBUG: Invoice email sent successfully for", doc.name)
|
||||
except Exception as e:
|
||||
print(f"ERROR: Failed to send invoice email: {str(e)}")
|
||||
# Don't raise the exception - we don't want to block the invoice submission
|
||||
frappe.log_error(f"Failed to send invoice email for {doc.name}: {str(e)}", "Invoice Email Error")
|
||||
|
||||
@ -76,68 +76,10 @@ def after_insert(doc, method):
|
||||
if doc.requires_half_payment:
|
||||
try:
|
||||
print("DEBUG: Sales Order requires half payment, preparing to send down payment email")
|
||||
from custom_ui.services.email_service import EmailService
|
||||
|
||||
# Get the customer/lead document to find the billing contact
|
||||
customer_doc = frappe.get_doc("Customer", doc.customer)
|
||||
|
||||
# Get billing contact email
|
||||
email = None
|
||||
if hasattr(customer_doc, 'primary_contact') and customer_doc.primary_contact:
|
||||
primary_contact = frappe.get_doc("Contact", customer_doc.primary_contact)
|
||||
email = primary_contact.email_id
|
||||
|
||||
# Fallback to primary contact or first available contact
|
||||
if not email and hasattr(customer_doc, 'customer_primary_contact') and customer_doc.customer_primary_contact:
|
||||
primary_contact = frappe.get_doc("Contact", customer_doc.customer_primary_contact)
|
||||
email = primary_contact.email_id
|
||||
|
||||
# Last resort - try to get any contact from the customer
|
||||
if not email:
|
||||
contact_links = frappe.get_all("Dynamic Link",
|
||||
filters={
|
||||
"link_doctype": "Customer",
|
||||
"link_name": doc.customer,
|
||||
"parenttype": "Contact"
|
||||
},
|
||||
pluck="parent"
|
||||
)
|
||||
if contact_links:
|
||||
contact = frappe.get_doc("Contact", contact_links[0])
|
||||
email = contact.email_id
|
||||
|
||||
if not email:
|
||||
print(f"ERROR: No email found for customer {doc.customer}, cannot send down payment email")
|
||||
return
|
||||
|
||||
# Prepare template context
|
||||
half_down_amount = doc.custom_halfdown_amount or (doc.grand_total / 2)
|
||||
base_url = frappe.utils.get_url()
|
||||
|
||||
template_context = {
|
||||
"company_name": doc.company,
|
||||
"customer_name": doc.customer_name or doc.customer,
|
||||
"sales_order_number": doc.name,
|
||||
"total_amount": frappe.utils.fmt_money(half_down_amount, currency=doc.currency),
|
||||
"base_url": base_url
|
||||
}
|
||||
|
||||
# Render the email template
|
||||
template_path = "custom_ui/templates/emails/downpayment.html"
|
||||
message = frappe.render_template(template_path, template_context)
|
||||
subject = f"Down Payment Required - {doc.company} - {doc.name}"
|
||||
|
||||
print(f"DEBUG: Sending down payment email to {email}")
|
||||
|
||||
# Send email
|
||||
frappe.sendmail(
|
||||
recipients=email,
|
||||
subject=subject,
|
||||
message=message,
|
||||
doctype="Sales Order",
|
||||
name=doc.name
|
||||
)
|
||||
|
||||
print(f"DEBUG: Down payment email sent to {email} successfully")
|
||||
# Use EmailService to send the down payment email
|
||||
EmailService.send_downpayment_email(doc.name)
|
||||
|
||||
except Exception as e:
|
||||
print(f"ERROR: Failed to send down payment email: {str(e)}")
|
||||
@ -196,3 +138,4 @@ def create_sales_invoice_from_sales_order(doc, method):
|
||||
# except Exception as e:
|
||||
# print("ERROR creating Sales Invoice from Sales Order:", str(e))
|
||||
# frappe.log_error(f"Error creating Sales Invoice from Sales Order {doc.name}: {str(e)}", "Sales Order after_submit Error")
|
||||
|
||||
|
||||
@ -2678,6 +2678,63 @@
|
||||
"unique": 0,
|
||||
"width": null
|
||||
},
|
||||
{
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"collapsible_depends_on": null,
|
||||
"columns": 0,
|
||||
"default": null,
|
||||
"depends_on": null,
|
||||
"description": null,
|
||||
"docstatus": 0,
|
||||
"doctype": "Custom Field",
|
||||
"dt": "Stripe Settings",
|
||||
"fetch_from": null,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "custom_webhook_secret",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"hide_border": 0,
|
||||
"hide_days": 0,
|
||||
"hide_seconds": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_preview": 0,
|
||||
"in_standard_filter": 0,
|
||||
"insert_after": "publishable_key",
|
||||
"is_system_generated": 0,
|
||||
"is_virtual": 0,
|
||||
"label": "Webhook Secret",
|
||||
"length": 0,
|
||||
"link_filters": null,
|
||||
"mandatory_depends_on": null,
|
||||
"modified": "2026-02-06 07:59:54.465395",
|
||||
"module": null,
|
||||
"name": "Stripe Settings-custom_webhook_secret",
|
||||
"no_copy": 0,
|
||||
"non_negative": 0,
|
||||
"options": null,
|
||||
"permlevel": 0,
|
||||
"placeholder": null,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": null,
|
||||
"read_only": 0,
|
||||
"read_only_depends_on": null,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"show_dashboard": 0,
|
||||
"sort_options": 0,
|
||||
"translatable": 1,
|
||||
"unique": 0,
|
||||
"width": null
|
||||
},
|
||||
{
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
@ -4046,6 +4103,63 @@
|
||||
"unique": 0,
|
||||
"width": null
|
||||
},
|
||||
{
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"collapsible_depends_on": null,
|
||||
"columns": 0,
|
||||
"default": null,
|
||||
"depends_on": null,
|
||||
"description": null,
|
||||
"docstatus": 0,
|
||||
"doctype": "Custom Field",
|
||||
"dt": "Stripe Settings",
|
||||
"fetch_from": null,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "custom_company",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"hide_border": 0,
|
||||
"hide_days": 0,
|
||||
"hide_seconds": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_preview": 0,
|
||||
"in_standard_filter": 0,
|
||||
"insert_after": "secret_key",
|
||||
"is_system_generated": 0,
|
||||
"is_virtual": 0,
|
||||
"label": "Company",
|
||||
"length": 0,
|
||||
"link_filters": null,
|
||||
"mandatory_depends_on": null,
|
||||
"modified": "2026-02-06 07:59:54.534298",
|
||||
"module": null,
|
||||
"name": "Stripe Settings-custom_company",
|
||||
"no_copy": 0,
|
||||
"non_negative": 0,
|
||||
"options": "Company",
|
||||
"permlevel": 0,
|
||||
"placeholder": null,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": null,
|
||||
"read_only": 0,
|
||||
"read_only_depends_on": null,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"show_dashboard": 0,
|
||||
"sort_options": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0,
|
||||
"width": null
|
||||
},
|
||||
{
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 1,
|
||||
@ -4502,6 +4616,63 @@
|
||||
"unique": 0,
|
||||
"width": null
|
||||
},
|
||||
{
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"collapsible_depends_on": null,
|
||||
"columns": 0,
|
||||
"default": null,
|
||||
"depends_on": null,
|
||||
"description": null,
|
||||
"docstatus": 0,
|
||||
"doctype": "Custom Field",
|
||||
"dt": "Stripe Settings",
|
||||
"fetch_from": null,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "custom_account",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"hide_border": 0,
|
||||
"hide_days": 0,
|
||||
"hide_seconds": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_preview": 0,
|
||||
"in_standard_filter": 0,
|
||||
"insert_after": "custom_company",
|
||||
"is_system_generated": 0,
|
||||
"is_virtual": 0,
|
||||
"label": "Account",
|
||||
"length": 0,
|
||||
"link_filters": null,
|
||||
"mandatory_depends_on": null,
|
||||
"modified": "2026-02-06 07:59:54.597448",
|
||||
"module": null,
|
||||
"name": "Stripe Settings-custom_account",
|
||||
"no_copy": 0,
|
||||
"non_negative": 0,
|
||||
"options": "Account",
|
||||
"permlevel": 0,
|
||||
"placeholder": null,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": null,
|
||||
"read_only": 0,
|
||||
"read_only_depends_on": null,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"show_dashboard": 0,
|
||||
"sort_options": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0,
|
||||
"width": null
|
||||
},
|
||||
{
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
|
||||
@ -15134,5 +15134,21 @@
|
||||
"property_type": "Data",
|
||||
"row_name": null,
|
||||
"value": "[\"customer_section\", \"column_break0\", \"custom_installation_address\", \"custom_job_address\", \"requires_half_payment\", \"custom_project_template\", \"custom_requires_halfdown\", \"title\", \"naming_series\", \"customer\", \"customer_name\", \"tax_id\", \"custom_halfdown_is_paid\", \"custom_halfdown_amount\", \"column_break_7\", \"transaction_date\", \"order_type\", \"delivery_date\", \"custom_department_type\", \"custom_project_complete\", \"column_break1\", \"po_no\", \"po_date\", \"company\", \"skip_delivery_note\", \"has_unit_price_items\", \"amended_from\", \"custom_section_break_htf05\", \"custom_workflow_related_custom_fields__landry\", \"custom_coordinator_notification\", \"custom_sales_order_addon\", \"accounting_dimensions_section\", \"cost_center\", \"dimension_col_break\", \"project\", \"currency_and_price_list\", \"currency\", \"conversion_rate\", \"column_break2\", \"selling_price_list\", \"price_list_currency\", \"plc_conversion_rate\", \"ignore_pricing_rule\", \"sec_warehouse\", \"scan_barcode\", \"last_scanned_warehouse\", \"column_break_28\", \"set_warehouse\", \"reserve_stock\", \"items_section\", \"items\", \"section_break_31\", \"total_qty\", \"total_net_weight\", \"column_break_33\", \"base_total\", \"base_net_total\", \"column_break_33a\", \"total\", \"net_total\", \"taxes_section\", \"tax_category\", \"taxes_and_charges\", \"exempt_from_sales_tax\", \"column_break_38\", \"shipping_rule\", \"column_break_49\", \"incoterm\", \"named_place\", \"section_break_40\", \"taxes\", \"section_break_43\", \"base_total_taxes_and_charges\", \"column_break_46\", \"total_taxes_and_charges\", \"totals\", \"base_grand_total\", \"base_rounding_adjustment\", \"base_rounded_total\", \"base_in_words\", \"column_break3\", \"grand_total\", \"rounding_adjustment\", \"rounded_total\", \"in_words\", \"advance_paid\", \"disable_rounded_total\", \"section_break_48\", \"apply_discount_on\", \"base_discount_amount\", \"coupon_code\", \"column_break_50\", \"additional_discount_percentage\", \"discount_amount\", \"sec_tax_breakup\", \"other_charges_calculation\", \"packing_list\", \"packed_items\", \"pricing_rule_details\", \"pricing_rules\", \"contact_info\", \"billing_address_column\", \"customer_address\", \"address_display\", \"customer_group\", \"territory\", \"column_break_84\", \"contact_person\", \"contact_display\", \"contact_phone\", \"contact_mobile\", \"contact_email\", \"shipping_address_column\", \"shipping_address_name\", \"shipping_address\", \"column_break_93\", \"dispatch_address_name\", \"dispatch_address\", \"col_break46\", \"company_address\", \"column_break_92\", \"company_contact_person\", \"company_address_display\", \"payment_schedule_section\", \"payment_terms_section\", \"payment_terms_template\", \"payment_schedule\", \"terms_section_break\", \"tc_name\", \"terms\", \"more_info\", \"section_break_78\", \"status\", \"delivery_status\", \"per_delivered\", \"column_break_81\", \"per_billed\", \"per_picked\", \"billing_status\", \"sales_team_section_break\", \"sales_partner\", \"column_break7\", \"amount_eligible_for_commission\", \"commission_rate\", \"total_commission\", \"section_break1\", \"sales_team\", \"loyalty_points_redemption\", \"loyalty_points\", \"column_break_116\", \"loyalty_amount\", \"subscription_section\", \"from_date\", \"to_date\", \"column_break_108\", \"auto_repeat\", \"update_auto_repeat_reference\", \"printing_details\", \"letter_head\", \"group_same_items\", \"column_break4\", \"select_print_heading\", \"language\", \"additional_info_section\", \"is_internal_customer\", \"represents_company\", \"column_break_152\", \"source\", \"inter_company_order_reference\", \"campaign\", \"party_account_currency\", \"connections_tab\"]"
|
||||
},
|
||||
{
|
||||
"default_value": null,
|
||||
"doc_type": "Stripe Settings",
|
||||
"docstatus": 0,
|
||||
"doctype": "Property Setter",
|
||||
"doctype_or_field": "DocType",
|
||||
"field_name": null,
|
||||
"is_system_generated": 0,
|
||||
"modified": "2026-02-06 08:00:17.665416",
|
||||
"module": null,
|
||||
"name": "Stripe Settings-main-field_order",
|
||||
"property": "field_order",
|
||||
"property_type": "Data",
|
||||
"row_name": null,
|
||||
"value": "[\"gateway_name\", \"publishable_key\", \"custom_webhook_secret\", \"column_break_3\", \"secret_key\", \"custom_company\", \"custom_account\", \"section_break_5\", \"header_img\", \"column_break_7\", \"redirect_url\"]"
|
||||
}
|
||||
]
|
||||
@ -9,4 +9,6 @@ from .service_appointment_service import ServiceAppointmentService
|
||||
from .stripe_service import StripeService
|
||||
from .payment_service import PaymentService
|
||||
from .item_service import ItemService
|
||||
from .project_service import ProjectService
|
||||
from .project_service import ProjectService
|
||||
from .sales_order_service import SalesOrderService
|
||||
from .email_service import EmailService
|
||||
@ -0,0 +1,240 @@
|
||||
import frappe
|
||||
from frappe.utils import get_url
|
||||
|
||||
class EmailService:
|
||||
|
||||
@staticmethod
|
||||
def get_customer_email(customer_name: str, doctype: str = "Customer") -> str | None:
|
||||
"""
|
||||
Get the primary email for a customer or lead.
|
||||
|
||||
Args:
|
||||
customer_name: Name of the Customer or Lead
|
||||
doctype: Either "Customer" or "Lead"
|
||||
|
||||
Returns:
|
||||
Email address if found, None otherwise
|
||||
"""
|
||||
try:
|
||||
customer_doc = frappe.get_doc(doctype, customer_name)
|
||||
email = None
|
||||
|
||||
# Try primary_contact field
|
||||
if hasattr(customer_doc, 'primary_contact') and customer_doc.primary_contact:
|
||||
try:
|
||||
primary_contact = frappe.get_doc("Contact", customer_doc.primary_contact)
|
||||
email = primary_contact.email_id
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not get primary_contact: {str(e)}")
|
||||
|
||||
# Fallback to customer_primary_contact
|
||||
if not email and hasattr(customer_doc, 'customer_primary_contact') and customer_doc.customer_primary_contact:
|
||||
try:
|
||||
primary_contact = frappe.get_doc("Contact", customer_doc.customer_primary_contact)
|
||||
email = primary_contact.email_id
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not get customer_primary_contact: {str(e)}")
|
||||
|
||||
# Last resort - get any contact linked to this customer/lead
|
||||
if not email:
|
||||
contact_links = frappe.get_all("Dynamic Link",
|
||||
filters={
|
||||
"link_doctype": doctype,
|
||||
"link_name": customer_name,
|
||||
"parenttype": "Contact"
|
||||
},
|
||||
pluck="parent"
|
||||
)
|
||||
if contact_links:
|
||||
try:
|
||||
contact = frappe.get_doc("Contact", contact_links[0])
|
||||
email = contact.email_id
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not get contact from dynamic link: {str(e)}")
|
||||
|
||||
return email
|
||||
|
||||
except Exception as e:
|
||||
print(f"ERROR: Failed to get email for {doctype} {customer_name}: {str(e)}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def send_templated_email(
|
||||
recipients: str | list,
|
||||
subject: str,
|
||||
template_path: str,
|
||||
template_context: dict,
|
||||
doctype: str = None,
|
||||
docname: str = None,
|
||||
cc: str | list = None,
|
||||
bcc: str | list = None,
|
||||
attachments: list = None
|
||||
) -> bool:
|
||||
"""
|
||||
Send an email using a Jinja2 template.
|
||||
|
||||
Args:
|
||||
recipients: Email address(es) to send to
|
||||
subject: Email subject line
|
||||
template_path: Path to the Jinja2 template (relative to app root)
|
||||
template_context: Dictionary of variables to pass to template
|
||||
doctype: Optional doctype to link email to
|
||||
docname: Optional document name to link email to
|
||||
cc: Optional CC recipients
|
||||
bcc: Optional BCC recipients
|
||||
attachments: Optional list of attachments
|
||||
|
||||
Returns:
|
||||
True if email sent successfully, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Render the email template
|
||||
message = frappe.render_template(template_path, template_context)
|
||||
|
||||
# Prepare sendmail arguments
|
||||
email_args = {
|
||||
"recipients": recipients,
|
||||
"subject": subject,
|
||||
"message": message,
|
||||
}
|
||||
|
||||
if doctype:
|
||||
email_args["doctype"] = doctype
|
||||
if docname:
|
||||
email_args["name"] = docname
|
||||
if cc:
|
||||
email_args["cc"] = cc
|
||||
if bcc:
|
||||
email_args["bcc"] = bcc
|
||||
if attachments:
|
||||
email_args["attachments"] = attachments
|
||||
|
||||
# Send email
|
||||
frappe.sendmail(**email_args)
|
||||
|
||||
print(f"DEBUG: Email sent successfully to {recipients}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"ERROR: Failed to send email: {str(e)}")
|
||||
frappe.log_error(f"Failed to send email to {recipients}: {str(e)}", "Email Service Error")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def send_downpayment_email(sales_order_name: str) -> bool:
|
||||
"""
|
||||
Send a down payment email for a Sales Order.
|
||||
|
||||
Args:
|
||||
sales_order_name: Name of the Sales Order
|
||||
|
||||
Returns:
|
||||
True if email sent successfully, False otherwise
|
||||
"""
|
||||
try:
|
||||
doc = frappe.get_doc("Sales Order", sales_order_name)
|
||||
|
||||
# Get customer email
|
||||
email = EmailService.get_customer_email(doc.customer, "Customer")
|
||||
|
||||
if not email:
|
||||
print(f"ERROR: No email found for customer {doc.customer}, cannot send down payment email")
|
||||
return False
|
||||
|
||||
# Prepare template context
|
||||
half_down_amount = doc.custom_halfdown_amount or (doc.grand_total / 2)
|
||||
base_url = get_url()
|
||||
|
||||
template_context = {
|
||||
"company_name": doc.company,
|
||||
"customer_name": doc.customer_name or doc.customer,
|
||||
"sales_order_number": doc.name,
|
||||
"total_amount": frappe.utils.fmt_money(half_down_amount, currency=doc.currency),
|
||||
"base_url": base_url
|
||||
}
|
||||
|
||||
# Send email
|
||||
template_path = "custom_ui/templates/emails/downpayment.html"
|
||||
subject = f"Down Payment Required - {doc.company} - {doc.name}"
|
||||
|
||||
return EmailService.send_templated_email(
|
||||
recipients=email,
|
||||
subject=subject,
|
||||
template_path=template_path,
|
||||
template_context=template_context,
|
||||
doctype="Sales Order",
|
||||
docname=doc.name
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"ERROR: Failed to send down payment email for {sales_order_name}: {str(e)}")
|
||||
frappe.log_error(f"Failed to send down payment email for {sales_order_name}: {str(e)}", "Down Payment Email Error")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def send_invoice_email(sales_invoice_name: str) -> bool:
|
||||
"""
|
||||
Send an invoice email for a Sales Invoice.
|
||||
|
||||
Args:
|
||||
sales_invoice_name: Name of the Sales Invoice
|
||||
|
||||
Returns:
|
||||
True if email sent successfully, False otherwise
|
||||
"""
|
||||
try:
|
||||
doc = frappe.get_doc("Sales Invoice", sales_invoice_name)
|
||||
|
||||
# Get customer email
|
||||
email = EmailService.get_customer_email(doc.customer, "Customer")
|
||||
|
||||
if not email:
|
||||
print(f"ERROR: No email found for customer {doc.customer}, cannot send invoice email")
|
||||
return False
|
||||
|
||||
# Calculate amounts
|
||||
outstanding_amount = doc.outstanding_amount
|
||||
paid_amount = doc.grand_total - outstanding_amount
|
||||
|
||||
# Get related Sales Order if available
|
||||
sales_order = None
|
||||
if hasattr(doc, 'items') and doc.items:
|
||||
for item in doc.items:
|
||||
if item.sales_order:
|
||||
sales_order = item.sales_order
|
||||
break
|
||||
|
||||
# Prepare template context
|
||||
base_url = get_url()
|
||||
|
||||
template_context = {
|
||||
"company_name": doc.company,
|
||||
"customer_name": doc.customer_name or doc.customer,
|
||||
"invoice_number": doc.name,
|
||||
"invoice_date": doc.posting_date,
|
||||
"due_date": doc.due_date,
|
||||
"grand_total": frappe.utils.fmt_money(doc.grand_total, currency=doc.currency),
|
||||
"outstanding_amount": frappe.utils.fmt_money(outstanding_amount, currency=doc.currency),
|
||||
"paid_amount": frappe.utils.fmt_money(paid_amount, currency=doc.currency),
|
||||
"sales_order": sales_order,
|
||||
"base_url": base_url,
|
||||
"payment_url": f"{base_url}/api/method/custom_ui.api.public.payments.invoice_stripe_payment?sales_invoice={doc.name}" if outstanding_amount > 0 else None
|
||||
}
|
||||
|
||||
# Send email
|
||||
template_path = "custom_ui/templates/emails/invoice.html"
|
||||
subject = f"Invoice {doc.name} - {doc.company}"
|
||||
|
||||
return EmailService.send_templated_email(
|
||||
recipients=email,
|
||||
subject=subject,
|
||||
template_path=template_path,
|
||||
template_context=template_context,
|
||||
doctype="Sales Invoice",
|
||||
docname=doc.name
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"ERROR: Failed to send invoice email for {sales_invoice_name}: {str(e)}")
|
||||
frappe.log_error(f"Failed to send invoice email for {sales_invoice_name}: {str(e)}", "Invoice Email Error")
|
||||
return False
|
||||
@ -13,7 +13,7 @@ class PaymentService:
|
||||
print(f"DEBUG: Creating Payment Entry for {data.reference_doc_name} with data: {data}")
|
||||
reference_doctype = PaymentService.determine_reference_doctype(data.reference_doc_name)
|
||||
reference_doc = DbService.get_or_throw(reference_doctype, data.reference_doc_name)
|
||||
account = StripeService.get_stripe_settings(data.company).account
|
||||
account = StripeService.get_stripe_settings(data.company).custom_account
|
||||
pe = frappe.get_doc({
|
||||
"doctype": "Payment Entry",
|
||||
"company": data.company,
|
||||
|
||||
@ -1,2 +1,23 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe.utils import today
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
|
||||
|
||||
class SalesOrderService:
|
||||
|
||||
@staticmethod
|
||||
def create_sales_invoice_from_sales_order(sales_order_name):
|
||||
try:
|
||||
sales_order_doc = frappe.get_doc("Sales Order", sales_order_name)
|
||||
sales_invoice = make_sales_invoice(sales_order_doc.name)
|
||||
sales_invoice.project = sales_order_doc.project
|
||||
sales_invoice.posting_date = today()
|
||||
sales_invoice.due_date = today()
|
||||
sales_invoice.remarks = f"Auto-generated from Sales Order {sales_order_doc.name}"
|
||||
sales_invoice.job_address = sales_order_doc.custom_job_address
|
||||
sales_invoice.project_template = sales_order_doc.custom_project_template
|
||||
sales_invoice.insert()
|
||||
sales_invoice.submit()
|
||||
return sales_invoice.name
|
||||
except Exception as e:
|
||||
print("ERROR creating Sales Invoice from Sales Order:", str(e))
|
||||
return None
|
||||
@ -9,7 +9,7 @@ class StripeService:
|
||||
@staticmethod
|
||||
def get_stripe_settings(company: str):
|
||||
"""Fetch Stripe settings for a given company."""
|
||||
settings_name = frappe.get_all("Stripe Settings", pluck="name", filters={"company": company})
|
||||
settings_name = frappe.get_all("Stripe Settings", pluck="name", filters={"custom_company": company})
|
||||
if not settings_name:
|
||||
frappe.throw(f"Stripe Settings not found for company: {company}")
|
||||
settings = frappe.get_doc("Stripe Settings", settings_name[0]) if settings_name else None
|
||||
@ -25,35 +25,84 @@ class StripeService:
|
||||
def get_webhook_secret(company: str) -> str:
|
||||
"""Retrieve the Stripe webhook secret for the specified company."""
|
||||
settings = StripeService.get_stripe_settings(company)
|
||||
if not settings.webhook_secret:
|
||||
if not settings.custom_webhook_secret:
|
||||
frappe.throw(f"Stripe Webhook Secret not configured for company: {company}")
|
||||
return settings.webhook_secret
|
||||
return settings.custom_webhook_secret
|
||||
|
||||
@staticmethod
|
||||
def create_checkout_session(company: str, amount: float, service: str, order_num: str, currency: str = "usd", for_advance_payment: bool = False, line_items: list | None = None) -> stripe.checkout.Session:
|
||||
"""Create a Stripe Checkout Session. order_num is a Sales Order name if for_advance_payment is True, otherwise it is a Sales Invoice name."""
|
||||
def create_checkout_session(
|
||||
company: str,
|
||||
amount: float,
|
||||
service: str,
|
||||
order_num: str,
|
||||
currency: str = "usd",
|
||||
for_advance_payment: bool = False,
|
||||
line_items: list | None = None,
|
||||
sales_invoice: str = None
|
||||
) -> stripe.checkout.Session:
|
||||
"""
|
||||
Create a Stripe Checkout Session.
|
||||
|
||||
Args:
|
||||
company: Company name
|
||||
amount: Payment amount (should be the outstanding amount for invoices)
|
||||
service: Service description
|
||||
order_num: Sales Order name if for_advance_payment is True, otherwise Sales Invoice name
|
||||
currency: Currency code (default: "usd")
|
||||
for_advance_payment: True if this is an advance/down payment, False for full invoice payment
|
||||
line_items: Optional custom line items for the checkout session
|
||||
sales_invoice: Sales Invoice name (for full payments)
|
||||
|
||||
Returns:
|
||||
stripe.checkout.Session object
|
||||
"""
|
||||
stripe.api_key = StripeService.get_api_key(company)
|
||||
|
||||
# Determine payment description
|
||||
if for_advance_payment:
|
||||
description = f"Advance payment for {company}{' - ' + service if service else ''}"
|
||||
else:
|
||||
description = f"Invoice payment for {company}{' - ' + service if service else ''}"
|
||||
if sales_invoice:
|
||||
description = f"Invoice {sales_invoice} - {company}"
|
||||
|
||||
# Use custom line items if provided and not an advance payment, otherwise create default line item
|
||||
line_items = line_items if line_items and not for_advance_payment else [{
|
||||
"price_data": {
|
||||
"currency": currency.lower(),
|
||||
"product_data": {
|
||||
"name": f"Advance payment for {company}{' - ' + service if service else ''}"
|
||||
"name": description
|
||||
},
|
||||
"unit_amount": int(amount * 100),
|
||||
"unit_amount": int(amount * 100), # Stripe expects amount in cents
|
||||
},
|
||||
"quantity": 1,
|
||||
}]
|
||||
|
||||
# Prepare metadata
|
||||
metadata = {
|
||||
"company": company,
|
||||
"payment_type": "advance" if for_advance_payment else "full"
|
||||
}
|
||||
|
||||
# Add appropriate document reference to metadata
|
||||
if for_advance_payment:
|
||||
metadata["sales_order"] = order_num
|
||||
else:
|
||||
metadata["sales_invoice"] = sales_invoice or order_num
|
||||
if sales_invoice:
|
||||
# Check if there's a related sales order
|
||||
invoice_doc = frappe.get_doc("Sales Invoice", sales_invoice)
|
||||
if hasattr(invoice_doc, 'items') and invoice_doc.items:
|
||||
for item in invoice_doc.items:
|
||||
if item.sales_order:
|
||||
metadata["sales_order"] = item.sales_order
|
||||
break
|
||||
|
||||
session = stripe.checkout.Session.create(
|
||||
mode="payment",
|
||||
payment_method_types=["card"],
|
||||
line_items=line_items,
|
||||
metadata={
|
||||
"order_num": order_num,
|
||||
"company": company,
|
||||
"payment_type": "advance" if for_advance_payment else "full"
|
||||
},
|
||||
metadata=metadata,
|
||||
success_url=f"{get_url()}/payment_success?session_id={{CHECKOUT_SESSION_ID}}",
|
||||
cancel_url=f"{get_url()}/payment_cancelled",
|
||||
)
|
||||
|
||||
@ -75,7 +75,7 @@
|
||||
<div class="payment-details">
|
||||
<h2>Payment Details</h2>
|
||||
<p><strong>Sales Order Number:</strong> {{ sales_order_number }}</p>
|
||||
<p><strong>Down Payment Amount:</strong> ${{ total_amount }}</p>
|
||||
<p><strong>Down Payment Amount:</strong> {{ total_amount }}</p>
|
||||
</div>
|
||||
<p>Please click the button below to make your secure payment through our payment processor:</p>
|
||||
<a href="{{ base_url }}/api/method/custom_ui.api.public.payments.half_down_stripe_payment?sales_order={{ sales_order_number }}" class="cta-button">Make Payment</a>
|
||||
|
||||
152
custom_ui/templates/emails/invoice.html
Normal file
152
custom_ui/templates/emails/invoice.html
Normal file
@ -0,0 +1,152 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Invoice - {{ invoice_number }}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background-color: #f4f4f4;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
background-color: #ffffff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.header h1 {
|
||||
color: #2c3e50;
|
||||
margin: 0;
|
||||
}
|
||||
.content {
|
||||
padding: 20px 0;
|
||||
}
|
||||
.invoice-details {
|
||||
background-color: #ecf0f1;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.invoice-details h2 {
|
||||
margin-top: 0;
|
||||
color: #3498db;
|
||||
}
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 5px 0;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
.detail-row:last-child {
|
||||
border-bottom: none;
|
||||
font-weight: bold;
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 2px solid #3498db;
|
||||
}
|
||||
.detail-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
.cta-button {
|
||||
display: inline-block;
|
||||
background-color: #27ae60;
|
||||
color: #ffffff;
|
||||
padding: 12px 24px;
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #eee;
|
||||
color: #7f8c8d;
|
||||
font-size: 14px;
|
||||
}
|
||||
.note {
|
||||
background-color: #fff3cd;
|
||||
border-left: 4px solid #ffc107;
|
||||
padding: 10px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>Invoice</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Dear {{ customer_name }},</p>
|
||||
<p>Thank you for your business with {{ company_name }}. Please find your invoice details below:</p>
|
||||
|
||||
<div class="invoice-details">
|
||||
<h2>Invoice Details</h2>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Invoice Number:</span>
|
||||
<span>{{ invoice_number }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Invoice Date:</span>
|
||||
<span>{{ invoice_date }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Due Date:</span>
|
||||
<span>{{ due_date }}</span>
|
||||
</div>
|
||||
{% if sales_order %}
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Related Sales Order:</span>
|
||||
<span>{{ sales_order }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Invoice Total:</span>
|
||||
<span>{{ grand_total }}</span>
|
||||
</div>
|
||||
{% if paid_amount and paid_amount != "$0.00" %}
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Amount Paid:</span>
|
||||
<span>{{ paid_amount }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Amount Due:</span>
|
||||
<span>{{ outstanding_amount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if payment_url and outstanding_amount != "$0.00" %}
|
||||
<div class="note">
|
||||
<strong>Payment Required:</strong> There is an outstanding balance on this invoice. Please click the button below to make a secure payment.
|
||||
</div>
|
||||
<a href="{{ payment_url }}" class="cta-button">Pay Now</a>
|
||||
{% else %}
|
||||
<div class="note">
|
||||
<strong>Paid in Full:</strong> This invoice has been paid in full. Thank you!
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<p>If you have any questions about this invoice, please don't hesitate to contact us.</p>
|
||||
<p>Best regards,<br>The Team at {{ company_name }}</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>This is an automated email. Please do not reply directly.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user