From 8192d70f830709d55e57a512462a58b02afb6303 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 29 May 2026 12:32:57 +0530 Subject: [PATCH] refactor(delivery_note): move mapping functions to mapper.py --- .../doctype/delivery_note/delivery_note.py | 582 +---------------- erpnext/stock/doctype/delivery_note/mapper.py | 583 ++++++++++++++++++ 2 files changed, 593 insertions(+), 572 deletions(-) create mode 100644 erpnext/stock/doctype/delivery_note/mapper.py diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index f13c2d9c393..1229b1bad10 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -2,25 +2,27 @@ # License: GNU General Public License v3. See license.txt -import json - import frappe from frappe import _ -from frappe.contacts.doctype.address.address import get_company_address -from frappe.contacts.doctype.contact.contact import get_default_contact from frappe.desk.notifications import clear_doctype_notifications from frappe.model.document import Document -from frappe.model.mapper import get_mapped_doc -from frappe.model.utils import get_fetch_values from frappe.query_builder import DocType from frappe.query_builder.functions import Abs, Sum from frappe.utils import cint, flt -from erpnext.accounts.party import get_due_date -from erpnext.controllers.accounts_controller import get_taxes_and_charges, merge_taxes from erpnext.controllers.selling_controller import SellingController from erpnext.stock.doctype.packed_item.packed_item import make_packing_list +from .mapper import ( + make_delivery_trip, + make_installation_note, + make_inter_company_purchase_receipt, + make_packing_slip, + make_sales_invoice, + make_sales_return, + make_shipment, +) + form_grid_templates = {"items": "templates/form_grid/item_grid.html"} @@ -754,8 +756,6 @@ class DeliveryNote(SellingController): def update_billed_amount_based_on_so(so_detail, update_modified=True): - from frappe.query_builder.functions import Sum - # Billed against Sales Order directly si = frappe.qb.DocType("Sales Invoice").as_("si") si_item = frappe.qb.DocType("Sales Invoice Item").as_("si_item") @@ -850,569 +850,7 @@ def get_list_context(context=None): return list_context -def get_invoiced_qty_map(delivery_note): - """returns a map: {dn_detail: invoiced_qty}""" - sii = DocType("Sales Invoice Item") - - invoiced_qty_map = frappe._dict( - ( - frappe.qb.from_(sii) - .select(sii.dn_detail, Sum(sii.qty).as_("qty")) - .where((sii.delivery_note == delivery_note) & (sii.docstatus == 1)) - .groupby(sii.dn_detail) - ).run() - ) - - return invoiced_qty_map - - -def get_returned_qty_map(delivery_note): - """returns a map: {so_detail: returned_qty}""" - dn = DocType("Delivery Note") - dni = DocType("Delivery Note Item") - - returned_qty_map = frappe._dict( - ( - frappe.qb.from_(dni) - .join(dn) - .on(dn.name == dni.parent) - .select(dni.dn_detail, Sum(Abs(dni.qty)).as_("qty")) - .where( - (dn.docstatus == 1) - & (dn.is_return == 1) - & (dn.return_against == delivery_note) - & (dni.qty <= 0) - ) - .groupby(dni.dn_detail) - ).run() - ) - - return returned_qty_map - - -@frappe.whitelist() -def make_sales_invoice( - source_name: str, target_doc: str | Document | None = None, args: dict | str | None = None -): - if args is None: - args = {} - if isinstance(args, str): - args = json.loads(args) - - doc = frappe.get_doc("Delivery Note", source_name) - - to_make_invoice_qty_map = {} - returned_qty_map = get_returned_qty_map(source_name) - invoiced_qty_map = get_invoiced_qty_map(source_name) - - def set_missing_values(source, target): - target.run_method("set_missing_values") - target.run_method("set_po_nos") - - if len(target.get("items")) == 0: - frappe.throw(_("All these items have already been Invoiced/Returned")) - - if args and args.get("merge_taxes"): - merge_taxes(source, target) - - target.run_method("calculate_taxes_and_totals") - - # set company address - if source.company_address: - target.update({"company_address": source.company_address}) - else: - # set company address - target.update(get_company_address(target.company)) - - if target.company_address: - target.update(get_fetch_values("Sales Invoice", "company_address", target.company_address)) - - def update_item(source_doc, target_doc, source_parent): - target_doc.qty = to_make_invoice_qty_map[source_doc.name] - target_doc._old_name = source_doc.name - - def get_pending_qty(item_row): - pending_qty = item_row.qty - invoiced_qty_map.get(item_row.name, 0) - - returned_qty = 0 - if returned_qty_map.get(item_row.name, 0) > 0: - returned_qty = flt(returned_qty_map.get(item_row.name, 0)) - returned_qty_map[item_row.name] -= pending_qty - - if returned_qty: - if returned_qty >= pending_qty: - pending_qty = 0 - returned_qty -= pending_qty - else: - pending_qty -= returned_qty - returned_qty = 0 - - to_make_invoice_qty_map[item_row.name] = pending_qty - - return pending_qty - - def select_item(d): - filtered_items = args.get("filtered_children", []) - child_filter = d.name in filtered_items if filtered_items else True - return child_filter - - doc = get_mapped_doc( - "Delivery Note", - source_name, - { - "Delivery Note": { - "doctype": "Sales Invoice", - "field_map": {"is_return": "is_return"}, - "validation": {"docstatus": ["=", 1]}, - }, - "Delivery Note Item": { - "doctype": "Sales Invoice Item", - "field_map": { - "name": "dn_detail", - "parent": "delivery_note", - "so_detail": "so_detail", - "against_sales_order": "sales_order", - "cost_center": "cost_center", - }, - "postprocess": update_item, - "filter": lambda d: get_pending_qty(d) <= 0 - if not doc.get("is_return") - else get_pending_qty(d) > 0, - "condition": select_item, - }, - "Sales Taxes and Charges": { - "doctype": "Sales Taxes and Charges", - "reset_value": not (args and args.get("merge_taxes")), - "ignore": args.get("merge_taxes") if args else 0, - }, - "Sales Team": { - "doctype": "Sales Team", - "field_map": {"incentives": "incentives"}, - "add_if_empty": True, - }, - }, - target_doc, - set_missing_values, - ) - - automatically_fetch_payment_terms = cint( - frappe.get_single_value("Accounts Settings", "automatically_fetch_payment_terms") - ) - - if not doc.is_return: - from erpnext.accounts.services.payment_schedule import PaymentScheduleService - - ps = PaymentScheduleService(doc) - so, doctype, fieldname = ps.get_order_details() - if ( - ps.linked_order_has_payment_terms(so, fieldname, doctype) - and not automatically_fetch_payment_terms - ): - payment_terms_template = frappe.db.get_value(doctype, so, "payment_terms_template") - doc.payment_terms_template = payment_terms_template - doc.due_date = get_due_date( - doc.posting_date, - "Customer", - doc.customer, - doc.company, - template_name=doc.payment_terms_template, - ) - - elif automatically_fetch_payment_terms: - ps.set_payment_schedule() - - return doc - - -@frappe.whitelist() -def make_delivery_trip( - source_name: str, target_doc: str | Document | None = None, kwargs: dict | None = None -): - if not target_doc: - target_doc = frappe.new_doc("Delivery Trip") - - def update_address(source_doc, target_doc, source_parent): - target_doc.address = source_doc.shipping_address_name or source_doc.customer_address - target_doc.customer_address = source_doc.shipping_address or source_doc.address_display - - doclist = get_mapped_doc( - "Delivery Note", - source_name, - { - "Delivery Note": { - "doctype": "Delivery Stop", - "on_parent": target_doc, - "field_map": { - "name": "delivery_note", - "contact_person": "contact", - "contact_display": "customer_contact", - }, - "postprocess": update_address, - }, - }, - ignore_child_tables=True, - ) - - return doclist - - -@frappe.whitelist() -def make_installation_note( - source_name: str, target_doc: str | Document | None = None, kwargs: dict | None = None -): - def update_item(obj, target, source_parent): - target.qty = flt(obj.qty) - flt(obj.installed_qty) - target.serial_no = obj.serial_no - - doclist = get_mapped_doc( - "Delivery Note", - source_name, - { - "Delivery Note": {"doctype": "Installation Note", "validation": {"docstatus": ["=", 1]}}, - "Delivery Note Item": { - "doctype": "Installation Note Item", - "field_map": { - "name": "prevdoc_detail_docname", - "parent": "prevdoc_docname", - "parenttype": "prevdoc_doctype", - }, - "postprocess": update_item, - "condition": lambda doc: doc.installed_qty < doc.qty, - }, - }, - target_doc, - ) - - return doclist - - -@frappe.whitelist() -def make_packing_slip(source_name: str, target_doc: str | Document | None = None): - def set_missing_values(source, target): - target.run_method("set_missing_values") - - def update_item(obj, target, source_parent): - target.qty = flt(obj.qty) - flt(obj.packed_qty) - - doclist = get_mapped_doc( - "Delivery Note", - source_name, - { - "Delivery Note": { - "doctype": "Packing Slip", - "field_map": {"name": "delivery_note", "letter_head": "letter_head"}, - "validation": {"docstatus": ["=", 0]}, - }, - "Delivery Note Item": { - "doctype": "Packing Slip Item", - "field_map": { - "item_code": "item_code", - "item_name": "item_name", - "batch_no": "batch_no", - "description": "description", - "qty": "qty", - "uom": "stock_uom", - "name": "dn_detail", - }, - "postprocess": update_item, - "condition": lambda item: ( - not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code, "disabled": 0}) - and flt(item.packed_qty) < flt(item.qty) - ), - }, - "Packed Item": { - "doctype": "Packing Slip Item", - "field_map": { - "item_code": "item_code", - "item_name": "item_name", - "batch_no": "batch_no", - "description": "description", - "qty": "qty", - "name": "pi_detail", - }, - "postprocess": update_item, - "condition": lambda item: (flt(item.packed_qty) < flt(item.qty)), - }, - }, - target_doc, - set_missing_values, - ) - - return doclist - - -@frappe.whitelist() -def make_shipment(source_name: str, target_doc: str | Document | None = None): - def postprocess(source, target): - user = frappe.db.get_value( - "User", frappe.session.user, ["email", "full_name", "phone", "mobile_no"], as_dict=1 - ) - target.pickup_contact_email = user.email - pickup_contact_display = f"{user.full_name}" - if user: - if user.email: - pickup_contact_display += "
" + user.email - if user.phone: - pickup_contact_display += "
" + user.phone - if user.mobile_no and not user.phone: - pickup_contact_display += "
" + user.mobile_no - target.pickup_contact = pickup_contact_display - - # As we are using session user details in the pickup_contact then pickup_contact_person will be session user - target.pickup_contact_person = frappe.session.user - - contact_person = source.contact_person or get_default_contact("Customer", source.customer) - if contact_person: - contact = frappe.db.get_value( - "Contact", contact_person, ["email_id", "phone", "mobile_no"], as_dict=1 - ) - - delivery_contact_display = source.contact_display or contact_person or "" - if contact and not source.contact_display: - if contact.email_id: - delivery_contact_display += "
" + contact.email_id - if contact.phone: - delivery_contact_display += "
" + contact.phone - if contact.mobile_no and not contact.phone: - delivery_contact_display += "
" + contact.mobile_no - - target.delivery_contact_name = contact_person - if contact and contact.email_id and not target.delivery_contact_email: - target.delivery_contact_email = contact.email_id - target.delivery_contact = delivery_contact_display - - if source.shipping_address_name: - target.delivery_address_name = source.shipping_address_name - target.delivery_address = source.shipping_address - elif source.customer_address: - target.delivery_address_name = source.customer_address - target.delivery_address = source.address_display - - doclist = get_mapped_doc( - "Delivery Note", - source_name, - { - "Delivery Note": { - "doctype": "Shipment", - "field_map": { - "grand_total": "value_of_goods", - "company": "pickup_company", - "company_address": "pickup_address_name", - "company_address_display": "pickup_address", - "customer": "delivery_customer", - "contact_person": "delivery_contact_name", - "contact_email": "delivery_contact_email", - }, - "validation": {"docstatus": ["=", 1]}, - }, - "Delivery Note Item": { - "doctype": "Shipment Delivery Note", - "field_map": { - "name": "prevdoc_detail_docname", - "parent": "prevdoc_docname", - "parenttype": "prevdoc_doctype", - "base_amount": "grand_total", - }, - }, - }, - target_doc, - postprocess, - ) - - return doclist - - -@frappe.whitelist() -def make_sales_return(source_name: str, target_doc: str | Document | None = None): - from erpnext.controllers.sales_and_purchase_return import make_return_doc - - return make_return_doc("Delivery Note", source_name, target_doc) - - @frappe.whitelist() def update_delivery_note_status(docname: str, status: str): dn = frappe.get_lazy_doc("Delivery Note", docname) dn.update_status(status) - - -@frappe.whitelist() -def make_inter_company_purchase_receipt(source_name: str, target_doc: str | Document | None = None): - return make_inter_company_transaction("Delivery Note", source_name, target_doc) - - -def make_inter_company_transaction(doctype, source_name, target_doc=None): - from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( - get_inter_company_details, - set_purchase_references, - update_address, - update_taxes, - validate_inter_company_transaction, - ) - - if doctype == "Delivery Note": - source_doc = frappe.get_doc(doctype, source_name) - target_doctype = "Purchase Receipt" - source_document_warehouse_field = "target_warehouse" - target_document_warehouse_field = "from_warehouse" - else: - source_doc = frappe.get_doc(doctype, source_name) - target_doctype = "Delivery Note" - source_document_warehouse_field = "from_warehouse" - target_document_warehouse_field = "target_warehouse" - - validate_inter_company_transaction(source_doc, doctype) - details = get_inter_company_details(source_doc, doctype) - - def set_missing_values(source, target): - target.run_method("set_missing_values") - set_purchase_references(target) - - if target.doctype == "Purchase Receipt": - master_doctype = "Purchase Taxes and Charges Template" - else: - master_doctype = "Sales Taxes and Charges Template" - - if not target.get("taxes") and target.get("taxes_and_charges"): - for tax in get_taxes_and_charges(master_doctype, target.get("taxes_and_charges")): - target.append("taxes", tax) - - if not target.get("items"): - frappe.throw(_("All items have already been received")) - - def update_details(source_doc, target_doc, source_parent): - def _validate_address_link(address, link_doctype, link_name): - return frappe.db.get_value( - "Dynamic Link", - { - "parent": address, - "parenttype": "Address", - "link_doctype": link_doctype, - "link_name": link_name, - }, - "parent", - ) - - target_doc.inter_company_invoice_reference = source_doc.name - if target_doc.doctype == "Purchase Receipt": - target_doc.company = details.get("company") - target_doc.supplier = details.get("party") - target_doc.buying_price_list = source_doc.selling_price_list - target_doc.is_internal_supplier = 1 - target_doc.inter_company_reference = source_doc.name - - # Invert the address on target doc creation - if source_doc.company_address and _validate_address_link( - source_doc.company_address, "Supplier", details.get("party") - ): - update_address(target_doc, "supplier_address", "address_display", source_doc.company_address) - if source_doc.dispatch_address_name and _validate_address_link( - source_doc.dispatch_address_name, "Company", details.get("company") - ): - update_address( - target_doc, - "dispatch_address", - "dispatch_address_display", - source_doc.dispatch_address_name, - ) - if source_doc.shipping_address_name and _validate_address_link( - source_doc.shipping_address_name, "Company", details.get("company") - ): - update_address( - target_doc, - "shipping_address", - "shipping_address_display", - source_doc.shipping_address_name, - ) - if source_doc.customer_address and _validate_address_link( - source_doc.customer_address, "Company", details.get("company") - ): - update_address( - target_doc, "billing_address", "billing_address_display", source_doc.customer_address - ) - - update_taxes( - target_doc, - party=target_doc.supplier, - party_type="Supplier", - company=target_doc.company, - doctype=target_doc.doctype, - party_address=target_doc.supplier_address, - company_address=target_doc.shipping_address, - ) - else: - target_doc.company = details.get("company") - target_doc.customer = details.get("party") - target_doc.company_address = source_doc.supplier_address - target_doc.selling_price_list = source_doc.buying_price_list - target_doc.is_internal_customer = 1 - target_doc.inter_company_reference = source_doc.name - - # Invert the address on target doc creation - if source_doc.supplier_address and _validate_address_link( - source_doc.supplier_address, "Company", details.get("company") - ): - update_address( - target_doc, "company_address", "company_address_display", source_doc.supplier_address - ) - if source_doc.shipping_address and _validate_address_link( - source_doc.shipping_address, "Customer", details.get("party") - ): - update_address( - target_doc, "shipping_address_name", "shipping_address", source_doc.shipping_address - ) - if source_doc.shipping_address and _validate_address_link( - source_doc.shipping_address, "Customer", details.get("party") - ): - update_address(target_doc, "customer_address", "address_display", source_doc.shipping_address) - - update_taxes( - target_doc, - party=target_doc.customer, - party_type="Customer", - company=target_doc.company, - doctype=target_doc.doctype, - party_address=target_doc.customer_address, - company_address=target_doc.company_address, - shipping_address_name=target_doc.shipping_address_name, - ) - - def update_item(source, target, source_parent): - if source_parent.doctype == "Delivery Note" and source.received_qty: - target.qty = flt(source.qty) + flt(source.returned_qty) - flt(source.received_qty) - - if source.get("use_serial_batch_fields"): - target.set("use_serial_batch_fields", 1) - - if (source.get("serial_no") or source.get("batch_no")) and not source.get("serial_and_batch_bundle"): - target.set("use_serial_batch_fields", 1) - - doclist = get_mapped_doc( - doctype, - source_name, - { - doctype: { - "doctype": target_doctype, - "postprocess": update_details, - "field_no_map": ["taxes_and_charges", "set_warehouse"], - "field_map": {"shipping_address_name": "shipping_address"}, - }, - doctype + " Item": { - "doctype": target_doctype + " Item", - "field_map": { - source_document_warehouse_field: target_document_warehouse_field, - "name": "delivery_note_item", - "purchase_order": "purchase_order", - "purchase_order_item": "purchase_order_item", - "material_request": "material_request", - "Material_request_item": "material_request_item", - }, - "field_no_map": ["warehouse"], - "condition": lambda item: item.received_qty < item.qty + item.returned_qty, - "postprocess": update_item, - }, - }, - target_doc, - set_missing_values, - ) - - return doclist diff --git a/erpnext/stock/doctype/delivery_note/mapper.py b/erpnext/stock/doctype/delivery_note/mapper.py new file mode 100644 index 00000000000..ad9417f4db8 --- /dev/null +++ b/erpnext/stock/doctype/delivery_note/mapper.py @@ -0,0 +1,583 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +import json + +import frappe +from frappe import _ +from frappe.contacts.doctype.contact.contact import get_default_contact +from frappe.model.document import Document +from frappe.model.mapper import get_mapped_doc +from frappe.model.utils import get_fetch_values +from frappe.query_builder import DocType +from frappe.query_builder.functions import Abs, Sum +from frappe.utils import flt + +from erpnext.accounts.party import get_due_date +from erpnext.controllers.accounts_controller import get_taxes_and_charges, merge_taxes + + +def get_invoiced_qty_map(delivery_note: str) -> dict: + """returns a map: {dn_detail: invoiced_qty}""" + sii = DocType("Sales Invoice Item") + + invoiced_qty_map = frappe._dict( + ( + frappe.qb.from_(sii) + .select(sii.dn_detail, Sum(sii.qty).as_("qty")) + .where((sii.delivery_note == delivery_note) & (sii.docstatus == 1)) + .groupby(sii.dn_detail) + ).run() + ) + + return invoiced_qty_map + + +def get_returned_qty_map(delivery_note: str) -> dict: + """returns a map: {so_detail: returned_qty}""" + dn = DocType("Delivery Note") + dni = DocType("Delivery Note Item") + + returned_qty_map = frappe._dict( + ( + frappe.qb.from_(dni) + .join(dn) + .on(dn.name == dni.parent) + .select(dni.dn_detail, Sum(Abs(dni.qty)).as_("qty")) + .where( + (dn.docstatus == 1) + & (dn.is_return == 1) + & (dn.return_against == delivery_note) + & (dni.qty <= 0) + ) + .groupby(dni.dn_detail) + ).run() + ) + + return returned_qty_map + + +@frappe.whitelist() +def make_sales_invoice( + source_name: str, target_doc: str | Document | None = None, args: dict | str | None = None +): + from frappe.contacts.doctype.address.address import get_company_address + + if args is None: + args = {} + if isinstance(args, str): + args = json.loads(args) + + doc = frappe.get_doc("Delivery Note", source_name) + + to_make_invoice_qty_map = {} + returned_qty_map = get_returned_qty_map(source_name) + invoiced_qty_map = get_invoiced_qty_map(source_name) + + def set_missing_values(source, target): + target.run_method("set_missing_values") + target.run_method("set_po_nos") + + if len(target.get("items")) == 0: + frappe.throw(_("All these items have already been Invoiced/Returned")) + + if args and args.get("merge_taxes"): + merge_taxes(source, target) + + target.run_method("calculate_taxes_and_totals") + + # set company address + if source.company_address: + target.update({"company_address": source.company_address}) + else: + # set company address + target.update(get_company_address(target.company)) + + if target.company_address: + target.update(get_fetch_values("Sales Invoice", "company_address", target.company_address)) + + def update_item(source_doc, target_doc, source_parent): + target_doc.qty = to_make_invoice_qty_map[source_doc.name] + target_doc._old_name = source_doc.name + + def get_pending_qty(item_row): + pending_qty = item_row.qty - invoiced_qty_map.get(item_row.name, 0) + + returned_qty = 0 + if returned_qty_map.get(item_row.name, 0) > 0: + returned_qty = flt(returned_qty_map.get(item_row.name, 0)) + returned_qty_map[item_row.name] -= pending_qty + + if returned_qty: + if returned_qty >= pending_qty: + pending_qty = 0 + returned_qty -= pending_qty + else: + pending_qty -= returned_qty + returned_qty = 0 + + to_make_invoice_qty_map[item_row.name] = pending_qty + + return pending_qty + + def select_item(d): + filtered_items = args.get("filtered_children", []) + child_filter = d.name in filtered_items if filtered_items else True + return child_filter + + doc = get_mapped_doc( + "Delivery Note", + source_name, + { + "Delivery Note": { + "doctype": "Sales Invoice", + "field_map": {"is_return": "is_return"}, + "validation": {"docstatus": ["=", 1]}, + }, + "Delivery Note Item": { + "doctype": "Sales Invoice Item", + "field_map": { + "name": "dn_detail", + "parent": "delivery_note", + "so_detail": "so_detail", + "against_sales_order": "sales_order", + "cost_center": "cost_center", + }, + "postprocess": update_item, + "filter": lambda d: get_pending_qty(d) <= 0 + if not doc.get("is_return") + else get_pending_qty(d) > 0, + "condition": select_item, + }, + "Sales Taxes and Charges": { + "doctype": "Sales Taxes and Charges", + "reset_value": not (args and args.get("merge_taxes")), + "ignore": args.get("merge_taxes") if args else 0, + }, + "Sales Team": { + "doctype": "Sales Team", + "field_map": {"incentives": "incentives"}, + "add_if_empty": True, + }, + }, + target_doc, + set_missing_values, + ) + + from frappe.utils import cint + + automatically_fetch_payment_terms = cint( + frappe.get_single_value("Accounts Settings", "automatically_fetch_payment_terms") + ) + + if not doc.is_return: + from erpnext.accounts.services.payment_schedule import PaymentScheduleService + + ps = PaymentScheduleService(doc) + so, doctype, fieldname = ps.get_order_details() + if ( + ps.linked_order_has_payment_terms(so, fieldname, doctype) + and not automatically_fetch_payment_terms + ): + payment_terms_template = frappe.db.get_value(doctype, so, "payment_terms_template") + doc.payment_terms_template = payment_terms_template + doc.due_date = get_due_date( + doc.posting_date, + "Customer", + doc.customer, + doc.company, + template_name=doc.payment_terms_template, + ) + + elif automatically_fetch_payment_terms: + ps.set_payment_schedule() + + return doc + + +@frappe.whitelist() +def make_delivery_trip( + source_name: str, target_doc: str | Document | None = None, kwargs: dict | None = None +): + if not target_doc: + target_doc = frappe.new_doc("Delivery Trip") + + def update_address(source_doc, target_doc, source_parent): + target_doc.address = source_doc.shipping_address_name or source_doc.customer_address + target_doc.customer_address = source_doc.shipping_address or source_doc.address_display + + doclist = get_mapped_doc( + "Delivery Note", + source_name, + { + "Delivery Note": { + "doctype": "Delivery Stop", + "on_parent": target_doc, + "field_map": { + "name": "delivery_note", + "contact_person": "contact", + "contact_display": "customer_contact", + }, + "postprocess": update_address, + }, + }, + ignore_child_tables=True, + ) + + return doclist + + +@frappe.whitelist() +def make_installation_note( + source_name: str, target_doc: str | Document | None = None, kwargs: dict | None = None +): + def update_item(obj, target, source_parent): + target.qty = flt(obj.qty) - flt(obj.installed_qty) + target.serial_no = obj.serial_no + + doclist = get_mapped_doc( + "Delivery Note", + source_name, + { + "Delivery Note": {"doctype": "Installation Note", "validation": {"docstatus": ["=", 1]}}, + "Delivery Note Item": { + "doctype": "Installation Note Item", + "field_map": { + "name": "prevdoc_detail_docname", + "parent": "prevdoc_docname", + "parenttype": "prevdoc_doctype", + }, + "postprocess": update_item, + "condition": lambda doc: doc.installed_qty < doc.qty, + }, + }, + target_doc, + ) + + return doclist + + +@frappe.whitelist() +def make_packing_slip(source_name: str, target_doc: str | Document | None = None): + def set_missing_values(source, target): + target.run_method("set_missing_values") + + def update_item(obj, target, source_parent): + target.qty = flt(obj.qty) - flt(obj.packed_qty) + + doclist = get_mapped_doc( + "Delivery Note", + source_name, + { + "Delivery Note": { + "doctype": "Packing Slip", + "field_map": {"name": "delivery_note", "letter_head": "letter_head"}, + "validation": {"docstatus": ["=", 0]}, + }, + "Delivery Note Item": { + "doctype": "Packing Slip Item", + "field_map": { + "item_code": "item_code", + "item_name": "item_name", + "batch_no": "batch_no", + "description": "description", + "qty": "qty", + "uom": "stock_uom", + "name": "dn_detail", + }, + "postprocess": update_item, + "condition": lambda item: ( + not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code, "disabled": 0}) + and flt(item.packed_qty) < flt(item.qty) + ), + }, + "Packed Item": { + "doctype": "Packing Slip Item", + "field_map": { + "item_code": "item_code", + "item_name": "item_name", + "batch_no": "batch_no", + "description": "description", + "qty": "qty", + "name": "pi_detail", + }, + "postprocess": update_item, + "condition": lambda item: (flt(item.packed_qty) < flt(item.qty)), + }, + }, + target_doc, + set_missing_values, + ) + + return doclist + + +@frappe.whitelist() +def make_shipment(source_name: str, target_doc: str | Document | None = None): + def postprocess(source, target): + user = frappe.db.get_value( + "User", frappe.session.user, ["email", "full_name", "phone", "mobile_no"], as_dict=1 + ) + target.pickup_contact_email = user.email + pickup_contact_display = f"{user.full_name}" + if user: + if user.email: + pickup_contact_display += "
" + user.email + if user.phone: + pickup_contact_display += "
" + user.phone + if user.mobile_no and not user.phone: + pickup_contact_display += "
" + user.mobile_no + target.pickup_contact = pickup_contact_display + + # As we are using session user details in the pickup_contact then pickup_contact_person will be session user + target.pickup_contact_person = frappe.session.user + + contact_person = source.contact_person or get_default_contact("Customer", source.customer) + if contact_person: + contact = frappe.db.get_value( + "Contact", contact_person, ["email_id", "phone", "mobile_no"], as_dict=1 + ) + + delivery_contact_display = source.contact_display or contact_person or "" + if contact and not source.contact_display: + if contact.email_id: + delivery_contact_display += "
" + contact.email_id + if contact.phone: + delivery_contact_display += "
" + contact.phone + if contact.mobile_no and not contact.phone: + delivery_contact_display += "
" + contact.mobile_no + + target.delivery_contact_name = contact_person + if contact and contact.email_id and not target.delivery_contact_email: + target.delivery_contact_email = contact.email_id + target.delivery_contact = delivery_contact_display + + if source.shipping_address_name: + target.delivery_address_name = source.shipping_address_name + target.delivery_address = source.shipping_address + elif source.customer_address: + target.delivery_address_name = source.customer_address + target.delivery_address = source.address_display + + doclist = get_mapped_doc( + "Delivery Note", + source_name, + { + "Delivery Note": { + "doctype": "Shipment", + "field_map": { + "grand_total": "value_of_goods", + "company": "pickup_company", + "company_address": "pickup_address_name", + "company_address_display": "pickup_address", + "customer": "delivery_customer", + "contact_person": "delivery_contact_name", + "contact_email": "delivery_contact_email", + }, + "validation": {"docstatus": ["=", 1]}, + }, + "Delivery Note Item": { + "doctype": "Shipment Delivery Note", + "field_map": { + "name": "prevdoc_detail_docname", + "parent": "prevdoc_docname", + "parenttype": "prevdoc_doctype", + "base_amount": "grand_total", + }, + }, + }, + target_doc, + postprocess, + ) + + return doclist + + +@frappe.whitelist() +def make_sales_return(source_name: str, target_doc: str | Document | None = None): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + + return make_return_doc("Delivery Note", source_name, target_doc) + + +@frappe.whitelist() +def make_inter_company_purchase_receipt(source_name: str, target_doc: str | Document | None = None): + return make_inter_company_transaction("Delivery Note", source_name, target_doc) + + +def make_inter_company_transaction(doctype: str, source_name: str, target_doc=None): + from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( + get_inter_company_details, + set_purchase_references, + update_address, + update_taxes, + validate_inter_company_transaction, + ) + + if doctype == "Delivery Note": + source_doc = frappe.get_doc(doctype, source_name) + target_doctype = "Purchase Receipt" + source_document_warehouse_field = "target_warehouse" + target_document_warehouse_field = "from_warehouse" + else: + source_doc = frappe.get_doc(doctype, source_name) + target_doctype = "Delivery Note" + source_document_warehouse_field = "from_warehouse" + target_document_warehouse_field = "target_warehouse" + + validate_inter_company_transaction(source_doc, doctype) + details = get_inter_company_details(source_doc, doctype) + + def set_missing_values(source, target): + target.run_method("set_missing_values") + set_purchase_references(target) + + if target.doctype == "Purchase Receipt": + master_doctype = "Purchase Taxes and Charges Template" + else: + master_doctype = "Sales Taxes and Charges Template" + + if not target.get("taxes") and target.get("taxes_and_charges"): + for tax in get_taxes_and_charges(master_doctype, target.get("taxes_and_charges")): + target.append("taxes", tax) + + if not target.get("items"): + frappe.throw(_("All items have already been received")) + + def update_details(source_doc, target_doc, source_parent): + def _validate_address_link(address, link_doctype, link_name): + return frappe.db.get_value( + "Dynamic Link", + { + "parent": address, + "parenttype": "Address", + "link_doctype": link_doctype, + "link_name": link_name, + }, + "parent", + ) + + target_doc.inter_company_invoice_reference = source_doc.name + if target_doc.doctype == "Purchase Receipt": + target_doc.company = details.get("company") + target_doc.supplier = details.get("party") + target_doc.buying_price_list = source_doc.selling_price_list + target_doc.is_internal_supplier = 1 + target_doc.inter_company_reference = source_doc.name + + # Invert the address on target doc creation + if source_doc.company_address and _validate_address_link( + source_doc.company_address, "Supplier", details.get("party") + ): + update_address(target_doc, "supplier_address", "address_display", source_doc.company_address) + if source_doc.dispatch_address_name and _validate_address_link( + source_doc.dispatch_address_name, "Company", details.get("company") + ): + update_address( + target_doc, + "dispatch_address", + "dispatch_address_display", + source_doc.dispatch_address_name, + ) + if source_doc.shipping_address_name and _validate_address_link( + source_doc.shipping_address_name, "Company", details.get("company") + ): + update_address( + target_doc, + "shipping_address", + "shipping_address_display", + source_doc.shipping_address_name, + ) + if source_doc.customer_address and _validate_address_link( + source_doc.customer_address, "Company", details.get("company") + ): + update_address( + target_doc, "billing_address", "billing_address_display", source_doc.customer_address + ) + + update_taxes( + target_doc, + party=target_doc.supplier, + party_type="Supplier", + company=target_doc.company, + doctype=target_doc.doctype, + party_address=target_doc.supplier_address, + company_address=target_doc.shipping_address, + ) + else: + target_doc.company = details.get("company") + target_doc.customer = details.get("party") + target_doc.company_address = source_doc.supplier_address + target_doc.selling_price_list = source_doc.buying_price_list + target_doc.is_internal_customer = 1 + target_doc.inter_company_reference = source_doc.name + + # Invert the address on target doc creation + if source_doc.supplier_address and _validate_address_link( + source_doc.supplier_address, "Company", details.get("company") + ): + update_address( + target_doc, "company_address", "company_address_display", source_doc.supplier_address + ) + if source_doc.shipping_address and _validate_address_link( + source_doc.shipping_address, "Customer", details.get("party") + ): + update_address( + target_doc, "shipping_address_name", "shipping_address", source_doc.shipping_address + ) + if source_doc.shipping_address and _validate_address_link( + source_doc.shipping_address, "Customer", details.get("party") + ): + update_address(target_doc, "customer_address", "address_display", source_doc.shipping_address) + + update_taxes( + target_doc, + party=target_doc.customer, + party_type="Customer", + company=target_doc.company, + doctype=target_doc.doctype, + party_address=target_doc.customer_address, + company_address=target_doc.company_address, + shipping_address_name=target_doc.shipping_address_name, + ) + + def update_item(source, target, source_parent): + if source_parent.doctype == "Delivery Note" and source.received_qty: + target.qty = flt(source.qty) + flt(source.returned_qty) - flt(source.received_qty) + + if source.get("use_serial_batch_fields"): + target.set("use_serial_batch_fields", 1) + + if (source.get("serial_no") or source.get("batch_no")) and not source.get("serial_and_batch_bundle"): + target.set("use_serial_batch_fields", 1) + + doclist = get_mapped_doc( + doctype, + source_name, + { + doctype: { + "doctype": target_doctype, + "postprocess": update_details, + "field_no_map": ["taxes_and_charges", "set_warehouse"], + "field_map": {"shipping_address_name": "shipping_address"}, + }, + doctype + " Item": { + "doctype": target_doctype + " Item", + "field_map": { + source_document_warehouse_field: target_document_warehouse_field, + "name": "delivery_note_item", + "purchase_order": "purchase_order", + "purchase_order_item": "purchase_order_item", + "material_request": "material_request", + "Material_request_item": "material_request_item", + }, + "field_no_map": ["warehouse"], + "condition": lambda item: item.received_qty < item.qty + item.returned_qty, + "postprocess": update_item, + }, + }, + target_doc, + set_missing_values, + ) + + return doclist