diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 4008863e9b..1f5879d7bf 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -710,6 +710,7 @@ class SalesInvoice(SellingController): if ( cint(frappe.db.get_single_value("Selling Settings", "maintain_same_sales_rate")) and not self.is_return + and not self.is_internal_customer ): self.validate_rate_with_reference_doc( [["Sales Order", "sales_order", "so_detail"], ["Delivery Note", "delivery_note", "dn_detail"]] @@ -2161,6 +2162,17 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): def update_item(source, target, source_parent): target.qty = flt(source.qty) - received_items.get(source.name, 0.0) + if source.doctype == "Purchase Order Item" and target.doctype == "Sales Order Item": + target.purchase_order = source.parent + target.purchase_order_item = source.name + + if ( + source.get("purchase_order") + and source.get("purchase_order_item") + and target.doctype == "Purchase Invoice Item" + ): + target.purchase_order = source.purchase_order + target.po_detail = source.purchase_order_item item_field_map = { "doctype": target_doctype + " Item", @@ -2187,6 +2199,12 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): "serial_no": "serial_no", } ) + elif target_doctype == "Sales Order": + item_field_map["field_map"].update( + { + source_document_warehouse_field: "warehouse", + } + ) doclist = get_mapped_doc( doctype, @@ -2231,6 +2249,7 @@ def get_received_items(reference_name, doctype, reference_fieldname): def set_purchase_references(doc): # add internal PO or PR links if any + if doc.is_internal_transfer(): if doc.doctype == "Purchase Receipt": so_item_map = get_delivery_note_details(doc.inter_company_invoice_reference) @@ -2260,15 +2279,6 @@ def set_purchase_references(doc): warehouse_map, ) - if list(so_item_map.values()): - pd_item_map, parent_child_map, warehouse_map = get_pd_details( - "Purchase Order Item", so_item_map, "sales_order_item" - ) - - update_pi_items( - doc, "po_detail", "purchase_order", so_item_map, pd_item_map, parent_child_map, warehouse_map - ) - def update_pi_items( doc, @@ -2284,13 +2294,19 @@ def update_pi_items( item.set(parent_field, parent_child_map.get(sales_item_map.get(item.sales_invoice_item))) if doc.update_stock: item.warehouse = warehouse_map.get(sales_item_map.get(item.sales_invoice_item)) + if not item.warehouse and item.get("purchase_order") and item.get("purchase_order_item"): + item.warehouse = frappe.db.get_value( + "Purchase Order Item", item.purchase_order_item, "warehouse" + ) def update_pr_items(doc, sales_item_map, purchase_item_map, parent_child_map, warehouse_map): for item in doc.get("items"): - item.purchase_order_item = purchase_item_map.get(sales_item_map.get(item.delivery_note_item)) item.warehouse = warehouse_map.get(sales_item_map.get(item.delivery_note_item)) - item.purchase_order = parent_child_map.get(sales_item_map.get(item.delivery_note_item)) + if not item.warehouse and item.get("purchase_order") and item.get("purchase_order_item"): + item.warehouse = frappe.db.get_value( + "Purchase Order Item", item.purchase_order_item, "warehouse" + ) def get_delivery_note_details(internal_reference): diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index 7cddf123e2..4f97b63789 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -96,6 +96,10 @@ "delivery_note", "dn_detail", "delivered_qty", + "internal_transfer_section", + "purchase_order", + "column_break_92", + "purchase_order_item", "accounting_dimensions_section", "cost_center", "dimension_col_break", @@ -840,12 +844,38 @@ "fieldtype": "Check", "label": "Grant Commission", "read_only": 1 + }, + { + "collapsible": 1, + "depends_on": "eval:parent.is_internal_customer == 1", + "fieldname": "internal_transfer_section", + "fieldtype": "Section Break", + "label": "Internal Transfer" + }, + { + "fieldname": "purchase_order", + "fieldtype": "Link", + "label": "Purchase Order", + "options": "Purchase Order", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_92", + "fieldtype": "Column Break" + }, + { + "fieldname": "purchase_order_item", + "fieldtype": "Data", + "label": "Purchase Order Item", + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2022-08-26 12:06:31.205417", + "modified": "2022-09-06 14:17:43.394309", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index aa50487d78..acca380672 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -60,6 +60,7 @@ "section_break_45", "before_items_section", "scan_barcode", + "set_from_warehouse", "items_col_break", "set_warehouse", "items_section", @@ -1166,13 +1167,20 @@ "hidden": 1, "label": "Is Old Subcontracting Flow", "read_only": 1 + }, + { + "depends_on": "is_internal_supplier", + "fieldname": "set_from_warehouse", + "fieldtype": "Link", + "label": "Set From Warehouse", + "options": "Warehouse" } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2022-06-15 15:40:58.527065", + "modified": "2022-09-07 11:06:46.035093", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py b/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py index 01b55c00d6..05b5a8e7b8 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py @@ -23,5 +23,6 @@ def get_data(): "items": ["Material Request", "Supplier Quotation", "Project", "Auto Repeat"], }, {"label": _("Sub-contracting"), "items": ["Subcontracting Order", "Stock Entry"]}, + {"label": _("Internal"), "items": ["Sales Order"]}, ], } diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index bd7e4e8d86..6c1bcc7dd4 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -7,8 +7,10 @@ import json import frappe from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, flt, getdate, nowdate +from frappe.utils.data import today from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry +from erpnext.buying.doctype.purchase_order.purchase_order import make_inter_company_sales_order from erpnext.buying.doctype.purchase_order.purchase_order import ( make_purchase_invoice as make_pi_from_po, ) @@ -796,6 +798,111 @@ class TestPurchaseOrder(FrappeTestCase): automatically_fetch_payment_terms(enable=0) + def test_internal_transfer_flow(self): + from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( + make_inter_company_purchase_invoice, + ) + from erpnext.selling.doctype.sales_order.sales_order import ( + make_delivery_note, + make_sales_invoice, + ) + from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt + + frappe.db.set_value("Selling Settings", None, "maintain_same_sales_rate", 1) + frappe.db.set_value("Buying Settings", None, "maintain_same_rate", 1) + + prepare_data_for_internal_transfer() + supplier = "_Test Internal Supplier 2" + + po = create_purchase_order( + company="_Test Company with perpetual inventory", + supplier=supplier, + warehouse="Stores - TCP1", + from_warehouse="_Test Internal Warehouse New 1 - TCP1", + qty=2, + rate=1, + ) + + so = make_inter_company_sales_order(po.name) + so.items[0].delivery_date = today() + self.assertEqual(so.items[0].warehouse, "_Test Internal Warehouse New 1 - TCP1") + self.assertTrue(so.items[0].purchase_order) + self.assertTrue(so.items[0].purchase_order_item) + so.submit() + + dn = make_delivery_note(so.name) + dn.items[0].target_warehouse = "_Test Internal Warehouse GIT - TCP1" + self.assertEqual(dn.items[0].warehouse, "_Test Internal Warehouse New 1 - TCP1") + self.assertTrue(dn.items[0].purchase_order) + self.assertTrue(dn.items[0].purchase_order_item) + + self.assertEqual(po.items[0].name, dn.items[0].purchase_order_item) + dn.submit() + + pr = make_inter_company_purchase_receipt(dn.name) + self.assertEqual(pr.items[0].warehouse, "Stores - TCP1") + self.assertTrue(pr.items[0].purchase_order) + self.assertTrue(pr.items[0].purchase_order_item) + self.assertEqual(po.items[0].name, pr.items[0].purchase_order_item) + pr.submit() + + si = make_sales_invoice(so.name) + self.assertEqual(si.items[0].warehouse, "_Test Internal Warehouse New 1 - TCP1") + self.assertTrue(si.items[0].purchase_order) + self.assertTrue(si.items[0].purchase_order_item) + si.submit() + + pi = make_inter_company_purchase_invoice(si.name) + self.assertTrue(pi.items[0].purchase_order) + self.assertTrue(pi.items[0].po_detail) + pi.submit() + + po.load_from_db() + self.assertEqual(po.status, "Completed") + + +def prepare_data_for_internal_transfer(): + from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier + from erpnext.selling.doctype.customer.test_customer import create_internal_customer + from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + company = "_Test Company with perpetual inventory" + + create_internal_customer( + "_Test Internal Customer 2", + company, + company, + ) + + create_internal_supplier( + "_Test Internal Supplier 2", + company, + company, + ) + + warehouse = create_warehouse("_Test Internal Warehouse New 1", company=company) + + create_warehouse("_Test Internal Warehouse GIT", company=company) + + make_purchase_receipt(company=company, warehouse=warehouse, qty=2, rate=100) + + if not frappe.db.get_value("Company", company, "unrealized_profit_loss_account"): + account = "Unrealized Profit and Loss - TCP1" + if not frappe.db.exists("Account", account): + frappe.get_doc( + { + "doctype": "Account", + "account_name": "Unrealized Profit and Loss", + "parent_account": "Direct Income - TCP1", + "company": company, + "is_group": 0, + "account_type": "Income Account", + } + ).insert() + + frappe.db.set_value("Company", company, "unrealized_profit_loss_account", account) + def make_pr_against_po(po, received_qty=0): pr = make_purchase_receipt(po) @@ -847,6 +954,7 @@ def create_purchase_order(**args): { "item_code": args.item or args.item_code or "_Test Item", "warehouse": args.warehouse or "_Test Warehouse - _TC", + "from_warehouse": args.from_warehouse, "qty": args.qty or 10, "rate": args.rate or 500, "schedule_date": add_days(nowdate(), 1), diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index 1a9845396f..82e92e87bc 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -10,12 +10,14 @@ "item_code", "supplier_part_no", "item_name", + "brand", "product_bundle", "fg_item", "fg_item_qty", "column_break_4", "schedule_date", "expected_delivery_date", + "item_group", "section_break_5", "description", "col_break1", @@ -58,9 +60,12 @@ "base_net_rate", "base_net_amount", "warehouse_and_reference", + "from_warehouse", "warehouse", + "column_break_54", "actual_qty", "company_total_stock", + "references_section", "material_request", "material_request_item", "sales_order", @@ -73,8 +78,6 @@ "against_blanket_order", "blanket_order", "blanket_order_rate", - "item_group", - "brand", "section_break_56", "received_qty", "returned_qty", @@ -442,13 +445,13 @@ { "fieldname": "warehouse_and_reference", "fieldtype": "Section Break", - "label": "Warehouse and Reference" + "label": "Warehouse Settings" }, { "fieldname": "warehouse", "fieldtype": "Link", "in_list_view": 1, - "label": "Warehouse", + "label": "Target Warehouse", "oldfieldname": "warehouse", "oldfieldtype": "Link", "options": "Warehouse", @@ -760,7 +763,7 @@ "allow_on_submit": 1, "fieldname": "actual_qty", "fieldtype": "Float", - "label": "Available Qty at Warehouse", + "label": "Available Qty at Target Warehouse", "print_hide": 1, "read_only": 1 }, @@ -868,13 +871,30 @@ "fieldtype": "Float", "label": "Finished Good Item Qty", "mandatory_depends_on": "eval:parent.is_subcontracted && !parent.is_old_subcontracting_flow" + }, + { + "depends_on": "eval:parent.is_internal_supplier", + "fieldname": "from_warehouse", + "fieldtype": "Link", + "label": "From Warehouse", + "options": "Warehouse" + }, + { + "collapsible": 1, + "fieldname": "references_section", + "fieldtype": "Section Break", + "label": "References" + }, + { + "fieldname": "column_break_54", + "fieldtype": "Column Break" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-06-17 05:29:40.602349", + "modified": "2022-09-07 11:12:38.634976", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index e689d567a1..6f321f4766 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -373,7 +373,7 @@ class AccountsController(TransactionBase): ) def validate_inter_company_reference(self): - if self.doctype not in ("Purchase Invoice", "Purchase Receipt", "Purchase Order"): + if self.doctype not in ("Purchase Invoice", "Purchase Receipt"): return if self.is_internal_transfer(): diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 197d2ba2dc..6e7d2b33c2 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -307,6 +307,20 @@ class StatusUpdater(Document): def limits_crossed_error(self, args, item, qty_or_amount): """Raise exception for limits crossed""" + if ( + self.doctype in ["Sales Invoice", "Delivery Note"] + and qty_or_amount == "amount" + and self.is_internal_customer + ): + return + + elif ( + self.doctype in ["Purchase Invoice", "Purchase Receipt"] + and qty_or_amount == "amount" + and self.is_internal_supplier + ): + return + if qty_or_amount == "qty": action_msg = _( 'To allow over receipt / delivery, update "Over Receipt/Delivery Allowance" in Stock Settings or the Item.' diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 6b6ea89b63..386c12b638 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -59,7 +59,36 @@ frappe.ui.form.on("Sales Order", { }) }); } + + if (frm.doc.docstatus === 0 && frm.doc.is_internal_customer) { + frm.events.get_items_from_internal_purchase_order(frm); + } }, + + get_items_from_internal_purchase_order(frm) { + frm.add_custom_button(__('Purchase Order'), () => { + erpnext.utils.map_current_doc({ + method: 'erpnext.buying.doctype.purchase_order.purchase_order.make_inter_company_sales_order', + source_doctype: 'Purchase Order', + target: frm, + setters: [ + { + label: 'Supplier', + fieldname: 'supplier', + fieldtype: 'Link', + options: 'Supplier' + } + ], + get_query_filters: { + company: frm.doc.company, + is_internal_supplier: 1, + docstatus: 1, + status: ['!=', 'Completed'] + } + }); + }, __('Get Items From')); + }, + onload: function(frm) { if (!frm.doc.transaction_date){ frm.set_value('transaction_date', frappe.datetime.get_today()) diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index 318799907e..2cf836f9fc 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -92,7 +92,11 @@ "section_break_63", "page_break", "item_tax_rate", - "transaction_date" + "transaction_date", + "inter_transfer_reference_section", + "purchase_order", + "column_break_89", + "purchase_order_item" ], "fields": [ { @@ -809,12 +813,36 @@ "label": "Picked Qty (in Stock UOM)", "no_copy": 1, "read_only": 1 + }, + { + "fieldname": "inter_transfer_reference_section", + "fieldtype": "Section Break", + "label": "Inter Transfer Reference" + }, + { + "fieldname": "purchase_order", + "fieldtype": "Link", + "label": "Purchase Order", + "options": "Purchase Order", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_89", + "fieldtype": "Column Break" + }, + { + "fieldname": "purchase_order_item", + "fieldtype": "Data", + "label": "Purchase Order Item", + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2022-06-17 05:27:41.603006", + "modified": "2022-09-06 13:24:18.065312", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 0e68e85806..36d5a6ce0e 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -178,6 +178,7 @@ class DeliveryNote(SellingController): if ( cint(frappe.db.get_single_value("Selling Settings", "maintain_same_sales_rate")) and not self.is_return + and not self.is_internal_customer ): self.validate_rate_with_reference_doc( [ @@ -896,6 +897,8 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): "name": "delivery_note_item", "batch_no": "batch_no", "serial_no": "serial_no", + "purchase_order": "purchase_order", + "purchase_order_item": "purchase_order_item", }, "field_no_map": ["warehouse"], }, diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index 2de4842ebe..0911cdb476 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -86,6 +86,10 @@ "expense_account", "allow_zero_valuation_rate", "column_break_71", + "internal_transfer_section", + "purchase_order", + "column_break_82", + "purchase_order_item", "accounting_dimensions_section", "cost_center", "dimension_col_break", @@ -777,13 +781,39 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "collapsible": 1, + "depends_on": "eval:parent.is_internal_customer == 1", + "fieldname": "internal_transfer_section", + "fieldtype": "Section Break", + "label": "Internal Transfer" + }, + { + "fieldname": "purchase_order", + "fieldtype": "Link", + "label": "Purchase Order", + "options": "Purchase Order", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_82", + "fieldtype": "Column Break" + }, + { + "fieldname": "purchase_order_item", + "fieldtype": "Data", + "label": "Purchase Order Item", + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-06-17 05:25:47.711177", + "modified": "2022-09-06 14:19:42.876357", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index cd1bf9f321..21a0a551b6 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -71,6 +71,9 @@ class TransactionBase(StatusUpdater): self.validate_value(field, condition, prevdoc_values[field], doc) def validate_rate_with_reference_doc(self, ref_details): + if self.get("is_internal_supplier"): + return + buying_doctypes = ["Purchase Order", "Purchase Invoice", "Purchase Receipt"] if self.doctype in buying_doctypes: