diff --git a/erpnext/stock/doctype/purchase_receipt/mapper.py b/erpnext/stock/doctype/purchase_receipt/mapper.py new file mode 100644 index 00000000000..efbe5e73d88 --- /dev/null +++ b/erpnext/stock/doctype/purchase_receipt/mapper.py @@ -0,0 +1,254 @@ +# 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.model.document import Document +from frappe.model.mapper import get_mapped_doc +from frappe.query_builder.functions import Abs, Sum +from frappe.utils import flt + +from erpnext.controllers.accounts_controller import merge_taxes +from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_transaction +from erpnext.stock.serial_batch_bundle import ( + SerialBatchCreation, + get_batches_from_bundle, + get_serial_nos_from_bundle, +) + + +def get_invoiced_qty_map(purchase_receipt: str) -> dict: + """returns a map: {pr_detail: invoiced_qty}""" + invoiced_qty_map = {} + + for pr_detail, qty in frappe.db.sql( + """select pr_detail, qty from `tabPurchase Invoice Item` + where purchase_receipt=%s and docstatus=1""", + purchase_receipt, + ): + if not invoiced_qty_map.get(pr_detail): + invoiced_qty_map[pr_detail] = 0 + invoiced_qty_map[pr_detail] += qty + + return invoiced_qty_map + + +def get_returned_qty_map(purchase_receipt: str) -> dict: + """returns a map: {pr_detail: returned_qty}""" + pr = frappe.qb.DocType("Purchase Receipt") + pr_item = frappe.qb.DocType("Purchase Receipt Item") + + query = ( + frappe.qb.from_(pr) + .inner_join(pr_item) + .on(pr.name == pr_item.parent) + .select(pr_item.purchase_receipt_item, Sum(Abs(pr_item.qty)).as_("qty")) + .where( + (pr.docstatus == 1) + & (pr.is_return == 1) + & (pr.return_against == purchase_receipt) + & (pr_item.purchase_receipt_item.isnotnull()) + ) + .groupby(pr_item.purchase_receipt_item) + ).run(as_list=1) + + return frappe._dict(query) if query else frappe._dict() + + +@frappe.whitelist() +def make_purchase_invoice( + source_name: str | None, target_doc: str | Document | None = None, args: dict | str | None = None +): + if args is None: + args = {} + if isinstance(args, str): + args = json.loads(args) + + from erpnext.accounts.party import get_payment_terms_template + + doc = frappe.get_doc("Purchase Receipt", source_name) + returned_qty_map = get_returned_qty_map(source_name) + invoiced_qty_map = get_invoiced_qty_map(source_name) + + def set_missing_values(source, target): + if len(target.get("items")) == 0: + frappe.throw(_("All items have already been Invoiced/Returned")) + + doc = frappe.get_doc(target) + doc.payment_terms_template = get_payment_terms_template(source.supplier, "Supplier", source.company) + doc.run_method("onload") + doc.run_method("set_missing_values") + + if args and args.get("merge_taxes"): + merge_taxes(source, doc) + + doc.run_method("calculate_taxes_and_totals") + from erpnext.accounts.services.payment_schedule import PaymentScheduleService + + PaymentScheduleService(doc).set_payment_schedule() + + def update_item(source_doc, target_doc, source_parent): + target_doc.qty, returned_qty = get_pending_qty(source_doc) + if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"): + target_doc.rejected_qty = 0 + target_doc.stock_qty = flt(target_doc.qty) * flt( + target_doc.conversion_factor, target_doc.precision("conversion_factor") + ) + returned_qty_map[source_doc.name] = returned_qty + target_doc._old_name = source_doc.name + + def get_pending_qty(item_row): + qty = item_row.qty + if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"): + qty = item_row.received_qty + + pending_qty = qty - invoiced_qty_map.get(item_row.name, 0) + + if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"): + return pending_qty, 0 + + returned_qty = flt(returned_qty_map.get(item_row.name, 0)) + if item_row.rejected_qty and returned_qty: + returned_qty -= item_row.rejected_qty + + if returned_qty: + if returned_qty >= pending_qty: + pending_qty = 0 + returned_qty -= pending_qty + else: + pending_qty -= returned_qty + returned_qty = 0 + + return pending_qty, returned_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 + + doclist = get_mapped_doc( + "Purchase Receipt", + source_name, + { + "Purchase Receipt": { + "doctype": "Purchase Invoice", + "field_map": { + "supplier_warehouse": "supplier_warehouse", + "is_return": "is_return", + "bill_date": "bill_date", + }, + "validation": { + "docstatus": ["=", 1], + }, + }, + "Purchase Receipt Item": { + "doctype": "Purchase Invoice Item", + "field_map": { + "name": "pr_detail", + "parent": "purchase_receipt", + "qty": "received_qty", + "purchase_order_item": "po_detail", + "purchase_order": "purchase_order", + "is_fixed_asset": "is_fixed_asset", + "asset_location": "asset_location", + "asset_category": "asset_category", + "wip_composite_asset": "wip_composite_asset", + }, + "postprocess": update_item, + "filter": lambda d: ( + get_pending_qty(d)[0] <= 0 if not doc.get("is_return") else get_pending_qty(d)[0] > 0 + ), + "condition": select_item, + }, + "Purchase Taxes and Charges": { + "doctype": "Purchase Taxes and Charges", + "reset_value": not (args and args.get("merge_taxes")), + "ignore": args.get("merge_taxes") if args else 0, + }, + }, + target_doc, + set_missing_values, + ) + + return doclist + + +@frappe.whitelist() +def make_purchase_return_against_rejected_warehouse(source_name: str): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + + return make_return_doc("Purchase Receipt", source_name, return_against_rejected_qty=True) + + +@frappe.whitelist() +def make_purchase_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("Purchase Receipt", source_name, target_doc) + + +@frappe.whitelist() +def make_stock_entry(source_name: str, target_doc: str | Document | None = None): + def set_missing_values(source, target): + target.stock_entry_type = "Material Transfer" + target.purpose = "Material Transfer" + target.set_missing_values() + + def update_item(source_doc, target_doc, source_parent): + if source_doc.serial_and_batch_bundle: + serial_nos = get_serial_nos_from_bundle(source_doc.serial_and_batch_bundle) + if serial_nos: + serial_nos = "\n".join(serial_nos) + + batches = get_batches_from_bundle(source_doc.serial_and_batch_bundle) + if batches: + if len(batches) == 1: + target_doc.use_serial_batch_fields = 1 + target_doc.batch_no = next(iter(batches)) + elif not serial_nos: + cls_obj = SerialBatchCreation( + { + "type_of_transaction": "Outward", + "serial_and_batch_bundle": source_doc.serial_and_batch_bundle, + "item_code": source_doc.item_code, + "warehouse": source_doc.warehouse, + } + ) + + cls_obj.duplicate_package() + + target_doc.serial_and_batch_bundle = cls_obj.serial_and_batch_bundle + + if serial_nos: + target_doc.use_serial_batch_fields = 1 + target_doc.serial_no = serial_nos + + doclist = get_mapped_doc( + "Purchase Receipt", + source_name, + { + "Purchase Receipt": { + "doctype": "Stock Entry", + }, + "Purchase Receipt Item": { + "doctype": "Stock Entry Detail", + "field_map": { + "warehouse": "s_warehouse", + "parent": "reference_purchase_receipt", + "batch_no": "batch_no", + }, + "postprocess": update_item, + }, + }, + target_doc, + set_missing_values, + ) + + return doclist + + +@frappe.whitelist() +def make_inter_company_delivery_note(source_name: str, target_doc: str | Document | None = None): + return make_inter_company_transaction("Purchase Receipt", source_name, target_doc) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 0a48b00776c..710eb63c6ab 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -2,14 +2,11 @@ # License: GNU General Public License v3. See license.txt -import json - import frappe from frappe import _, throw from frappe.desk.notifications import clear_doctype_notifications from frappe.model.document import Document -from frappe.model.mapper import get_mapped_doc -from frappe.query_builder.functions import Abs, CombineDatetime, Sum +from frappe.query_builder.functions import CombineDatetime from frappe.utils import cint, flt, get_datetime, getdate, nowdate from pypika import functions as fn @@ -17,14 +14,15 @@ import erpnext from erpnext.accounts.utils import get_account_currency from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled from erpnext.buying.utils import check_on_hold_or_closed_status -from erpnext.controllers.accounts_controller import merge_taxes from erpnext.controllers.buying_controller import BuyingController -from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_transaction from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import StockReservation -from erpnext.stock.serial_batch_bundle import ( - SerialBatchCreation, - get_batches_from_bundle, - get_serial_nos_from_bundle, + +from .mapper import ( + make_inter_company_delivery_note, + make_purchase_invoice, + make_purchase_return, + make_purchase_return_against_rejected_warehouse, + make_stock_entry, ) form_grid_templates = {"items": "templates/form_grid/item_grid.html"} @@ -1084,248 +1082,12 @@ def get_item_wise_returned_qty(pr_doc): ) -@frappe.whitelist() -def make_purchase_invoice( - source_name: str | None, target_doc: str | Document | None = None, args: dict | str | None = None -): - if args is None: - args = {} - if isinstance(args, str): - args = json.loads(args) - - from erpnext.accounts.party import get_payment_terms_template - - doc = frappe.get_doc("Purchase Receipt", source_name) - returned_qty_map = get_returned_qty_map(source_name) - invoiced_qty_map = get_invoiced_qty_map(source_name) - - def set_missing_values(source, target): - if len(target.get("items")) == 0: - frappe.throw(_("All items have already been Invoiced/Returned")) - - doc = frappe.get_doc(target) - doc.payment_terms_template = get_payment_terms_template(source.supplier, "Supplier", source.company) - doc.run_method("onload") - doc.run_method("set_missing_values") - - if args and args.get("merge_taxes"): - merge_taxes(source, doc) - - doc.run_method("calculate_taxes_and_totals") - from erpnext.accounts.services.payment_schedule import PaymentScheduleService - - PaymentScheduleService(doc).set_payment_schedule() - - def update_item(source_doc, target_doc, source_parent): - target_doc.qty, returned_qty = get_pending_qty(source_doc) - if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"): - target_doc.rejected_qty = 0 - target_doc.stock_qty = flt(target_doc.qty) * flt( - target_doc.conversion_factor, target_doc.precision("conversion_factor") - ) - returned_qty_map[source_doc.name] = returned_qty - target_doc._old_name = source_doc.name - - def get_pending_qty(item_row): - qty = item_row.qty - if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"): - qty = item_row.received_qty - - pending_qty = qty - invoiced_qty_map.get(item_row.name, 0) - - if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"): - return pending_qty, 0 - - returned_qty = flt(returned_qty_map.get(item_row.name, 0)) - if item_row.rejected_qty and returned_qty: - returned_qty -= item_row.rejected_qty - - if returned_qty: - if returned_qty >= pending_qty: - pending_qty = 0 - returned_qty -= pending_qty - else: - pending_qty -= returned_qty - returned_qty = 0 - - return pending_qty, returned_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 - - doclist = get_mapped_doc( - "Purchase Receipt", - source_name, - { - "Purchase Receipt": { - "doctype": "Purchase Invoice", - "field_map": { - "supplier_warehouse": "supplier_warehouse", - "is_return": "is_return", - "bill_date": "bill_date", - }, - "validation": { - "docstatus": ["=", 1], - }, - }, - "Purchase Receipt Item": { - "doctype": "Purchase Invoice Item", - "field_map": { - "name": "pr_detail", - "parent": "purchase_receipt", - "qty": "received_qty", - "purchase_order_item": "po_detail", - "purchase_order": "purchase_order", - "is_fixed_asset": "is_fixed_asset", - "asset_location": "asset_location", - "asset_category": "asset_category", - "wip_composite_asset": "wip_composite_asset", - }, - "postprocess": update_item, - "filter": lambda d: ( - get_pending_qty(d)[0] <= 0 if not doc.get("is_return") else get_pending_qty(d)[0] > 0 - ), - "condition": select_item, - }, - "Purchase Taxes and Charges": { - "doctype": "Purchase Taxes and Charges", - "reset_value": not (args and args.get("merge_taxes")), - "ignore": args.get("merge_taxes") if args else 0, - }, - }, - target_doc, - set_missing_values, - ) - - return doclist - - -def get_invoiced_qty_map(purchase_receipt): - """returns a map: {pr_detail: invoiced_qty}""" - invoiced_qty_map = {} - - for pr_detail, qty in frappe.db.sql( - """select pr_detail, qty from `tabPurchase Invoice Item` - where purchase_receipt=%s and docstatus=1""", - purchase_receipt, - ): - if not invoiced_qty_map.get(pr_detail): - invoiced_qty_map[pr_detail] = 0 - invoiced_qty_map[pr_detail] += qty - - return invoiced_qty_map - - -def get_returned_qty_map(purchase_receipt): - """returns a map: {pr_detail: returned_qty}""" - - pr = frappe.qb.DocType("Purchase Receipt") - pr_item = frappe.qb.DocType("Purchase Receipt Item") - - query = ( - frappe.qb.from_(pr) - .inner_join(pr_item) - .on(pr.name == pr_item.parent) - .select(pr_item.purchase_receipt_item, Sum(Abs(pr_item.qty)).as_("qty")) - .where( - (pr.docstatus == 1) - & (pr.is_return == 1) - & (pr.return_against == purchase_receipt) - & (pr_item.purchase_receipt_item.isnotnull()) - ) - .groupby(pr_item.purchase_receipt_item) - ).run(as_list=1) - - return frappe._dict(query) if query else frappe._dict() - - -@frappe.whitelist() -def make_purchase_return_against_rejected_warehouse(source_name: str): - from erpnext.controllers.sales_and_purchase_return import make_return_doc - - return make_return_doc("Purchase Receipt", source_name, return_against_rejected_qty=True) - - -@frappe.whitelist() -def make_purchase_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("Purchase Receipt", source_name, target_doc) - - @frappe.whitelist() def update_purchase_receipt_status(docname: str, status: str): pr = frappe.get_lazy_doc("Purchase Receipt", docname, check_permission="submit") pr.update_status(status) -@frappe.whitelist() -def make_stock_entry(source_name: str, target_doc: str | Document | None = None): - def set_missing_values(source, target): - target.stock_entry_type = "Material Transfer" - target.purpose = "Material Transfer" - target.set_missing_values() - - def update_item(source_doc, target_doc, source_parent): - if source_doc.serial_and_batch_bundle: - serial_nos = get_serial_nos_from_bundle(source_doc.serial_and_batch_bundle) - if serial_nos: - serial_nos = "\n".join(serial_nos) - - batches = get_batches_from_bundle(source_doc.serial_and_batch_bundle) - if batches: - if len(batches) == 1: - target_doc.use_serial_batch_fields = 1 - target_doc.batch_no = next(iter(batches)) - elif not serial_nos: - cls_obj = SerialBatchCreation( - { - "type_of_transaction": "Outward", - "serial_and_batch_bundle": source_doc.serial_and_batch_bundle, - "item_code": source_doc.item_code, - "warehouse": source_doc.warehouse, - } - ) - - cls_obj.duplicate_package() - - target_doc.serial_and_batch_bundle = cls_obj.serial_and_batch_bundle - - if serial_nos: - target_doc.use_serial_batch_fields = 1 - target_doc.serial_no = serial_nos - - doclist = get_mapped_doc( - "Purchase Receipt", - source_name, - { - "Purchase Receipt": { - "doctype": "Stock Entry", - }, - "Purchase Receipt Item": { - "doctype": "Stock Entry Detail", - "field_map": { - "warehouse": "s_warehouse", - "parent": "reference_purchase_receipt", - "batch_no": "batch_no", - }, - "postprocess": update_item, - }, - }, - target_doc, - set_missing_values, - ) - - return doclist - - -@frappe.whitelist() -def make_inter_company_delivery_note(source_name: str, target_doc: str | Document | None = None): - return make_inter_company_transaction("Purchase Receipt", source_name, target_doc) - - @erpnext.allow_regional def update_regional_gl_entries(gl_list, doc): return