diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
index 4585b3307b2..d59b50c60ad 100644
--- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
+++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
@@ -82,15 +82,19 @@ erpnext.PointOfSale.PastOrderSummary = class {
return `
`;
}
@@ -100,8 +104,8 @@ erpnext.PointOfSale.PastOrderSummary = class {
return `
-
${item_data.item_name}
-
${item_data.qty || 0} ${item_data.uom}
+
${frappe.utils.escape_html(item_data.item_name)}
+
${item_data.qty || 0} ${frappe.utils.escape_html(item_data.uom)}
${get_rate_discount_html()}
@@ -166,7 +170,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
.map((t) => {
return `
-
${t.description}
+
${frappe.utils.escape_html(t.description)}
${format_currency(t.tax_amount_after_discount_amount, doc.currency)}
`;
@@ -185,7 +189,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
get_payment_html(doc, payment) {
return `
-
${__(payment.mode_of_payment)}
+
${frappe.utils.escape_html(__(payment.mode_of_payment))}
${format_currency(payment.amount, doc.currency)}
`;
}
diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js
index a92c8958917..bf8c9f44049 100644
--- a/erpnext/selling/page/point_of_sale/pos_payment.js
+++ b/erpnext/selling/page/point_of_sale/pos_payment.js
@@ -519,7 +519,7 @@ erpnext.PointOfSale.Payment = class {
return `
- ${p.mode_of_payment}
+ ${frappe.utils.escape_html(p.mode_of_payment)}
${amount}
@@ -603,7 +603,7 @@ erpnext.PointOfSale.Payment = class {
Redeem Loyalty Points
${amount}
-
${loyalty_program}
+
${frappe.utils.escape_html(loyalty_program)}
`
diff --git a/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py b/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py
index d9caa9b8bad..f6783abfbe5 100644
--- a/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py
+++ b/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py
@@ -7,7 +7,7 @@ from frappe import _, qb
from frappe.query_builder import Criterion
from erpnext import get_default_company
-from erpnext.accounts.party import get_party_details
+from erpnext.accounts.party import _get_party_details
def execute(filters=None):
@@ -125,7 +125,7 @@ def get_data(filters=None):
def get_customer_details(filters):
- customer_details = get_party_details(party=filters.get("customer"), party_type="Customer")
+ customer_details = _get_party_details(party=filters.get("customer"), party_type="Customer")
customer_details.update(
{"company": get_default_company(), "price_list": customer_details.get("selling_price_list")}
)
diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py
index 1b583967a47..ca7e338e936 100644
--- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py
+++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py
@@ -3,7 +3,7 @@ import datetime
import frappe
from frappe.utils import add_days, add_months, nowdate
-from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
+from erpnext.selling.doctype.sales_order.mapper import make_sales_invoice
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_status_for_sales_order import (
execute,
diff --git a/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py b/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py
index 3f540a3b94a..166ff34b7a0 100644
--- a/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py
+++ b/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py
@@ -4,7 +4,7 @@
from frappe.utils import add_months, nowdate
-from erpnext.selling.doctype.sales_order.sales_order import make_material_request
+from erpnext.selling.doctype.sales_order.mapper import make_material_request
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.selling.report.pending_so_items_for_purchase_request.pending_so_items_for_purchase_request import (
execute,
diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.py b/erpnext/selling/report/sales_analytics/sales_analytics.py
index 2aac07ce3b5..e36690b4384 100644
--- a/erpnext/selling/report/sales_analytics/sales_analytics.py
+++ b/erpnext/selling/report/sales_analytics/sales_analytics.py
@@ -497,14 +497,16 @@ class Analytics:
break
def get_groups(self):
- if self.filters.tree_type == "Territory":
- parent = "parent_territory"
- if self.filters.tree_type == "Customer Group":
- parent = "parent_customer_group"
- if self.filters.tree_type == "Item Group":
- parent = "parent_item_group"
- if self.filters.tree_type == "Supplier Group":
- parent = "parent_supplier_group"
+ parent_field_map = {
+ "Territory": "parent_territory",
+ "Customer Group": "parent_customer_group",
+ "Item Group": "parent_item_group",
+ "Supplier Group": "parent_supplier_group",
+ }
+ if self.filters.tree_type not in parent_field_map:
+ frappe.throw(_("Invalid Tree Type {0}").format(self.filters.tree_type))
+
+ parent = parent_field_map[self.filters.tree_type]
self.depth_map = frappe._dict()
@@ -523,6 +525,9 @@ class Analytics:
def get_teams(self):
self.depth_map = frappe._dict()
+ if not frappe.db.exists("DocType", self.filters.doc_type):
+ frappe.throw(_("Invalid Document Type {0}").format(self.filters.doc_type))
+
self.group_entries = frappe.db.sql(
f""" select * from (select "Order Types" as name, 0 as lft,
2 as rgt, '' as parent union select distinct order_type as name, 1 as lft, 1 as rgt, "Order Types" as parent
diff --git a/erpnext/selling/report/sales_order_analysis/test_sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/test_sales_order_analysis.py
index 1a200c7eba3..4d351742ee6 100644
--- a/erpnext/selling/report/sales_order_analysis/test_sales_order_analysis.py
+++ b/erpnext/selling/report/sales_order_analysis/test_sales_order_analysis.py
@@ -1,7 +1,7 @@
import frappe
from frappe.utils import add_days
-from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note, make_sales_invoice
+from erpnext.selling.doctype.sales_order.mapper import make_delivery_note, make_sales_invoice
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.selling.report.sales_order_analysis.sales_order_analysis import execute
from erpnext.stock.doctype.item.test_item import create_item
diff --git a/erpnext/setup/demo.py b/erpnext/setup/demo.py
index c460b1520c4..29049a54794 100644
--- a/erpnext/setup/demo.py
+++ b/erpnext/setup/demo.py
@@ -11,8 +11,8 @@ from frappe.utils import add_days, get_url_to_form, getdate
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.utils import get_fiscal_year
-from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice
-from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
+from erpnext.buying.doctype.purchase_order.mapper import make_purchase_invoice
+from erpnext.selling.doctype.sales_order.mapper import make_sales_invoice
from erpnext.setup.setup_wizard.operations.install_fixtures import create_bank_account
diff --git a/erpnext/setup/doctype/authorization_control/authorization_control.py b/erpnext/setup/doctype/authorization_control/authorization_control.py
index ef703de698f..98bc2aa7d9f 100644
--- a/erpnext/setup/doctype/authorization_control/authorization_control.py
+++ b/erpnext/setup/doctype/authorization_control/authorization_control.py
@@ -120,7 +120,9 @@ class AuthorizationControl(TransactionBase):
if val == 1:
add_cond += " and system_user = {}".format(frappe.db.escape(session["user"]))
elif val == 2:
- add_cond += " and system_role IN %s" % ("('" + "','".join(frappe.get_roles()) + "')")
+ add_cond += " and system_role IN (%s)" % ", ".join(
+ frappe.db.escape(r) for r in frappe.get_roles()
+ )
else:
add_cond += " and ifnull(system_user,'') = '' and ifnull(system_role,'') = ''"
@@ -206,8 +208,8 @@ class AuthorizationControl(TransactionBase):
and docstatus != 2
""".format(
"%s",
- "'" + "','".join(frappe.get_roles()) + "'",
- "'" + "','".join(final_based_on) + "'",
+ ", ".join(frappe.db.escape(r) for r in frappe.get_roles()),
+ ", ".join(frappe.db.escape(b) for b in final_based_on),
"%s",
),
(doctype_name, company),
diff --git a/erpnext/setup/doctype/company/test_company.py b/erpnext/setup/doctype/company/test_company.py
index 566a976afd1..4bada0b4e6e 100644
--- a/erpnext/setup/doctype/company/test_company.py
+++ b/erpnext/setup/doctype/company/test_company.py
@@ -119,12 +119,12 @@ class TestCompany(ERPNextTestSuite):
self.assertTrue(lft)
self.assertTrue(rgt)
- self.assertTrue(lft < rgt)
- self.assertTrue(parent_lft < parent_rgt)
- self.assertTrue(lft > parent_lft)
- self.assertTrue(rgt < parent_rgt)
- self.assertTrue(lft >= min_lft)
- self.assertTrue(rgt <= max_rgt)
+ self.assertLess(lft, rgt)
+ self.assertLess(parent_lft, parent_rgt)
+ self.assertGreater(lft, parent_lft)
+ self.assertLess(rgt, parent_rgt)
+ self.assertGreaterEqual(lft, min_lft)
+ self.assertLessEqual(rgt, max_rgt)
def test_primary_address(self):
company = "_Test Company"
diff --git a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py
index 68cd796318e..87b46d60e72 100644
--- a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py
+++ b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py
@@ -104,11 +104,11 @@ class TestCurrencyExchange(ERPNextTestSuite):
# Exchange rate as on 15th Dec, 2015
self.clear_cache()
exchange_rate = get_exchange_rate("USD", "INR", "2015-12-15", "for_selling")
- self.assertFalse(exchange_rate == 60)
+ self.assertNotEqual(exchange_rate, 60)
self.assertEqual(flt(exchange_rate, 3), 66.999)
exchange_rate = get_exchange_rate("USD", "INR", "2016-01-20", "for_buying")
- self.assertFalse(exchange_rate == 60)
+ self.assertNotEqual(exchange_rate, 60)
self.assertEqual(flt(exchange_rate, 3), 65.1)
def test_exchange_rate_via_exchangerate_host(self, mock_get):
@@ -134,11 +134,11 @@ class TestCurrencyExchange(ERPNextTestSuite):
# Exchange rate as on 15th Dec, 2015
self.clear_cache()
exchange_rate = get_exchange_rate("USD", "INR", "2015-12-15", "for_selling")
- self.assertFalse(exchange_rate == 60)
+ self.assertNotEqual(exchange_rate, 60)
self.assertEqual(flt(exchange_rate, 3), 66.999)
exchange_rate = get_exchange_rate("USD", "INR", "2016-01-20", "for_buying")
- self.assertFalse(exchange_rate == 60)
+ self.assertNotEqual(exchange_rate, 60)
self.assertEqual(flt(exchange_rate, 3), 65.1)
settings = frappe.get_single("Currency Exchange Settings")
@@ -175,5 +175,5 @@ class TestCurrencyExchange(ERPNextTestSuite):
self.clear_cache()
exchange_rate = get_exchange_rate("USD", "INR", "2016-01-30", "for_buying")
- self.assertFalse(exchange_rate == 65)
+ self.assertNotEqual(exchange_rate, 65)
self.assertEqual(flt(exchange_rate, 3), 62.9)
diff --git a/erpnext/setup/doctype/employee/test_employee.py b/erpnext/setup/doctype/employee/test_employee.py
index 801a08ae5b6..c1616aa0d58 100644
--- a/erpnext/setup/doctype/employee/test_employee.py
+++ b/erpnext/setup/doctype/employee/test_employee.py
@@ -28,10 +28,10 @@ class TestEmployee(ERPNextTestSuite):
employee = make_employee("test_emp_user_creation@company.com", company="_Test Company")
employee_doc = frappe.get_doc("Employee", employee)
user = employee_doc.user_id
- self.assertTrue("Employee" in frappe.get_roles(user))
+ self.assertIn("Employee", frappe.get_roles(user))
employee_doc.user_id = ""
employee_doc.save()
- self.assertTrue("Employee" not in frappe.get_roles(user))
+ self.assertNotIn("Employee", frappe.get_roles(user))
def test_employee_user_permission(self):
employee1 = make_employee(
diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py
index 043ac0a7193..1bbeff753df 100644
--- a/erpnext/setup/install.py
+++ b/erpnext/setup/install.py
@@ -38,7 +38,6 @@ def after_install():
make_default_operations()
update_pegged_currencies()
set_default_print_formats()
- create_letter_head()
toggle_hidden_fields()
frappe.db.commit()
@@ -374,30 +373,6 @@ def set_default_print_formats():
)
-def create_letter_head():
- base_path = frappe.get_app_path("erpnext", "accounts", "letterhead")
-
- letterheads = {
- "Company Letterhead": "company_letterhead.html",
- "Company Letterhead - Grey": "company_letterhead_grey.html",
- }
-
- for name, filename in letterheads.items():
- if not frappe.db.exists("Letter Head", name):
- content = frappe.read_file(os.path.join(base_path, filename))
- doc = frappe.get_doc(
- {
- "doctype": "Letter Head",
- "letter_head_name": name,
- "source": "HTML",
- "content": content,
- "is_default": 1 if name == "Company Letterhead - Grey" else 0,
- "letter_head_for": "Report",
- }
- )
- doc.insert(ignore_permissions=True)
-
-
def toggle_hidden_fields():
from erpnext.accounts.doctype.accounts_settings.accounts_settings import (
toggle_accounting_dimension_sections,
diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json
index 87cb7a0c871..469b5c7baed 100644
--- a/erpnext/setup/setup_wizard/data/country_wise_tax.json
+++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json
@@ -262,7 +262,15 @@
},
"Belgium VAT 12%": {
"account_name": "VAT 12%",
- "tax_rate": 12
+ "tax_rate": 12.00
+ },
+ "Belgium VAT 6%": {
+ "account_name": "VAT 6%",
+ "tax_rate": 6.00
+ },
+ "Belgium VAT 0%": {
+ "account_name": "VAT 0%",
+ "tax_rate": 0.00
}
},
diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py
index ed4a8c5509c..284cabe8255 100644
--- a/erpnext/stock/doctype/batch/test_batch.py
+++ b/erpnext/stock/doctype/batch/test_batch.py
@@ -387,7 +387,7 @@ class TestBatch(ERPNextTestSuite):
self.assertEqual(get_batch_qty("batch a", "_Test Warehouse - _TC"), 90)
def test_ignore_reserved_qty(self):
- from erpnext.selling.doctype.sales_order.sales_order import create_pick_list
+ from erpnext.selling.doctype.sales_order.mapper import create_pick_list
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
batch_item_name = "Reserve Batch Item"
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js
index 1b7f147f30c..6c5e1fadf04 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.js
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.js
@@ -89,7 +89,7 @@ frappe.ui.form.on("Delivery Note", {
__("Credit Note"),
function () {
frappe.model.open_mapped_doc({
- method: "erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",
+ method: "erpnext.stock.doctype.delivery_note.mapper.make_sales_invoice",
frm: cur_frm,
});
},
@@ -114,7 +114,7 @@ frappe.ui.form.on("Delivery Note", {
__(button_label),
function () {
frappe.model.open_mapped_doc({
- method: "erpnext.stock.doctype.delivery_note.delivery_note.make_inter_company_purchase_receipt",
+ method: "erpnext.stock.doctype.delivery_note.mapper.make_inter_company_purchase_receipt",
frm: frm,
});
},
@@ -163,7 +163,7 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends (
});
}
erpnext.utils.map_current_doc({
- method: "erpnext.selling.doctype.sales_order.sales_order.make_delivery_note",
+ method: "erpnext.selling.doctype.sales_order.mapper.make_delivery_note",
args: {
for_reserved_stock: 1,
},
@@ -205,7 +205,7 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends (
});
}
erpnext.utils.map_current_doc({
- method: "erpnext.stock.doctype.pick_list.pick_list.create_dn_for_pick_lists",
+ method: "erpnext.stock.doctype.pick_list.mapper.create_dn_for_pick_lists",
source_doctype: "Pick List",
target: me.frm,
setters: [
@@ -296,7 +296,7 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends (
__("Packing Slip"),
function () {
frappe.model.open_mapped_doc({
- method: "erpnext.stock.doctype.delivery_note.delivery_note.make_packing_slip",
+ method: "erpnext.stock.doctype.delivery_note.mapper.make_packing_slip",
frm: me.frm,
});
},
@@ -367,7 +367,7 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends (
make_shipment() {
frappe.model.open_mapped_doc({
- method: "erpnext.stock.doctype.delivery_note.delivery_note.make_shipment",
+ method: "erpnext.stock.doctype.delivery_note.mapper.make_shipment",
frm: this.frm,
});
}
@@ -383,28 +383,28 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends (
make_sales_invoice() {
frappe.model.open_mapped_doc({
- method: "erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",
+ method: "erpnext.stock.doctype.delivery_note.mapper.make_sales_invoice",
frm: this.frm,
});
}
make_installation_note() {
frappe.model.open_mapped_doc({
- method: "erpnext.stock.doctype.delivery_note.delivery_note.make_installation_note",
+ method: "erpnext.stock.doctype.delivery_note.mapper.make_installation_note",
frm: this.frm,
});
}
make_sales_return() {
frappe.model.open_mapped_doc({
- method: "erpnext.stock.doctype.delivery_note.delivery_note.make_sales_return",
+ method: "erpnext.stock.doctype.delivery_note.mapper.make_sales_return",
frm: this.frm,
});
}
make_delivery_trip() {
frappe.model.open_mapped_doc({
- method: "erpnext.stock.doctype.delivery_note.delivery_note.make_delivery_trip",
+ method: "erpnext.stock.doctype.delivery_note.mapper.make_delivery_trip",
frm: cur_frm,
});
}
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index f85ed1dc2a9..a86055692b6 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -2,25 +2,21 @@
# 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_sales_invoice,
+)
+
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
@@ -754,8 +750,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,566 +844,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:
- so, doctype, fieldname = doc.get_order_details()
- if (
- doc.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:
- doc.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/delivery_note_list.js b/erpnext/stock/doctype/delivery_note/delivery_note_list.js
index 56698ccf76b..bf55636133a 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note_list.js
+++ b/erpnext/stock/doctype/delivery_note/delivery_note_list.js
@@ -43,7 +43,7 @@ frappe.listview_settings["Delivery Note"] = {
type: "POST",
method: "frappe.model.mapper.map_docs",
args: {
- method: "erpnext.stock.doctype.delivery_note.delivery_note.make_delivery_trip",
+ method: "erpnext.stock.doctype.delivery_note.mapper.make_delivery_trip",
source_names: docnames,
target_doc: cur_frm.doc,
},
diff --git a/erpnext/stock/doctype/delivery_note/mapper.py b/erpnext/stock/doctype/delivery_note/mapper.py
new file mode 100644
index 00000000000..81fdf04f6fd
--- /dev/null
+++ b/erpnext/stock/doctype/delivery_note/mapper.py
@@ -0,0 +1,582 @@
+# 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 CROSS_PARTY_FIELD_NO_MAP, 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.mapper 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": [*CROSS_PARTY_FIELD_NO_MAP, "set_warehouse"],
+ },
+ 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/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 58f5d71b3d4..5b3a9e1861a 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -19,7 +19,7 @@ from erpnext.selling.doctype.sales_order.test_sales_order import (
create_dn_against_so,
make_sales_order,
)
-from erpnext.stock.doctype.delivery_note.delivery_note import (
+from erpnext.stock.doctype.delivery_note.mapper import (
make_delivery_trip,
make_sales_invoice,
)
@@ -218,7 +218,7 @@ class TestDeliveryNote(ERPNextTestSuite):
self.assertEqual(cstr(serial_no.get(field)), value)
def test_delivery_note_return_against_denormalized_serial_no(self):
- from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
+ from erpnext.stock.doctype.delivery_note.mapper import make_sales_return
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
frappe.flags.ignore_serial_batch_bundle_validation = True
@@ -307,7 +307,7 @@ class TestDeliveryNote(ERPNextTestSuite):
returned_serial_nos1 = get_serial_nos_from_bundle(dn1.items[0].serial_and_batch_bundle)
for serial_no in returned_serial_nos1:
- self.assertTrue(serial_no in serial_nos)
+ self.assertIn(serial_no, serial_nos)
dn2 = make_sales_return(dn.name)
@@ -318,8 +318,8 @@ class TestDeliveryNote(ERPNextTestSuite):
returned_serial_nos2 = get_serial_nos_from_bundle(dn2.items[0].serial_and_batch_bundle)
for serial_no in returned_serial_nos2:
- self.assertTrue(serial_no in serial_nos)
- self.assertFalse(serial_no in returned_serial_nos1)
+ self.assertIn(serial_no, serial_nos)
+ self.assertNotIn(serial_no, returned_serial_nos1)
def test_sales_return_for_non_bundled_items_partial(self):
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
@@ -1012,7 +1012,7 @@ class TestDeliveryNote(ERPNextTestSuite):
def test_dn_billing_status_case2(self):
# SO -> SI and SO -> DN1, DN2
- from erpnext.selling.doctype.sales_order.sales_order import (
+ from erpnext.selling.doctype.sales_order.mapper import (
make_delivery_note,
make_sales_invoice,
)
@@ -1054,7 +1054,7 @@ class TestDeliveryNote(ERPNextTestSuite):
@ERPNextTestSuite.change_settings("Accounts Settings", {"delete_linked_ledger_entries": True})
def test_sales_invoice_qty_after_return(self):
- from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
+ from erpnext.stock.doctype.delivery_note.mapper import make_sales_return
item = make_item(
"Test Sales Invoice Qty After Return",
@@ -1085,8 +1085,8 @@ class TestDeliveryNote(ERPNextTestSuite):
def test_dn_billing_status_case3(self):
# SO -> DN1 -> SI and SO -> SI and SO -> DN2
- from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
- from erpnext.selling.doctype.sales_order.sales_order import (
+ from erpnext.selling.doctype.sales_order.mapper import make_delivery_note
+ from erpnext.selling.doctype.sales_order.mapper import (
make_sales_invoice as make_sales_invoice_from_so,
)
@@ -1136,8 +1136,8 @@ class TestDeliveryNote(ERPNextTestSuite):
def test_dn_billing_status_case4(self):
# SO -> SI -> DN
- from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note
- from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
+ from erpnext.accounts.doctype.sales_invoice.mapper import make_delivery_note
+ from erpnext.selling.doctype.sales_order.mapper import make_sales_invoice
so = make_sales_order(po_no="12345")
@@ -1160,7 +1160,7 @@ class TestDeliveryNote(ERPNextTestSuite):
def test_dn_billing_status_case5(self):
# SO -> SI(with update stock partial invoice)
# SO -> DN
- from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note, make_sales_invoice
+ from erpnext.selling.doctype.sales_order.mapper import make_delivery_note, make_sales_invoice
so = make_sales_order(po_no="12345")
@@ -1260,8 +1260,8 @@ class TestDeliveryNote(ERPNextTestSuite):
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
def test_make_sales_invoice_from_dn_for_returned_qty(self):
- from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
- from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
+ from erpnext.selling.doctype.sales_order.mapper import make_delivery_note
+ from erpnext.stock.doctype.delivery_note.mapper import make_sales_invoice
so = make_sales_order(qty=2)
so.submit()
@@ -1280,7 +1280,7 @@ class TestDeliveryNote(ERPNextTestSuite):
@ERPNextTestSuite.change_settings("Selling Settings", {"allow_multiple_items": 1})
def test_make_sales_invoice_from_dn_with_returned_qty_duplicate_items(self):
- from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
+ from erpnext.stock.doctype.delivery_note.mapper import make_sales_invoice
dn = create_delivery_note(qty=8, do_not_submit=True)
dn.append(
@@ -1387,8 +1387,8 @@ class TestDeliveryNote(ERPNextTestSuite):
# |
# |---> DN(Partial Sales Return) ---> SI(Credit Note)
- from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note
- from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
+ from erpnext.accounts.doctype.sales_invoice.mapper import make_delivery_note
+ from erpnext.selling.doctype.sales_order.mapper import make_sales_invoice
so = make_sales_order(qty=10)
si = make_sales_invoice(so.name)
@@ -1400,7 +1400,7 @@ class TestDeliveryNote(ERPNextTestSuite):
self.assertEqual(dn.items[0].returned_qty, 0)
self.assertEqual(dn.per_billed, 100)
- from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
+ from erpnext.stock.doctype.delivery_note.mapper import make_sales_invoice
dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-3)
si1 = make_sales_invoice(dn1.name)
@@ -1557,7 +1557,7 @@ class TestDeliveryNote(ERPNextTestSuite):
return_dn = make_return_doc(dn.doctype, dn.name)
return_dn.save().submit()
- self.assertTrue(return_dn.docstatus == 1)
+ self.assertEqual(return_dn.docstatus, 1)
def test_reserve_qty_on_sales_return(self):
frappe.db.set_single_value("Selling Settings", "dont_reserve_sales_order_qty_on_sales_return", 0)
@@ -1569,7 +1569,7 @@ class TestDeliveryNote(ERPNextTestSuite):
def reserved_qty_check(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
- from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
+ from erpnext.selling.doctype.sales_order.mapper import make_delivery_note
from erpnext.stock.stock_balance import get_reserved_qty
dont_reserve_qty = frappe.db.get_single_value(
@@ -1776,7 +1776,7 @@ class TestDeliveryNote(ERPNextTestSuite):
def test_internal_transfer_for_non_stock_item(self):
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
- from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
+ from erpnext.selling.doctype.sales_order.mapper import make_delivery_note
item = make_item(properties={"is_stock_item": 0}).name
warehouse = "_Test Warehouse - _TC"
@@ -1965,7 +1965,7 @@ class TestDeliveryNote(ERPNextTestSuite):
self.assertEqual(sle_data.stock_value_difference, 200.0 * -1)
def test_sales_return_batch_no_for_batched_item_in_dn(self):
- from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
+ from erpnext.stock.doctype.delivery_note.mapper import make_sales_return
item_code = make_item(
"Test Batched Item for Sales Return 11",
@@ -1994,7 +1994,7 @@ class TestDeliveryNote(ERPNextTestSuite):
self.assertEqual(batch_no, returned_batch_no)
def test_partial_sales_return_batch_no_for_batched_item_in_dn(self):
- from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
+ from erpnext.stock.doctype.delivery_note.mapper import make_sales_return
item_code = make_item(
"Test Partial Batched Item for Sales Return 11",
@@ -2041,7 +2041,7 @@ class TestDeliveryNote(ERPNextTestSuite):
self.assertEqual(sabb_qty, 2)
def test_sales_return_serial_no_for_serial_item_in_dn(self):
- from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
+ from erpnext.stock.doctype.delivery_note.mapper import make_sales_return
item_code = make_item(
"Test Serial Item for Sales Return 11",
@@ -2190,7 +2190,7 @@ class TestDeliveryNote(ERPNextTestSuite):
self.assertEqual(sn.warranty_period, 100)
def test_batch_return_dn(self):
- from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
+ from erpnext.stock.doctype.delivery_note.mapper import make_sales_return
item_code = make_item(
"Test Batch Return DN Item 1",
@@ -2231,7 +2231,7 @@ class TestDeliveryNote(ERPNextTestSuite):
self.assertEqual(stock_value_difference, 100.0 * 5)
def test_delivery_note_return_valuation_without_use_serial_batch_field(self):
- from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
+ from erpnext.stock.doctype.delivery_note.mapper import make_sales_return
batch_item = make_item(
"_Test Delivery Note Return Valuation Batch Item",
@@ -2351,7 +2351,7 @@ class TestDeliveryNote(ERPNextTestSuite):
@ERPNextTestSuite.change_settings("Selling Settings", {"allow_multiple_items": 1})
def test_delivery_note_return_valuation_with_use_serial_batch_field(self):
- from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
+ from erpnext.stock.doctype.delivery_note.mapper import make_sales_return
batch_item = make_item(
"_Test Delivery Note Return Valuation WITH Batch Item",
@@ -2561,7 +2561,7 @@ class TestDeliveryNote(ERPNextTestSuite):
self.assertTrue(row.serial_no)
def test_delivery_note_return_for_batch_item_with_different_warehouse(self):
- from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
+ from erpnext.stock.doctype.delivery_note.mapper import make_sales_return
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
batch_item = make_item(
@@ -2631,7 +2631,7 @@ class TestDeliveryNote(ERPNextTestSuite):
self.assertEqual(d.incoming_rate, batch_no_valuation[d.batch_no])
def test_delivery_note_per_billed_after_return(self):
- from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
+ from erpnext.selling.doctype.sales_order.mapper import make_delivery_note
so = make_sales_order(qty=2)
dn = make_delivery_note(so.name)
@@ -2699,7 +2699,7 @@ class TestDeliveryNote(ERPNextTestSuite):
def test_sales_return_for_product_bundle(self):
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
- from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
+ from erpnext.stock.doctype.delivery_note.mapper import make_sales_return
from erpnext.stock.doctype.item.test_item import make_item
rm_items = []
@@ -2772,7 +2772,7 @@ class TestDeliveryNote(ERPNextTestSuite):
doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle)
for entry in doc.entries:
if entry.serial_no:
- self.assertTrue(entry.serial_no in serial_batch_map[row.item_code].serial_nos)
+ self.assertIn(entry.serial_no, serial_batch_map[row.item_code].serial_nos)
self.assertEqual(
entry.incoming_rate,
serial_batch_map[row.item_code].serial_no_valuation[entry.serial_no],
@@ -2782,7 +2782,7 @@ class TestDeliveryNote(ERPNextTestSuite):
elif entry.batch_no:
serial_batch_map[row.item_code].batches[entry.batch_no] += entry.qty
- self.assertTrue(entry.batch_no in serial_batch_map[row.item_code].batches)
+ self.assertIn(entry.batch_no, serial_batch_map[row.item_code].batches)
self.assertEqual(entry.qty, 2.0)
self.assertEqual(
entry.incoming_rate,
@@ -2798,7 +2798,7 @@ class TestDeliveryNote(ERPNextTestSuite):
doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle)
for entry in doc.entries:
if entry.serial_no:
- self.assertTrue(entry.serial_no in serial_batch_map[row.item_code].serial_nos)
+ self.assertIn(entry.serial_no, serial_batch_map[row.item_code].serial_nos)
self.assertEqual(
entry.incoming_rate,
serial_batch_map[row.item_code].serial_no_valuation[entry.serial_no],
@@ -2810,7 +2810,7 @@ class TestDeliveryNote(ERPNextTestSuite):
serial_batch_map[row.item_code].batches[entry.batch_no] += entry.qty
self.assertEqual(serial_batch_map[row.item_code].batches[entry.batch_no], 0.0)
- self.assertTrue(entry.batch_no in serial_batch_map[row.item_code].batches)
+ self.assertIn(entry.batch_no, serial_batch_map[row.item_code].batches)
self.assertEqual(entry.qty, 3.0)
self.assertEqual(
@@ -3150,7 +3150,7 @@ class TestDeliveryNote(ERPNextTestSuite):
def test_sdbnb_skip_for_dn_against_sales_invoice(self):
"""Test that DN items with against_sales_invoice reference skips SDBNB account assignment."""
- from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
+ from erpnext.accounts.doctype.sales_invoice.mapper import (
make_delivery_note as make_dn_from_si,
)
diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.js b/erpnext/stock/doctype/delivery_trip/delivery_trip.js
index 61c6743054f..9eb5b1f83c3 100755
--- a/erpnext/stock/doctype/delivery_trip/delivery_trip.js
+++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.js
@@ -54,7 +54,7 @@ frappe.ui.form.on("Delivery Trip", {
__("Delivery Note"),
() => {
erpnext.utils.map_current_doc({
- method: "erpnext.stock.doctype.delivery_note.delivery_note.make_delivery_trip",
+ method: "erpnext.stock.doctype.delivery_note.mapper.make_delivery_trip",
source_doctype: "Delivery Note",
target: frm,
date_field: "posting_date",
diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py
index 3a054abf722..655d781126c 100644
--- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py
+++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py
@@ -364,7 +364,7 @@ class TestInventoryDimension(ERPNextTestSuite):
def test_inter_transfer_return_against_inventory_dimension(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
- from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
+ from erpnext.stock.doctype.delivery_note.mapper import make_inter_company_purchase_receipt
data = prepare_data_for_internal_transfer()
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index a37c0925b71..2261cb7733f 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -935,11 +935,17 @@ $.extend(erpnext.item, {
if (!row.disabled) {
if (row.numeric_values) {
- fieldtype = "Float";
+ const all_are_int =
+ flt(row.from_range) === cint(row.from_range) &&
+ flt(row.to_range) === cint(row.to_range) &&
+ flt(row.increment) === cint(row.increment);
+ fieldtype = all_are_int ? "Int" : "Float";
+ const df = { fieldtype };
+ const options = all_are_int ? { inline: 1 } : { always_show_decimals: true, inline: 1 };
desc = __("Min Value: {0}, Max Value: {1}, in Increments of: {2}", [
- frappe.format(row.from_range, { fieldtype: "Float" }, { always_show_decimals: true }),
- frappe.format(row.to_range, { fieldtype: "Float" }, { always_show_decimals: true }),
- frappe.format(row.increment, { fieldtype: "Float" }, { always_show_decimals: true }),
+ frappe.format(row.from_range, df, options),
+ frappe.format(row.to_range, df, options),
+ frappe.format(row.increment, df, options),
]);
} else {
fieldtype = "Data";
diff --git a/erpnext/stock/doctype/item/item_list.js b/erpnext/stock/doctype/item/item_list.js
index 34e0fae07d0..05d0151a932 100644
--- a/erpnext/stock/doctype/item/item_list.js
+++ b/erpnext/stock/doctype/item/item_list.js
@@ -33,19 +33,21 @@ frappe.listview_settings["Item"] = {
},
onload: function (listview) {
- listview.columns = listview.columns.map((col) => {
- if (!col.df) return col;
- const renames = {
- is_fixed_asset: __("Item Type"),
- is_sales_item: __("Purpose"),
- stock_uom: __("UOM"),
- };
- if (col.df.fieldname in renames) {
- return { ...col, df: { ...col.df, label: renames[col.df.fieldname] } };
- }
- return col;
- });
- listview.render_header(true);
+ if (listview.view === "List") {
+ listview.columns = listview.columns.map((col) => {
+ if (!col.df) return col;
+ const renames = {
+ is_fixed_asset: __("Item Type"),
+ is_sales_item: __("Purpose"),
+ stock_uom: __("UOM"),
+ };
+ if (col.df.fieldname in renames) {
+ return { ...col, df: { ...col.df, label: renames[col.df.fieldname] } };
+ }
+ return col;
+ });
+ listview.render_header(true);
+ }
},
get_indicator: function (doc) {
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index 0725dacc18b..5dd4da05768 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -391,8 +391,9 @@ class TestItem(ERPNextTestSuite):
},
)
- self.assertTrue(
- "belong to company" in str(ve.exception).lower(),
+ self.assertIn(
+ "belong to company",
+ str(ve.exception).lower(),
msg="Mismatching company entities in item defaults should not be allowed.",
)
@@ -676,7 +677,7 @@ class TestItem(ERPNextTestSuite):
self.assertIsInstance(timestamp, int)
self.assertTrue(one_year_ago <= timestamp <= now)
self.assertIsInstance(count, int)
- self.assertTrue(count >= 0)
+ self.assertGreaterEqual(count, 0)
def test_index_creation(self):
"check if index is getting created in db"
@@ -849,7 +850,7 @@ class TestItem(ERPNextTestSuite):
for _row in range(3):
item.append("customer_items", {"ref_code": frappe.generate_hash("", 120)})
item.save()
- self.assertTrue(len(item.customer_code) > 140)
+ self.assertGreater(len(item.customer_code), 140)
def test_update_is_stock_item(self):
# Step - 1: Create an Item with Maintain Stock enabled
@@ -890,7 +891,7 @@ class TestItem(ERPNextTestSuite):
data = item_query("Item", "Test Item", "", 0, 20, filters={"item_name": "Test Item"}, as_dict=True)
self.assertEqual(data[0].name, item.name)
self.assertEqual(data[0].item_name, item.item_name)
- self.assertTrue("description" not in data[0])
+ self.assertNotIn("description", data[0])
make_property_setter(
"Item", None, "search_fields", "item_name, description", "Data", for_doctype="Doctype"
@@ -899,7 +900,7 @@ class TestItem(ERPNextTestSuite):
self.assertEqual(data[0].name, item.name)
self.assertEqual(data[0].item_name, item.item_name)
self.assertEqual(data[0].description, item.description)
- self.assertTrue("description" in data[0])
+ self.assertIn("description", data[0])
def test_group_warehouse_for_reorder_item(self):
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
@@ -956,8 +957,9 @@ class TestItem(ERPNextTestSuite):
}
).insert()
- self.assertTrue(
- "must be same as in Template" in str(ve.exception),
+ self.assertIn(
+ "must be same as in Template",
+ str(ve.exception),
msg="Different Variant UOM should not be allowed when `allow_different_uom` is disabled.",
)
diff --git a/erpnext/stock/doctype/item_alternative/test_item_alternative.py b/erpnext/stock/doctype/item_alternative/test_item_alternative.py
index 0a2119af3ec..2be54c82036 100644
--- a/erpnext/stock/doctype/item_alternative/test_item_alternative.py
+++ b/erpnext/stock/doctype/item_alternative/test_item_alternative.py
@@ -11,8 +11,8 @@ from erpnext.controllers.tests.test_subcontracting_controller import (
set_backflush_based_on,
)
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
+from erpnext.manufacturing.doctype.work_order.mapper import make_stock_entry
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
-from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
EmptyStockReconciliationItemsError,
diff --git a/erpnext/stock/doctype/item_price/test_item_price.py b/erpnext/stock/doctype/item_price/test_item_price.py
index 7a1400863bf..d98339bb0c7 100644
--- a/erpnext/stock/doctype/item_price/test_item_price.py
+++ b/erpnext/stock/doctype/item_price/test_item_price.py
@@ -54,7 +54,7 @@ class TestItemPrice(ERPNextTestSuite):
doc_fields = frappe.copy_doc(self.globalTestRecords["Item Price"][1]).__dict__.keys()
for test_field in test_fields_existance:
- self.assertTrue(test_field in doc_fields)
+ self.assertIn(test_field, doc_fields)
def test_dates_validation_error(self):
doc = frappe.copy_doc(self.globalTestRecords["Item Price"][1])
diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
index 7fa2e0a2548..6c44cec46b4 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
@@ -1130,10 +1130,10 @@ class TestLandedCostVoucher(ERPNextTestSuite):
make_stock_transfer_entry,
)
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
- from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
- from erpnext.manufacturing.doctype.work_order.work_order import (
+ from erpnext.manufacturing.doctype.work_order.mapper import (
make_stock_entry as make_stock_entry_for_wo,
)
+ from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
diff --git a/erpnext/stock/doctype/material_request/mapper.py b/erpnext/stock/doctype/material_request/mapper.py
new file mode 100644
index 00000000000..1ecc842a7b4
--- /dev/null
+++ b/erpnext/stock/doctype/material_request/mapper.py
@@ -0,0 +1,370 @@
+# 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.utils import cint, flt, getdate, nowdate
+
+from erpnext.subcontracting.doctype.subcontracting_bom.subcontracting_bom import (
+ get_subcontracting_boms_for_finished_goods,
+)
+
+
+def set_missing_values(source, target_doc):
+ if target_doc.doctype == "Purchase Order" and getdate(target_doc.schedule_date) < getdate(nowdate()):
+ target_doc.schedule_date = None
+ target_doc.run_method("set_missing_values")
+ target_doc.run_method("calculate_taxes_and_totals")
+
+
+def update_item(obj, target, source_parent):
+ target.conversion_factor = obj.conversion_factor
+
+ qty = obj.ordered_qty or obj.received_qty
+ target.qty = flt(flt(obj.stock_qty) - flt(qty)) / target.conversion_factor
+ target.stock_qty = target.qty * target.conversion_factor
+ if getdate(target.schedule_date) < getdate(nowdate()):
+ target.schedule_date = None
+
+ if target.fg_item:
+ target.fg_item_qty = obj.stock_qty
+ if sc_bom := get_subcontracting_boms_for_finished_goods(target.fg_item):
+ target.item_code = sc_bom.service_item
+ target.uom = sc_bom.service_item_uom
+ target.conversion_factor = (
+ frappe.db.get_value(
+ "UOM Conversion Detail",
+ {"parent": sc_bom.service_item, "uom": sc_bom.service_item_uom},
+ "conversion_factor",
+ )
+ or 1
+ )
+ target.qty = target.fg_item_qty * sc_bom.conversion_factor
+ target.stock_qty = target.qty * target.conversion_factor
+
+
+@frappe.whitelist()
+def make_purchase_order(
+ 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)
+
+ is_subcontracted = (
+ frappe.db.get_value("Material Request", source_name, "material_request_type") == "Subcontracting"
+ )
+
+ def postprocess(source, target_doc):
+ target_doc.is_subcontracted = is_subcontracted
+ set_missing_values(source, target_doc)
+
+ def select_item(d):
+ filtered_items = args.get("filtered_children", [])
+ child_filter = d.name in filtered_items if filtered_items else True
+
+ qty = d.ordered_qty or d.received_qty
+
+ return qty < d.stock_qty and child_filter
+
+ def generate_field_map():
+ field_map = [
+ ["name", "material_request_item"],
+ ["parent", "material_request"],
+ ["sales_order", "sales_order"],
+ ["sales_order_item", "sales_order_item"],
+ ["wip_composite_asset", "wip_composite_asset"],
+ ]
+
+ if is_subcontracted:
+ field_map.extend([["item_code", "fg_item"], ["qty", "fg_item_qty"]])
+ else:
+ field_map.extend([["uom", "stock_uom"], ["uom", "uom"]])
+
+ return field_map
+
+ doclist = get_mapped_doc(
+ "Material Request",
+ source_name,
+ {
+ "Material Request": {
+ "doctype": "Purchase Order",
+ "validation": {
+ "docstatus": ["=", 1],
+ "material_request_type": ["in", ["Purchase", "Subcontracting"]],
+ },
+ },
+ "Material Request Item": {
+ "doctype": "Purchase Order Item",
+ "field_map": generate_field_map(),
+ "field_no_map": ["item_code", "item_name", "qty"] if is_subcontracted else [],
+ "postprocess": update_item,
+ "condition": select_item,
+ },
+ },
+ target_doc,
+ postprocess,
+ )
+
+ doclist.set_onload("load_after_mapping", False)
+ return doclist
+
+
+@frappe.whitelist()
+def make_request_for_quotation(source_name: str, target_doc: str | Document | None = None):
+ doclist = get_mapped_doc(
+ "Material Request",
+ source_name,
+ {
+ "Material Request": {
+ "doctype": "Request for Quotation",
+ "validation": {"docstatus": ["=", 1], "material_request_type": ["=", "Purchase"]},
+ },
+ "Material Request Item": {
+ "doctype": "Request for Quotation Item",
+ "field_map": [
+ ["name", "material_request_item"],
+ ["parent", "material_request"],
+ ["project", "project_name"],
+ ],
+ },
+ },
+ target_doc,
+ )
+
+ return doclist
+
+
+@frappe.whitelist()
+def get_items_based_on_default_supplier(supplier: str):
+ supplier_items = [
+ d.parent
+ for d in frappe.db.get_all(
+ "Item Default", {"default_supplier": supplier, "parenttype": "Item"}, "parent"
+ )
+ ]
+
+ return supplier_items
+
+
+@frappe.whitelist()
+def make_purchase_order_based_on_supplier(
+ source_name: str, target_doc: str | Document | None = None, args: dict | None = None
+):
+ mr = source_name
+
+ supplier_items = get_items_based_on_default_supplier(args.get("supplier"))
+
+ def postprocess(source, target_doc):
+ target_doc.supplier = args.get("supplier")
+ if getdate(target_doc.schedule_date) < getdate(nowdate()):
+ target_doc.schedule_date = None
+ target_doc.set(
+ "items",
+ [d for d in target_doc.get("items") if d.get("item_code") in supplier_items and d.get("qty") > 0],
+ )
+
+ set_missing_values(source, target_doc)
+
+ target_doc = get_mapped_doc(
+ "Material Request",
+ mr,
+ {
+ "Material Request": {
+ "doctype": "Purchase Order",
+ },
+ "Material Request Item": {
+ "doctype": "Purchase Order Item",
+ "field_map": [
+ ["name", "material_request_item"],
+ ["parent", "material_request"],
+ ["uom", "stock_uom"],
+ ["uom", "uom"],
+ ],
+ "postprocess": update_item,
+ "condition": lambda doc: doc.ordered_qty < doc.qty,
+ },
+ },
+ target_doc,
+ postprocess,
+ )
+
+ return target_doc
+
+
+@frappe.whitelist()
+def make_supplier_quotation(source_name: str, target_doc: str | Document | None = None):
+ def postprocess(source, target_doc):
+ set_missing_values(source, target_doc)
+
+ doclist = get_mapped_doc(
+ "Material Request",
+ source_name,
+ {
+ "Material Request": {
+ "doctype": "Supplier Quotation",
+ "validation": {"docstatus": ["=", 1], "material_request_type": ["=", "Purchase"]},
+ },
+ "Material Request Item": {
+ "doctype": "Supplier Quotation Item",
+ "field_map": {
+ "name": "material_request_item",
+ "parent": "material_request",
+ "sales_order": "sales_order",
+ },
+ },
+ },
+ target_doc,
+ postprocess,
+ )
+
+ doclist.set_onload("load_after_mapping", False)
+ return doclist
+
+
+@frappe.whitelist()
+def make_stock_entry(source_name: str, target_doc: str | Document | None = None):
+ def update_item(obj, target, source_parent):
+ qty = (
+ flt(flt(obj.stock_qty) - flt(obj.ordered_qty)) / target.conversion_factor
+ if flt(obj.stock_qty) > flt(obj.ordered_qty)
+ else 0
+ )
+ target.qty = qty
+ target.transfer_qty = qty * obj.conversion_factor
+ target.conversion_factor = obj.conversion_factor
+
+ if (
+ source_parent.material_request_type == "Material Transfer"
+ or source_parent.material_request_type == "Customer Provided"
+ ):
+ target.t_warehouse = obj.warehouse
+ else:
+ target.s_warehouse = obj.warehouse
+
+ if source_parent.material_request_type == "Customer Provided":
+ target.allow_zero_valuation_rate = 1
+
+ if source_parent.material_request_type == "Material Transfer":
+ target.s_warehouse = obj.from_warehouse
+
+ def set_missing_values(source, target):
+ target.purpose = source.material_request_type
+ target.from_warehouse = source.set_from_warehouse
+ target.to_warehouse = source.set_warehouse
+ if source.material_request_type == "Material Issue":
+ target.from_warehouse = source.set_warehouse
+ target.to_warehouse = None
+
+ if source.job_card:
+ target.purpose = "Material Transfer for Manufacture"
+
+ if source.material_request_type == "Customer Provided":
+ target.purpose = "Material Receipt"
+
+ target.set_transfer_qty()
+ target.set_actual_qty()
+ target.calculate_rate_and_amount(raise_error_if_no_rate=False)
+ target.stock_entry_type = target.purpose
+
+ if source.job_card:
+ job_card_details = frappe.get_all(
+ "Job Card", filters={"name": source.job_card}, fields=["bom_no", "for_quantity"]
+ )
+
+ if job_card_details and job_card_details[0]:
+ target.bom_no = job_card_details[0].bom_no
+ target.fg_completed_qty = job_card_details[0].for_quantity
+ target.from_bom = 1
+
+ doclist = get_mapped_doc(
+ "Material Request",
+ source_name,
+ {
+ "Material Request": {
+ "doctype": "Stock Entry",
+ "validation": {
+ "docstatus": ["=", 1],
+ "material_request_type": [
+ "in",
+ ["Material Transfer", "Material Issue", "Customer Provided"],
+ ],
+ },
+ },
+ "Material Request Item": {
+ "doctype": "Stock Entry Detail",
+ "field_map": {
+ "name": "material_request_item",
+ "parent": "material_request",
+ "uom": "stock_uom",
+ "job_card_item": "job_card_item",
+ },
+ "field_no_map": ["expense_account"],
+ "postprocess": update_item,
+ "condition": lambda doc: (
+ flt(doc.ordered_qty, doc.precision("ordered_qty"))
+ < flt(doc.stock_qty, doc.precision("ordered_qty"))
+ ),
+ },
+ },
+ target_doc,
+ set_missing_values,
+ )
+
+ return doclist
+
+
+@frappe.whitelist()
+def create_pick_list(source_name: str, target_doc: str | Document | None = None):
+ def update_item(obj, target, source_parent):
+ qty = flt((obj.stock_qty - obj.picked_qty) / target.conversion_factor, obj.precision("qty"))
+ target.qty = qty
+ target.stock_qty = qty * obj.conversion_factor
+ target.conversion_factor = obj.conversion_factor
+
+ doc = get_mapped_doc(
+ "Material Request",
+ source_name,
+ {
+ "Material Request": {
+ "doctype": "Pick List",
+ "field_map": {"material_request_type": "purpose"},
+ "validation": {"docstatus": ["=", 1]},
+ },
+ "Material Request Item": {
+ "doctype": "Pick List Item",
+ "field_map": {
+ "name": "material_request_item",
+ "stock_qty": "stock_qty",
+ "from_warehouse": "warehouse",
+ },
+ "postprocess": update_item,
+ "condition": lambda doc: (
+ flt(doc.picked_qty, doc.precision("picked_qty"))
+ < flt(doc.stock_qty, doc.precision("stock_qty"))
+ ),
+ },
+ },
+ target_doc,
+ )
+
+ doc.set_item_locations()
+
+ return doc
+
+
+@frappe.whitelist()
+def make_in_transit_stock_entry(source_name: str, in_transit_warehouse: str):
+ ste_doc = make_stock_entry(source_name)
+ ste_doc.add_to_transit = 1
+ ste_doc.to_warehouse = in_transit_warehouse
+
+ for row in ste_doc.items:
+ row.t_warehouse = in_transit_warehouse
+
+ return ste_doc
diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js
index e0bbff4cbda..0e48296323b 100644
--- a/erpnext/stock/doctype/material_request/material_request.js
+++ b/erpnext/stock/doctype/material_request/material_request.js
@@ -258,7 +258,7 @@ frappe.ui.form.on("Material Request", {
get_items_from_sales_order: function (frm) {
erpnext.utils.map_current_doc({
- method: "erpnext.selling.doctype.sales_order.sales_order.make_material_request",
+ method: "erpnext.selling.doctype.sales_order.mapper.make_material_request",
source_doctype: "Sales Order",
target: frm,
setters: {
@@ -411,7 +411,7 @@ frappe.ui.form.on("Material Request", {
make_purchase_order: function (frm) {
frappe.model.open_mapped_doc({
- method: "erpnext.stock.doctype.material_request.material_request.make_purchase_order",
+ method: "erpnext.stock.doctype.material_request.mapper.make_purchase_order",
frm: frm,
run_link_triggers: true,
});
@@ -419,7 +419,7 @@ frappe.ui.form.on("Material Request", {
make_request_for_quotation: function (frm) {
frappe.model.open_mapped_doc({
- method: "erpnext.stock.doctype.material_request.material_request.make_request_for_quotation",
+ method: "erpnext.stock.doctype.material_request.mapper.make_request_for_quotation",
frm: frm,
run_link_triggers: true,
});
@@ -427,14 +427,14 @@ frappe.ui.form.on("Material Request", {
make_supplier_quotation: function (frm) {
frappe.model.open_mapped_doc({
- method: "erpnext.stock.doctype.material_request.material_request.make_supplier_quotation",
+ method: "erpnext.stock.doctype.material_request.mapper.make_supplier_quotation",
frm: frm,
});
},
make_stock_entry: function (frm) {
frappe.model.open_mapped_doc({
- method: "erpnext.stock.doctype.material_request.material_request.make_stock_entry",
+ method: "erpnext.stock.doctype.material_request.mapper.make_stock_entry",
frm: frm,
});
},
@@ -461,7 +461,7 @@ frappe.ui.form.on("Material Request", {
],
(values) => {
frappe.call({
- method: "erpnext.stock.doctype.material_request.material_request.make_in_transit_stock_entry",
+ method: "erpnext.stock.doctype.material_request.mapper.make_in_transit_stock_entry",
args: {
source_name: frm.doc.name,
in_transit_warehouse: values.in_transit_warehouse,
@@ -481,7 +481,7 @@ frappe.ui.form.on("Material Request", {
create_pick_list: (frm) => {
frappe.model.open_mapped_doc({
- method: "erpnext.stock.doctype.material_request.material_request.create_pick_list",
+ method: "erpnext.stock.doctype.material_request.mapper.create_pick_list",
frm: frm,
});
},
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index 8d8239626a1..c020ab35f90 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -5,14 +5,12 @@
# For license information, please see license.txt
-import json
from typing import Any
import frappe
import frappe.defaults
from frappe import _, msgprint
from frappe.model.document import Document
-from frappe.model.mapper import get_mapped_doc
from frappe.query_builder import Order
from frappe.query_builder.functions import Sum
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, new_line_sep, nowdate
@@ -21,8 +19,9 @@ from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_it
from erpnext.controllers.buying_controller import BuyingController
from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
from erpnext.stock.stock_balance import get_indented_qty, update_bin_qty
-from erpnext.subcontracting.doctype.subcontracting_bom.subcontracting_bom import (
- get_subcontracting_boms_for_finished_goods,
+
+from .mapper import (
+ get_items_based_on_default_supplier,
)
form_grid_templates = {"items": "templates/form_grid/material_request_grid.html"}
@@ -251,7 +250,7 @@ class MaterialRequest(BuyingController):
def check_modified_date(self):
mod_db = frappe.db.sql("""select modified from `tabMaterial Request` where name = %s""", self.name)
- date_diff = frappe.db.sql(f"""select TIMEDIFF('{mod_db[0][0]}', '{cstr(self.modified)}')""")
+ date_diff = frappe.db.sql("""select TIMEDIFF(%s, %s)""", (mod_db[0][0], cstr(self.modified)))
if date_diff and date_diff[0][0]:
frappe.throw(_("{0} {1} has been modified. Please refresh.").format(_(self.doctype), self.name))
@@ -441,39 +440,6 @@ def update_completed_and_requested_qty(stock_entry, method):
mr_obj.update_requested_qty(mr_item_rows)
-def set_missing_values(source, target_doc):
- if target_doc.doctype == "Purchase Order" and getdate(target_doc.schedule_date) < getdate(nowdate()):
- target_doc.schedule_date = None
- target_doc.run_method("set_missing_values")
- target_doc.run_method("calculate_taxes_and_totals")
-
-
-def update_item(obj, target, source_parent):
- target.conversion_factor = obj.conversion_factor
-
- qty = obj.ordered_qty or obj.received_qty
- target.qty = flt(flt(obj.stock_qty) - flt(qty)) / target.conversion_factor
- target.stock_qty = target.qty * target.conversion_factor
- if getdate(target.schedule_date) < getdate(nowdate()):
- target.schedule_date = None
-
- if target.fg_item:
- target.fg_item_qty = obj.stock_qty
- if sc_bom := get_subcontracting_boms_for_finished_goods(target.fg_item):
- target.item_code = sc_bom.service_item
- target.uom = sc_bom.service_item_uom
- target.conversion_factor = (
- frappe.db.get_value(
- "UOM Conversion Detail",
- {"parent": sc_bom.service_item, "uom": sc_bom.service_item_uom},
- "conversion_factor",
- )
- or 1
- )
- target.qty = target.fg_item_qty * sc_bom.conversion_factor
- target.stock_qty = target.qty * target.conversion_factor
-
-
def get_list_context(context=None):
from erpnext.controllers.website_list_for_contact import get_list_context
@@ -498,156 +464,6 @@ def update_status(name: str, status: str):
material_request.update_status(status)
-@frappe.whitelist()
-def make_purchase_order(
- 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)
-
- is_subcontracted = (
- frappe.db.get_value("Material Request", source_name, "material_request_type") == "Subcontracting"
- )
-
- def postprocess(source, target_doc):
- target_doc.is_subcontracted = is_subcontracted
- set_missing_values(source, target_doc)
-
- def select_item(d):
- filtered_items = args.get("filtered_children", [])
- child_filter = d.name in filtered_items if filtered_items else True
-
- qty = d.ordered_qty or d.received_qty
-
- return qty < d.stock_qty and child_filter
-
- def generate_field_map():
- field_map = [
- ["name", "material_request_item"],
- ["parent", "material_request"],
- ["sales_order", "sales_order"],
- ["sales_order_item", "sales_order_item"],
- ["wip_composite_asset", "wip_composite_asset"],
- ]
-
- if is_subcontracted:
- field_map.extend([["item_code", "fg_item"], ["qty", "fg_item_qty"]])
- else:
- field_map.extend([["uom", "stock_uom"], ["uom", "uom"]])
-
- return field_map
-
- doclist = get_mapped_doc(
- "Material Request",
- source_name,
- {
- "Material Request": {
- "doctype": "Purchase Order",
- "validation": {
- "docstatus": ["=", 1],
- "material_request_type": ["in", ["Purchase", "Subcontracting"]],
- },
- },
- "Material Request Item": {
- "doctype": "Purchase Order Item",
- "field_map": generate_field_map(),
- "field_no_map": ["item_code", "item_name", "qty"] if is_subcontracted else [],
- "postprocess": update_item,
- "condition": select_item,
- },
- },
- target_doc,
- postprocess,
- )
-
- doclist.set_onload("load_after_mapping", False)
- return doclist
-
-
-@frappe.whitelist()
-def make_request_for_quotation(source_name: str, target_doc: str | Document | None = None):
- doclist = get_mapped_doc(
- "Material Request",
- source_name,
- {
- "Material Request": {
- "doctype": "Request for Quotation",
- "validation": {"docstatus": ["=", 1], "material_request_type": ["=", "Purchase"]},
- },
- "Material Request Item": {
- "doctype": "Request for Quotation Item",
- "field_map": [
- ["name", "material_request_item"],
- ["parent", "material_request"],
- ["project", "project_name"],
- ],
- },
- },
- target_doc,
- )
-
- return doclist
-
-
-@frappe.whitelist()
-def make_purchase_order_based_on_supplier(
- source_name: str, target_doc: str | Document | None = None, args: dict | None = None
-):
- mr = source_name
-
- supplier_items = get_items_based_on_default_supplier(args.get("supplier"))
-
- def postprocess(source, target_doc):
- target_doc.supplier = args.get("supplier")
- if getdate(target_doc.schedule_date) < getdate(nowdate()):
- target_doc.schedule_date = None
- target_doc.set(
- "items",
- [d for d in target_doc.get("items") if d.get("item_code") in supplier_items and d.get("qty") > 0],
- )
-
- set_missing_values(source, target_doc)
-
- target_doc = get_mapped_doc(
- "Material Request",
- mr,
- {
- "Material Request": {
- "doctype": "Purchase Order",
- },
- "Material Request Item": {
- "doctype": "Purchase Order Item",
- "field_map": [
- ["name", "material_request_item"],
- ["parent", "material_request"],
- ["uom", "stock_uom"],
- ["uom", "uom"],
- ],
- "postprocess": update_item,
- "condition": lambda doc: doc.ordered_qty < doc.qty,
- },
- },
- target_doc,
- postprocess,
- )
-
- return target_doc
-
-
-@frappe.whitelist()
-def get_items_based_on_default_supplier(supplier: str):
- supplier_items = [
- d.parent
- for d in frappe.db.get_all(
- "Item Default", {"default_supplier": supplier, "parenttype": "Item"}, "parent"
- )
- ]
-
- return supplier_items
-
-
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_material_requests_based_on_supplier(
@@ -694,128 +510,6 @@ def get_material_requests_based_on_supplier(
return material_requests
-@frappe.whitelist()
-def make_supplier_quotation(source_name: str, target_doc: str | Document | None = None):
- def postprocess(source, target_doc):
- set_missing_values(source, target_doc)
-
- doclist = get_mapped_doc(
- "Material Request",
- source_name,
- {
- "Material Request": {
- "doctype": "Supplier Quotation",
- "validation": {"docstatus": ["=", 1], "material_request_type": ["=", "Purchase"]},
- },
- "Material Request Item": {
- "doctype": "Supplier Quotation Item",
- "field_map": {
- "name": "material_request_item",
- "parent": "material_request",
- "sales_order": "sales_order",
- },
- },
- },
- target_doc,
- postprocess,
- )
-
- doclist.set_onload("load_after_mapping", False)
- return doclist
-
-
-@frappe.whitelist()
-def make_stock_entry(source_name: str, target_doc: str | Document | None = None):
- def update_item(obj, target, source_parent):
- qty = (
- flt(flt(obj.stock_qty) - flt(obj.ordered_qty)) / target.conversion_factor
- if flt(obj.stock_qty) > flt(obj.ordered_qty)
- else 0
- )
- target.qty = qty
- target.transfer_qty = qty * obj.conversion_factor
- target.conversion_factor = obj.conversion_factor
-
- if (
- source_parent.material_request_type == "Material Transfer"
- or source_parent.material_request_type == "Customer Provided"
- ):
- target.t_warehouse = obj.warehouse
- else:
- target.s_warehouse = obj.warehouse
-
- if source_parent.material_request_type == "Customer Provided":
- target.allow_zero_valuation_rate = 1
-
- if source_parent.material_request_type == "Material Transfer":
- target.s_warehouse = obj.from_warehouse
-
- def set_missing_values(source, target):
- target.purpose = source.material_request_type
- target.from_warehouse = source.set_from_warehouse
- target.to_warehouse = source.set_warehouse
- if source.material_request_type == "Material Issue":
- target.from_warehouse = source.set_warehouse
- target.to_warehouse = None
-
- if source.job_card:
- target.purpose = "Material Transfer for Manufacture"
-
- if source.material_request_type == "Customer Provided":
- target.purpose = "Material Receipt"
-
- target.set_transfer_qty()
- target.set_actual_qty()
- target.calculate_rate_and_amount(raise_error_if_no_rate=False)
- target.stock_entry_type = target.purpose
-
- if source.job_card:
- job_card_details = frappe.get_all(
- "Job Card", filters={"name": source.job_card}, fields=["bom_no", "for_quantity"]
- )
-
- if job_card_details and job_card_details[0]:
- target.bom_no = job_card_details[0].bom_no
- target.fg_completed_qty = job_card_details[0].for_quantity
- target.from_bom = 1
-
- doclist = get_mapped_doc(
- "Material Request",
- source_name,
- {
- "Material Request": {
- "doctype": "Stock Entry",
- "validation": {
- "docstatus": ["=", 1],
- "material_request_type": [
- "in",
- ["Material Transfer", "Material Issue", "Customer Provided"],
- ],
- },
- },
- "Material Request Item": {
- "doctype": "Stock Entry Detail",
- "field_map": {
- "name": "material_request_item",
- "parent": "material_request",
- "uom": "stock_uom",
- "job_card_item": "job_card_item",
- },
- "field_no_map": ["expense_account"],
- "postprocess": update_item,
- "condition": lambda doc: (
- flt(doc.ordered_qty, doc.precision("ordered_qty"))
- < flt(doc.stock_qty, doc.precision("ordered_qty"))
- ),
- },
- },
- target_doc,
- set_missing_values,
- )
-
- return doclist
-
-
@frappe.whitelist()
def raise_work_orders(material_request: str, company: str):
mr = frappe.get_doc("Material Request", material_request)
@@ -885,54 +579,3 @@ def raise_work_orders(material_request: str, company: str):
)
return work_orders
-
-
-@frappe.whitelist()
-def create_pick_list(source_name: str, target_doc: str | Document | None = None):
- def update_item(obj, target, source_parent):
- qty = flt((obj.stock_qty - obj.picked_qty) / target.conversion_factor, obj.precision("qty"))
- target.qty = qty
- target.stock_qty = qty * obj.conversion_factor
- target.conversion_factor = obj.conversion_factor
-
- doc = get_mapped_doc(
- "Material Request",
- source_name,
- {
- "Material Request": {
- "doctype": "Pick List",
- "field_map": {"material_request_type": "purpose"},
- "validation": {"docstatus": ["=", 1]},
- },
- "Material Request Item": {
- "doctype": "Pick List Item",
- "field_map": {
- "name": "material_request_item",
- "stock_qty": "stock_qty",
- "from_warehouse": "warehouse",
- },
- "postprocess": update_item,
- "condition": lambda doc: (
- flt(doc.picked_qty, doc.precision("picked_qty"))
- < flt(doc.stock_qty, doc.precision("stock_qty"))
- ),
- },
- },
- target_doc,
- )
-
- doc.set_item_locations()
-
- return doc
-
-
-@frappe.whitelist()
-def make_in_transit_stock_entry(source_name: str, in_transit_warehouse: str):
- ste_doc = make_stock_entry(source_name)
- ste_doc.add_to_transit = 1
- ste_doc.to_warehouse = in_transit_warehouse
-
- for row in ste_doc.items:
- row.t_warehouse = in_transit_warehouse
-
- return ste_doc
diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py
index c25a6ecd62d..180ab3ae3bb 100644
--- a/erpnext/stock/doctype/material_request/test_material_request.py
+++ b/erpnext/stock/doctype/material_request/test_material_request.py
@@ -10,12 +10,14 @@ from frappe.utils import flt, today
from erpnext.controllers.accounts_controller import InvalidQtyError
from erpnext.stock.doctype.item.test_item import create_item
-from erpnext.stock.doctype.material_request.material_request import (
+from erpnext.stock.doctype.material_request.mapper import (
create_pick_list,
make_in_transit_stock_entry,
make_purchase_order,
make_stock_entry,
make_supplier_quotation,
+)
+from erpnext.stock.doctype.material_request.material_request import (
raise_work_orders,
)
from erpnext.stock.doctype.stock_entry.stock_entry import make_stock_in_entry
@@ -915,7 +917,7 @@ class TestMaterialRequest(ERPNextTestSuite):
for company, _mr_list in comapnywise_mr_list.items():
emails = get_email_list(company)
- self.assertTrue(comapnywise_users[company] in emails)
+ self.assertIn(comapnywise_users[company], emails)
for perm in permissions:
perm.delete()
@@ -980,7 +982,7 @@ class TestMaterialRequest(ERPNextTestSuite):
from frappe.utils import add_to_date, today
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
- from erpnext.selling.doctype.sales_order.sales_order import make_material_request
+ from erpnext.selling.doctype.sales_order.mapper import make_material_request
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
sub_item_a = "_Test Bundle ItemA"
@@ -1019,7 +1021,7 @@ class TestMaterialRequest(ERPNextTestSuite):
"""Test for pick list mapped doc qty from partially received Material Request Transfer"""
import json
- from erpnext.stock.doctype.pick_list.pick_list import create_stock_entry
+ from erpnext.stock.doctype.pick_list.mapper import create_stock_entry
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
new_item = create_item("_Test Pick List Item", is_stock_item=1)
diff --git a/erpnext/stock/doctype/packed_item/test_packed_item.py b/erpnext/stock/doctype/packed_item/test_packed_item.py
index e7b22d04033..8189343a820 100644
--- a/erpnext/stock/doctype/packed_item/test_packed_item.py
+++ b/erpnext/stock/doctype/packed_item/test_packed_item.py
@@ -5,7 +5,7 @@
import frappe
from frappe.utils import add_to_date, nowdate
-from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
+from erpnext.selling.doctype.sales_order.mapper import make_delivery_note
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries
@@ -190,7 +190,7 @@ class TestPackedItem(ERPNextTestSuite):
self.assertEqual(sent_item.qty, -1 * returned_item.qty)
def test_returning_full_bundles(self):
- from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
+ from erpnext.stock.doctype.delivery_note.mapper import make_sales_return
item_list = [
{
@@ -219,7 +219,7 @@ class TestPackedItem(ERPNextTestSuite):
self.assertReturns(dn.packed_items, dn_ret.packed_items)
def test_returning_partial_bundles(self):
- from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
+ from erpnext.stock.doctype.delivery_note.mapper import make_sales_return
item_list = [
{
@@ -256,7 +256,7 @@ class TestPackedItem(ERPNextTestSuite):
self.assertReturns(expected_returns, dn_ret.packed_items)
def test_returning_partial_bundle_qty(self):
- from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
+ from erpnext.stock.doctype.delivery_note.mapper import make_sales_return
so = make_sales_order(item_code=self.bundle, warehouse=self.warehouse, qty=2)
diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.js b/erpnext/stock/doctype/packing_slip/packing_slip.js
index 682631f1b74..45f84a39598 100644
--- a/erpnext/stock/doctype/packing_slip/packing_slip.js
+++ b/erpnext/stock/doctype/packing_slip/packing_slip.js
@@ -35,7 +35,7 @@ frappe.ui.form.on("Packing Slip", {
if (frm.doc.delivery_note) {
erpnext.utils.map_current_doc({
- method: "erpnext.stock.doctype.delivery_note.delivery_note.make_packing_slip",
+ method: "erpnext.stock.doctype.delivery_note.mapper.make_packing_slip",
source_name: frm.doc.delivery_note,
target_doc: frm,
freeze: true,
diff --git a/erpnext/stock/doctype/packing_slip/test_packing_slip.py b/erpnext/stock/doctype/packing_slip/test_packing_slip.py
index 19e6c976edc..55a51f847e3 100644
--- a/erpnext/stock/doctype/packing_slip/test_packing_slip.py
+++ b/erpnext/stock/doctype/packing_slip/test_packing_slip.py
@@ -5,7 +5,7 @@
import frappe
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
-from erpnext.stock.doctype.delivery_note.delivery_note import make_packing_slip
+from erpnext.stock.doctype.delivery_note.mapper import make_packing_slip
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.tests.utils import ERPNextTestSuite
diff --git a/erpnext/stock/doctype/pick_list/mapper.py b/erpnext/stock/doctype/pick_list/mapper.py
new file mode 100644
index 00000000000..cb7a1d7af22
--- /dev/null
+++ b/erpnext/stock/doctype/pick_list/mapper.py
@@ -0,0 +1,365 @@
+# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import json
+from itertools import groupby
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+from frappe.model.mapper import map_child_doc
+from frappe.utils import flt, get_link_to_form
+
+from erpnext.selling.doctype.sales_order.mapper import (
+ make_delivery_note as create_delivery_note_from_sales_order,
+)
+
+
+def validate_item_locations(pick_list):
+ if not pick_list.locations:
+ frappe.throw(_("Add items in the Item Locations table"))
+
+
+@frappe.whitelist()
+def create_delivery_note(source_name: str, target_doc: str | Document | None = None):
+ pick_list = frappe.get_doc("Pick List", source_name)
+ validate_item_locations(pick_list)
+ sales_dict = dict()
+ sales_orders = []
+ delivery_notes = []
+ for location in pick_list.locations:
+ if location.sales_order:
+ sales_orders.append(
+ frappe.db.get_value(
+ "Sales Order",
+ location.sales_order,
+ [
+ "customer",
+ "name as sales_order",
+ "company_address",
+ "dispatch_address_name",
+ "shipping_address_name",
+ "customer_address",
+ ],
+ as_dict=True,
+ )
+ )
+
+ group_key = lambda so: ( # noqa
+ so["customer"],
+ so["company_address"] or "",
+ so["dispatch_address_name"] or "",
+ so["shipping_address_name"] or "",
+ so["customer_address"] or "",
+ )
+ for key, rows in groupby(sorted(sales_orders, key=group_key), key=group_key):
+ sales_dict[key] = {row.sales_order for row in rows}
+
+ if sales_dict:
+ delivery_notes.extend(create_dn_with_so(sales_dict, pick_list))
+
+ if not all(item.sales_order for item in pick_list.locations):
+ delivery_notes.append(create_dn_wo_so(pick_list))
+
+ if len(delivery_notes) == 1:
+ return delivery_notes[0]
+ else:
+ from frappe.utils import comma_and
+
+ doc_list = [get_link_to_form("Delivery Note", p.name) for p in delivery_notes]
+ frappe.msgprint(_("{0} created").format(comma_and(doc_list)))
+
+
+def create_dn_wo_so(pick_list, delivery_note=None):
+ if not delivery_note:
+ delivery_note = frappe.new_doc("Delivery Note")
+
+ delivery_note.company = pick_list.company
+
+ item_table_mapper_without_so = {
+ "doctype": "Delivery Note Item",
+ "field_map": {
+ "rate": "rate",
+ "name": "name",
+ "parent": "",
+ },
+ }
+ map_pl_locations(pick_list, item_table_mapper_without_so, delivery_note)
+ delivery_note.flags.ignore_mandatory = True
+ delivery_note.save()
+
+ return delivery_note
+
+
+@frappe.whitelist()
+def create_dn_for_pick_lists(
+ source_name: str, target_doc: str | Document | None = None, kwargs: dict | str | None = None
+):
+ """Get Items from Multiple Pick Lists and create a Delivery Note for filtered customer"""
+ if kwargs is None:
+ kwargs = {}
+ if isinstance(kwargs, str):
+ kwargs = json.loads(kwargs)
+
+ pick_list = frappe.get_doc("Pick List", source_name)
+ validate_item_locations(pick_list)
+
+ sales_order_arg = kwargs.get("sales_order")
+ customer_arg = kwargs.get("customer")
+
+ if sales_order_arg:
+ sales_orders = {sales_order_arg}
+ else:
+ sales_orders = {row.sales_order for row in pick_list.locations if row.sales_order}
+
+ if customer_arg:
+ sales_orders = frappe.get_all(
+ "Sales Order",
+ filters={"customer": customer_arg, "name": ["in", list(sales_orders)]},
+ pluck="name",
+ )
+
+ delivery_note = create_dn_from_so(pick_list, sales_orders, delivery_note=target_doc, kwargs=kwargs)
+
+ if not sales_order_arg and not all(item.sales_order for item in pick_list.locations):
+ if isinstance(delivery_note, str):
+ delivery_note = frappe.get_doc(frappe.parse_json(delivery_note))
+
+ delivery_note = create_dn_wo_so(pick_list, delivery_note)
+
+ return delivery_note
+
+
+def create_dn_with_so(sales_dict, pick_list):
+ """Create Delivery Note for each customer (based on SO) in a Pick List."""
+ delivery_notes = []
+
+ for key in sales_dict:
+ delivery_note = create_dn_from_so(pick_list, sales_dict[key], None)
+ if delivery_note:
+ delivery_note.flags.ignore_mandatory = True
+ # updates packed_items on save
+ # save as multiple customers are possible
+ delivery_note.save()
+ delivery_notes.append(delivery_note)
+
+ return delivery_notes
+
+
+def create_dn_from_so(pick_list, sales_order_list, delivery_note=None, kwargs=None):
+ if not sales_order_list:
+ return delivery_note
+
+ def select_item(d):
+ filtered_items = kwargs.get("filtered_children", [])
+ child_filter = d.name in filtered_items if filtered_items else True
+ return child_filter
+
+ item_table_mapper = {
+ "doctype": "Delivery Note Item",
+ "field_map": {
+ "rate": "rate",
+ "name": "so_detail",
+ "parent": "against_sales_order",
+ },
+ "condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty)
+ and doc.delivered_by_supplier != 1
+ and select_item(doc),
+ }
+
+ kwargs = {"skip_item_mapping": True, "ignore_pricing_rule": pick_list.ignore_pricing_rule}
+
+ delivery_note = create_delivery_note_from_sales_order(
+ next(iter(sales_order_list)), delivery_note, kwargs=kwargs
+ )
+
+ if not delivery_note:
+ return
+
+ for so in sales_order_list:
+ map_pl_locations(pick_list, item_table_mapper, delivery_note, so)
+
+ return delivery_note
+
+
+def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None):
+ for location in pick_list.locations:
+ if location.sales_order != sales_order or location.product_bundle_item:
+ continue
+
+ if location.sales_order_item:
+ sales_order_item = frappe.get_doc("Sales Order Item", location.sales_order_item)
+ else:
+ sales_order_item = None
+
+ source_doc = sales_order_item or location
+
+ dn_item = map_child_doc(source_doc, delivery_note, item_mapper)
+
+ if dn_item:
+ dn_item.against_pick_list = pick_list.name
+ dn_item.pick_list_item = location.name
+ dn_item.warehouse = location.warehouse
+ dn_item.qty = flt(location.picked_qty - location.delivered_qty) / (
+ flt(dn_item.conversion_factor) or 1
+ )
+ dn_item.batch_no = location.batch_no
+ dn_item.serial_no = location.serial_no
+ dn_item.use_serial_batch_fields = location.use_serial_batch_fields
+
+ update_delivery_note_item(source_doc, dn_item, delivery_note)
+
+ add_product_bundles_to_delivery_note(pick_list, delivery_note, item_mapper, sales_order)
+ set_delivery_note_missing_values(delivery_note)
+
+ delivery_note.company = pick_list.company
+ if sales_order:
+ delivery_note.customer = frappe.get_value("Sales Order", sales_order, "customer")
+
+
+def add_product_bundles_to_delivery_note(pick_list, delivery_note, item_mapper, sales_order=None) -> None:
+ """Add product bundles found in pick list to delivery note.
+
+ When mapping pick list items, the bundle item itself isn't part of the
+ locations. Dynamically fetch and add parent bundle item into DN."""
+ product_bundles = pick_list._get_product_bundles()
+ product_bundle_qty_map = pick_list._get_product_bundle_qty_map(product_bundles.values())
+
+ for so_row, value in product_bundles.items():
+ sales_order_item = frappe.get_doc("Sales Order Item", so_row)
+ if sales_order and sales_order_item.parent != sales_order:
+ continue
+
+ dn_bundle_item = map_child_doc(sales_order_item, delivery_note, item_mapper)
+ dn_bundle_item.qty = pick_list._compute_picked_qty_for_bundle(
+ so_row, product_bundle_qty_map[value.item_code]
+ )
+ dn_bundle_item.pick_list_item = value.pick_list_item
+ dn_bundle_item.against_pick_list = pick_list.name
+ update_delivery_note_item(sales_order_item, dn_bundle_item, delivery_note)
+
+
+@frappe.whitelist()
+def create_stock_entry(pick_list: str):
+ pick_list = frappe.get_doc(json.loads(pick_list))
+ validate_item_locations(pick_list)
+
+ if stock_entry_exists(pick_list.get("name")):
+ return frappe.msgprint(_("Stock Entry has been already created against this Pick List"))
+
+ stock_entry = frappe.new_doc("Stock Entry")
+ stock_entry.pick_list = pick_list.get("name")
+ stock_entry.purpose = pick_list.get("purpose")
+ stock_entry.company = pick_list.get("company")
+ stock_entry.set_stock_entry_type()
+
+ if pick_list.get("work_order"):
+ stock_entry = update_stock_entry_based_on_work_order(pick_list, stock_entry)
+ elif pick_list.get("material_request"):
+ stock_entry = update_stock_entry_based_on_material_request(pick_list, stock_entry)
+ else:
+ stock_entry = update_stock_entry_items_with_no_reference(pick_list, stock_entry)
+
+ stock_entry.set_missing_values()
+
+ return stock_entry.as_dict()
+
+
+def update_delivery_note_item(source, target, delivery_note):
+ cost_center = frappe.db.get_value("Project", delivery_note.project, "cost_center")
+ if not cost_center:
+ cost_center = get_cost_center(source.item_code, "Item", delivery_note.company)
+
+ if not cost_center:
+ cost_center = get_cost_center(source.item_group, "Item Group", delivery_note.company)
+
+ target.cost_center = cost_center
+
+
+def get_cost_center(for_item, from_doctype, company):
+ """Returns Cost Center for Item or Item Group"""
+ return frappe.db.get_value(
+ "Item Default",
+ fieldname=["buying_cost_center"],
+ filters={"parent": for_item, "parenttype": from_doctype, "company": company},
+ )
+
+
+def set_delivery_note_missing_values(target):
+ target.run_method("set_missing_values")
+ target.run_method("set_po_nos")
+ target.run_method("calculate_taxes_and_totals")
+
+
+def stock_entry_exists(pick_list_name):
+ return frappe.db.exists("Stock Entry", {"pick_list": pick_list_name})
+
+
+def update_stock_entry_based_on_work_order(pick_list, stock_entry):
+ work_order = frappe.get_doc("Work Order", pick_list.get("work_order"))
+
+ stock_entry.work_order = work_order.name
+ stock_entry.company = work_order.company
+ stock_entry.from_bom = 1
+ stock_entry.bom_no = work_order.bom_no
+ stock_entry.use_multi_level_bom = work_order.use_multi_level_bom
+ stock_entry.fg_completed_qty = pick_list.for_qty
+ if work_order.bom_no:
+ stock_entry.inspection_required = frappe.db.get_value("BOM", work_order.bom_no, "inspection_required")
+
+ is_wip_warehouse_group = frappe.db.get_value("Warehouse", work_order.wip_warehouse, "is_group")
+ if not (is_wip_warehouse_group and work_order.skip_transfer):
+ wip_warehouse = work_order.wip_warehouse
+ else:
+ wip_warehouse = None
+ stock_entry.to_warehouse = wip_warehouse
+
+ stock_entry.project = work_order.project
+
+ for location in pick_list.locations:
+ item = frappe._dict()
+ update_common_item_properties(item, location)
+ item.t_warehouse = wip_warehouse
+
+ stock_entry.append("items", item)
+
+ return stock_entry
+
+
+def update_stock_entry_based_on_material_request(pick_list, stock_entry):
+ for location in pick_list.locations:
+ target_warehouse = None
+ if location.material_request_item:
+ target_warehouse = frappe.get_value(
+ "Material Request Item", location.material_request_item, "warehouse"
+ )
+ item = frappe._dict()
+ update_common_item_properties(item, location)
+ item.t_warehouse = target_warehouse
+ stock_entry.append("items", item)
+
+ return stock_entry
+
+
+def update_stock_entry_items_with_no_reference(pick_list, stock_entry):
+ for location in pick_list.locations:
+ item = frappe._dict()
+ update_common_item_properties(item, location)
+
+ stock_entry.append("items", item)
+
+ return stock_entry
+
+
+def update_common_item_properties(item, location):
+ item.item_code = location.item_code
+ item.s_warehouse = location.warehouse
+ item.transfer_qty = location.picked_qty
+ item.qty = flt(location.picked_qty / (location.conversion_factor or 1), location.precision("qty"))
+ item.uom = location.uom
+ item.conversion_factor = location.conversion_factor
+ item.stock_uom = location.stock_uom
+ item.material_request = location.material_request
+ item.serial_no = location.serial_no
+ item.batch_no = location.batch_no
+ item.material_request_item = location.material_request_item
diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js
index 750466a4a40..ee83a303791 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.js
+++ b/erpnext/stock/doctype/pick_list/pick_list.js
@@ -211,7 +211,7 @@ frappe.ui.form.on("Pick List", {
}
frm.clear_table("locations");
erpnext.utils.map_current_doc({
- method: "erpnext.manufacturing.doctype.work_order.work_order.create_pick_list",
+ method: "erpnext.manufacturing.doctype.work_order.mapper.create_pick_list",
target: frm,
source_name: frm.doc.work_order,
});
@@ -223,7 +223,7 @@ frappe.ui.form.on("Pick List", {
},
material_request: (frm) => {
erpnext.utils.map_current_doc({
- method: "erpnext.stock.doctype.material_request.material_request.create_pick_list",
+ method: "erpnext.stock.doctype.material_request.mapper.create_pick_list",
target: frm,
source_name: frm.doc.material_request,
});
@@ -234,13 +234,13 @@ frappe.ui.form.on("Pick List", {
},
create_delivery_note: (frm) => {
frappe.model.open_mapped_doc({
- method: "erpnext.stock.doctype.pick_list.pick_list.create_delivery_note",
+ method: "erpnext.stock.doctype.pick_list.mapper.create_delivery_note",
frm: frm,
});
},
create_stock_entry: (frm) => {
frappe
- .xcall("erpnext.stock.doctype.pick_list.pick_list.create_stock_entry", {
+ .xcall("erpnext.stock.doctype.pick_list.mapper.create_stock_entry", {
pick_list: frm.doc,
})
.then((stock_entry) => {
@@ -262,7 +262,7 @@ frappe.ui.form.on("Pick List", {
};
frm.get_items_btn = frm.add_custom_button(__("Get Items"), () => {
erpnext.utils.map_current_doc({
- method: "erpnext.selling.doctype.sales_order.sales_order.create_pick_list",
+ method: "erpnext.selling.doctype.sales_order.mapper.create_pick_list",
source_doctype: "Sales Order",
target: frm,
setters: {
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 7f6a6421c69..535a20ec0f3 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -3,22 +3,17 @@
import json
from collections import OrderedDict, defaultdict
-from itertools import groupby
from typing import Any
import frappe
from frappe import _, bold
from frappe.model.document import Document
-from frappe.model.mapper import map_child_doc
from frappe.query_builder import Case
from frappe.query_builder.custom import GROUP_CONCAT
from frappe.query_builder.functions import Coalesce, Locate, Replace, Sum
from frappe.utils import cint, floor, flt, get_link_to_form
from frappe.utils.nestedset import get_descendants_of
-from erpnext.selling.doctype.sales_order.sales_order import (
- make_delivery_note as create_delivery_note_from_sales_order,
-)
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
get_auto_batch_nos,
)
@@ -30,6 +25,10 @@ from erpnext.stock.serial_batch_bundle import (
)
from erpnext.utilities.transaction_base import TransactionBase
+from .mapper import (
+ stock_entry_exists,
+)
+
class MissingWarehouseValidationError(frappe.ValidationError):
pass
@@ -931,11 +930,6 @@ def get_picked_items_qty(items, contains_packed_items=False) -> list[dict]:
return query.run(as_dict=True)
-def validate_item_locations(pick_list):
- if not pick_list.locations:
- frappe.throw(_("Add items in the Item Locations table"))
-
-
def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus):
available_locations = item_location_map.get(item_doc.item_code)
locations = []
@@ -1284,253 +1278,6 @@ def get_available_item_locations_for_other_item(
return item_locations
-@frappe.whitelist()
-def create_delivery_note(source_name: str, target_doc: str | Document | None = None):
- pick_list = frappe.get_doc("Pick List", source_name)
- validate_item_locations(pick_list)
- sales_dict = dict()
- sales_orders = []
- delivery_notes = []
- for location in pick_list.locations:
- if location.sales_order:
- sales_orders.append(
- frappe.db.get_value(
- "Sales Order",
- location.sales_order,
- [
- "customer",
- "name as sales_order",
- "company_address",
- "dispatch_address_name",
- "shipping_address_name",
- "customer_address",
- ],
- as_dict=True,
- )
- )
-
- group_key = lambda so: ( # noqa
- so["customer"],
- so["company_address"] or "",
- so["dispatch_address_name"] or "",
- so["shipping_address_name"] or "",
- so["customer_address"] or "",
- )
- for key, rows in groupby(sorted(sales_orders, key=group_key), key=group_key):
- sales_dict[key] = {row.sales_order for row in rows}
-
- if sales_dict:
- delivery_notes.extend(create_dn_with_so(sales_dict, pick_list))
-
- if not all(item.sales_order for item in pick_list.locations):
- delivery_notes.append(create_dn_wo_so(pick_list))
-
- if len(delivery_notes) == 1:
- return delivery_notes[0]
- else:
- from frappe.utils import comma_and
-
- doc_list = [get_link_to_form("Delivery Note", p.name) for p in delivery_notes]
- frappe.msgprint(_("{0} created").format(comma_and(doc_list)))
-
-
-def create_dn_wo_so(pick_list, delivery_note=None):
- if not delivery_note:
- delivery_note = frappe.new_doc("Delivery Note")
-
- delivery_note.company = pick_list.company
-
- item_table_mapper_without_so = {
- "doctype": "Delivery Note Item",
- "field_map": {
- "rate": "rate",
- "name": "name",
- "parent": "",
- },
- }
- map_pl_locations(pick_list, item_table_mapper_without_so, delivery_note)
- delivery_note.flags.ignore_mandatory = True
- delivery_note.save()
-
- return delivery_note
-
-
-@frappe.whitelist()
-def create_dn_for_pick_lists(
- source_name: str, target_doc: str | Document | None = None, kwargs: dict | str | None = None
-):
- """Get Items from Multiple Pick Lists and create a Delivery Note for filtered customer"""
- if kwargs is None:
- kwargs = {}
- if isinstance(kwargs, str):
- kwargs = json.loads(kwargs)
-
- pick_list = frappe.get_doc("Pick List", source_name)
- validate_item_locations(pick_list)
-
- sales_order_arg = kwargs.get("sales_order")
- customer_arg = kwargs.get("customer")
-
- if sales_order_arg:
- sales_orders = {sales_order_arg}
- else:
- sales_orders = {row.sales_order for row in pick_list.locations if row.sales_order}
-
- if customer_arg:
- sales_orders = frappe.get_all(
- "Sales Order",
- filters={"customer": customer_arg, "name": ["in", list(sales_orders)]},
- pluck="name",
- )
-
- delivery_note = create_dn_from_so(pick_list, sales_orders, delivery_note=target_doc, kwargs=kwargs)
-
- if not sales_order_arg and not all(item.sales_order for item in pick_list.locations):
- if isinstance(delivery_note, str):
- delivery_note = frappe.get_doc(frappe.parse_json(delivery_note))
-
- delivery_note = create_dn_wo_so(pick_list, delivery_note)
-
- return delivery_note
-
-
-def create_dn_with_so(sales_dict, pick_list):
- """Create Delivery Note for each customer (based on SO) in a Pick List."""
- delivery_notes = []
-
- for key in sales_dict:
- delivery_note = create_dn_from_so(pick_list, sales_dict[key], None)
- if delivery_note:
- delivery_note.flags.ignore_mandatory = True
- # updates packed_items on save
- # save as multiple customers are possible
- delivery_note.save()
- delivery_notes.append(delivery_note)
-
- return delivery_notes
-
-
-def create_dn_from_so(pick_list, sales_order_list, delivery_note=None, kwargs=None):
- if not sales_order_list:
- return delivery_note
-
- def select_item(d):
- filtered_items = kwargs.get("filtered_children", [])
- child_filter = d.name in filtered_items if filtered_items else True
- return child_filter
-
- item_table_mapper = {
- "doctype": "Delivery Note Item",
- "field_map": {
- "rate": "rate",
- "name": "so_detail",
- "parent": "against_sales_order",
- },
- "condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty)
- and doc.delivered_by_supplier != 1
- and select_item(doc),
- }
-
- kwargs = {"skip_item_mapping": True, "ignore_pricing_rule": pick_list.ignore_pricing_rule}
-
- delivery_note = create_delivery_note_from_sales_order(
- next(iter(sales_order_list)), delivery_note, kwargs=kwargs
- )
-
- if not delivery_note:
- return
-
- for so in sales_order_list:
- map_pl_locations(pick_list, item_table_mapper, delivery_note, so)
-
- return delivery_note
-
-
-def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None):
- for location in pick_list.locations:
- if location.sales_order != sales_order or location.product_bundle_item:
- continue
-
- if location.sales_order_item:
- sales_order_item = frappe.get_doc("Sales Order Item", location.sales_order_item)
- else:
- sales_order_item = None
-
- source_doc = sales_order_item or location
-
- dn_item = map_child_doc(source_doc, delivery_note, item_mapper)
-
- if dn_item:
- dn_item.against_pick_list = pick_list.name
- dn_item.pick_list_item = location.name
- dn_item.warehouse = location.warehouse
- dn_item.qty = flt(location.picked_qty - location.delivered_qty) / (
- flt(dn_item.conversion_factor) or 1
- )
- dn_item.batch_no = location.batch_no
- dn_item.serial_no = location.serial_no
- dn_item.use_serial_batch_fields = location.use_serial_batch_fields
-
- update_delivery_note_item(source_doc, dn_item, delivery_note)
-
- add_product_bundles_to_delivery_note(pick_list, delivery_note, item_mapper, sales_order)
- set_delivery_note_missing_values(delivery_note)
-
- delivery_note.company = pick_list.company
- if sales_order:
- delivery_note.customer = frappe.get_value("Sales Order", sales_order, "customer")
-
-
-def add_product_bundles_to_delivery_note(
- pick_list: "PickList", delivery_note, item_mapper, sales_order=None
-) -> None:
- """Add product bundles found in pick list to delivery note.
-
- When mapping pick list items, the bundle item itself isn't part of the
- locations. Dynamically fetch and add parent bundle item into DN."""
- product_bundles = pick_list._get_product_bundles()
- product_bundle_qty_map = pick_list._get_product_bundle_qty_map(product_bundles.values())
-
- for so_row, value in product_bundles.items():
- sales_order_item = frappe.get_doc("Sales Order Item", so_row)
- if sales_order and sales_order_item.parent != sales_order:
- continue
-
- dn_bundle_item = map_child_doc(sales_order_item, delivery_note, item_mapper)
- dn_bundle_item.qty = pick_list._compute_picked_qty_for_bundle(
- so_row, product_bundle_qty_map[value.item_code]
- )
- dn_bundle_item.pick_list_item = value.pick_list_item
- dn_bundle_item.against_pick_list = pick_list.name
- update_delivery_note_item(sales_order_item, dn_bundle_item, delivery_note)
-
-
-@frappe.whitelist()
-def create_stock_entry(pick_list: str):
- pick_list = frappe.get_doc(json.loads(pick_list))
- validate_item_locations(pick_list)
-
- if stock_entry_exists(pick_list.get("name")):
- return frappe.msgprint(_("Stock Entry has been already created against this Pick List"))
-
- stock_entry = frappe.new_doc("Stock Entry")
- stock_entry.pick_list = pick_list.get("name")
- stock_entry.purpose = pick_list.get("purpose")
- stock_entry.company = pick_list.get("company")
- stock_entry.set_stock_entry_type()
-
- if pick_list.get("work_order"):
- stock_entry = update_stock_entry_based_on_work_order(pick_list, stock_entry)
- elif pick_list.get("material_request"):
- stock_entry = update_stock_entry_based_on_material_request(pick_list, stock_entry)
- else:
- stock_entry = update_stock_entry_items_with_no_reference(pick_list, stock_entry)
-
- stock_entry.set_missing_values()
-
- return stock_entry.as_dict()
-
-
@frappe.whitelist()
def get_pending_work_orders(
doctype: Any,
@@ -1585,106 +1332,6 @@ def get_actual_qty(item_code, warehouse):
)
-def update_delivery_note_item(source, target, delivery_note):
- cost_center = frappe.db.get_value("Project", delivery_note.project, "cost_center")
- if not cost_center:
- cost_center = get_cost_center(source.item_code, "Item", delivery_note.company)
-
- if not cost_center:
- cost_center = get_cost_center(source.item_group, "Item Group", delivery_note.company)
-
- target.cost_center = cost_center
-
-
-def get_cost_center(for_item, from_doctype, company):
- """Returns Cost Center for Item or Item Group"""
- return frappe.db.get_value(
- "Item Default",
- fieldname=["buying_cost_center"],
- filters={"parent": for_item, "parenttype": from_doctype, "company": company},
- )
-
-
-def set_delivery_note_missing_values(target):
- target.run_method("set_missing_values")
- target.run_method("set_po_nos")
- target.run_method("calculate_taxes_and_totals")
-
-
-def stock_entry_exists(pick_list_name):
- return frappe.db.exists("Stock Entry", {"pick_list": pick_list_name})
-
-
-def update_stock_entry_based_on_work_order(pick_list, stock_entry):
- work_order = frappe.get_doc("Work Order", pick_list.get("work_order"))
-
- stock_entry.work_order = work_order.name
- stock_entry.company = work_order.company
- stock_entry.from_bom = 1
- stock_entry.bom_no = work_order.bom_no
- stock_entry.use_multi_level_bom = work_order.use_multi_level_bom
- stock_entry.fg_completed_qty = pick_list.for_qty
- if work_order.bom_no:
- stock_entry.inspection_required = frappe.db.get_value("BOM", work_order.bom_no, "inspection_required")
-
- is_wip_warehouse_group = frappe.db.get_value("Warehouse", work_order.wip_warehouse, "is_group")
- if not (is_wip_warehouse_group and work_order.skip_transfer):
- wip_warehouse = work_order.wip_warehouse
- else:
- wip_warehouse = None
- stock_entry.to_warehouse = wip_warehouse
-
- stock_entry.project = work_order.project
-
- for location in pick_list.locations:
- item = frappe._dict()
- update_common_item_properties(item, location)
- item.t_warehouse = wip_warehouse
-
- stock_entry.append("items", item)
-
- return stock_entry
-
-
-def update_stock_entry_based_on_material_request(pick_list, stock_entry):
- for location in pick_list.locations:
- target_warehouse = None
- if location.material_request_item:
- target_warehouse = frappe.get_value(
- "Material Request Item", location.material_request_item, "warehouse"
- )
- item = frappe._dict()
- update_common_item_properties(item, location)
- item.t_warehouse = target_warehouse
- stock_entry.append("items", item)
-
- return stock_entry
-
-
-def update_stock_entry_items_with_no_reference(pick_list, stock_entry):
- for location in pick_list.locations:
- item = frappe._dict()
- update_common_item_properties(item, location)
-
- stock_entry.append("items", item)
-
- return stock_entry
-
-
-def update_common_item_properties(item, location):
- item.item_code = location.item_code
- item.s_warehouse = location.warehouse
- item.transfer_qty = location.picked_qty
- item.qty = flt(location.picked_qty / (location.conversion_factor or 1), location.precision("qty"))
- item.uom = location.uom
- item.conversion_factor = location.conversion_factor
- item.stock_uom = location.stock_uom
- item.material_request = location.material_request
- item.serial_no = location.serial_no
- item.batch_no = location.batch_no
- item.material_request_item = location.material_request_item
-
-
def get_rejected_warehouses():
if not hasattr(frappe.local, "rejected_warehouses"):
frappe.local.rejected_warehouses = []
diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py
index 85a45f1686b..d6f15dffddc 100644
--- a/erpnext/stock/doctype/pick_list/test_pick_list.py
+++ b/erpnext/stock/doctype/pick_list/test_pick_list.py
@@ -5,11 +5,11 @@ import frappe
from frappe import _dict
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
-from erpnext.selling.doctype.sales_order.sales_order import create_pick_list
+from erpnext.selling.doctype.sales_order.mapper import create_pick_list
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.stock.doctype.item.test_item import create_item, make_item
from erpnext.stock.doctype.packed_item.test_packed_item import create_product_bundle
-from erpnext.stock.doctype.pick_list.pick_list import create_delivery_note, create_dn_for_pick_lists
+from erpnext.stock.doctype.pick_list.mapper import create_delivery_note, create_dn_for_pick_lists
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
get_batch_from_bundle,
@@ -876,7 +876,7 @@ class TestPickList(ERPNextTestSuite):
)
for d in data:
- self.assertTrue(d.batch_no in ["PICKLT-000001", "PICKLT-000002"])
+ self.assertIn(d.batch_no, ["PICKLT-000001", "PICKLT-000002"])
if d.batch_no == "PICKLT-000001":
self.assertEqual(d.qty, 5.0 * -1)
elif d.batch_no == "PICKLT-000002":
@@ -927,7 +927,7 @@ class TestPickList(ERPNextTestSuite):
self.assertEqual(len(data), 10)
for d in data:
- self.assertTrue(d.serial_no not in picked_serial_nos)
+ self.assertNotIn(d.serial_no, picked_serial_nos)
pl1.cancel()
pl.cancel()
@@ -1052,7 +1052,8 @@ class TestPickList(ERPNextTestSuite):
def test_pick_list_warehouse_for_work_order(self):
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
- from erpnext.manufacturing.doctype.work_order.work_order import create_pick_list, make_work_order
+ from erpnext.manufacturing.doctype.work_order.mapper import create_pick_list
+ from erpnext.manufacturing.doctype.work_order.work_order import make_work_order
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
# Create Warehouses for Work Order
@@ -1311,7 +1312,7 @@ class TestPickList(ERPNextTestSuite):
self.assertEqual(len(new_serial_nos), 110)
for sn in serial_nos:
- self.assertFalse(sn in new_serial_nos)
+ self.assertNotIn(sn, new_serial_nos)
pl1.submit()
@@ -1536,7 +1537,7 @@ class TestPickList(ERPNextTestSuite):
@ERPNextTestSuite.change_settings("Selling Settings", {"allow_multiple_items": 1})
def test_multiple_pick_lists_delivery_note(self):
- from erpnext.stock.doctype.pick_list.pick_list import create_dn_for_pick_lists
+ from erpnext.stock.doctype.pick_list.mapper import create_dn_for_pick_lists
item_code = make_item().name
warehouse = "_Test Warehouse - _TC"
@@ -1745,7 +1746,7 @@ class TestPickList(ERPNextTestSuite):
pick_list = frappe.new_doc("Pick List")
map_docs(
- "erpnext.selling.doctype.sales_order.sales_order.create_pick_list",
+ "erpnext.selling.doctype.sales_order.mapper.create_pick_list",
dumps([sales_order1.name, sales_order2.name, sales_order3.name]),
pick_list,
)
@@ -1765,5 +1766,5 @@ class TestPickList(ERPNextTestSuite):
else:
self.assertEqual(doc.shipping_address_name, customer_shipping_address_1.name)
item_codes = [item.item_code for item in doc.items]
- self.assertTrue(item1 in item_codes)
- self.assertTrue(item2 in item_codes)
+ self.assertIn(item1, item_codes)
+ self.assertIn(item2, item_codes)
diff --git a/erpnext/stock/doctype/purchase_receipt/mapper.py b/erpnext/stock/doctype/purchase_receipt/mapper.py
new file mode 100644
index 00000000000..9ea7371554a
--- /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.mapper 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.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
index 4e959229e15..6524bd30265 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
@@ -45,7 +45,7 @@ frappe.ui.form.on("Purchase Receipt", {
__("Debit Note"),
function () {
frappe.model.open_mapped_doc({
- method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_purchase_invoice",
+ method: "erpnext.stock.doctype.purchase_receipt.mapper.make_purchase_invoice",
frm: cur_frm,
});
},
@@ -59,7 +59,7 @@ frappe.ui.form.on("Purchase Receipt", {
__("Delivery Note"),
function () {
frappe.model.open_mapped_doc({
- method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_inter_company_delivery_note",
+ method: "erpnext.stock.doctype.purchase_receipt.mapper.make_inter_company_delivery_note",
frm: cur_frm,
});
},
@@ -124,7 +124,7 @@ frappe.ui.form.on("Purchase Receipt", {
});
}
erpnext.utils.map_current_doc({
- method: "erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_purchase_receipt",
+ method: "erpnext.accounts.doctype.purchase_invoice.mapper.make_purchase_receipt",
source_doctype: "Purchase Invoice",
target: frm,
setters: {
@@ -223,7 +223,7 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend
});
}
erpnext.utils.map_current_doc({
- method: "erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_receipt",
+ method: "erpnext.buying.doctype.purchase_order.mapper.make_purchase_receipt",
source_doctype: "Purchase Order",
target: me.frm,
setters: {
@@ -282,7 +282,7 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend
make_purchase_invoice() {
frappe.model.open_mapped_doc({
- method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_purchase_invoice",
+ method: "erpnext.stock.doctype.purchase_receipt.mapper.make_purchase_invoice",
frm: cur_frm,
});
}
@@ -309,7 +309,7 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend
function (values) {
if (values.return_for_rejected_warehouse) {
frappe.call({
- method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_purchase_return_against_rejected_warehouse",
+ method: "erpnext.stock.doctype.purchase_receipt.mapper.make_purchase_return_against_rejected_warehouse",
args: {
source_name: cur_frm.doc.name,
},
@@ -439,14 +439,14 @@ frappe.ui.form.on("Purchase Receipt Item", {
cur_frm.cscript._make_purchase_return = function () {
frappe.model.open_mapped_doc({
- method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_purchase_return",
+ method: "erpnext.stock.doctype.purchase_receipt.mapper.make_purchase_return",
frm: cur_frm,
});
};
cur_frm.cscript["Make Stock Entry"] = function () {
frappe.model.open_mapped_doc({
- method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_stock_entry",
+ method: "erpnext.stock.doctype.purchase_receipt.mapper.make_stock_entry",
frm: cur_frm,
});
};
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 30afd561482..0ec7cfbac16 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -2,30 +2,19 @@
# 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
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,
-)
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
@@ -265,7 +254,7 @@ class PurchaseReceipt(BuyingController):
self.validate_cwip_accounts()
self.validate_provisional_expense_account()
- self.check_on_hold_or_closed_status()
+ self.check_for_on_hold_or_closed_status("Purchase Order", "purchase_order")
if getdate(self.posting_date) > getdate(nowdate()):
throw(_("Posting Date cannot be future date"))
@@ -373,14 +362,6 @@ class PurchaseReceipt(BuyingController):
po_qty, po_warehouse = frappe.db.get_value("Purchase Order Item", po_detail, ["qty", "warehouse"])
return po_qty, po_warehouse
- # Check for Closed status
- def check_on_hold_or_closed_status(self):
- check_list = []
- for d in self.get("items"):
- if d.meta.get_field("purchase_order") and d.purchase_order and d.purchase_order not in check_list:
- check_list.append(d.purchase_order)
- check_on_hold_or_closed_status("Purchase Order", d.purchase_order)
-
# on submit
def on_submit(self):
super().on_submit()
@@ -456,7 +437,7 @@ class PurchaseReceipt(BuyingController):
def on_cancel(self):
super().on_cancel()
- self.check_on_hold_or_closed_status()
+ self.check_for_on_hold_or_closed_status("Purchase Order", "purchase_order")
# Check if Purchase Invoice has been submitted against current Purchase Order
submitted = frappe.db.sql(
"""select t1.name
@@ -494,350 +475,11 @@ class PurchaseReceipt(BuyingController):
item.amount_difference_with_purchase_invoice = 0
def get_gl_entries(self, inventory_account_map=None, via_landed_cost_voucher=False):
- from erpnext.accounts.general_ledger import process_gl_map
-
- gl_entries = []
-
- self.make_item_gl_entries(gl_entries, inventory_account_map=inventory_account_map)
- self.make_tax_gl_entries(gl_entries, via_landed_cost_voucher)
- self.set_gl_entry_for_purchase_expense(gl_entries)
- update_regional_gl_entries(gl_entries, self)
-
- return process_gl_map(gl_entries, from_repost=frappe.flags.through_repost_item_valuation)
-
- def make_item_gl_entries(self, gl_entries, inventory_account_map=None):
- from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import (
- get_purchase_document_details,
+ from erpnext.stock.doctype.purchase_receipt.services.gl_composer import (
+ PurchaseReceiptGLComposer,
)
- provisional_accounting_for_non_stock_items = cint(
- frappe.db.get_value("Company", self.company, "enable_provisional_accounting_for_non_stock_items")
- )
-
- exchange_rate_map, net_rate_map = get_purchase_document_details(self)
-
- def validate_account(account_type):
- frappe.throw(_("{0} account not found while submitting purchase receipt").format(account_type))
-
- def make_item_asset_inward_gl_entry(item, stock_value_diff, stock_asset_account_name):
- account_currency = get_account_currency(stock_asset_account_name)
-
- if not stock_asset_account_name:
- validate_account("Asset or warehouse account")
-
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=stock_asset_account_name,
- cost_center=d.cost_center,
- debit=stock_value_diff,
- credit=0.0,
- remarks=remarks,
- against_account=stock_asset_rbnb,
- account_currency=account_currency,
- item=item,
- )
-
- def make_stock_received_but_not_billed_entry(item):
- if (
- self.get("is_return")
- and item.return_qty_from_rejected_warehouse
- and not frappe.db.get_single_value(
- "Buying Settings", "set_valuation_rate_for_rejected_materials"
- )
- ):
- return 0.0
-
- account = stock_asset_rbnb
- if item.from_warehouse:
- _inv_dict = self.get_inventory_account_dict(item, inventory_account_map, "from_warehouse")
- account = _inv_dict["account"]
-
- account_currency = get_account_currency(account)
-
- # GL Entry for from warehouse or Stock Received but not billed
- # Intentionally passed negative debit amount to avoid incorrect GL Entry validation
- credit_amount = (
- flt(item.base_net_amount, item.precision("base_net_amount"))
- if account_currency == self.company_currency
- else flt(item.net_amount, item.precision("net_amount"))
- )
-
- outgoing_amount = item.base_net_amount
- if self.is_internal_transfer() and item.valuation_rate:
- outgoing_amount = abs(get_stock_value_difference(self.name, item.name, item.from_warehouse))
- credit_amount = outgoing_amount
-
- if item.get("rejected_qty") and frappe.db.get_single_value(
- "Buying Settings", "set_valuation_rate_for_rejected_materials"
- ):
- outgoing_amount += get_stock_value_difference(self.name, item.name, item.rejected_warehouse)
- credit_amount = outgoing_amount
-
- if credit_amount:
- if not account:
- validate_account("Stock or Asset Received But Not Billed")
-
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=account,
- cost_center=item.cost_center,
- debit=-1 * flt(outgoing_amount, item.precision("base_net_amount")),
- credit=0.0,
- remarks=remarks,
- against_account=stock_asset_account_name,
- debit_in_account_currency=-1 * flt(outgoing_amount, item.precision("base_net_amount")),
- account_currency=account_currency,
- item=item,
- )
-
- # check if the exchange rate has changed
- if d.get("purchase_invoice"):
- if (
- exchange_rate_map[item.purchase_invoice]
- and self.conversion_rate != exchange_rate_map[item.purchase_invoice]
- and item.net_rate == net_rate_map[item.purchase_invoice_item]
- ):
- discrepancy_caused_by_exchange_rate_difference = (item.qty * item.net_rate) * (
- exchange_rate_map[item.purchase_invoice] - self.conversion_rate
- )
-
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=account,
- cost_center=item.cost_center,
- debit=0.0,
- credit=discrepancy_caused_by_exchange_rate_difference,
- remarks=remarks,
- against_account=self.supplier,
- debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
- account_currency=account_currency,
- item=item,
- )
-
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=self.get_company_default("exchange_gain_loss_account"),
- cost_center=d.cost_center,
- debit=discrepancy_caused_by_exchange_rate_difference,
- credit=0.0,
- remarks=remarks,
- against_account=self.supplier,
- debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
- account_currency=account_currency,
- item=item,
- )
-
- return outgoing_amount
-
- def make_landed_cost_gl_entries(item):
- # Amount added through landed-cost-voucher
- if item.landed_cost_voucher_amount and landed_cost_entries:
- if (item.item_code, item.name) in landed_cost_entries:
- for account, amount in landed_cost_entries[(item.item_code, item.name)].items():
- account_currency = get_account_currency(account)
- credit_amount = (
- flt(amount["base_amount"])
- if (amount["base_amount"] or account_currency != self.company_currency)
- else flt(amount["amount"])
- )
-
- if not account:
- validate_account("Landed Cost Account")
-
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=account,
- cost_center=item.cost_center,
- debit=0.0,
- credit=credit_amount,
- remarks=remarks,
- against_account=stock_asset_account_name,
- credit_in_account_currency=flt(amount["amount"]),
- account_currency=account_currency,
- project=item.project,
- item=item,
- )
-
- def make_amount_difference_entry(item):
- if item.amount_difference_with_purchase_invoice and stock_asset_rbnb:
- account_currency = get_account_currency(stock_asset_rbnb)
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=stock_asset_rbnb,
- cost_center=item.cost_center,
- debit=0.0,
- credit=flt(item.amount_difference_with_purchase_invoice),
- remarks=_("Adjustment based on Purchase Invoice rate"),
- against_account=stock_asset_account_name,
- account_currency=account_currency,
- project=item.project,
- item=item,
- )
-
- def make_sub_contracting_gl_entries(item):
- # sub-contracting warehouse
- if flt(item.rm_supp_cost) and supplier_warehouse_account:
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=supplier_warehouse_account,
- cost_center=item.cost_center,
- debit=0.0,
- credit=flt(item.rm_supp_cost),
- remarks=remarks,
- against_account=stock_asset_account_name,
- account_currency=supplier_warehouse_account_currency,
- item=item,
- )
-
- def make_divisional_loss_gl_entry(item, outgoing_amount):
- if item.is_fixed_asset:
- return
-
- # divisional loss adjustment
- valuation_amount_as_per_doc = (
- flt(outgoing_amount, d.precision("base_net_amount"))
- + flt(item.landed_cost_voucher_amount)
- + flt(item.rm_supp_cost)
- + flt(item.item_tax_amount)
- + flt(item.amount_difference_with_purchase_invoice)
- )
-
- divisional_loss = flt(
- valuation_amount_as_per_doc - flt(stock_value_diff), item.precision("base_net_amount")
- )
-
- if item.get("rejected_qty") and frappe.db.get_single_value(
- "Buying Settings", "set_valuation_rate_for_rejected_materials"
- ):
- rejected_item_cost = get_stock_value_difference(self.name, item.name, item.rejected_warehouse)
- divisional_loss -= rejected_item_cost
-
- if divisional_loss:
- loss_account = (
- self.get_company_default("default_expense_account", ignore_validation=True)
- or stock_asset_rbnb
- )
-
- if self.is_return and item.expense_account:
- loss_account = item.expense_account
-
- cost_center = item.cost_center or frappe.get_cached_value(
- "Company", self.company, "cost_center"
- )
- account_currency = get_account_currency(loss_account)
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=loss_account,
- cost_center=cost_center,
- debit=divisional_loss,
- credit=0.0,
- remarks=remarks,
- against_account=stock_asset_account_name,
- account_currency=account_currency,
- project=item.project,
- item=item,
- )
-
- stock_items = self.get_stock_items()
- warehouse_with_no_account = []
-
- for d in self.get("items"):
- remarks = self.get("remarks") or _("Accounting Entry for {0}").format(
- "Asset" if d.is_fixed_asset else "Stock"
- )
-
- if (
- provisional_accounting_for_non_stock_items
- and d.item_code not in stock_items
- and flt(d.qty)
- and d.get("provisional_expense_account")
- and not d.is_fixed_asset
- ):
- self.add_provisional_gl_entry(
- d, gl_entries, self.posting_date, d.get("provisional_expense_account")
- )
- elif flt(d.qty) and (flt(d.valuation_rate) or self.is_return):
- if not (
- (erpnext.is_perpetual_inventory_enabled(self.company) and d.item_code in stock_items)
- or (d.is_fixed_asset and not d.purchase_invoice)
- ):
- continue
-
- stock_asset_rbnb = (
- self.get_company_default("asset_received_but_not_billed")
- if d.is_fixed_asset
- else self.get_company_default("stock_received_but_not_billed")
- )
- landed_cost_entries = self.get_item_account_wise_lcv_entries()
- if d.is_fixed_asset:
- stock_asset_account_name = d.expense_account
- stock_value_diff = (
- flt(d.base_net_amount) + flt(d.item_tax_amount) + flt(d.landed_cost_voucher_amount)
- )
- elif inventory_account := self.get_inventory_account_dict(d, inventory_account_map):
- stock_value_diff = get_stock_value_difference(self.name, d.name, d.warehouse)
- stock_asset_account_name = inventory_account["account"]
-
- supplier_warehouse_account = None
- supplier_warehouse_account_currency = None
- if self.supplier_warehouse:
- if _inv_dict := self.get_inventory_account_dict(
- d, inventory_account_map, "supplier_warehouse"
- ):
- supplier_warehouse_account = _inv_dict["account"]
- supplier_warehouse_account_currency = _inv_dict["account_currency"]
-
- # If PR is sub-contracted and fg item rate is zero
- # in that case if account for source and target warehouse are same,
- # then GL entries should not be posted
- if (
- flt(stock_value_diff) == flt(d.rm_supp_cost)
- and supplier_warehouse_account
- and stock_asset_account_name == supplier_warehouse_account
- ):
- continue
-
- if (flt(d.valuation_rate) or self.is_return or d.is_fixed_asset) and flt(d.qty):
- make_item_asset_inward_gl_entry(d, stock_value_diff, stock_asset_account_name)
- outgoing_amount = make_stock_received_but_not_billed_entry(d)
- make_landed_cost_gl_entries(d)
- make_amount_difference_entry(d)
- make_sub_contracting_gl_entries(d)
- make_divisional_loss_gl_entry(d, outgoing_amount)
- elif (d.warehouse and d.qty and d.warehouse not in warehouse_with_no_account) or (
- not frappe.db.get_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials")
- and d.rejected_warehouse
- and d.rejected_warehouse not in warehouse_with_no_account
- ):
- warehouse_with_no_account.append(d.warehouse or d.rejected_warehouse)
-
- if d.is_fixed_asset and d.landed_cost_voucher_amount:
- self.update_assets(d, d.valuation_rate)
-
- if d.rejected_qty and frappe.db.get_single_value(
- "Buying Settings", "set_valuation_rate_for_rejected_materials"
- ):
- stock_asset_rbnb = (
- self.get_company_default("asset_received_but_not_billed")
- if d.is_fixed_asset
- else self.get_company_default("stock_received_but_not_billed")
- )
-
- stock_value_diff = get_stock_value_difference(self.name, d.name, d.rejected_warehouse)
- _inv_dict = self.get_inventory_account_dict(d, inventory_account_map, "rejected_warehouse")
-
- stock_asset_account_name = _inv_dict["account"]
-
- make_item_asset_inward_gl_entry(d, stock_value_diff, stock_asset_account_name)
- if not d.qty:
- make_stock_received_but_not_billed_entry(d)
-
- if warehouse_with_no_account:
- frappe.msgprint(
- _("No accounting entries for the following warehouses")
- + ": \n"
- + "\n".join(warehouse_with_no_account)
- )
+ return PurchaseReceiptGLComposer(self).compose(inventory_account_map, via_landed_cost_voucher)
def add_provisional_gl_entry(
self, item, gl_entries, posting_date, provisional_account, reverse=0, item_amount=None
@@ -894,57 +536,6 @@ class PurchaseReceipt(BuyingController):
return False
- def make_tax_gl_entries(self, gl_entries, via_landed_cost_voucher=False):
- negative_expense_to_be_booked = sum([flt(d.item_tax_amount) for d in self.get("items")])
- # Cost center-wise amount breakup for other charges included for valuation
- valuation_tax = {}
- for tax in self.get("taxes"):
- if tax.category in ("Valuation", "Valuation and Total") and flt(
- tax.base_tax_amount_after_discount_amount
- ):
- if not tax.cost_center:
- frappe.throw(
- _("Cost Center is required in row {0} in Taxes table for type {1}").format(
- tax.idx, _(tax.category)
- )
- )
- valuation_tax.setdefault(tax.name, 0)
- valuation_tax[tax.name] += (tax.add_deduct_tax == "Add" and 1 or -1) * flt(
- tax.base_tax_amount_after_discount_amount
- )
-
- if negative_expense_to_be_booked and valuation_tax:
- # Backward compatibility:
- # and charges added via Landed Cost Voucher,
- # post valuation related charges on "Stock Received But Not Billed"
- against_accounts = ", ".join([d.account for d in gl_entries if flt(d.debit) > 0])
- total_valuation_amount = sum(valuation_tax.values())
- amount_including_divisional_loss = negative_expense_to_be_booked
- i = 1
- for tax in self.get("taxes"):
- if valuation_tax.get(tax.name):
- account = tax.account_head
- if i == len(valuation_tax):
- applicable_amount = amount_including_divisional_loss
- else:
- applicable_amount = negative_expense_to_be_booked * (
- valuation_tax[tax.name] / total_valuation_amount
- )
- amount_including_divisional_loss -= applicable_amount
-
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=account,
- cost_center=tax.cost_center,
- debit=0.0,
- credit=applicable_amount,
- remarks=self.remarks or _("Accounting Entry for Stock"),
- against_account=against_accounts,
- item=tax,
- )
-
- i += 1
-
def update_assets(self, item, valuation_rate):
assets = frappe.db.get_all(
"Asset",
@@ -1474,246 +1065,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")
- 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
diff --git a/erpnext/stock/doctype/purchase_receipt/services/__init__.py b/erpnext/stock/doctype/purchase_receipt/services/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/stock/doctype/purchase_receipt/services/gl_composer.py b/erpnext/stock/doctype/purchase_receipt/services/gl_composer.py
new file mode 100644
index 00000000000..6a01a0484ce
--- /dev/null
+++ b/erpnext/stock/doctype/purchase_receipt/services/gl_composer.py
@@ -0,0 +1,408 @@
+# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+import frappe
+from frappe import _
+from frappe.utils import cint, flt
+
+import erpnext
+from erpnext.accounts.general_ledger import process_gl_map
+from erpnext.accounts.utils import get_account_currency
+from erpnext.stock.services.base_stock_gl_composer import BaseStockGLComposer
+
+
+class PurchaseReceiptGLComposer(BaseStockGLComposer):
+ """GL composer for Purchase Receipt.
+
+ Builds GL entries for stock/asset inward, taxes, purchase expense, and
+ regional adjustments. Does not delegate to the base stock GL loop —
+ PR has its own per-item logic (provisional accounting, fixed assets, LCV,
+ sub-contracting, divisional loss).
+ """
+
+ def compose(
+ self,
+ inventory_account_map: dict | None = None,
+ via_landed_cost_voucher: bool = False,
+ ) -> list:
+ gl_entries = []
+ self._make_item_gl_entries(gl_entries, inventory_account_map)
+ self._make_tax_gl_entries(gl_entries, via_landed_cost_voucher)
+ self.doc.set_gl_entry_for_purchase_expense(gl_entries)
+
+ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_regional_gl_entries
+
+ update_regional_gl_entries(gl_entries, self.doc)
+
+ return process_gl_map(gl_entries, from_repost=frappe.flags.through_repost_item_valuation)
+
+ def _make_item_gl_entries(self, gl_entries: list, inventory_account_map: dict | None) -> None:
+ from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import (
+ get_purchase_document_details,
+ )
+ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import get_stock_value_difference
+
+ doc = self.doc
+ provisional_accounting_for_non_stock_items = cint(
+ frappe.db.get_value("Company", doc.company, "enable_provisional_accounting_for_non_stock_items")
+ )
+
+ exchange_rate_map, net_rate_map = get_purchase_document_details(doc)
+ stock_items = doc.get_stock_items()
+ warehouse_with_no_account = []
+
+ def validate_account(account_type):
+ frappe.throw(_("{0} account not found while submitting purchase receipt").format(account_type))
+
+ def make_item_asset_inward_gl_entry(item, stock_value_diff, stock_asset_account_name):
+ account_currency = get_account_currency(stock_asset_account_name)
+ if not stock_asset_account_name:
+ validate_account("Asset or warehouse account")
+ self.add_gl_entry(
+ gl_entries=gl_entries,
+ account=stock_asset_account_name,
+ cost_center=d.cost_center,
+ debit=stock_value_diff,
+ credit=0.0,
+ remarks=remarks,
+ against_account=stock_asset_rbnb,
+ account_currency=account_currency,
+ item=item,
+ )
+
+ def make_stock_received_but_not_billed_entry(item):
+ if (
+ doc.get("is_return")
+ and item.return_qty_from_rejected_warehouse
+ and not frappe.db.get_single_value(
+ "Buying Settings", "set_valuation_rate_for_rejected_materials"
+ )
+ ):
+ return 0.0
+
+ account = stock_asset_rbnb
+ if item.from_warehouse:
+ _inv_dict = doc.get_inventory_account_dict(item, inventory_account_map, "from_warehouse")
+ account = _inv_dict["account"]
+
+ account_currency = get_account_currency(account)
+
+ credit_amount = (
+ flt(item.base_net_amount, item.precision("base_net_amount"))
+ if account_currency == doc.company_currency
+ else flt(item.net_amount, item.precision("net_amount"))
+ )
+
+ outgoing_amount = item.base_net_amount
+ if doc.is_internal_transfer() and item.valuation_rate:
+ outgoing_amount = abs(get_stock_value_difference(doc.name, item.name, item.from_warehouse))
+ credit_amount = outgoing_amount
+
+ if item.get("rejected_qty") and frappe.db.get_single_value(
+ "Buying Settings", "set_valuation_rate_for_rejected_materials"
+ ):
+ outgoing_amount += get_stock_value_difference(doc.name, item.name, item.rejected_warehouse)
+ credit_amount = outgoing_amount
+
+ if credit_amount:
+ if not account:
+ validate_account("Stock or Asset Received But Not Billed")
+
+ self.add_gl_entry(
+ gl_entries=gl_entries,
+ account=account,
+ cost_center=item.cost_center,
+ debit=-1 * flt(outgoing_amount, item.precision("base_net_amount")),
+ credit=0.0,
+ remarks=remarks,
+ against_account=stock_asset_account_name,
+ debit_in_account_currency=-1 * flt(outgoing_amount, item.precision("base_net_amount")),
+ account_currency=account_currency,
+ item=item,
+ )
+
+ if d.get("purchase_invoice"):
+ if (
+ exchange_rate_map[item.purchase_invoice]
+ and doc.conversion_rate != exchange_rate_map[item.purchase_invoice]
+ and item.net_rate == net_rate_map[item.purchase_invoice_item]
+ ):
+ discrepancy_caused_by_exchange_rate_difference = (item.qty * item.net_rate) * (
+ exchange_rate_map[item.purchase_invoice] - doc.conversion_rate
+ )
+
+ self.add_gl_entry(
+ gl_entries=gl_entries,
+ account=account,
+ cost_center=item.cost_center,
+ debit=0.0,
+ credit=discrepancy_caused_by_exchange_rate_difference,
+ remarks=remarks,
+ against_account=doc.supplier,
+ debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
+ account_currency=account_currency,
+ item=item,
+ )
+
+ self.add_gl_entry(
+ gl_entries=gl_entries,
+ account=doc.get_company_default("exchange_gain_loss_account"),
+ cost_center=d.cost_center,
+ debit=discrepancy_caused_by_exchange_rate_difference,
+ credit=0.0,
+ remarks=remarks,
+ against_account=doc.supplier,
+ debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
+ account_currency=account_currency,
+ item=item,
+ )
+
+ return outgoing_amount
+
+ def make_landed_cost_gl_entries(item):
+ if item.landed_cost_voucher_amount and landed_cost_entries:
+ if (item.item_code, item.name) in landed_cost_entries:
+ for account, amount in landed_cost_entries[(item.item_code, item.name)].items():
+ account_currency = get_account_currency(account)
+ credit_amount = (
+ flt(amount["base_amount"])
+ if (amount["base_amount"] or account_currency != doc.company_currency)
+ else flt(amount["amount"])
+ )
+
+ if not account:
+ validate_account("Landed Cost Account")
+
+ self.add_gl_entry(
+ gl_entries=gl_entries,
+ account=account,
+ cost_center=item.cost_center,
+ debit=0.0,
+ credit=credit_amount,
+ remarks=remarks,
+ against_account=stock_asset_account_name,
+ credit_in_account_currency=flt(amount["amount"]),
+ account_currency=account_currency,
+ project=item.project,
+ item=item,
+ )
+
+ def make_amount_difference_entry(item):
+ if item.amount_difference_with_purchase_invoice and stock_asset_rbnb:
+ account_currency = get_account_currency(stock_asset_rbnb)
+ self.add_gl_entry(
+ gl_entries=gl_entries,
+ account=stock_asset_rbnb,
+ cost_center=item.cost_center,
+ debit=0.0,
+ credit=flt(item.amount_difference_with_purchase_invoice),
+ remarks=_("Adjustment based on Purchase Invoice rate"),
+ against_account=stock_asset_account_name,
+ account_currency=account_currency,
+ project=item.project,
+ item=item,
+ )
+
+ def make_sub_contracting_gl_entries(item):
+ if flt(item.rm_supp_cost) and supplier_warehouse_account:
+ self.add_gl_entry(
+ gl_entries=gl_entries,
+ account=supplier_warehouse_account,
+ cost_center=item.cost_center,
+ debit=0.0,
+ credit=flt(item.rm_supp_cost),
+ remarks=remarks,
+ against_account=stock_asset_account_name,
+ account_currency=supplier_warehouse_account_currency,
+ item=item,
+ )
+
+ def make_divisional_loss_gl_entry(item, outgoing_amount):
+ if item.is_fixed_asset:
+ return
+
+ valuation_amount_as_per_doc = (
+ flt(outgoing_amount, d.precision("base_net_amount"))
+ + flt(item.landed_cost_voucher_amount)
+ + flt(item.rm_supp_cost)
+ + flt(item.item_tax_amount)
+ + flt(item.amount_difference_with_purchase_invoice)
+ )
+
+ divisional_loss = flt(
+ valuation_amount_as_per_doc - flt(stock_value_diff), item.precision("base_net_amount")
+ )
+
+ if item.get("rejected_qty") and frappe.db.get_single_value(
+ "Buying Settings", "set_valuation_rate_for_rejected_materials"
+ ):
+ rejected_item_cost = get_stock_value_difference(doc.name, item.name, item.rejected_warehouse)
+ divisional_loss -= rejected_item_cost
+
+ if divisional_loss:
+ loss_account = (
+ doc.get_company_default("default_expense_account", ignore_validation=True)
+ or stock_asset_rbnb
+ )
+
+ if doc.is_return and item.expense_account:
+ loss_account = item.expense_account
+
+ cost_center = item.cost_center or frappe.get_cached_value(
+ "Company", doc.company, "cost_center"
+ )
+ account_currency = get_account_currency(loss_account)
+ self.add_gl_entry(
+ gl_entries=gl_entries,
+ account=loss_account,
+ cost_center=cost_center,
+ debit=divisional_loss,
+ credit=0.0,
+ remarks=remarks,
+ against_account=stock_asset_account_name,
+ account_currency=account_currency,
+ project=item.project,
+ item=item,
+ )
+
+ for d in doc.get("items"):
+ remarks = doc.get("remarks") or _("Accounting Entry for {0}").format(
+ "Asset" if d.is_fixed_asset else "Stock"
+ )
+
+ if (
+ provisional_accounting_for_non_stock_items
+ and d.item_code not in stock_items
+ and flt(d.qty)
+ and d.get("provisional_expense_account")
+ and not d.is_fixed_asset
+ ):
+ doc.add_provisional_gl_entry(
+ d, gl_entries, doc.posting_date, d.get("provisional_expense_account")
+ )
+ elif flt(d.qty) and (flt(d.valuation_rate) or doc.is_return):
+ if not (
+ (erpnext.is_perpetual_inventory_enabled(doc.company) and d.item_code in stock_items)
+ or (d.is_fixed_asset and not d.purchase_invoice)
+ ):
+ continue
+
+ stock_asset_rbnb = (
+ doc.get_company_default("asset_received_but_not_billed")
+ if d.is_fixed_asset
+ else doc.get_company_default("stock_received_but_not_billed")
+ )
+ landed_cost_entries = doc.get_item_account_wise_lcv_entries()
+ if d.is_fixed_asset:
+ stock_asset_account_name = d.expense_account
+ stock_value_diff = (
+ flt(d.base_net_amount) + flt(d.item_tax_amount) + flt(d.landed_cost_voucher_amount)
+ )
+ elif inventory_account := doc.get_inventory_account_dict(d, inventory_account_map):
+ stock_value_diff = get_stock_value_difference(doc.name, d.name, d.warehouse)
+ stock_asset_account_name = inventory_account["account"]
+
+ supplier_warehouse_account = None
+ supplier_warehouse_account_currency = None
+ if doc.supplier_warehouse:
+ if _inv_dict := doc.get_inventory_account_dict(
+ d, inventory_account_map, "supplier_warehouse"
+ ):
+ supplier_warehouse_account = _inv_dict["account"]
+ supplier_warehouse_account_currency = _inv_dict["account_currency"]
+
+ if (
+ flt(stock_value_diff) == flt(d.rm_supp_cost)
+ and supplier_warehouse_account
+ and stock_asset_account_name == supplier_warehouse_account
+ ):
+ continue
+
+ if (flt(d.valuation_rate) or doc.is_return or d.is_fixed_asset) and flt(d.qty):
+ make_item_asset_inward_gl_entry(d, stock_value_diff, stock_asset_account_name)
+ outgoing_amount = make_stock_received_but_not_billed_entry(d)
+ make_landed_cost_gl_entries(d)
+ make_amount_difference_entry(d)
+ make_sub_contracting_gl_entries(d)
+ make_divisional_loss_gl_entry(d, outgoing_amount)
+ elif (d.warehouse and d.qty and d.warehouse not in warehouse_with_no_account) or (
+ not frappe.db.get_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials")
+ and d.rejected_warehouse
+ and d.rejected_warehouse not in warehouse_with_no_account
+ ):
+ warehouse_with_no_account.append(d.warehouse or d.rejected_warehouse)
+
+ if d.is_fixed_asset and d.landed_cost_voucher_amount:
+ doc.update_assets(d, d.valuation_rate)
+
+ if d.rejected_qty and frappe.db.get_single_value(
+ "Buying Settings", "set_valuation_rate_for_rejected_materials"
+ ):
+ stock_asset_rbnb = (
+ doc.get_company_default("asset_received_but_not_billed")
+ if d.is_fixed_asset
+ else doc.get_company_default("stock_received_but_not_billed")
+ )
+
+ stock_value_diff = get_stock_value_difference(doc.name, d.name, d.rejected_warehouse)
+ _inv_dict = doc.get_inventory_account_dict(d, inventory_account_map, "rejected_warehouse")
+ stock_asset_account_name = _inv_dict["account"]
+
+ make_item_asset_inward_gl_entry(d, stock_value_diff, stock_asset_account_name)
+ if not d.qty:
+ make_stock_received_but_not_billed_entry(d)
+
+ if warehouse_with_no_account:
+ frappe.msgprint(
+ _("No accounting entries for the following warehouses")
+ + ": \n"
+ + "\n".join(warehouse_with_no_account)
+ )
+
+ def _make_tax_gl_entries(self, gl_entries: list, via_landed_cost_voucher: bool = False) -> None:
+ doc = self.doc
+ negative_expense_to_be_booked = sum([flt(d.item_tax_amount) for d in doc.get("items")])
+ valuation_tax = {}
+ for tax in doc.get("taxes"):
+ if tax.category in ("Valuation", "Valuation and Total") and flt(
+ tax.base_tax_amount_after_discount_amount
+ ):
+ if not tax.cost_center:
+ frappe.throw(
+ _("Cost Center is required in row {0} in Taxes table for type {1}").format(
+ tax.idx, _(tax.category)
+ )
+ )
+ valuation_tax.setdefault(tax.name, 0)
+ valuation_tax[tax.name] += (tax.add_deduct_tax == "Add" and 1 or -1) * flt(
+ tax.base_tax_amount_after_discount_amount
+ )
+
+ if negative_expense_to_be_booked and valuation_tax:
+ against_accounts = ", ".join([d.account for d in gl_entries if flt(d.debit) > 0])
+ total_valuation_amount = sum(valuation_tax.values())
+ amount_including_divisional_loss = negative_expense_to_be_booked
+ i = 1
+ for tax in doc.get("taxes"):
+ if valuation_tax.get(tax.name):
+ account = tax.account_head
+ if i == len(valuation_tax):
+ applicable_amount = amount_including_divisional_loss
+ else:
+ applicable_amount = negative_expense_to_be_booked * (
+ valuation_tax[tax.name] / total_valuation_amount
+ )
+ amount_including_divisional_loss -= applicable_amount
+
+ self.add_gl_entry(
+ gl_entries=gl_entries,
+ account=account,
+ cost_center=tax.cost_center,
+ debit=0.0,
+ credit=applicable_amount,
+ remarks=doc.remarks or _("Accounting Entry for Stock"),
+ against_account=against_accounts,
+ item=tax,
+ )
+
+ i += 1
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 191f2812135..5f54cab0d2f 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -14,8 +14,8 @@ from erpnext.controllers.accounts_controller import InvalidQtyError
from erpnext.controllers.buying_controller import QtyMismatchError
from erpnext.stock import get_warehouse_account_map
from erpnext.stock.doctype.item.test_item import create_item, make_item
-from erpnext.stock.doctype.material_request.material_request import make_purchase_order
-from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice
+from erpnext.stock.doctype.material_request.mapper import make_purchase_order
+from erpnext.stock.doctype.purchase_receipt.mapper import make_purchase_invoice
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
SerialNoDuplicateError,
SerialNoExistsInFutureTransactionError,
@@ -707,10 +707,10 @@ class TestPurchaseReceipt(ERPNextTestSuite):
2. PO -> PI
3. PO -> PR2.
"""
- from erpnext.buying.doctype.purchase_order.purchase_order import (
+ from erpnext.buying.doctype.purchase_order.mapper import (
make_purchase_invoice as make_purchase_invoice_from_po,
)
- from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
+ from erpnext.buying.doctype.purchase_order.mapper import make_purchase_receipt
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
frappe.flags.print_test_messages = False
@@ -861,7 +861,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
pr.cancel()
def test_purchase_return_with_submitted_asset(self):
- from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_return
+ from erpnext.stock.doctype.purchase_receipt.mapper import make_purchase_return
pr = make_purchase_receipt(item_code="Test Asset Item", qty=1)
@@ -1010,7 +1010,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
pr1.cancel()
def test_stock_transfer_from_purchase_receipt(self):
- from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
+ from erpnext.stock.doctype.delivery_note.mapper import make_inter_company_purchase_receipt
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
prepare_data_for_internal_transfer()
@@ -1051,8 +1051,42 @@ class TestPurchaseReceipt(ERPNextTestSuite):
pr.cancel()
+ def test_inter_company_purchase_receipt_does_not_inherit_party_fields(self):
+ """
+ Party-derived fields on DN (from Customer) must not leak into the mapped PR.
+ """
+ from erpnext.stock.doctype.delivery_note.mapper import make_inter_company_purchase_receipt
+ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+
+ prepare_data_for_internal_transfer()
+
+ customer = "_Test Internal Customer 2"
+ company = "_Test Company with perpetual inventory"
+
+ dn = create_delivery_note(
+ company=company,
+ customer=customer,
+ cost_center="Main - TCP1",
+ expense_account="Cost of Goods Sold - TCP1",
+ qty=1,
+ rate=100,
+ warehouse="Stores - TCP1",
+ target_warehouse="Work In Progress - TCP1",
+ do_not_submit=True,
+ )
+ # Stamp customer-side party fields onto the DN
+ dn.tax_category = "_Test Tax Category 2"
+ dn.language = "ar"
+ dn.submit()
+
+ pr = make_inter_company_purchase_receipt(dn.name)
+
+ supplier = frappe.get_doc("Supplier", "_Test Internal Supplier 2")
+ self.assertEqual(pr.tax_category or None, supplier.tax_category or None)
+ self.assertEqual(pr.language or None, supplier.language or None)
+
def test_lcv_for_internal_transfer(self):
- from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
+ from erpnext.stock.doctype.delivery_note.mapper import make_inter_company_purchase_receipt
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
make_landed_cost_voucher,
@@ -1145,10 +1179,10 @@ class TestPurchaseReceipt(ERPNextTestSuite):
new_cost = frappe.db.get_value("Serial and Batch Bundle", new_inward_sabb[0], "total_amount")
self.assertEqual(new_cost, original_cost + 100)
- self.assertTrue(new_inward_sabb[0] == inward_sabb[0])
+ self.assertEqual(new_inward_sabb[0], inward_sabb[0])
def test_stock_transfer_from_purchase_receipt_with_valuation(self):
- from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
+ from erpnext.stock.doctype.delivery_note.mapper import make_inter_company_purchase_receipt
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
@@ -1305,7 +1339,8 @@ class TestPurchaseReceipt(ERPNextTestSuite):
- Create PI from PO and submit
- Create PR from PO and submit
"""
- from erpnext.buying.doctype.purchase_order import purchase_order, test_purchase_order
+ from erpnext.buying.doctype.purchase_order import mapper as purchase_order
+ from erpnext.buying.doctype.purchase_order import test_purchase_order
po = test_purchase_order.create_purchase_order()
@@ -1326,7 +1361,8 @@ class TestPurchaseReceipt(ERPNextTestSuite):
- Create partial PI from PO and submit
- Create PR from PO and submit
"""
- from erpnext.buying.doctype.purchase_order import purchase_order, test_purchase_order
+ from erpnext.buying.doctype.purchase_order import mapper as purchase_order
+ from erpnext.buying.doctype.purchase_order import test_purchase_order
po = test_purchase_order.create_purchase_order()
@@ -1348,7 +1384,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
self.assertAlmostEqual(pr.per_billed, 50.0, places=2)
def test_purchase_receipt_with_exchange_rate_difference(self):
- from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import (
+ from erpnext.accounts.doctype.purchase_invoice.mapper import (
make_purchase_receipt as create_purchase_receipt,
)
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import (
@@ -1455,7 +1491,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
self.assertEqual(gle.credit, 50)
def test_backdated_transaction_for_internal_transfer(self):
- from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
+ from erpnext.stock.doctype.delivery_note.mapper import make_inter_company_purchase_receipt
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
prepare_data_for_internal_transfer()
@@ -1543,7 +1579,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
def test_backdated_transaction_for_internal_transfer_in_trasit_warehouse_for_purchase_receipt(
self,
):
- from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
+ from erpnext.stock.doctype.delivery_note.mapper import make_inter_company_purchase_receipt
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
prepare_data_for_internal_transfer()
@@ -1653,7 +1689,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import (
make_purchase_invoice as make_purchase_invoice_for_si,
)
- from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
+ from erpnext.accounts.doctype.sales_invoice.mapper import (
make_inter_company_purchase_invoice,
)
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
@@ -1797,7 +1833,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
return_pi = make_return_doc(pi.doctype, pi.name)
return_pi.save().submit()
- self.assertTrue(return_pi.docstatus == 1)
+ self.assertEqual(return_pi.docstatus, 1)
def test_disable_last_purchase_rate(self):
from erpnext.stock.get_item_details import ItemDetailsCtx, get_item_details
@@ -1881,7 +1917,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
)
# Step 4: Create Internal Purchase Receipt
- from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
+ from erpnext.stock.doctype.delivery_note.mapper import make_inter_company_purchase_receipt
pr = make_inter_company_purchase_receipt(dn.name)
pr.set_posting_time = 1
@@ -1913,7 +1949,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 0)
def test_internal_pr_gl_entries(self):
- from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
+ from erpnext.stock.doctype.delivery_note.mapper import make_inter_company_purchase_receipt
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
@@ -2029,7 +2065,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
)
# Step 4: Create Internal Purchase Receipt
- from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
+ from erpnext.stock.doctype.delivery_note.mapper import make_inter_company_purchase_receipt
pr = make_inter_company_purchase_receipt(dn.name)
pr.inter_company_reference = ""
@@ -2079,7 +2115,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
)
# Step 3: Create Purchase Return for 2 qty
- from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_return
+ from erpnext.stock.doctype.purchase_receipt.mapper import make_purchase_return
pr_return = make_purchase_return(pr.name)
pr_return.items[0].qty = 2 * -1
@@ -2098,7 +2134,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
self.assertEqual(abs(data["stock_value_difference"]), 400.00)
def test_return_from_rejected_warehouse(self):
- from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
+ from erpnext.stock.doctype.purchase_receipt.mapper import (
make_purchase_return_against_rejected_warehouse,
)
@@ -2504,7 +2540,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
sbb_doc = frappe.get_doc("Serial and Batch Bundle", pr.items[0].serial_and_batch_bundle)
for row in sbb_doc.entries:
- self.assertTrue(row.serial_no in serial_nos)
+ self.assertIn(row.serial_no, serial_nos)
serial_nos.remove("SNU-TSFISI-000015")
@@ -2537,7 +2573,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
serial_no_status = frappe.db.get_value("Serial No", "SNU-TSFISI-000015", "status")
- self.assertTrue(serial_no_status != "Active")
+ self.assertNotEqual(serial_no_status, "Active")
dn = create_delivery_note(
item_code=item_code,
@@ -2550,11 +2586,11 @@ class TestPurchaseReceipt(ERPNextTestSuite):
self.assertEqual(dn.items[0].qty, 4)
doc = frappe.get_doc("Serial and Batch Bundle", dn.items[0].serial_and_batch_bundle)
for row in doc.entries:
- self.assertTrue(row.serial_no in new_serial_nos)
+ self.assertIn(row.serial_no, new_serial_nos)
for sn in new_serial_nos:
serial_no_status = frappe.db.get_value("Serial No", sn, "status")
- self.assertTrue(serial_no_status != "Active")
+ self.assertNotEqual(serial_no_status, "Active")
frappe.db.set_single_value(
"Stock Settings", "do_not_update_serial_batch_on_creation_of_auto_bundle", 1
@@ -2678,8 +2714,8 @@ class TestPurchaseReceipt(ERPNextTestSuite):
)
def test_pr_billed_amount_against_return_entry(self):
- from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import make_debit_note
- from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
+ from erpnext.accounts.doctype.purchase_invoice.mapper import make_debit_note
+ from erpnext.stock.doctype.purchase_receipt.mapper import (
make_purchase_invoice as make_pi_from_pr,
)
@@ -2845,7 +2881,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
def test_internal_transfer_with_serial_batch_items_and_their_valuation(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
- from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
+ from erpnext.stock.doctype.delivery_note.mapper import make_inter_company_purchase_receipt
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
prepare_data_for_internal_transfer()
@@ -2965,7 +3001,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
serial_no_details = frappe.db.get_value(
"Serial No", sn, ["status", "warehouse"], as_dict=1
)
- self.assertTrue(serial_no_details.status == "Active")
+ self.assertEqual(serial_no_details.status, "Active")
self.assertEqual(serial_no_details.warehouse, "Work In Progress - TCP1")
inter_transfer_dn_return = make_return_doc("Delivery Note", inter_transfer_dn.name)
@@ -2982,7 +3018,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
def test_internal_transfer_with_serial_batch_items_without_use_serial_batch_fields(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
- from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
+ from erpnext.stock.doctype.delivery_note.mapper import make_inter_company_purchase_receipt
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0)
@@ -3104,7 +3140,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
serial_no_details = frappe.db.get_value(
"Serial No", sn, ["status", "warehouse"], as_dict=1
)
- self.assertTrue(serial_no_details.status == "Active")
+ self.assertEqual(serial_no_details.status, "Active")
self.assertEqual(serial_no_details.warehouse, "Work In Progress - TCP1")
inter_transfer_dn_return = make_return_doc("Delivery Note", inter_transfer_dn.name)
@@ -3200,7 +3236,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
self.assertEqual(row.incoming_rate, 0)
def test_purchase_return_from_accepted_and_rejected_warehouse(self):
- from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
+ from erpnext.stock.doctype.purchase_receipt.mapper import (
make_purchase_return,
)
@@ -3278,7 +3314,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
self.assertEqual(batch.expiry_date, getdate(add_days(today(), 5)))
def test_purchase_return_from_rejected_warehouse(self):
- from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
+ from erpnext.stock.doctype.purchase_receipt.mapper import (
make_purchase_return_against_rejected_warehouse,
)
@@ -3318,7 +3354,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
create_purchase_order,
make_pr_against_po,
)
- from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice
+ from erpnext.stock.doctype.purchase_receipt.mapper import make_purchase_invoice
stock_rbnb = "Stock Received But Not Billed - _TC"
stock_in_hand = "Stock In Hand - _TC"
@@ -3468,7 +3504,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
create_purchase_order,
make_pr_against_po,
)
- from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice
+ from erpnext.stock.doctype.purchase_receipt.mapper import make_purchase_invoice
stock_rbnb = "Stock Received But Not Billed - _TC"
stock_in_hand = "Stock In Hand - _TC"
@@ -3637,7 +3673,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
self.assertEqual(pr.status, "Completed")
def test_internal_transfer_for_batch_items_with_cancel(self):
- from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
+ from erpnext.stock.doctype.delivery_note.mapper import make_inter_company_purchase_receipt
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0)
@@ -3752,7 +3788,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
def test_internal_transfer_for_batch_items_with_cancel_use_serial_batch_fields(self):
- from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
+ from erpnext.stock.doctype.delivery_note.mapper import make_inter_company_purchase_receipt
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
@@ -3947,7 +3983,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
self.assertEqual(pr.items[0].conversion_factor, 1.0)
def test_purchase_receipt_return_valuation_without_use_serial_batch_field(self):
- from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_return
+ from erpnext.stock.doctype.purchase_receipt.mapper import make_purchase_return
batch_item = make_item(
"_Test Purchase Receipt Return Valuation Batch Item",
@@ -4051,7 +4087,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
self.assertEqual(incoming_rate, 0)
def test_purchase_receipt_return_valuation_with_use_serial_batch_field(self):
- from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_return
+ from erpnext.stock.doctype.purchase_receipt.mapper import make_purchase_return
batch_item = make_item(
"_Test Purchase Receipt Return Valuation With Batch Item",
@@ -4236,7 +4272,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
serial_no = get_serial_nos_from_bundle(pr.items[0].serial_and_batch_bundle)[0]
status = frappe.db.get_value("Serial No", serial_no, "status")
- self.assertTrue(status == "Active")
+ self.assertEqual(status, "Active")
make_stock_entry(
item_code=item_code,
@@ -4247,7 +4283,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
)
status = frappe.db.get_value("Serial No", serial_no, "status")
- self.assertFalse(status == "Active")
+ self.assertNotEqual(status, "Active")
pr = make_purchase_receipt(
item_code=item_code, qty=1, rate=100, use_serial_batch_fields=1, do_not_submit=1
@@ -4260,7 +4296,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
frappe.db.set_single_value("Stock Settings", "allow_existing_serial_no", 1)
def test_seral_no_return_validation(self):
- from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
+ from erpnext.stock.doctype.purchase_receipt.mapper import (
make_purchase_return,
)
@@ -4292,7 +4328,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
sn_return.submit()
def test_batch_no_return_validation(self):
- from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
+ from erpnext.stock.doctype.purchase_receipt.mapper import (
make_purchase_return,
)
@@ -4325,10 +4361,10 @@ class TestPurchaseReceipt(ERPNextTestSuite):
batch_return.submit()
def test_pr_status_based_on_invoices_with_update_stock(self):
- from erpnext.buying.doctype.purchase_order.purchase_order import (
+ from erpnext.buying.doctype.purchase_order.mapper import (
make_purchase_invoice as _make_purchase_invoice,
)
- from erpnext.buying.doctype.purchase_order.purchase_order import (
+ from erpnext.buying.doctype.purchase_order.mapper import (
make_purchase_receipt as _make_purchase_receipt,
)
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
@@ -4431,7 +4467,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
self.assertRaises(frappe.ValidationError, repost_doc.save)
def test_internal_pr_qty_change_only_single_batch(self):
- from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
+ from erpnext.stock.doctype.delivery_note.mapper import make_inter_company_purchase_receipt
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
prepare_data_for_internal_transfer()
@@ -4759,8 +4795,8 @@ class TestPurchaseReceipt(ERPNextTestSuite):
gl_entries = get_gl_entries(pr.doctype, pr.name)
accounts = [d.account for d in gl_entries]
- self.assertTrue(expense_account in accounts)
- self.assertTrue(expense_contra_account in accounts)
+ self.assertIn(expense_account, accounts)
+ self.assertIn(expense_contra_account, accounts)
for row in gl_entries:
if row.account == expense_account:
@@ -4798,7 +4834,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
gl_entries = get_gl_entries(se.doctype, se.name)
for row in gl_entries:
- self.assertTrue(row.account in ["Stock In Hand - TCP1", "Stock Adjustment - TCP1"])
+ self.assertIn(row.account, ["Stock In Hand - TCP1", "Stock Adjustment - TCP1"])
se.items[0].db_set("expense_account", account)
se.reload()
@@ -4820,7 +4856,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
gl_entries = get_gl_entries(se.doctype, se.name)
for row in gl_entries:
- self.assertTrue(row.account in ["Stock In Hand - TCP1", account])
+ self.assertIn(row.account, ["Stock In Hand - TCP1", account])
def test_lcv_for_repack_entry(self):
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
@@ -4948,10 +4984,10 @@ class TestPurchaseReceipt(ERPNextTestSuite):
)
@ERPNextTestSuite.change_settings("Accounts Settings", {"over_billing_allowance": 100})
def test_set_lcv_from_pi_created_against_po(self):
- from erpnext.buying.doctype.purchase_order.purchase_order import (
+ from erpnext.buying.doctype.purchase_order.mapper import (
make_purchase_invoice as make_pi_against_po,
)
- from erpnext.buying.doctype.purchase_order.purchase_order import (
+ from erpnext.buying.doctype.purchase_order.mapper import (
make_purchase_receipt as make_pr_against_po,
)
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
@@ -4981,10 +5017,10 @@ class TestPurchaseReceipt(ERPNextTestSuite):
self.assertEqual(row.amount_difference_with_purchase_invoice, amt_diff)
def test_purchase_return_with_and_without_return_against_rejected_qty(self):
- from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
+ from erpnext.stock.doctype.purchase_receipt.mapper import (
make_purchase_return as _make_purchase_return,
)
- from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
+ from erpnext.stock.doctype.purchase_receipt.mapper import (
make_purchase_return_against_rejected_warehouse,
)
@@ -5056,7 +5092,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
doc.db_set("use_batchwise_valuation", 0)
doc.reload()
- self.assertTrue(doc.use_batchwise_valuation == 0)
+ self.assertEqual(doc.use_batchwise_valuation, 0)
doc = frappe.new_doc("Batch")
doc.update(
@@ -5066,7 +5102,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
}
).insert()
- self.assertTrue(doc.use_batchwise_valuation == 1)
+ self.assertEqual(doc.use_batchwise_valuation, 1)
warehouse = "_Test Warehouse - _TC"
make_stock_entry(
@@ -5223,7 +5259,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
"""
To test inter branch transaction incoming rate calculation with lcv after item reposting
"""
- from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
+ from erpnext.stock.doctype.delivery_note.mapper import make_inter_company_purchase_receipt
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
prepare_data_for_internal_transfer()
@@ -5458,7 +5494,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
self.assertEqual(pr.conversion_rate, 80)
gl_entries = get_gl_entries(pr.doctype, pr.name)
- self.assertTrue(len(gl_entries) == 2)
+ self.assertEqual(len(gl_entries), 2)
for row in gl_entries:
amount = row.credit or row.debit
self.assertEqual(amount, 8000.0)
@@ -5471,13 +5507,13 @@ class TestPurchaseReceipt(ERPNextTestSuite):
pi.submit()
gl_entries = get_gl_entries(pi.doctype, pi.name)
- self.assertTrue(len(gl_entries) == 2)
+ self.assertEqual(len(gl_entries), 2)
accounts = ["USD Party Account Creditors - TCP1", "Stock Received But Not Billed - TCP1"]
for row in gl_entries:
amount = row.credit or row.debit
self.assertEqual(amount, 9000.0)
- self.assertTrue(row.account in accounts)
+ self.assertIn(row.account, accounts)
frappe.db.set_single_value(
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", original_value
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
index 4df99dd21a6..dac3ca21038 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
@@ -8,7 +8,7 @@ import frappe
from frappe import _
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
-from frappe.utils import cint, cstr, flt, get_link_to_form, get_number_format_info
+from frappe.utils import cint, flt, get_link_to_form, get_number_format_info
from erpnext.stock.doctype.quality_inspection_template.quality_inspection_template import (
get_template_details,
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
index 6a8d3cc7ffa..1828528866f 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -15,7 +15,7 @@ from frappe.utils.user import get_users_with_role
from rq.timeouts import JobTimeoutException
import erpnext
-from erpnext.accounts.general_ledger import validate_accounting_period
+from erpnext.accounts.services.gl_validator import validate_accounting_period
from erpnext.accounts.utils import get_future_stock_vouchers, repost_gle_for_stock_vouchers
from erpnext.stock.stock_ledger import (
get_affected_transactions,
diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
index e0ddd6faa8e..82b2dbe6e45 100644
--- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
@@ -100,14 +100,14 @@ class TestRepostItemValuation(ERPNextTestSuite, StockTestMixin):
repost_doc.db_update_all()
logs = frappe.get_all("Repost Item Valuation", filters={"status": "Skipped"})
- self.assertTrue(len(logs) > 10)
+ self.assertGreater(len(logs), 10)
from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import RepostItemValuation
RepostItemValuation.clear_old_logs(days=1)
logs = frappe.get_all("Repost Item Valuation", filters={"status": "Skipped"})
- self.assertTrue(len(logs) == 0)
+ self.assertEqual(len(logs), 0)
def test_create_item_wise_repost_item_valuation_entries(self):
pr = make_purchase_receipt(
@@ -379,13 +379,13 @@ class TestRepostItemValuation(ERPNextTestSuite, StockTestMixin):
get_multiple_items=True,
)
- self.assertTrue(pr.docstatus == 1)
+ self.assertEqual(pr.docstatus, 1)
self.assertFalse(frappe.db.exists("Repost Item Valuation", {"voucher_no": pr.name}))
pr.load_from_db()
pr.cancel()
- self.assertTrue(pr.docstatus == 2)
+ self.assertEqual(pr.docstatus, 2)
self.assertTrue(frappe.db.exists("Repost Item Valuation", {"voucher_no": pr.name}))
def test_repost_item_valuation_for_closing_stock_balance(self):
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
index 43f67c64eb0..daf978c8c6c 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
@@ -2268,25 +2268,6 @@ def update_serial_batch_no_ledgers(bundle, entries, child_row, parent_doc, wareh
return doc
-@frappe.whitelist()
-def update_serial_or_batch(bundle_id: str, serial_no: str | None = None, batch_no: str | None = None):
- if batch_no and not serial_no:
- if qty := frappe.db.get_value(
- "Serial and Batch Entry", {"parent": bundle_id, "batch_no": batch_no}, "qty"
- ):
- frappe.db.set_value(
- "Serial and Batch Entry", {"parent": bundle_id, "batch_no": batch_no}, "qty", qty + 1
- )
- return
-
- doc = frappe.get_cached_doc("Serial and Batch Bundle", bundle_id)
- if not serial_no and not batch_no:
- return
-
- doc.append("entries", {"serial_no": serial_no, "batch_no": batch_no, "qty": 1})
- doc.save(ignore_permissions=True)
-
-
def get_serial_and_batch_ledger(**kwargs):
kwargs = frappe._dict(kwargs)
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py
index 37d4a45f954..9939df10835 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py
@@ -1070,11 +1070,11 @@ class TestSerialandBatchBundle(ERPNextTestSuite):
se.remove(se.items[1])
se.save()
- self.assertTrue(len(se.items) == 1)
+ self.assertEqual(len(se.items), 1)
se.submit()
bundle_doc.reload()
- self.assertTrue(bundle_doc.docstatus == 0)
+ self.assertEqual(bundle_doc.docstatus, 0)
self.assertRaises(frappe.ValidationError, bundle_doc.submit)
def test_reference_voucher_on_cancel(self):
diff --git a/erpnext/stock/doctype/shipment/test_shipment.py b/erpnext/stock/doctype/shipment/test_shipment.py
index 0ee89f62ee8..045df06880f 100644
--- a/erpnext/stock/doctype/shipment/test_shipment.py
+++ b/erpnext/stock/doctype/shipment/test_shipment.py
@@ -5,7 +5,7 @@ from datetime import date, timedelta
import frappe
-from erpnext.stock.doctype.delivery_note.delivery_note import make_shipment
+from erpnext.stock.doctype.delivery_note.mapper import make_shipment
from erpnext.tests.utils import ERPNextTestSuite
diff --git a/erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py b/erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py
index eff86d41c55..9ac9280f056 100644
--- a/erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py
+++ b/erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py
@@ -137,8 +137,7 @@ class StockClosingEntry(Document):
attached_file = frappe.get_doc("File", attachment.name)
data = gzip.decompress(attached_file.get_content())
- if data := json.loads(data.decode("utf-8")):
- data = data
+ data = json.loads(data.decode("utf-8"))
return parse_json(data)
diff --git a/erpnext/stock/doctype/stock_entry/services/__init__.py b/erpnext/stock/doctype/stock_entry/services/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/stock/doctype/stock_entry/services/gl_composer.py b/erpnext/stock/doctype/stock_entry/services/gl_composer.py
new file mode 100644
index 00000000000..4788346833f
--- /dev/null
+++ b/erpnext/stock/doctype/stock_entry/services/gl_composer.py
@@ -0,0 +1,157 @@
+# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+import frappe
+from frappe import _
+from frappe.utils import flt
+
+from erpnext.accounts.general_ledger import process_gl_map
+from erpnext.accounts.utils import get_account_currency
+from erpnext.stock.services.base_stock_gl_composer import BaseStockGLComposer
+
+
+class StockEntryGLComposer(BaseStockGLComposer):
+ """GL composer for Stock Entry.
+
+ Extends the base stock GL loop with additional-cost entries (from the
+ ``additional_costs`` child table) and landed-cost voucher adjustments.
+ """
+
+ def compose(self, inventory_account_map: dict | None = None) -> list:
+ doc = self.doc
+ gl_entries = super().compose(inventory_account_map)
+
+ if doc.purpose in ("Repack", "Manufacture"):
+ total_basic_amount = sum(flt(t.basic_amount) for t in doc.get("items") if t.is_finished_item)
+ else:
+ total_basic_amount = sum(flt(t.basic_amount) for t in doc.get("items") if t.t_warehouse)
+
+ divide_based_on = total_basic_amount
+ if doc.get("additional_costs") and not total_basic_amount:
+ divide_based_on = sum(item.qty for item in doc.get("items"))
+
+ item_account_wise_additional_cost = self._build_additional_cost_per_item_account(
+ total_basic_amount, divide_based_on
+ )
+ if item_account_wise_additional_cost:
+ self._append_additional_cost_gl_entries(gl_entries, item_account_wise_additional_cost)
+
+ self._append_lcv_gl_entries(gl_entries, inventory_account_map)
+
+ return process_gl_map(gl_entries, from_repost=frappe.flags.through_repost_item_valuation)
+
+ def _build_additional_cost_per_item_account(
+ self, total_basic_amount: float, divide_based_on: float
+ ) -> dict:
+ doc = self.doc
+ item_account_wise_additional_cost = {}
+
+ for t in doc.get("additional_costs"):
+ for d in doc.get("items"):
+ if doc.purpose in ("Repack", "Manufacture") and not d.is_finished_item:
+ continue
+ elif not d.t_warehouse:
+ continue
+
+ item_account_wise_additional_cost.setdefault((d.item_code, d.name), {})
+ item_account_wise_additional_cost[(d.item_code, d.name)].setdefault(
+ t.expense_account, {"amount": 0.0, "base_amount": 0.0}
+ )
+
+ multiply_based_on = d.basic_amount if total_basic_amount else d.qty
+ entry = item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]
+ entry["amount"] += flt(t.amount * multiply_based_on) / divide_based_on
+ entry["base_amount"] += flt(t.base_amount * multiply_based_on) / divide_based_on
+
+ return item_account_wise_additional_cost
+
+ def _append_additional_cost_gl_entries(
+ self, gl_entries: list, item_account_wise_additional_cost: dict
+ ) -> None:
+ doc = self.doc
+ for d in doc.get("items"):
+ for account, amount in item_account_wise_additional_cost.get((d.item_code, d.name), {}).items():
+ if not amount:
+ continue
+
+ gl_entries.append(
+ self.get_gl_dict(
+ {
+ "account": account,
+ "against": d.expense_account,
+ "cost_center": d.cost_center,
+ "remarks": doc.get("remarks") or _("Accounting Entry for Stock"),
+ "credit_in_account_currency": flt(amount["amount"]),
+ "credit": flt(amount["base_amount"]),
+ },
+ item=d,
+ )
+ )
+
+ gl_entries.append(
+ self.get_gl_dict(
+ {
+ "account": d.expense_account,
+ "against": account,
+ "cost_center": d.cost_center,
+ "remarks": doc.get("remarks") or _("Accounting Entry for Stock"),
+ "credit": -1 * amount["base_amount"],
+ },
+ item=d,
+ )
+ )
+
+ def _append_lcv_gl_entries(self, gl_entries: list, inventory_account_map: dict) -> None:
+ doc = self.doc
+ landed_cost_entries = doc.get_item_account_wise_lcv_entries()
+ if not landed_cost_entries:
+ return
+
+ for item in doc.get("items"):
+ if item.s_warehouse:
+ continue
+
+ if (item.item_code, item.name) in landed_cost_entries:
+ for account, amount in landed_cost_entries[(item.item_code, item.name)].items():
+ account_currency = get_account_currency(account)
+ credit_amount = (
+ flt(amount["base_amount"])
+ if (amount["base_amount"] or account_currency != doc.company_currency)
+ else flt(amount["amount"])
+ )
+
+ _inv_dict = doc.get_inventory_account_dict(item, inventory_account_map, "t_warehouse")
+ gl_entries.append(
+ self.get_gl_dict(
+ {
+ "account": account,
+ "against": _inv_dict["account"],
+ "cost_center": item.cost_center,
+ "debit": 0.0,
+ "credit": credit_amount,
+ "remarks": _("Accounting Entry for LCV in Stock Entry {0}").format(doc.name),
+ "credit_in_account_currency": flt(amount["amount"]),
+ "account_currency": account_currency,
+ "project": item.project,
+ },
+ item=item,
+ )
+ )
+
+ account_currency = get_account_currency(item.expense_account)
+ gl_entries.append(
+ self.get_gl_dict(
+ {
+ "account": item.expense_account,
+ "against": _inv_dict["account"],
+ "cost_center": item.cost_center,
+ "debit": 0.0,
+ "credit": credit_amount * -1,
+ "remarks": _("Accounting Entry for LCV in Stock Entry {0}").format(doc.name),
+ "debit_in_account_currency": flt(amount["amount"]),
+ "account_currency": account_currency,
+ "project": item.project,
+ },
+ item=item,
+ )
+ )
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index 3e5a8a10a8d..e8c341a3c3b 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -384,7 +384,7 @@ frappe.ui.form.on("Stock Entry", {
async (data) => {
if (frm.doc.work_order) {
let stock_entry = await frappe.xcall(
- "erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry",
+ "erpnext.manufacturing.doctype.work_order.mapper.make_stock_entry",
{
work_order_id: frm.doc.work_order,
purpose: "Disassemble",
@@ -424,7 +424,7 @@ frappe.ui.form.on("Stock Entry", {
__("Purchase Invoice"),
function () {
erpnext.utils.map_current_doc({
- method: "erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_stock_entry",
+ method: "erpnext.accounts.doctype.purchase_invoice.mapper.make_stock_entry",
source_doctype: "Purchase Invoice",
target: frm,
date_field: "posting_date",
@@ -449,7 +449,7 @@ frappe.ui.form.on("Stock Entry", {
];
const depends_on_condition = "eval:doc.material_request_type==='Customer Provided'";
const d = erpnext.utils.map_current_doc({
- method: "erpnext.stock.doctype.material_request.material_request.make_stock_entry",
+ method: "erpnext.stock.doctype.material_request.mapper.make_stock_entry",
source_doctype: "Material Request",
target: frm,
date_field: "schedule_date",
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 01c15b5193c..1574efa67e9 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -6,38 +6,27 @@ import json
from collections import defaultdict
import frappe
-from frappe import _, bold
+from frappe import _
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
-from frappe.query_builder import DocType
-from frappe.query_builder.functions import Max, Sum
+from frappe.query_builder.functions import Sum
from frappe.utils import (
cint,
- comma_or,
cstr,
flt,
- format_time,
- formatdate,
get_link_to_form,
- getdate,
nowdate,
)
import erpnext
-from erpnext.accounts.general_ledger import process_gl_map
-from erpnext.accounts.utils import get_account_currency
from erpnext.buying.utils import check_on_hold_or_closed_status
from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals
from erpnext.manufacturing.doctype.bom.bom import (
- add_additional_cost,
get_op_cost_from_sub_assemblies,
- get_secondary_items_from_sub_assemblies,
validate_bom_no,
)
from erpnext.setup.doctype.brand.brand import get_brand_defaults
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
-from erpnext.stock.doctype.item.item import get_item_defaults
-from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.get_item_details import (
ItemDetailsCtx,
get_barcode_data,
@@ -45,12 +34,6 @@ from erpnext.stock.get_item_details import (
get_conversion_factor,
get_default_cost_center,
)
-from erpnext.stock.serial_batch_bundle import (
- SerialBatchCreation,
- get_batch_nos,
- get_empty_batches_based_work_order,
- get_serial_or_batch_items,
-)
from erpnext.stock.stock_ledger import get_previous_sle, get_valuation_rate
from erpnext.stock.utils import get_incoming_rate
@@ -1050,137 +1033,9 @@ class StockEntry(StockController, SubcontractingInwardController):
sl_entries.append(sle)
def get_gl_entries(self, inventory_account_map):
- gl_entries = super().get_gl_entries(inventory_account_map)
+ from erpnext.stock.doctype.stock_entry.services.gl_composer import StockEntryGLComposer
- if self.purpose in ("Repack", "Manufacture"):
- total_basic_amount = sum(flt(t.basic_amount) for t in self.get("items") if t.is_finished_item)
- else:
- total_basic_amount = sum(flt(t.basic_amount) for t in self.get("items") if t.t_warehouse)
-
- divide_based_on = total_basic_amount
- if self.get("additional_costs") and not total_basic_amount:
- divide_based_on = sum(item.qty for item in self.get("items"))
-
- item_account_wise_additional_cost = self._build_additional_cost_per_item_account(
- total_basic_amount, divide_based_on
- )
-
- if item_account_wise_additional_cost:
- self._append_additional_cost_gl_entries(gl_entries, item_account_wise_additional_cost)
-
- self.set_gl_entries_for_landed_cost_voucher(gl_entries, inventory_account_map)
- return process_gl_map(gl_entries, from_repost=frappe.flags.through_repost_item_valuation)
-
- def _build_additional_cost_per_item_account(self, total_basic_amount, divide_based_on):
- item_account_wise_additional_cost = {}
-
- for t in self.get("additional_costs"):
- for d in self.get("items"):
- if self.purpose in ("Repack", "Manufacture") and not d.is_finished_item:
- continue
- elif not d.t_warehouse:
- continue
-
- item_account_wise_additional_cost.setdefault((d.item_code, d.name), {})
- item_account_wise_additional_cost[(d.item_code, d.name)].setdefault(
- t.expense_account, {"amount": 0.0, "base_amount": 0.0}
- )
-
- multiply_based_on = d.basic_amount if total_basic_amount else d.qty
- entry = item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]
- entry["amount"] += flt(t.amount * multiply_based_on) / divide_based_on
- entry["base_amount"] += flt(t.base_amount * multiply_based_on) / divide_based_on
-
- return item_account_wise_additional_cost
-
- def _append_additional_cost_gl_entries(self, gl_entries, item_account_wise_additional_cost):
- for d in self.get("items"):
- for account, amount in item_account_wise_additional_cost.get((d.item_code, d.name), {}).items():
- if not amount:
- continue
-
- gl_entries.append(
- self.get_gl_dict(
- {
- "account": account,
- "against": d.expense_account,
- "cost_center": d.cost_center,
- "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "credit_in_account_currency": flt(amount["amount"]),
- "credit": flt(amount["base_amount"]),
- },
- item=d,
- )
- )
-
- gl_entries.append(
- self.get_gl_dict(
- {
- "account": d.expense_account,
- "against": account,
- "cost_center": d.cost_center,
- "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "credit": -1 * amount["base_amount"], # negative credit instead of debit
- },
- item=d,
- )
- )
-
- def set_gl_entries_for_landed_cost_voucher(self, gl_entries, inventory_account_map):
- landed_cost_entries = self.get_item_account_wise_lcv_entries()
- if not landed_cost_entries:
- return
-
- for item in self.get("items"):
- if item.s_warehouse:
- continue
-
- if (item.item_code, item.name) in landed_cost_entries:
- for account, amount in landed_cost_entries[(item.item_code, item.name)].items():
- account_currency = get_account_currency(account)
- credit_amount = (
- flt(amount["base_amount"])
- if (amount["base_amount"] or account_currency != self.company_currency)
- else flt(amount["amount"])
- )
-
- _inv_dict = self.get_inventory_account_dict(item, inventory_account_map, "t_warehouse")
- gl_entries.append(
- self.get_gl_dict(
- {
- "account": account,
- "against": _inv_dict["account"],
- "cost_center": item.cost_center,
- "debit": 0.0,
- "credit": credit_amount,
- "remarks": _("Accounting Entry for LCV in Stock Entry {0}").format(self.name),
- "credit_in_account_currency": flt(amount["amount"]),
- "account_currency": account_currency,
- "project": item.project,
- },
- item=item,
- )
- )
-
- account_currency = get_account_currency(item.expense_account)
-
- # credit amount in negative to knock off the debit entry
- gl_entries.append(
- self.get_gl_dict(
- {
- "account": item.expense_account,
- "against": _inv_dict["account"],
- "cost_center": item.cost_center,
- "debit": 0.0,
- "credit": credit_amount * -1,
- "remarks": _("Accounting Entry for LCV in Stock Entry {0}").format(self.name),
- "debit_in_account_currency": flt(amount["amount"]),
- "account_currency": account_currency,
- "project": item.project,
- },
- item=item,
- )
- )
+ return StockEntryGLComposer(self).compose(inventory_account_map)
@property
def pro_doc(self):
@@ -1190,6 +1045,10 @@ class StockEntry(StockController, SubcontractingInwardController):
return getattr(self, "_wo_doc", None)
def make_stock_reserve_for_wip_and_fg(self):
+ from erpnext.manufacturing.doctype.work_order.services.stock_reservation import (
+ StockReservationService,
+ )
+
if self.is_stock_reserve_for_work_order():
pro_doc = frappe.get_doc("Work Order", self.work_order)
if (
@@ -1201,7 +1060,7 @@ class StockEntry(StockController, SubcontractingInwardController):
):
return
- pro_doc.set_reserved_qty_for_wip_and_fg(self)
+ StockReservationService(pro_doc).set_reserved_qty_for_wip_and_fg(self)
def reserve_stock_for_subcontracting(self):
if self.purpose == "Send to Subcontractor" and frappe.get_value(
@@ -1228,6 +1087,10 @@ class StockEntry(StockController, SubcontractingInwardController):
)
def cancel_stock_reserve_for_wip_and_fg(self):
+ from erpnext.manufacturing.doctype.work_order.services.stock_reservation import (
+ StockReservationService,
+ )
+
if self.is_stock_reserve_for_work_order():
pro_doc = frappe.get_doc("Work Order", self.work_order)
if (
@@ -1237,7 +1100,7 @@ class StockEntry(StockController, SubcontractingInwardController):
):
return
- pro_doc.cancel_reserved_qty_for_wip_and_fg(self)
+ StockReservationService(pro_doc).cancel_reserved_qty_for_wip_and_fg(self)
def is_stock_reserve_for_work_order(self):
if (
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_handler/manufacturing.py b/erpnext/stock/doctype/stock_entry/stock_entry_handler/manufacturing.py
index 648b91ab9d4..6ce37f7c4ef 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry_handler/manufacturing.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry_handler/manufacturing.py
@@ -7,10 +7,13 @@ from frappe.query_builder.functions import Sum
from frappe.utils import ceil, cint, flt, get_link_to_form
from erpnext.manufacturing.doctype.bom.bom import add_additional_cost
+from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.serial_batch_bundle import (
SerialBatchCreation,
get_batch_nos,
+ get_batches_from_bundle,
get_empty_batches_based_work_order,
+ get_serial_nos_from_bundle,
)
from .base import BaseStockEntry
@@ -171,25 +174,30 @@ class BaseManufactureStockEntry(BaseStockEntry):
else:
self.doc.append("items", item_details)
- def set_serial_nos_for_finished_good(self, item_details):
+ def set_serial_nos_for_finished_good(self, item_details, existing_row=None):
serial_nos = self.get_available_serial_nos_for_fg(item_details.item_code)
- if serial_nos:
- row = frappe._dict({"serial_nos": serial_nos[0 : cint(item_details.qty)]})
+ if not serial_nos:
+ return
- _id = create_serial_and_batch_bundle(
- self.doc,
- row,
- frappe._dict(
- {
- "item_code": item_details.item_code,
- "warehouse": item_details.t_warehouse,
- }
- ),
- )
+ row = frappe._dict({"serial_nos": serial_nos[0 : cint(item_details.qty)]})
+ _id = create_serial_and_batch_bundle(
+ self.doc,
+ row,
+ frappe._dict(
+ {
+ "item_code": item_details.item_code,
+ "warehouse": item_details.t_warehouse,
+ }
+ ),
+ )
+
+ if existing_row:
+ existing_row.serial_and_batch_bundle = _id
+ existing_row.use_serial_batch_fields = 0
+ else:
item_details.serial_and_batch_bundle = _id
item_details.use_serial_batch_fields = 0
-
self.doc.append("items", item_details)
def get_available_serial_nos_for_fg(self, item_code) -> list[str]:
@@ -205,22 +213,23 @@ class BaseManufactureStockEntry(BaseStockEntry):
order_by="creation asc",
)
- def set_batchwise_finished_goods(self, item_details):
- batches = get_empty_batches_based_work_order(self.doc.work_order, self.doc.pro_doc.production_item)
+ def set_batchwise_finished_goods(self, item_details, existing_row=None):
+ batches = get_empty_batches_based_work_order(self.doc.work_order, self.wo_doc.production_item)
if not batches:
- self.doc.append("items", item_details)
+ if not existing_row:
+ self.doc.append("items", item_details)
else:
- self.add_batchwise_finished_good(batches, item_details)
+ self.add_batchwise_finished_good(batches, item_details, existing_row=existing_row)
- def add_batchwise_finished_good(self, batches, item_details):
+ def add_batchwise_finished_good(self, batches, item_details, existing_row=None):
qty = flt(self.doc.fg_completed_qty)
row = frappe._dict({"batches_to_be_consume": defaultdict(float)})
self.update_batches_to_be_consume(batches, row, qty)
if row.batches_to_be_consume:
- self._link_fg_bundle_and_append(item_details, row)
+ self._link_fg_bundle_and_append(item_details, row, existing_row=existing_row)
- def _link_fg_bundle_and_append(self, item_details, row):
+ def _link_fg_bundle_and_append(self, item_details, row, existing_row=None):
_id = create_serial_and_batch_bundle(
self.doc,
row,
@@ -228,8 +237,13 @@ class BaseManufactureStockEntry(BaseStockEntry):
{"item_code": self.wo_doc.production_item, "warehouse": item_details.get("t_warehouse")}
),
)
- item_details["serial_and_batch_bundle"] = _id
- self.doc.append("items", item_details)
+ if existing_row:
+ existing_row.serial_and_batch_bundle = _id
+ existing_row.use_serial_batch_fields = 0
+ else:
+ item_details["serial_and_batch_bundle"] = _id
+ item_details["use_serial_batch_fields"] = 0
+ self.doc.append("items", item_details)
def update_batches_to_be_consume(self, batches, row, qty):
qty_to_be_consumed = qty
@@ -259,6 +273,81 @@ class ManufactureStockEntry(BaseManufactureStockEntry):
self.validate_warehouse()
self.validate_raw_materials_exists()
self.validate_component_and_quantities()
+ self.validate_finished_good_serial_batch_for_work_order()
+
+ def validate_finished_good_serial_batch_for_work_order(self):
+ if not (
+ self.doc.work_order
+ and self.wo_doc
+ and self.wo_doc.track_semi_finished_goods != 1
+ and cint(
+ frappe.db.get_single_value(
+ "Manufacturing Settings", "make_serial_no_batch_from_work_order", cache=True
+ )
+ )
+ and (self.wo_doc.has_serial_no or self.wo_doc.has_batch_no)
+ ):
+ return
+
+ for row in self.doc.items:
+ if not row.is_finished_item:
+ continue
+
+ if self.check_invalid_serial_batch_nos_for_finished_good_item(row):
+ self.reset_serial_batch_on_fg_row(row)
+ frappe.msgprint(
+ _(
+ "Row {0}: Serial/Batch has been reset to values linked with Work Order {1}"
+ " because the previously selected serial/batch does not belong to this Work Order."
+ ).format(row.idx, frappe.bold(self.doc.work_order))
+ )
+
+ def check_invalid_serial_batch_nos_for_finished_good_item(self, row) -> bool:
+ if self.wo_doc.has_serial_no:
+ serial_nos = get_serial_nos(row.serial_no) if row.serial_no else []
+ if not serial_nos and row.serial_and_batch_bundle:
+ serial_nos = get_serial_nos_from_bundle(row.serial_and_batch_bundle)
+ if serial_nos:
+ valid_serial_nos = frappe.get_all(
+ "Serial No",
+ filters={"name": ("in", serial_nos), "work_order": self.doc.work_order},
+ pluck="name",
+ )
+ return bool(set(serial_nos) - set(valid_serial_nos))
+ else:
+ return True
+
+ if self.wo_doc.has_batch_no:
+ batch_nos = [row.batch_no] if row.batch_no else []
+ if not batch_nos and row.serial_and_batch_bundle:
+ batch_nos = list(get_batches_from_bundle(row.serial_and_batch_bundle).keys())
+ if batch_nos:
+ valid_batch_nos = frappe.get_all(
+ "Batch",
+ filters={"name": ("in", batch_nos), "reference_name": self.doc.work_order},
+ pluck="name",
+ )
+ return bool(set(batch_nos) - set(valid_batch_nos))
+ else:
+ return True
+
+ def reset_serial_batch_on_fg_row(self, row):
+ item_details = frappe._dict(
+ {
+ "item_code": row.item_code,
+ "t_warehouse": row.t_warehouse,
+ "qty": row.qty,
+ }
+ )
+
+ row.serial_no = None
+ row.batch_no = None
+ row.serial_and_batch_bundle = None
+
+ if self.wo_doc.has_serial_no:
+ self.set_serial_nos_for_finished_good(item_details, existing_row=row)
+ elif self.wo_doc.has_batch_no:
+ self.set_batchwise_finished_goods(item_details, existing_row=row)
def set_job_card_data(self):
if self.doc.job_card and not self.doc.work_order:
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index c30d1f76de3..f8a5d6bda32 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -13,7 +13,7 @@ from erpnext.stock.doctype.item.test_item import (
make_item_variant,
set_item_variant_settings,
)
-from erpnext.stock.doctype.material_request.material_request import (
+from erpnext.stock.doctype.material_request.mapper import (
make_in_transit_stock_entry,
)
from erpnext.stock.doctype.material_request.test_material_request import (
@@ -184,7 +184,7 @@ class TestStockEntry(ERPNextTestSuite):
for d in mr.items:
items.append(d.item_code)
- self.assertTrue(item_code in items)
+ self.assertIn(item_code, items)
def test_add_to_transit_entry(self):
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
@@ -830,7 +830,7 @@ class TestStockEntry(ERPNextTestSuite):
frappe.db.set_single_value("Stock Settings", "stock_frozen_upto_days", 0)
def test_work_order(self):
- from erpnext.manufacturing.doctype.work_order.work_order import (
+ from erpnext.manufacturing.doctype.work_order.mapper import (
make_stock_entry as _make_stock_entry,
)
@@ -868,7 +868,7 @@ class TestStockEntry(ERPNextTestSuite):
@ERPNextTestSuite.change_settings("Manufacturing Settings", {"material_consumption": 1})
def test_work_order_manufacture_with_material_consumption(self):
- from erpnext.manufacturing.doctype.work_order.work_order import (
+ from erpnext.manufacturing.doctype.work_order.mapper import (
make_stock_entry as _make_stock_entry,
)
@@ -949,11 +949,11 @@ class TestStockEntry(ERPNextTestSuite):
work_order.insert()
work_order.submit()
- from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry
+ from erpnext.manufacturing.doctype.work_order.mapper import make_stock_entry
stock_entry = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1))
stock_entry.insert()
- self.assertTrue("_Test Variant Item-S" in [d.item_code for d in stock_entry.items])
+ self.assertIn("_Test Variant Item-S", [d.item_code for d in stock_entry.items])
def test_nagative_stock_for_batch(self):
item = make_item(
@@ -992,7 +992,7 @@ class TestStockEntry(ERPNextTestSuite):
self.assertRaises(frappe.ValidationError, ste.submit)
def test_quality_check_for_secondary_item(self):
- from erpnext.manufacturing.doctype.work_order.work_order import (
+ from erpnext.manufacturing.doctype.work_order.mapper import (
make_stock_entry as _make_stock_entry,
)
@@ -1441,7 +1441,7 @@ class TestStockEntry(ERPNextTestSuite):
def test_mapped_stock_entry(self):
"Check if rate and stock details are populated in mapped SE given warehouse."
- from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_stock_entry
+ from erpnext.stock.doctype.purchase_receipt.mapper import make_stock_entry
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
item_code = "_TestMappedItem"
@@ -2405,7 +2405,7 @@ class TestStockEntry(ERPNextTestSuite):
)
def test_validation_as_per_bom_with_continuous_raw_material_consumption(self):
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
- from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry as _make_stock_entry
+ from erpnext.manufacturing.doctype.work_order.mapper import make_stock_entry as _make_stock_entry
from erpnext.manufacturing.doctype.work_order.work_order import make_work_order
fg_item = make_item("_Mobiles", properties={"is_stock_item": 1}).name
@@ -2796,7 +2796,7 @@ class TestStockEntryCoverage(ERPNextTestSuite):
def test_get_available_materials_tracks_transferred_qty(self):
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
- from erpnext.manufacturing.doctype.work_order.work_order import (
+ from erpnext.manufacturing.doctype.work_order.mapper import (
make_stock_entry as _make_stock_entry,
)
from erpnext.stock.doctype.stock_entry.stock_entry_handler.disassemble import (
@@ -2841,7 +2841,7 @@ class TestStockEntryCoverage(ERPNextTestSuite):
def test_get_available_materials_reduces_qty_after_consumption(self):
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
- from erpnext.manufacturing.doctype.work_order.work_order import (
+ from erpnext.manufacturing.doctype.work_order.mapper import (
make_stock_entry as _make_stock_entry,
)
from erpnext.stock.doctype.stock_entry.stock_entry_handler.disassemble import (
@@ -2886,6 +2886,88 @@ class TestStockEntryCoverage(ERPNextTestSuite):
if key in materials:
self.assertEqual(materials[key].qty, 0)
+ @ERPNextTestSuite.change_settings("Manufacturing Settings", {"make_serial_no_batch_from_work_order": 1})
+ @ERPNextTestSuite.change_settings("Global Defaults", {"default_company": "_Test Company"})
+ def test_validate_fg_resets_invalid_serial_no_on_manufacture(self):
+ from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
+ from erpnext.manufacturing.doctype.work_order.mapper import (
+ make_stock_entry as _make_stock_entry,
+ )
+ from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
+
+ fg_item = "_FG Serial No Item"
+ rm_item = "RM for serial item"
+ create_nested_bom({fg_item: {rm_item: {}}}, prefix="")
+
+ item = frappe.get_doc("Item", fg_item)
+ item.has_serial_no = 1
+ item.serial_no_series = "FSNI-.####"
+ item.save()
+
+ make_stock_entry(item_code=rm_item, target="_Test Warehouse - _TC", qty=20, basic_rate=100)
+
+ wo1 = make_wo_order_test_record(item=fg_item, qty=2, skip_transfer=True)
+ wo2 = make_wo_order_test_record(item=fg_item, qty=2, skip_transfer=True)
+ wo1_serial_nos = frappe.get_all("Serial No", filters={"work_order": wo1.name}, pluck="name")
+ wo2_serial_nos = frappe.get_all("Serial No", filters={"work_order": wo2.name}, pluck="name")
+
+ se = frappe.get_doc(_make_stock_entry(wo1.name, "Manufacture", 2))
+ for row in se.items:
+ if row.is_finished_item:
+ row.serial_no = wo2_serial_nos[0]
+ row.serial_and_batch_bundle = None
+
+ se.save()
+
+ for row in se.items:
+ if row.is_finished_item:
+ self.assertIsNone(row.serial_no)
+ self.assertTrue(row.serial_and_batch_bundle)
+ for sn in get_serial_nos_from_bundle(row.serial_and_batch_bundle):
+ self.assertIn(sn, wo1_serial_nos)
+
+ @ERPNextTestSuite.change_settings("Manufacturing Settings", {"make_serial_no_batch_from_work_order": 1})
+ @ERPNextTestSuite.change_settings("Global Defaults", {"default_company": "_Test Company"})
+ def test_validate_fg_resets_invalid_batch_no_on_manufacture(self):
+ from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
+ from erpnext.manufacturing.doctype.work_order.mapper import (
+ make_stock_entry as _make_stock_entry,
+ )
+ from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
+ from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
+
+ fg_item = "_FG Batch No Item"
+ rm_item = "RM for Batch Item"
+ create_nested_bom({fg_item: {rm_item: {}}}, prefix="")
+
+ item = frappe.get_doc("Item", fg_item)
+ item.has_batch_no = 1
+ item.create_new_batch = 1
+ item.batch_number_series = "FBNI-.####"
+ item.save()
+
+ make_stock_entry(item_code=rm_item, target="_Test Warehouse - _TC", qty=20, basic_rate=100)
+
+ wo1 = make_wo_order_test_record(item=fg_item, qty=2, skip_transfer=True)
+ wo2 = make_wo_order_test_record(item=fg_item, qty=2, skip_transfer=True)
+ wo1_batches = frappe.get_all("Batch", filters={"reference_name": wo1.name}, pluck="name")
+ wo2_batches = frappe.get_all("Batch", filters={"reference_name": wo2.name}, pluck="name")
+
+ se = frappe.get_doc(_make_stock_entry(wo1.name, "Manufacture", 2))
+ for row in se.items:
+ if row.is_finished_item:
+ row.batch_no = wo2_batches[0]
+ row.serial_and_batch_bundle = None
+
+ se.save()
+
+ for row in se.items:
+ if row.is_finished_item:
+ self.assertIsNone(row.batch_no)
+ self.assertTrue(row.serial_and_batch_bundle)
+ for bn in list(get_batches_from_bundle(row.serial_and_batch_bundle).keys()):
+ self.assertIn(bn, wo1_batches)
+
def make_serialized_item(self, **args):
args = frappe._dict(args)
diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py
index a09daf35634..75f8b8a68ed 100644
--- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py
+++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py
@@ -5,20 +5,15 @@ import frappe
from frappe import _, bold
from frappe.model.document import Document
from frappe.utils import (
- cint,
- cstr,
flt,
- format_time,
- formatdate,
get_link_to_form,
getdate,
- nowdate,
)
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
OpeningEntryAccountError,
)
-from erpnext.stock.stock_ledger import NegativeStockError, get_previous_sle, is_negative_stock_allowed
+from erpnext.stock.stock_ledger import get_previous_sle
class StockEntryDetail(Document):
diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
index a92e6401be4..f63b7de01c2 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
@@ -1494,7 +1494,7 @@ def create_purchase_receipt_entries_for_batchwise_item_valuation_test(pr_entry_l
def create_delivery_note_entries_for_batchwise_item_valuation_test(dn_entry_list):
- from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
+ from erpnext.selling.doctype.sales_order.mapper import make_delivery_note
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
dns = []
diff --git a/erpnext/stock/doctype/stock_reconciliation/services/__init__.py b/erpnext/stock/doctype/stock_reconciliation/services/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/stock/doctype/stock_reconciliation/services/gl_composer.py b/erpnext/stock/doctype/stock_reconciliation/services/gl_composer.py
new file mode 100644
index 00000000000..0ca408729f0
--- /dev/null
+++ b/erpnext/stock/doctype/stock_reconciliation/services/gl_composer.py
@@ -0,0 +1,20 @@
+# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from frappe import _, msgprint
+
+from erpnext.stock.services.base_stock_gl_composer import BaseStockGLComposer
+
+
+class StockReconciliationGLComposer(BaseStockGLComposer):
+ """GL composer for Stock Reconciliation.
+
+ SR carries its own expense_account and cost_center which are passed as
+ defaults into the base stock GL composition loop.
+ """
+
+ def compose(self, inventory_account_map: dict | None = None) -> list:
+ doc = self.doc
+ if not doc.cost_center:
+ msgprint(_("Please enter Cost Center"), raise_exception=1)
+ return super().compose(inventory_account_map, doc.expense_account, doc.cost_center)
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index f21f4da7174..388ab6793b8 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -975,10 +975,11 @@ class StockReconciliation(StockController):
return new_sl_entries
def get_gl_entries(self, inventory_account_map=None):
- if not self.cost_center:
- msgprint(_("Please enter Cost Center"), raise_exception=1)
+ from erpnext.stock.doctype.stock_reconciliation.services.gl_composer import (
+ StockReconciliationGLComposer,
+ )
- return super().get_gl_entries(inventory_account_map, self.expense_account, self.cost_center)
+ return StockReconciliationGLComposer(self).compose(inventory_account_map)
def validate_expense_account(self):
if not cint(erpnext.is_perpetual_inventory_enabled(self.company)):
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index 2ad1fb42f13..02cd0e63e4a 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -1040,7 +1040,7 @@ class TestStockReconciliation(ERPNextTestSuite, StockTestMixin):
)
batch1 = get_batch_from_bundle(se1.items[0].serial_and_batch_bundle)
- self.assertFalse(batch1 == batch)
+ self.assertNotEqual(batch1, batch)
sr.reload()
self.assertTrue(sr.items[0].serial_and_batch_bundle)
@@ -1418,7 +1418,7 @@ class TestStockReconciliation(ERPNextTestSuite, StockTestMixin):
sr.save()
self.assertEqual(sr.items[0].current_valuation_rate, 100)
self.assertEqual(sr.difference_amount, 100 * -1)
- self.assertTrue(sr.items[0].qty == 0)
+ self.assertEqual(sr.items[0].qty, 0)
def test_stock_reco_recalculate_qty_for_backdated_entry(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
@@ -1456,7 +1456,7 @@ class TestStockReconciliation(ERPNextTestSuite, StockTestMixin):
pluck="name",
)
- self.assertTrue(len(stock_ledgers) == 1)
+ self.assertEqual(len(stock_ledgers), 1)
se = make_stock_entry(
item_code=item_code,
@@ -1515,7 +1515,7 @@ class TestStockReconciliation(ERPNextTestSuite, StockTestMixin):
"status",
)
- self.assertTrue(status == "Active")
+ self.assertEqual(status, "Active")
sr = create_stock_reconciliation(
item_code=serial_item,
@@ -1534,7 +1534,7 @@ class TestStockReconciliation(ERPNextTestSuite, StockTestMixin):
"status",
)
- self.assertTrue(status == "Active")
+ self.assertEqual(status, "Active")
se = make_stock_entry(
item_code=serial_item,
@@ -1550,7 +1550,7 @@ class TestStockReconciliation(ERPNextTestSuite, StockTestMixin):
"status",
)
- self.assertFalse(status == "Active")
+ self.assertNotEqual(status, "Active")
sr.cancel()
@@ -1560,7 +1560,7 @@ class TestStockReconciliation(ERPNextTestSuite, StockTestMixin):
"status",
)
- self.assertFalse(status == "Active")
+ self.assertNotEqual(status, "Active")
def test_change_valuation_of_batch_using_backdated_stock_reco(self):
from erpnext.stock.doctype.batch.batch import get_batch_qty
diff --git a/erpnext/stock/doctype/stock_reposting_settings/test_stock_reposting_settings.py b/erpnext/stock/doctype/stock_reposting_settings/test_stock_reposting_settings.py
index 4d73ad62c05..b3c6aedb7a3 100644
--- a/erpnext/stock/doctype/stock_reposting_settings/test_stock_reposting_settings.py
+++ b/erpnext/stock/doctype/stock_reposting_settings/test_stock_reposting_settings.py
@@ -31,9 +31,9 @@ class TestStockRepostingSettings(ERPNextTestSuite):
frappe.db.set_single_value("Stock Reposting Settings", "notify_reposting_error_to_role", "")
users = get_recipients()
- self.assertFalse(user in users)
+ self.assertNotIn(user, users)
frappe.db.set_single_value("Stock Reposting Settings", "notify_reposting_error_to_role", role)
users = get_recipients()
- self.assertTrue(user in users)
+ self.assertIn(user, users)
diff --git a/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py
index d11f33992ea..cc125ea3b8e 100644
--- a/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py
+++ b/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py
@@ -6,7 +6,7 @@ from random import randint
import frappe
from frappe.utils import today
-from erpnext.selling.doctype.sales_order.sales_order import create_pick_list, make_delivery_note
+from erpnext.selling.doctype.sales_order.mapper import create_pick_list, make_delivery_note
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.stock_entry.stock_entry import StockEntry
@@ -525,9 +525,9 @@ class TestStockReservationEntry(ERPNextTestSuite):
},
)
def test_stock_reservation_from_purchase_receipt(self) -> None:
- from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
- from erpnext.selling.doctype.sales_order.sales_order import make_material_request
- from erpnext.stock.doctype.material_request.material_request import make_purchase_order
+ from erpnext.buying.doctype.purchase_order.mapper import make_purchase_receipt
+ from erpnext.selling.doctype.sales_order.mapper import make_material_request
+ from erpnext.stock.doctype.material_request.mapper import make_purchase_order
items_details = create_items()
create_material_receipt(items_details, self.warehouse, qty=10)
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.js b/erpnext/stock/doctype/stock_settings/stock_settings.js
index 00f91ed6b41..e6289b1cca8 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.js
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.js
@@ -13,6 +13,65 @@ frappe.ui.form.on("Stock Settings", {
frm.set_query("default_warehouse", filters);
frm.set_query("sample_retention_warehouse", filters);
+
+ if (!frm.naming_controller) frm.naming_controller = new erpnext.NamingSeriesController(frm);
+ const item_display = frm.doc.item_naming_by === "Naming Series";
+ const serial_and_batch_naming_display =
+ frm.doc.set_serial_and_batch_bundle_naming_based_on_naming_series;
+
+ frm.set_df_property("naming_series_details", "hidden", !item_display);
+ frm.set_df_property("configure", "hidden", !item_display);
+ frm.set_df_property("naming_series_preview", "hidden", !serial_and_batch_naming_display);
+ frm.set_df_property("configure_series", "hidden", !serial_and_batch_naming_display);
+
+ if (item_display) {
+ frm.naming_controller.load_master_series("Item", "naming_series_details");
+ } else {
+ frm.doc.naming_series_details = "";
+ }
+
+ if (serial_and_batch_naming_display) {
+ frm.naming_controller.load_master_series("Serial and Batch Bundle", "naming_series_preview");
+ } else {
+ frm.doc.naming_series_preview = "";
+ }
+
+ frm.naming_controller.render_table("transaction_naming_html", get_transactions(frm));
+ },
+
+ item_naming_by(frm) {
+ const display = frm.doc.item_naming_by === "Naming Series";
+ frm.set_df_property("naming_series_details", "hidden", !display);
+ frm.set_df_property("configure", "hidden", !display);
+
+ if (display) {
+ frm.naming_controller.load_master_series("Item", "naming_series_details");
+ } else {
+ frm.doc.naming_series_details = "";
+ frm.refresh_field("naming_series_details");
+ }
+
+ frm.naming_controller.render_table("transaction_naming_html", get_transactions(frm));
+ },
+
+ set_serial_and_batch_bundle_naming_based_on_naming_series(frm) {
+ const display = frm.doc.set_serial_and_batch_bundle_naming_based_on_naming_series;
+ frm.set_df_property("naming_series_preview", "hidden", !display);
+ frm.set_df_property("configure_series", "hidden", !display);
+ if (display) {
+ frm.naming_controller.load_master_series("Serial and Batch Bundle", "naming_series_preview");
+ } else {
+ frm.doc.naming_series_preview = "";
+ frm.refresh_field("naming_series_preview");
+ }
+ },
+
+ configure(frm) {
+ configure_naming_series(frm, "Item", "naming_series_details");
+ },
+
+ configure_series(frm) {
+ configure_naming_series(frm, "Serial and Batch Bundle", "naming_series_preview");
},
enable_serial_and_batch_no_for_item(frm) {
@@ -84,3 +143,29 @@ frappe.ui.form.on("Stock Settings", {
}
},
});
+
+function get_transactions(frm) {
+ const transactions = [
+ { label: __("Item"), doctype: "Item" },
+ { label: __("Stock Entry"), doctype: "Stock Entry" },
+ { label: __("Purchase Receipt"), doctype: "Purchase Receipt" },
+ { label: __("Delivery Note"), doctype: "Delivery Note" },
+ { label: __("Material Request"), doctype: "Material Request" },
+ { label: __("Pick List"), doctype: "Pick List" },
+ { label: __("Stock Reconciliation"), doctype: "Stock Reconciliation" },
+ { label: __("Serial and Batch Bundle"), doctype: "Serial and Batch Bundle" },
+ ];
+
+ if (frm.doc.item_naming_by !== "Naming Series") {
+ return transactions.filter((t) => t.doctype !== "Item");
+ }
+
+ return transactions;
+}
+
+function configure_naming_series(frm, doctype, fieldname) {
+ frm.naming_controller.show_naming_series_dialog(doctype, ({ naming_series_options }) => {
+ frm.doc[fieldname] = naming_series_options;
+ frm.refresh_field(fieldname);
+ });
+}
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json
index 0b298bd2839..fd6fb21adfb 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.json
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.json
@@ -8,52 +8,57 @@
"defaults_tab",
"item_defaults_section",
"item_naming_by",
- "valuation_method",
- "item_group",
+ "naming_series_details",
+ "configure",
"column_break_4",
- "default_warehouse",
- "sample_retention_warehouse",
- "stock_uom",
+ "item_group",
+ "valuation_method",
"price_list_defaults_section",
"auto_insert_price_list_rate_if_missing",
- "update_price_list_based_on",
- "column_break_12",
"update_existing_price_list_rate",
+ "update_price_list_based_on",
"conversion_factor_section",
+ "stock_uom",
"allow_to_edit_stock_uom_qty_for_sales",
- "column_break_lznj",
"allow_to_edit_stock_uom_qty_for_purchase",
- "section_break_ylhd",
"allow_uom_with_conversion_rate_defined_in_item",
+ "warehouse_defaults_section",
+ "default_warehouse",
+ "sample_retention_warehouse",
"stock_validations_tab",
+ "negative_stock_section",
+ "allow_negative_stock",
"section_break_9",
"over_delivery_receipt_allowance",
"mr_qty_allowance",
"over_picking_allowance",
"column_break_121",
"role_allowed_to_over_deliver_receive",
- "allow_negative_stock",
+ "display_data_formatting_section",
"show_barcode_field",
"clean_description_html",
+ "internal_transfer_rules_section",
"allow_internal_transfer_at_arms_length_price",
"validate_material_transfer_warehouses",
"serial_and_batch_item_settings_tab",
"enable_serial_and_batch_no_for_item",
"section_break_7",
- "allow_existing_serial_no",
- "do_not_use_batchwise_valuation",
- "auto_create_serial_and_batch_bundle_for_outward",
"pick_serial_and_batch_based_on",
- "column_break_mhzc",
- "disable_serial_no_and_batch_selector",
+ "allow_existing_serial_no",
"use_serial_batch_fields",
- "do_not_update_serial_batch_on_creation_of_auto_bundle",
- "allow_negative_stock_for_batch",
+ "disable_serial_no_and_batch_selector",
"section_break_gnhq",
- "set_serial_and_batch_bundle_naming_based_on_naming_series",
+ "allow_negative_stock_for_batch",
+ "do_not_use_batchwise_valuation",
"use_naming_series",
- "column_break_wslv",
"naming_series_prefix",
+ "auto_bundle_section",
+ "auto_create_serial_and_batch_bundle_for_outward",
+ "do_not_update_serial_batch_on_creation_of_auto_bundle",
+ "set_serial_and_batch_bundle_naming_based_on_naming_series",
+ "naming_series_preview",
+ "configure_series",
+ "column_break_pjkx",
"stock_reservation_tab",
"enable_stock_reservation",
"auto_reserve_stock",
@@ -64,23 +69,23 @@
"auto_reserve_serial_and_batch",
"quality_tab",
"quality_inspection_settings_section",
- "action_if_quality_inspection_is_not_submitted",
- "column_break_23",
- "action_if_quality_inspection_is_rejected",
- "section_break_uiau",
"allow_to_make_quality_inspection_after_purchase_or_delivery",
+ "action_if_quality_inspection_is_rejected",
+ "action_if_quality_inspection_is_not_submitted",
"stock_planning_tab",
"auto_material_request",
"auto_indent",
- "column_break_27",
"reorder_email_notify",
"stock_closing_tab",
"control_historical_stock_transactions_section",
- "stock_frozen_upto",
"stock_frozen_upto_days",
+ "stock_frozen_upto",
"column_break_26",
+ "stock_auth_role",
+ "section_break_kcvr",
"role_allowed_to_create_edit_back_dated_transactions",
- "stock_auth_role"
+ "document_naming_tab",
+ "transaction_naming_html"
],
"fields": [
{
@@ -92,6 +97,7 @@
"options": "Item Code\nNaming Series"
},
{
+ "documentation_url": "https://docs.frappe.io/erpnext/stock-settings#21-default-item-group",
"fieldname": "item_group",
"fieldtype": "Link",
"in_list_view": 1,
@@ -112,6 +118,7 @@
"options": "Warehouse"
},
{
+ "documentation_url": "https://docs.frappe.io/erpnext/retain-sample-stock",
"fieldname": "sample_retention_warehouse",
"fieldtype": "Link",
"label": "Sample Retention Warehouse",
@@ -138,38 +145,41 @@
"default": "Stop",
"fieldname": "action_if_quality_inspection_is_not_submitted",
"fieldtype": "Select",
- "label": "Action If Quality Inspection Is Not Submitted",
+ "label": "Action if Quality Inspection is not submitted",
"options": "Stop\nWarn"
},
{
"default": "1",
"fieldname": "show_barcode_field",
"fieldtype": "Check",
- "label": "Show Barcode Field in Stock Transactions"
+ "label": "Show barcode field in stock transactions"
},
{
"default": "1",
"fieldname": "clean_description_html",
"fieldtype": "Check",
- "label": "Convert Item Description to Clean HTML in Transactions"
+ "label": "Convert Item description to clean HTML in transactions"
},
{
"depends_on": "enable_serial_and_batch_no_for_item",
"fieldname": "section_break_7",
"fieldtype": "Section Break",
- "label": "Serial & Batch Item Settings"
+ "label": "Serial Item settings"
},
{
"default": "0",
+ "documentation_url": "https://docs.frappe.io/erpnext/stock-settings#71-auto-insert-price-list-rate-if-missing",
"fieldname": "auto_insert_price_list_rate_if_missing",
"fieldtype": "Check",
- "label": "Auto Insert Item Price If Missing"
+ "label": "Auto insert Item Price if missing"
},
{
"default": "0",
+ "description": "This can be enabled at specific Item level as well",
+ "documentation_url": "https://docs.frappe.io/erpnext/stock-settings#allow-negative-stock",
"fieldname": "allow_negative_stock",
"fieldtype": "Check",
- "label": "Allow Negative Stock"
+ "label": "Allow negative stock"
},
{
"fieldname": "auto_material_request",
@@ -178,41 +188,44 @@
},
{
"default": "0",
+ "description": "This option is useful if you want to ensure a constant supply of raw materials/products and avoid shortage. A Material Request will be raised automatically when stock reached the re-order level defined in the Item form.",
"fieldname": "auto_indent",
"fieldtype": "Check",
- "label": "Raise Material Request When Stock Reaches Re-order Level"
+ "label": "Raise Material Request when stock reaches re-order level"
},
{
"default": "0",
+ "description": "An email will be sent to notify the User with the role 'Purchase Manager' when an automatic Material Request is created.",
"fieldname": "reorder_email_notify",
"fieldtype": "Check",
- "label": "Notify by Email on Creation of Automatic Material Request"
+ "label": "Notify by email on creation of automatic Material Request"
},
{
"description": "No stock transactions can be created or modified before this date.",
"fieldname": "stock_frozen_upto",
"fieldtype": "Date",
- "label": "Stock Frozen Up To"
+ "label": "Stock frozen up to"
},
{
"description": "Stock transactions that are older than the mentioned days cannot be modified.",
"fieldname": "stock_frozen_upto_days",
"fieldtype": "Int",
- "label": "Freeze Stocks Older Than (Days)"
+ "label": "Freeze stocks older than (days)"
},
{
"depends_on": "eval:(doc.stock_frozen_upto || doc.stock_frozen_upto_days)",
"description": "The users with this Role are allowed to create/modify a stock transaction, even though the transaction is frozen.",
"fieldname": "stock_auth_role",
"fieldtype": "Link",
- "label": "Role Allowed to Edit Frozen Stock",
+ "label": "Role allowed to edit frozen stock",
"options": "Role"
},
{
"default": "0",
+ "description": "This will be applied if no naming series is configured in Item master",
"fieldname": "use_naming_series",
"fieldtype": "Check",
- "label": "Have Default Naming Series for Batch ID?"
+ "label": "Have default Naming Series for Batch ID?"
},
{
"default": "BATCH-",
@@ -225,7 +238,7 @@
"description": "If mentioned, the system will allow only the users with this Role to create or modify any stock transaction earlier than the latest stock transaction for a specific item and warehouse. If set as blank, it allows all users to create/edit back-dated transactions.",
"fieldname": "role_allowed_to_create_edit_back_dated_transactions",
"fieldtype": "Link",
- "label": "Role Allowed to Create/Edit Back-dated Transactions",
+ "label": "Role allowed to create/edit back-dated transactions",
"options": "Role"
},
{
@@ -239,11 +252,13 @@
},
{
"default": "0",
+ "description": "If enabled, users must enter Serial No. / Batch data manually instead of using the selector dialog.",
"fieldname": "disable_serial_no_and_batch_selector",
"fieldtype": "Check",
- "label": "Disable Serial No And Batch Selector"
+ "label": "Disable Serial No and Batch selector"
},
{
+ "depends_on": "eval: doc.over_delivery_receipt_allowance>0",
"description": "Users with this role are allowed to over deliver/receive against orders above the allowance percentage",
"fieldname": "role_allowed_to_over_deliver_receive",
"fieldtype": "Link",
@@ -258,40 +273,31 @@
{
"fieldname": "section_break_9",
"fieldtype": "Section Break",
- "label": "Stock Transactions Settings"
- },
- {
- "fieldname": "column_break_12",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "column_break_27",
- "fieldtype": "Column Break"
+ "label": "Quantity Tolerance"
},
{
"fieldname": "quality_inspection_settings_section",
- "fieldtype": "Section Break",
- "label": "Quality Inspection Settings"
+ "fieldtype": "Section Break"
},
{
"default": "Stop",
"fieldname": "action_if_quality_inspection_is_rejected",
"fieldtype": "Select",
- "label": "Action If Quality Inspection Is Rejected",
+ "label": "Action if Quality Inspection is rejected",
"options": "Stop\nWarn"
},
{
"description": "The percentage you are allowed to transfer more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed transfer 110 units.",
"fieldname": "mr_qty_allowance",
"fieldtype": "Float",
- "label": "Over Transfer Allowance"
+ "label": "Over Transfer Allowance (%)"
},
{
"default": "0",
"depends_on": "auto_insert_price_list_rate_if_missing",
"fieldname": "update_existing_price_list_rate",
"fieldtype": "Check",
- "label": "Update Existing Price List Rate"
+ "label": "Update existing Price List Rate"
},
{
"fieldname": "defaults_tab",
@@ -318,10 +324,6 @@
"fieldtype": "Tab Break",
"label": "Serial & Batch Item"
},
- {
- "fieldname": "column_break_23",
- "fieldtype": "Column Break"
- },
{
"fieldname": "price_list_defaults_section",
"fieldtype": "Section Break",
@@ -341,7 +343,7 @@
"description": "Allows to keep aside a specific quantity of inventory for a particular order.",
"fieldname": "enable_stock_reservation",
"fieldtype": "Check",
- "label": "Enable Stock Reservation"
+ "label": "Enable stock reservation"
},
{
"fieldname": "column_break_rx3e",
@@ -353,11 +355,7 @@
"description": "Partial stock can be reserved. For example, If you have a Sales Order of 100 units and the Available Stock is 90 units then a Stock Reservation Entry will be created for 90 units. ",
"fieldname": "allow_partial_reservation",
"fieldtype": "Check",
- "label": "Allow Partial Reservation"
- },
- {
- "fieldname": "column_break_mhzc",
- "fieldtype": "Column Break"
+ "label": "Allow partial reservation"
},
{
"default": "FIFO",
@@ -372,7 +370,7 @@
"default": "1",
"fieldname": "auto_create_serial_and_batch_bundle_for_outward",
"fieldtype": "Check",
- "label": "Auto Create Serial and Batch Bundle For Outward"
+ "label": "Auto create Serial and Batch Bundle for outward"
},
{
"default": "1",
@@ -380,7 +378,7 @@
"description": "Serial and Batch Nos will be auto-reserved based on
Pick Serial / Batch Based On",
"fieldname": "auto_reserve_serial_and_batch",
"fieldtype": "Check",
- "label": "Auto Reserve Serial and Batch Nos"
+ "label": "Auto reserve Serial and Batch Nos"
},
{
"fieldname": "serial_and_batch_reservation_section",
@@ -390,23 +388,21 @@
{
"fieldname": "conversion_factor_section",
"fieldtype": "Section Break",
- "label": "Stock UOM Quantity"
- },
- {
- "fieldname": "column_break_lznj",
- "fieldtype": "Column Break"
+ "label": "UOM Defaults"
},
{
"default": "0",
+ "documentation_url": "https://docs.frappe.io/erpnext/stock-settings#why-to-edit-stock-qty-qty-as-per-stock-uom",
"fieldname": "allow_to_edit_stock_uom_qty_for_sales",
"fieldtype": "Check",
- "label": "Allow to Edit Stock UOM Qty for Sales Documents"
+ "label": "Allow to edit stock UOM qty for Sales documents"
},
{
"default": "0",
+ "documentation_url": "https://docs.frappe.io/erpnext/stock-settings#why-to-edit-stock-qty-qty-as-per-stock-uom",
"fieldname": "allow_to_edit_stock_uom_qty_for_purchase",
"fieldtype": "Check",
- "label": "Allow to Edit Stock UOM Qty for Purchase Documents"
+ "label": "Allow to edit stock UOM qty for Purchase documents"
},
{
"default": "0",
@@ -414,41 +410,41 @@
"description": "Stock will be reserved on submission of
Purchase Receipt created against Material Request for Sales Order.",
"fieldname": "auto_reserve_stock_for_sales_order_on_purchase",
"fieldtype": "Check",
- "label": "Auto Reserve Stock for Sales Order on Purchase"
+ "label": "Auto reserve Stock for Sales Order on Purchase"
},
{
"default": "1",
"description": "On submission of the stock transaction, system will auto create the Serial and Batch Bundle based on the Serial No / Batch fields.",
"fieldname": "use_serial_batch_fields",
"fieldtype": "Check",
- "label": "Use Serial / Batch Fields"
+ "label": "Use Serial / Batch fields"
},
{
"default": "1",
"description": "If enabled, do not update serial / batch values in the stock transactions on creation of auto Serial \n / Batch Bundle. ",
"fieldname": "do_not_update_serial_batch_on_creation_of_auto_bundle",
"fieldtype": "Check",
- "label": "Do Not Update Serial / Batch on Creation of Auto Bundle"
+ "label": "Do not update Serial / Batch on creation of auto bundle"
},
{
"default": "0",
"description": "If enabled, the item rate won't adjust to the valuation rate during internal transfers, but accounting will still use the valuation rate. This will allow the user to specify a different rate for printing or taxation purposes.",
"fieldname": "allow_internal_transfer_at_arms_length_price",
"fieldtype": "Check",
- "label": "Allow Internal Transfers at Arm's Length Price"
+ "label": "Allow internal transfers at user-defined rate"
},
{
"default": "0",
"description": "If enabled, the system will use the moving average valuation method to calculate the valuation rate for the batched items and will not consider the individual batch-wise incoming rate.",
"fieldname": "do_not_use_batchwise_valuation",
"fieldtype": "Check",
- "label": "Do Not Use Batch-wise Valuation"
+ "label": "Do not use Batch-wise Valuation"
},
{
"description": "The percentage you are allowed to pick more items in the pick list than the ordered quantity.",
"fieldname": "over_picking_allowance",
"fieldtype": "Percent",
- "label": "Over Picking Allowance"
+ "label": "Over Picking Allowance (%)"
},
{
"default": "1",
@@ -461,7 +457,7 @@
"description": "Upon submission of the Sales Order, Work Order, or Production Plan, the system will automatically reserve the stock.",
"fieldname": "auto_reserve_stock",
"fieldtype": "Check",
- "label": "Auto Reserve Stock"
+ "label": "Auto reserve stock"
},
{
"default": "0",
@@ -472,44 +468,33 @@
{
"depends_on": "enable_serial_and_batch_no_for_item",
"fieldname": "section_break_gnhq",
- "fieldtype": "Section Break"
- },
- {
- "fieldname": "column_break_wslv",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "section_break_ylhd",
- "fieldtype": "Section Break"
+ "fieldtype": "Section Break",
+ "label": "Batch Item settings"
},
{
"default": "0",
"description": "If enabled, the system will allow selecting UOMs in sales and purchase transactions only if the conversion rate is set in the item master.",
"fieldname": "allow_uom_with_conversion_rate_defined_in_item",
"fieldtype": "Check",
- "label": "Allow UOM with Conversion Rate Defined in Item"
+ "label": "Allow UOM with conversion rate defined in Item"
},
{
"fieldname": "quality_tab",
"fieldtype": "Tab Break",
"label": "Quality"
},
- {
- "fieldname": "section_break_uiau",
- "fieldtype": "Section Break"
- },
{
"default": "0",
"fieldname": "allow_to_make_quality_inspection_after_purchase_or_delivery",
"fieldtype": "Check",
- "label": "Allow to Make Quality Inspection after Purchase / Delivery"
+ "label": "Allow to make Quality Inspection after Purchase / Delivery"
},
{
"default": "Rate",
"depends_on": "eval: doc.auto_insert_price_list_rate_if_missing",
"fieldname": "update_price_list_based_on",
"fieldtype": "Select",
- "label": "Update Price List Based On",
+ "label": "Update Price List based on",
"mandatory_depends_on": "eval: doc.auto_insert_price_list_rate_if_missing",
"options": "Rate\nPrice List Rate"
},
@@ -518,20 +503,90 @@
"description": "If enabled, the source and target warehouse in the Material Transfer Stock Entry must be different else an error will be thrown. If inventory dimensions are present, same source and target warehouse can be allowed but atleast any one of the inventory dimension fields must be different.",
"fieldname": "validate_material_transfer_warehouses",
"fieldtype": "Check",
- "label": "Validate Material Transfer Warehouses"
+ "label": "Validate Material Transfer warehouses"
},
{
"default": "0",
"description": "If enabled, the system will allow negative stock entries for the batch. But, this may lead to incorrect valuation rates, so it is recommended to avoid using this option. The system will permit negative stock only when it is caused by backdated entries and will validate and block negative stock in all other cases.",
"fieldname": "allow_negative_stock_for_batch",
"fieldtype": "Check",
- "label": "Allow Negative Stock for Batch"
+ "label": "Allow negative stock for Batch"
},
{
"default": "0",
"fieldname": "enable_serial_and_batch_no_for_item",
"fieldtype": "Check",
"label": "Activate Serial / Batch No for Item"
+ },
+ {
+ "fieldname": "warehouse_defaults_section",
+ "fieldtype": "Section Break",
+ "label": "Warehouse Defaults"
+ },
+ {
+ "fieldname": "internal_transfer_rules_section",
+ "fieldtype": "Section Break",
+ "label": "Internal Transfer Rules"
+ },
+ {
+ "fieldname": "display_data_formatting_section",
+ "fieldtype": "Section Break",
+ "label": "Display & Data Formatting"
+ },
+ {
+ "fieldname": "naming_series_details",
+ "fieldtype": "Small Text",
+ "hidden": 1,
+ "is_virtual": 1,
+ "label": "Naming Series options",
+ "read_only": 1
+ },
+ {
+ "fieldname": "document_naming_tab",
+ "fieldtype": "Tab Break",
+ "label": "Document Naming"
+ },
+ {
+ "fieldname": "transaction_naming_html",
+ "fieldtype": "HTML"
+ },
+ {
+ "fieldname": "configure",
+ "fieldtype": "Button",
+ "hidden": 1,
+ "label": "Configure Series"
+ },
+ {
+ "fieldname": "negative_stock_section",
+ "fieldtype": "Section Break",
+ "label": "Negative Stock"
+ },
+ {
+ "fieldname": "auto_bundle_section",
+ "fieldtype": "Section Break",
+ "label": "Serial and Batch Bundle"
+ },
+ {
+ "fieldname": "naming_series_preview",
+ "fieldtype": "Small Text",
+ "hidden": 1,
+ "is_virtual": 1,
+ "label": "Naming Series options",
+ "read_only": 1
+ },
+ {
+ "fieldname": "configure_series",
+ "fieldtype": "Button",
+ "hidden": 1,
+ "label": "Configure Series"
+ },
+ {
+ "fieldname": "column_break_pjkx",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_kcvr",
+ "fieldtype": "Section Break"
}
],
"icon": "icon-cog",
@@ -539,7 +594,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2026-04-14 13:51:49.545114",
+ "modified": "2026-06-03 12:38:02.202183",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Settings",
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py
index 8250186dc6d..6b6b70b2187 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.py
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.py
@@ -8,7 +8,6 @@ import frappe
from frappe import _
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.model.document import Document
-from frappe.utils import cint
from frappe.utils.html_utils import clean_html
from erpnext.stock.utils import check_pending_reposting
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 38cb8b2eb1b..b6c57865d4a 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -558,10 +558,21 @@ def get_basic_details(ctx: ItemDetailsCtx, item, overwrite_warehouse=True) -> It
ctx.name, ctx.conversion_rate, item.name, out.conversion_factor
)
+ expense_account_field = "default_expense_account"
+ if (
+ item.is_stock_item
+ and erpnext.is_perpetual_inventory_enabled(ctx.company)
+ and (
+ ctx.doctype == "Purchase Receipt"
+ or (ctx.doctype == "Purchase Invoice" and ctx.get("update_stock"))
+ )
+ ):
+ expense_account_field = "stock_received_but_not_billed"
+
# if default specified in item is for another company, fetch from company
for d in [
["Account", "income_account", "default_income_account"],
- ["Account", "expense_account", "default_expense_account"],
+ ["Account", "expense_account", expense_account_field],
["Cost Center", "cost_center", "cost_center"],
["Warehouse", "warehouse", ""],
]:
diff --git a/erpnext/stock/services/__init__.py b/erpnext/stock/services/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/stock/services/base_stock_gl_composer.py b/erpnext/stock/services/base_stock_gl_composer.py
new file mode 100644
index 00000000000..89837db9909
--- /dev/null
+++ b/erpnext/stock/services/base_stock_gl_composer.py
@@ -0,0 +1,154 @@
+# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+import frappe
+from frappe import _
+from frappe.utils import flt
+
+from erpnext.accounts.general_ledger import process_gl_map
+from erpnext.accounts.services.base_gl_composer import BaseGLComposer
+
+
+class BaseStockGLComposer(BaseGLComposer):
+ """Shared GL composition logic for stock vouchers.
+
+ Subclasses override ``compose()`` and call ``super().compose()`` to get the
+ warehouse ↔ expense-account GL pairs, then append any doctype-specific
+ entries on top.
+ """
+
+ def compose(
+ self,
+ inventory_account_map: dict | None = None,
+ default_expense_account: str | None = None,
+ default_cost_center: str | None = None,
+ ) -> list:
+ doc = self.doc
+
+ if not inventory_account_map:
+ inventory_account_map = doc.get_inventory_account_map()
+
+ sle_map = doc.get_stock_ledger_details()
+ voucher_details = doc.get_voucher_details(default_expense_account, default_cost_center, sle_map)
+
+ gl_list = []
+ warehouse_with_no_account = []
+ precision = doc.get_debit_field_precision()
+
+ for item_row in voucher_details:
+ sle_list = sle_map.get(item_row.name)
+ sle_rounding_diff = 0.0
+ if sle_list:
+ for sle in sle_list:
+ _inv_dict = doc.get_inventory_account_dict(sle, inventory_account_map)
+
+ if _inv_dict.get("account"):
+ sle_rounding_diff += flt(sle.stock_value_difference)
+
+ doc.check_expense_account(item_row)
+
+ if item_row.get("target_warehouse"):
+ _target_wh_inv_dict = doc.get_inventory_account_dict(
+ item_row, inventory_account_map, warehouse_field="target_warehouse"
+ )
+ expense_account = _target_wh_inv_dict["account"]
+ else:
+ expense_account = item_row.expense_account
+
+ gl_list.append(
+ self.get_gl_dict(
+ {
+ "account": _inv_dict["account"],
+ "against": expense_account,
+ "cost_center": item_row.cost_center,
+ "project": sle.get("project") or item_row.project or doc.get("project"),
+ "remarks": doc.get("remarks") or _("Accounting Entry for Stock"),
+ "debit": flt(sle.stock_value_difference, precision),
+ "is_opening": item_row.get("is_opening") or doc.get("is_opening") or "No",
+ },
+ _inv_dict["account_currency"],
+ item=item_row,
+ )
+ )
+
+ gl_list.append(
+ self.get_gl_dict(
+ {
+ "account": expense_account,
+ "against": _inv_dict["account"],
+ "cost_center": item_row.cost_center,
+ "remarks": doc.get("remarks") or _("Accounting Entry for Stock"),
+ "debit": -1 * flt(sle.stock_value_difference, precision),
+ "project": sle.get("project")
+ or item_row.get("project")
+ or doc.get("project"),
+ "is_opening": item_row.get("is_opening") or doc.get("is_opening") or "No",
+ },
+ item=item_row,
+ )
+ )
+ elif sle.warehouse not in warehouse_with_no_account:
+ warehouse_with_no_account.append(sle.warehouse)
+
+ if abs(sle_rounding_diff) > (1.0 / (10**precision)) and doc.is_internal_transfer():
+ warehouse_asset_account = ""
+ if doc.get("is_internal_customer"):
+ _inv_dict = doc.get_inventory_account_dict(
+ item_row, inventory_account_map, warehouse_field="target_warehouse"
+ )
+ warehouse_asset_account = _inv_dict.get("account") if _inv_dict else None
+ elif doc.get("is_internal_supplier"):
+ _inv_dict = doc.get_inventory_account_dict(item_row, inventory_account_map)
+ warehouse_asset_account = _inv_dict.get("account") if _inv_dict else None
+
+ expense_account = frappe.get_cached_value("Company", doc.company, "default_expense_account")
+ if not expense_account:
+ frappe.throw(
+ _(
+ "Please set default cost of goods sold account in company {0} for booking rounding gain and loss during stock transfer"
+ ).format(frappe.bold(doc.company))
+ )
+
+ gl_list.append(
+ self.get_gl_dict(
+ {
+ "account": expense_account,
+ "against": warehouse_asset_account,
+ "cost_center": item_row.cost_center,
+ "project": item_row.project or doc.get("project"),
+ "remarks": _("Rounding gain/loss Entry for Stock Transfer"),
+ "debit": sle_rounding_diff,
+ "is_opening": item_row.get("is_opening") or doc.get("is_opening") or "No",
+ },
+ _inv_dict["account_currency"],
+ item=item_row,
+ )
+ )
+
+ gl_list.append(
+ self.get_gl_dict(
+ {
+ "account": warehouse_asset_account,
+ "against": expense_account,
+ "cost_center": item_row.cost_center,
+ "remarks": _("Rounding gain/loss Entry for Stock Transfer"),
+ "credit": sle_rounding_diff,
+ "project": item_row.get("project") or doc.get("project"),
+ "is_opening": item_row.get("is_opening") or doc.get("is_opening") or "No",
+ },
+ item=item_row,
+ )
+ )
+
+ if warehouse_with_no_account:
+ for wh in warehouse_with_no_account:
+ if frappe.get_cached_value("Warehouse", wh, "company"):
+ frappe.throw(
+ _(
+ "Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}."
+ ).format(wh, doc.company)
+ )
+
+ return process_gl_map(
+ gl_list, precision=precision, from_repost=frappe.flags.through_repost_item_valuation
+ )
diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py
index ebfa039f82a..2f853252723 100644
--- a/erpnext/stock/stock_balance.py
+++ b/erpnext/stock/stock_balance.py
@@ -284,7 +284,7 @@ def set_stock_balance_as_per_serial_no(
if not posting_time:
posting_time = nowtime()
- condition = " and item.name='%s'" % item_code.replace("'", "'") if item_code else ""
+ condition = " and item.name=%s" % frappe.db.escape(item_code, percent=False) if item_code else ""
bin = frappe.db.sql(
"""select bin.item_code, bin.warehouse, bin.actual_qty, item.stock_uom
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index fb8e63c37b7..f1b0e2035ea 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -17,7 +17,6 @@ from frappe.utils import (
format_date,
get_datetime,
get_link_to_form,
- getdate,
now,
nowdate,
nowtime,
@@ -434,8 +433,7 @@ def get_reposting_data(file_path) -> dict:
except Exception:
return frappe._dict()
- if data := json.loads(data.decode("utf-8")):
- data = data
+ data = json.loads(data.decode("utf-8"))
return parse_json(data)
@@ -1457,8 +1455,7 @@ class update_entries_after:
item.amount = flt(item.qty) * flt(item.valuation_rate)
item.quantity_difference = item.qty - item.current_qty
item.amount_difference = item.amount - item.current_amount
- else:
- sr.difference_amount = sum([item.amount_difference for item in sr.items])
+ sr.difference_amount = sum([item.amount_difference for item in sr.items])
sr.db_update()
for item in sr.items:
diff --git a/erpnext/stock/tests/test_get_item_details.py b/erpnext/stock/tests/test_get_item_details.py
index 7eadf125d0e..c1026eb9b65 100644
--- a/erpnext/stock/tests/test_get_item_details.py
+++ b/erpnext/stock/tests/test_get_item_details.py
@@ -78,7 +78,7 @@ class TestGetItemDetail(ERPNextTestSuite):
so = make_sales_order(item_code=item.item_code, qty=2, rate=75)
- from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
+ from erpnext.selling.doctype.sales_order.mapper import make_delivery_note
dn = make_delivery_note(so.name)
diff --git a/erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.js b/erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.js
index 20481791e38..eb3938430f0 100644
--- a/erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.js
+++ b/erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.js
@@ -99,7 +99,7 @@ frappe.ui.form.on("Subcontracting Inward Order", {
if (frm.doc.sales_order) {
erpnext.utils.map_current_doc({
- method: "erpnext.selling.doctype.sales_order.sales_order.make_subcontracting_inward_order",
+ method: "erpnext.selling.doctype.sales_order.mapper.make_subcontracting_inward_order",
source_name: frm.doc.sales_order,
target_doc: frm,
freeze: true,
diff --git a/erpnext/subcontracting/doctype/subcontracting_inward_order/test_subcontracting_inward_order.py b/erpnext/subcontracting/doctype/subcontracting_inward_order/test_subcontracting_inward_order.py
index a8aeca2f423..dc60423074c 100644
--- a/erpnext/subcontracting/doctype/subcontracting_inward_order/test_subcontracting_inward_order.py
+++ b/erpnext/subcontracting/doctype/subcontracting_inward_order/test_subcontracting_inward_order.py
@@ -3,8 +3,8 @@
import frappe
-from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry as make_stock_entry_from_wo
-from erpnext.selling.doctype.sales_order.sales_order import make_subcontracting_inward_order
+from erpnext.manufacturing.doctype.work_order.mapper import make_stock_entry as make_stock_entry_from_wo
+from erpnext.selling.doctype.sales_order.mapper import make_subcontracting_inward_order
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
@@ -368,7 +368,7 @@ class IntegrationTestSubcontractingInwardOrder(ERPNextTestSuite):
frappe.new_doc("Stock Entry").update(scio.make_subcontracting_delivery()).submit()
scio.reload()
- from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
+ from erpnext.selling.doctype.sales_order.mapper import make_sales_invoice
si = make_sales_invoice(so.name)
self.assertEqual(si.items[-1].item_code, "Self RM")
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js
index 76f1cc52094..5f319d9dde9 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js
+++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js
@@ -145,7 +145,7 @@ frappe.ui.form.on("Subcontracting Order", {
if (frm.doc.purchase_order) {
erpnext.utils.map_current_doc({
- method: "erpnext.buying.doctype.purchase_order.purchase_order.make_subcontracting_order",
+ method: "erpnext.buying.doctype.purchase_order.mapper.make_subcontracting_order",
source_name: frm.doc.purchase_order,
target_doc: frm,
freeze: true,
@@ -577,7 +577,7 @@ frappe.ui.form.on("Subcontracting Order", {
},
get_materials_from_supplier: function (frm) {
- let sco_rm_details = [];
+ const sco_rm_details = [];
if (frm.doc.status != "Closed" && frm.doc.supplied_items) {
frm.doc.supplied_items.forEach((d) => {
@@ -591,21 +591,16 @@ frappe.ui.form.on("Subcontracting Order", {
frm.add_custom_button(
__("Return of Components"),
() => {
- frm.call({
+ frappe.model.open_mapped_doc({
method: "erpnext.controllers.subcontracting_controller.get_materials_from_supplier",
- freeze: true,
- freeze_message: __("Creating Stock Entry"),
+ frm: frm,
args: {
subcontract_order: frm.doc.name,
rm_details: sco_rm_details,
- order_doctype: cur_frm.doc.doctype,
- },
- callback: function (r) {
- if (r && r.message) {
- const doc = frappe.model.sync(r.message);
- frappe.set_route("Form", doc[0].doctype, doc[0].name);
- }
+ order_doctype: frm.doc.doctype,
},
+ freeze: true,
+ freeze_message: __("Creating Return of Components ..."),
});
},
__("Create")
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py
index 4e74a714977..f5588d3b064 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py
+++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py
@@ -198,9 +198,8 @@ class SubcontractingOrder(SubcontractingController):
item.amount = item.qty * item.rate
total_qty += flt(item.qty)
total += flt(item.amount)
- else:
- self.total_qty = total_qty
- self.total = total
+ self.total_qty = total_qty
+ self.total = total
def update_ordered_qty_for_subcontracting(self, sco_item_rows=None):
item_wh_list = []
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py
index f0803733d53..9b6185d6f14 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py
+++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py
@@ -7,7 +7,7 @@ from collections import defaultdict
import frappe
from frappe.utils import flt
-from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_subcontracting_order
+from erpnext.buying.doctype.purchase_order.mapper import get_mapped_subcontracting_order
from erpnext.controllers.subcontracting_controller import (
get_materials_from_supplier,
make_rm_stock_entry,
@@ -554,7 +554,12 @@ class TestSubcontractingOrder(ERPNextTestSuite):
scr.submit()
# Get RM from Supplier
- ste = get_materials_from_supplier(sco.name, [d.name for d in sco.supplied_items])
+ frappe.flags.args = frappe._dict(
+ subcontract_order=sco.name,
+ rm_details=[d.name for d in sco.supplied_items],
+ order_doctype=sco.doctype,
+ )
+ ste = get_materials_from_supplier(sco.name)
ste.save()
ste.submit()
@@ -624,7 +629,7 @@ class TestSubcontractingOrder(ERPNextTestSuite):
self.assertEqual(ordered_qty + 10, new_ordered_qty)
def test_requested_qty_for_subcontracting_order(self):
- from erpnext.stock.doctype.material_request.material_request import make_purchase_order
+ from erpnext.stock.doctype.material_request.mapper import make_purchase_order
from erpnext.stock.doctype.material_request.test_material_request import make_material_request
requested_qty = frappe.db.get_value(
@@ -640,7 +645,7 @@ class TestSubcontractingOrder(ERPNextTestSuite):
qty=10,
)
- self.assertTrue(mr.docstatus == 1)
+ self.assertEqual(mr.docstatus, 1)
new_requested_qty = frappe.db.get_value(
"Bin",
@@ -726,7 +731,7 @@ class TestSubcontractingOrder(ERPNextTestSuite):
sco.submit()
sre_list = get_sre_details_for_voucher("Subcontracting Order", sco.name)
- self.assertTrue(len(sre_list) > 0)
+ self.assertGreater(len(sre_list), 0)
se_dict = make_rm_stock_entry(sco.name)
se = frappe.get_doc(se_dict)
@@ -843,9 +848,8 @@ def create_subcontracting_order(**args):
warehouses = []
for item in po.items:
warehouses.append(item.warehouse)
- else:
- for idx, val in enumerate(sco.items):
- val.warehouse = warehouses[idx]
+ for idx, val in enumerate(sco.items):
+ val.warehouse = warehouses[idx]
warehouses = set()
for item in sco.items:
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/mapper.py b/erpnext/subcontracting/doctype/subcontracting_receipt/mapper.py
new file mode 100644
index 00000000000..4927d6723c0
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/mapper.py
@@ -0,0 +1,168 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+from frappe.model.mapper import get_mapped_doc
+from frappe.utils import flt, get_link_to_form
+
+
+@frappe.whitelist()
+def make_subcontract_return_against_rejected_warehouse(source_name: str):
+ from erpnext.controllers.sales_and_purchase_return import make_return_doc
+
+ return make_return_doc("Subcontracting Receipt", source_name, return_against_rejected_qty=True)
+
+
+@frappe.whitelist()
+def make_subcontract_return(source_name: str, target_doc: Document | str | None = None):
+ from erpnext.controllers.sales_and_purchase_return import make_return_doc
+
+ return make_return_doc("Subcontracting Receipt", source_name, target_doc)
+
+
+@frappe.whitelist()
+def make_purchase_receipt(
+ source_name: Document | str,
+ target_doc: Document | str | None = None,
+ save: bool = False,
+ submit: bool = False,
+ notify: bool = False,
+):
+ if isinstance(source_name, str):
+ source_doc = frappe.get_doc("Subcontracting Receipt", source_name)
+ else:
+ source_doc = source_name
+
+ if source_doc.is_return:
+ return
+
+ po_sr_item_dict = {}
+ po_name = None
+ for item in source_doc.items:
+ if not item.purchase_order:
+ continue
+
+ if not po_name:
+ po_name = item.purchase_order
+
+ po_sr_item_dict[item.purchase_order_item] = {
+ "qty": flt(item.qty),
+ "rejected_qty": flt(item.rejected_qty),
+ "warehouse": item.warehouse,
+ "rejected_warehouse": item.rejected_warehouse,
+ "subcontracting_receipt_item": item.name,
+ }
+
+ if not po_name:
+ frappe.throw(
+ _("Purchase Order Item reference is missing in Subcontracting Receipt {0}").format(
+ source_doc.name
+ )
+ )
+
+ def update_item(obj, target, source_parent):
+ sr_item_details = po_sr_item_dict.get(obj.name)
+ ratio = flt(obj.qty) / flt(obj.fg_item_qty)
+
+ target.update(
+ {
+ "qty": ratio * sr_item_details["qty"],
+ "rejected_qty": ratio * sr_item_details["rejected_qty"],
+ "warehouse": sr_item_details["warehouse"],
+ "rejected_warehouse": sr_item_details["rejected_warehouse"],
+ "subcontracting_receipt_item": sr_item_details["subcontracting_receipt_item"],
+ }
+ )
+
+ def post_process(source, target):
+ target.set_missing_values()
+ target.update(
+ {
+ "posting_date": source_doc.posting_date,
+ "posting_time": source_doc.posting_time,
+ "subcontracting_receipt": source_doc.name,
+ "supplier_warehouse": source_doc.supplier_warehouse,
+ "is_subcontracted": 1,
+ "currency": frappe.get_cached_value("Company", target.company, "default_currency"),
+ }
+ )
+
+ target_doc = get_mapped_doc(
+ "Purchase Order",
+ po_name,
+ {
+ "Purchase Order": {
+ "doctype": "Purchase Receipt",
+ "field_map": {"supplier_warehouse": "supplier_warehouse"},
+ "validation": {
+ "docstatus": ["=", 1],
+ },
+ },
+ "Purchase Order Item": {
+ "doctype": "Purchase Receipt Item",
+ "field_map": {
+ "name": "purchase_order_item",
+ "parent": "purchase_order",
+ "bom": "bom",
+ },
+ "postprocess": update_item,
+ "condition": lambda doc: doc.name in po_sr_item_dict,
+ },
+ "Purchase Taxes and Charges": {
+ "doctype": "Purchase Taxes and Charges",
+ "reset_value": True,
+ },
+ },
+ postprocess=post_process,
+ )
+
+ if not target_doc.get("items"):
+ add_po_items_to_pr(source_doc, target_doc)
+
+ if (save or submit) and frappe.has_permission(target_doc.doctype, "create"):
+ target_doc.save()
+
+ if submit and frappe.has_permission(target_doc.doctype, "submit", target_doc):
+ try:
+ target_doc.submit()
+ except Exception as e:
+ target_doc.add_comment("Comment", _("Submit Action Failed") + "
" + str(e))
+
+ if notify:
+ frappe.msgprint(
+ _("Purchase Receipt {0} created.").format(
+ get_link_to_form(target_doc.doctype, target_doc.name)
+ ),
+ indicator="green",
+ alert=True,
+ )
+
+ return target_doc
+
+
+def add_po_items_to_pr(scr_doc, target_doc):
+ fg_items = {(item.item_code, item.purchase_order): item.qty for item in scr_doc.items}
+
+ for (item_code, po_name), fg_qty in fg_items.items():
+ po_doc = frappe.get_doc("Purchase Order", po_name)
+ for item in po_doc.items:
+ if item.fg_item != item_code:
+ continue
+
+ qty = (item.stock_qty - item.received_qty) * fg_qty / item.fg_item_qty
+ if qty:
+ target_doc.append(
+ "items",
+ {
+ "item_code": item.item_code,
+ "item_name": item.item_name,
+ "description": item.description,
+ "qty": qty,
+ "rate": item.rate,
+ "warehouse": item.warehouse,
+ "purchase_order": item.parent,
+ "purchase_order_item": item.name,
+ },
+ )
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/services/__init__.py b/erpnext/subcontracting/doctype/subcontracting_receipt/services/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/services/gl_composer.py b/erpnext/subcontracting/doctype/subcontracting_receipt/services/gl_composer.py
new file mode 100644
index 00000000000..7e31454ab23
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/services/gl_composer.py
@@ -0,0 +1,265 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# License: GNU General Public License v3. See license.txt
+
+import frappe
+from frappe import _
+from frappe.utils import flt
+
+from erpnext.accounts.general_ledger import process_gl_map
+from erpnext.accounts.utils import get_account_currency
+from erpnext.stock.services.base_stock_gl_composer import BaseStockGLComposer
+
+
+class SubcontractingReceiptGLComposer(BaseStockGLComposer):
+ """GL composer for Subcontracting Receipt.
+
+ Builds GL entries for accepted stock, service cost, supplier warehouse
+ (raw materials), additional costs, LCV, and divisional loss.
+ """
+
+ def compose(self, inventory_account_map: dict | None = None) -> list:
+ import erpnext
+
+ doc = self.doc
+ if not erpnext.is_perpetual_inventory_enabled(doc.company):
+ return []
+
+ gl_entries = []
+ self._make_item_gl_entries(gl_entries, inventory_account_map)
+ self._make_item_gl_entries_for_lcv(gl_entries, inventory_account_map)
+
+ return process_gl_map(gl_entries, from_repost=frappe.flags.through_repost_item_valuation)
+
+ def _make_item_gl_entries(self, gl_entries: list, inventory_account_map: dict | None) -> None:
+ doc = self.doc
+ warehouse_with_no_account = []
+
+ supplied_items_details = frappe._dict()
+ for item in doc.supplied_items:
+ supplied_items_details.setdefault(item.reference_name, []).append(
+ frappe._dict(
+ {
+ "item_code": item.rm_item_code,
+ "amount": item.amount,
+ "expense_account": item.expense_account,
+ "cost_center": item.cost_center,
+ }
+ )
+ )
+
+ for item in doc.items:
+ if flt(item.rate) and flt(item.qty):
+ _inv_dict = doc.get_inventory_account_dict(item, inventory_account_map)
+
+ if _inv_dict.get("account"):
+ stock_value_diff = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {
+ "voucher_type": "Subcontracting Receipt",
+ "voucher_no": doc.name,
+ "voucher_detail_no": item.name,
+ "warehouse": item.warehouse,
+ "is_cancelled": 0,
+ },
+ "stock_value_difference",
+ )
+
+ remarks = doc.get("remarks") or _("Accounting Entry for Stock")
+
+ self.add_gl_entry(
+ gl_entries=gl_entries,
+ account=_inv_dict["account"],
+ cost_center=item.cost_center,
+ debit=stock_value_diff,
+ credit=0.0,
+ remarks=remarks,
+ against_account=item.expense_account,
+ account_currency=_inv_dict["account_currency"],
+ project=item.project,
+ item=item,
+ )
+
+ service_cost = flt(
+ item.service_cost_per_qty, item.precision("service_cost_per_qty")
+ ) * flt(item.qty, item.precision("qty"))
+
+ self.add_gl_entry(
+ gl_entries=gl_entries,
+ account=item.expense_account,
+ cost_center=item.cost_center,
+ debit=0.0,
+ credit=flt(stock_value_diff) - service_cost,
+ remarks=remarks,
+ against_account=_inv_dict["account"],
+ account_currency=get_account_currency(item.expense_account),
+ project=item.project,
+ item=item,
+ )
+
+ service_account = item.service_expense_account or item.expense_account
+ self.add_gl_entry(
+ gl_entries=gl_entries,
+ account=service_account,
+ cost_center=item.cost_center,
+ debit=0.0,
+ credit=service_cost,
+ remarks=remarks,
+ against_account=_inv_dict["account"],
+ account_currency=get_account_currency(service_account),
+ project=item.project,
+ item=item,
+ )
+
+ if flt(item.rm_supp_cost):
+ for rm_item in supplied_items_details.get(item.name):
+ _inv_dict = doc.get_inventory_account_dict(
+ rm_item, inventory_account_map, "supplier_warehouse"
+ )
+
+ self.add_gl_entry(
+ gl_entries=gl_entries,
+ account=_inv_dict.get("account"),
+ cost_center=rm_item.cost_center or item.cost_center,
+ debit=0.0,
+ credit=flt(rm_item.amount),
+ remarks=remarks,
+ against_account=rm_item.expense_account or item.expense_account,
+ account_currency=_inv_dict.get("account_currency"),
+ project=item.project,
+ item=item,
+ )
+ self.add_gl_entry(
+ gl_entries=gl_entries,
+ account=rm_item.expense_account or item.expense_account,
+ cost_center=rm_item.cost_center or item.cost_center,
+ debit=flt(rm_item.amount),
+ credit=0.0,
+ remarks=remarks,
+ against_account=_inv_dict.get("account"),
+ account_currency=get_account_currency(item.expense_account),
+ project=item.project,
+ item=item,
+ )
+
+ if item.additional_cost_per_qty:
+ self.add_gl_entry(
+ gl_entries=gl_entries,
+ account=item.expense_account,
+ cost_center=doc.cost_center or doc.get_company_default("cost_center"),
+ debit=item.qty * item.additional_cost_per_qty,
+ credit=0.0,
+ remarks=remarks,
+ against_account=None,
+ account_currency=get_account_currency(item.expense_account),
+ )
+
+ if divisional_loss := flt(item.amount - stock_value_diff, item.precision("amount")):
+ loss_account = doc.get_company_default(
+ "stock_adjustment_account", ignore_validation=True
+ )
+
+ self.add_gl_entry(
+ gl_entries=gl_entries,
+ account=loss_account,
+ cost_center=item.cost_center,
+ debit=0.0,
+ credit=divisional_loss,
+ remarks=remarks,
+ against_account=item.expense_account,
+ account_currency=get_account_currency(loss_account),
+ project=item.project,
+ item=item,
+ )
+ self.add_gl_entry(
+ gl_entries=gl_entries,
+ account=item.expense_account,
+ cost_center=item.cost_center,
+ debit=divisional_loss,
+ credit=0.0,
+ remarks=remarks,
+ against_account=loss_account,
+ account_currency=get_account_currency(item.expense_account),
+ project=item.project,
+ item=item,
+ )
+ elif (
+ item.warehouse not in warehouse_with_no_account
+ or item.rejected_warehouse not in warehouse_with_no_account
+ ):
+ warehouse_with_no_account.append(item.warehouse)
+
+ for row in doc.additional_costs:
+ credit_amount = (
+ flt(row.base_amount)
+ if (row.base_amount or row.account_currency != doc.company_currency)
+ else flt(row.amount)
+ )
+
+ self.add_gl_entry(
+ gl_entries=gl_entries,
+ account=row.expense_account,
+ cost_center=doc.cost_center or doc.get_company_default("cost_center"),
+ debit=0.0,
+ credit=credit_amount,
+ remarks=remarks,
+ against_account=None,
+ account_currency=get_account_currency(row.expense_account),
+ )
+
+ if warehouse_with_no_account:
+ frappe.msgprint(
+ _("No accounting entries for the following warehouses")
+ + ": \n"
+ + "\n".join(warehouse_with_no_account)
+ )
+
+ def _make_item_gl_entries_for_lcv(self, gl_entries: list, inventory_account_map: dict | None) -> None:
+ doc = self.doc
+ landed_cost_entries = doc.get_item_account_wise_lcv_entries()
+
+ if not landed_cost_entries:
+ return
+
+ for item in doc.items:
+ if item.landed_cost_voucher_amount and landed_cost_entries:
+ remarks = _("Accounting Entry for Landed Cost Voucher for SCR {0}").format(doc.name)
+ if (item.item_code, item.name) in landed_cost_entries:
+ _inv_dict = doc.get_inventory_account_dict(item, inventory_account_map)
+
+ for account, amount in landed_cost_entries[(item.item_code, item.name)].items():
+ account_currency = get_account_currency(account)
+ credit_amount = (
+ flt(amount["base_amount"])
+ if (amount["base_amount"] or account_currency != doc.company_currency)
+ else flt(amount["amount"])
+ )
+
+ self.add_gl_entry(
+ gl_entries=gl_entries,
+ account=account,
+ cost_center=item.cost_center,
+ debit=0.0,
+ credit=credit_amount,
+ remarks=remarks,
+ against_account=_inv_dict["account"],
+ credit_in_account_currency=flt(amount["amount"]),
+ account_currency=account_currency,
+ project=item.project,
+ item=item,
+ )
+
+ account_currency = get_account_currency(item.expense_account)
+
+ self.add_gl_entry(
+ gl_entries=gl_entries,
+ account=item.expense_account,
+ cost_center=item.cost_center,
+ debit=0.0,
+ credit=credit_amount * -1,
+ remarks=remarks,
+ against_account=_inv_dict["account"],
+ debit_in_account_currency=flt(amount["amount"]),
+ account_currency=account_currency,
+ project=item.project,
+ item=item,
+ )
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
index 0c2a10705c4..b8b357627e5 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
@@ -68,7 +68,7 @@ frappe.ui.form.on("Subcontracting Receipt", {
__("Purchase Receipt"),
() => {
frappe.model.open_mapped_doc({
- method: "erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt.make_purchase_receipt",
+ method: "erpnext.subcontracting.doctype.subcontracting_receipt.mapper.make_purchase_receipt",
frm: frm,
freeze: true,
freeze_message: __("Creating Purchase Receipt ..."),
@@ -85,7 +85,7 @@ frappe.ui.form.on("Subcontracting Receipt", {
() => {
const make_standard_return = () => {
frappe.model.open_mapped_doc({
- method: "erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt.make_subcontract_return",
+ method: "erpnext.subcontracting.doctype.subcontracting_receipt.mapper.make_subcontract_return",
frm: frm,
});
};
@@ -109,7 +109,7 @@ frappe.ui.form.on("Subcontracting Receipt", {
function (values) {
if (values.return_for_rejected_warehouse) {
frappe.call({
- method: "erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt.make_subcontract_return_against_rejected_warehouse",
+ method: "erpnext.subcontracting.doctype.subcontracting_receipt.mapper.make_subcontract_return_against_rejected_warehouse",
args: {
source_name: frm.doc.name,
},
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
index 536ee67d237..a998b611df9 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
@@ -6,13 +6,10 @@ from collections import defaultdict
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 Sum
from frappe.utils import cint, flt, get_link_to_form, getdate, nowdate
import erpnext
-from erpnext.accounts.utils import get_account_currency
-from erpnext.buying.utils import check_on_hold_or_closed_status
from erpnext.controllers.subcontracting_controller import SubcontractingController
from erpnext.setup.doctype.brand.brand import get_brand_defaults
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
@@ -21,6 +18,10 @@ from erpnext.stock.doctype.item.item import get_item_defaults
from erpnext.stock.get_item_details import get_default_cost_center, get_default_expense_account
from erpnext.stock.stock_ledger import get_valuation_rate
+from .mapper import (
+ make_purchase_receipt,
+)
+
class BOMQuantityError(frappe.ValidationError):
pass
@@ -216,9 +217,7 @@ class SubcontractingReceipt(SubcontractingController):
self.create_raw_materials_supplied_or_received()
def validate_closed_subcontracting_order(self):
- for item in self.items:
- if item.subcontracting_order:
- check_on_hold_or_closed_status("Subcontracting Order", item.subcontracting_order)
+ self.check_for_on_hold_or_closed_status("Subcontracting Order", "subcontracting_order")
def update_job_card(self):
for row in self.get("items"):
@@ -490,11 +489,10 @@ class SubcontractingReceipt(SubcontractingController):
supplied_items_details[item.name][
supplied_item.rm_item_code
] += supplied_item.available_qty
- else:
- for item in self.get("supplied_items"):
- item.available_qty_for_consumption = supplied_items_details.get(item.reference_name, {}).get(
- item.rm_item_code, 0
- )
+ for item in self.get("supplied_items"):
+ item.available_qty_for_consumption = supplied_items_details.get(item.reference_name, {}).get(
+ item.rm_item_code, 0
+ )
def calculate_items_qty_and_amount(self):
rm_cost_map = {}
@@ -561,9 +559,8 @@ class SubcontractingReceipt(SubcontractingController):
total_qty += flt(item.qty) + flt(item.rejected_qty)
total_amount += item.amount
- else:
- self.total_qty = total_qty
- self.total = total_amount
+ self.total_qty = total_qty
+ self.total = total_amount
def validate_secondary_items(self):
for item in self.items:
@@ -731,257 +728,11 @@ class SubcontractingReceipt(SubcontractingController):
)
def get_gl_entries(self, inventory_account_map=None):
- from erpnext.accounts.general_ledger import process_gl_map
+ from erpnext.subcontracting.doctype.subcontracting_receipt.services.gl_composer import (
+ SubcontractingReceiptGLComposer,
+ )
- if not erpnext.is_perpetual_inventory_enabled(self.company):
- return []
-
- gl_entries = []
- self.make_item_gl_entries(gl_entries, inventory_account_map)
- self.make_item_gl_entries_for_lcv(gl_entries, inventory_account_map)
-
- return process_gl_map(gl_entries, from_repost=frappe.flags.through_repost_item_valuation)
-
- def make_item_gl_entries(self, gl_entries, inventory_account_map=None):
- warehouse_with_no_account = []
-
- supplied_items_details = frappe._dict()
- for item in self.supplied_items:
- supplied_items_details.setdefault(item.reference_name, []).append(
- frappe._dict(
- {
- "item_code": item.rm_item_code,
- "amount": item.amount,
- "expense_account": item.expense_account,
- "cost_center": item.cost_center,
- }
- )
- )
-
- for item in self.items:
- if flt(item.rate) and flt(item.qty):
- _inv_dict = self.get_inventory_account_dict(item, inventory_account_map)
-
- if _inv_dict.get("account"):
- stock_value_diff = frappe.db.get_value(
- "Stock Ledger Entry",
- {
- "voucher_type": "Subcontracting Receipt",
- "voucher_no": self.name,
- "voucher_detail_no": item.name,
- "warehouse": item.warehouse,
- "is_cancelled": 0,
- },
- "stock_value_difference",
- )
-
- remarks = self.get("remarks") or _("Accounting Entry for Stock")
-
- # Accepted Warehouse Account (Debit)
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=_inv_dict["account"],
- cost_center=item.cost_center,
- debit=stock_value_diff,
- credit=0.0,
- remarks=remarks,
- against_account=item.expense_account,
- account_currency=_inv_dict["account_currency"],
- project=item.project,
- item=item,
- )
-
- service_cost = flt(
- item.service_cost_per_qty, item.precision("service_cost_per_qty")
- ) * flt(item.qty, item.precision("qty"))
- # Expense Account (Credit)
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=item.expense_account,
- cost_center=item.cost_center,
- debit=0.0,
- credit=flt(stock_value_diff) - service_cost,
- remarks=remarks,
- against_account=_inv_dict["account"],
- account_currency=get_account_currency(item.expense_account),
- project=item.project,
- item=item,
- )
-
- service_account = item.service_expense_account or item.expense_account
- # Expense Account (Credit)
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=service_account,
- cost_center=item.cost_center,
- debit=0.0,
- credit=service_cost,
- remarks=remarks,
- against_account=_inv_dict["account"],
- account_currency=get_account_currency(service_account),
- project=item.project,
- item=item,
- )
-
- if flt(item.rm_supp_cost):
- for rm_item in supplied_items_details.get(item.name):
- _inv_dict = self.get_inventory_account_dict(
- rm_item, inventory_account_map, "supplier_warehouse"
- )
-
- # Supplier Warehouse Account (Credit)
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=_inv_dict.get("account"),
- cost_center=rm_item.cost_center or item.cost_center,
- debit=0.0,
- credit=flt(rm_item.amount),
- remarks=remarks,
- against_account=rm_item.expense_account or item.expense_account,
- account_currency=_inv_dict.get("account_currency"),
- project=item.project,
- item=item,
- )
- # Expense Account (Debit)
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=rm_item.expense_account or item.expense_account,
- cost_center=rm_item.cost_center or item.cost_center,
- debit=flt(rm_item.amount),
- credit=0.0,
- remarks=remarks,
- against_account=_inv_dict.get("account"),
- account_currency=get_account_currency(item.expense_account),
- project=item.project,
- item=item,
- )
-
- # Expense Account (Debit)
- if item.additional_cost_per_qty:
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=item.expense_account,
- cost_center=self.cost_center or self.get_company_default("cost_center"),
- debit=item.qty * item.additional_cost_per_qty,
- credit=0.0,
- remarks=remarks,
- against_account=None,
- account_currency=get_account_currency(item.expense_account),
- )
-
- if divisional_loss := flt(item.amount - stock_value_diff, item.precision("amount")):
- loss_account = self.get_company_default(
- "stock_adjustment_account", ignore_validation=True
- )
-
- # Loss Account (Credit)
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=loss_account,
- cost_center=item.cost_center,
- debit=0.0,
- credit=divisional_loss,
- remarks=remarks,
- against_account=item.expense_account,
- account_currency=get_account_currency(loss_account),
- project=item.project,
- item=item,
- )
- # Expense Account (Debit)
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=item.expense_account,
- cost_center=item.cost_center,
- debit=divisional_loss,
- credit=0.0,
- remarks=remarks,
- against_account=loss_account,
- account_currency=get_account_currency(item.expense_account),
- project=item.project,
- item=item,
- )
- elif (
- item.warehouse not in warehouse_with_no_account
- or item.rejected_warehouse not in warehouse_with_no_account
- ):
- warehouse_with_no_account.append(item.warehouse)
-
- for row in self.additional_costs:
- credit_amount = (
- flt(row.base_amount)
- if (row.base_amount or row.account_currency != self.company_currency)
- else flt(row.amount)
- )
-
- # Additional Cost Expense Account (Credit)
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=row.expense_account,
- cost_center=self.cost_center or self.get_company_default("cost_center"),
- debit=0.0,
- credit=credit_amount,
- remarks=remarks,
- against_account=None,
- account_currency=get_account_currency(row.expense_account),
- )
-
- if warehouse_with_no_account:
- frappe.msgprint(
- _("No accounting entries for the following warehouses")
- + ": \n"
- + "\n".join(warehouse_with_no_account)
- )
-
- def make_item_gl_entries_for_lcv(self, gl_entries, inventory_account_map):
- landed_cost_entries = self.get_item_account_wise_lcv_entries()
-
- if not landed_cost_entries:
- return
-
- for item in self.items:
- if item.landed_cost_voucher_amount and landed_cost_entries:
- remarks = _("Accounting Entry for Landed Cost Voucher for SCR {0}").format(self.name)
- if (item.item_code, item.name) in landed_cost_entries:
- _inv_dict = self.get_inventory_account_dict(item, inventory_account_map)
-
- for account, amount in landed_cost_entries[(item.item_code, item.name)].items():
- account_currency = get_account_currency(account)
- credit_amount = (
- flt(amount["base_amount"])
- if (amount["base_amount"] or account_currency != self.company_currency)
- else flt(amount["amount"])
- )
-
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=account,
- cost_center=item.cost_center,
- debit=0.0,
- credit=credit_amount,
- remarks=remarks,
- against_account=_inv_dict["account"],
- credit_in_account_currency=flt(amount["amount"]),
- account_currency=account_currency,
- project=item.project,
- item=item,
- )
-
- account_currency = get_account_currency(item.expense_account)
-
- # credit amount in negative to knock off the debit entry
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=item.expense_account,
- cost_center=item.cost_center,
- debit=0.0,
- credit=credit_amount * -1,
- remarks=remarks,
- against_account=_inv_dict["account"],
- debit_in_account_currency=flt(amount["amount"]),
- account_currency=account_currency,
- project=item.project,
- item=item,
- )
+ return SubcontractingReceiptGLComposer(self).compose(inventory_account_map)
def auto_create_purchase_receipt(self):
if frappe.db.get_single_value("Buying Settings", "auto_create_purchase_receipt"):
@@ -997,163 +748,3 @@ class SubcontractingReceipt(SubcontractingController):
return True
return False
-
-
-@frappe.whitelist()
-def make_subcontract_return_against_rejected_warehouse(source_name: str):
- from erpnext.controllers.sales_and_purchase_return import make_return_doc
-
- return make_return_doc("Subcontracting Receipt", source_name, return_against_rejected_qty=True)
-
-
-@frappe.whitelist()
-def make_subcontract_return(source_name: str, target_doc: Document | str | None = None):
- from erpnext.controllers.sales_and_purchase_return import make_return_doc
-
- return make_return_doc("Subcontracting Receipt", source_name, target_doc)
-
-
-@frappe.whitelist()
-def make_purchase_receipt(
- source_name: Document | str,
- target_doc: Document | str | None = None,
- save: bool = False,
- submit: bool = False,
- notify: bool = False,
-):
- if isinstance(source_name, str):
- source_doc = frappe.get_doc("Subcontracting Receipt", source_name)
- else:
- source_doc = source_name
-
- if source_doc.is_return:
- return
-
- po_sr_item_dict = {}
- po_name = None
- for item in source_doc.items:
- if not item.purchase_order:
- continue
-
- if not po_name:
- po_name = item.purchase_order
-
- po_sr_item_dict[item.purchase_order_item] = {
- "qty": flt(item.qty),
- "rejected_qty": flt(item.rejected_qty),
- "warehouse": item.warehouse,
- "rejected_warehouse": item.rejected_warehouse,
- "subcontracting_receipt_item": item.name,
- }
-
- if not po_name:
- frappe.throw(
- _("Purchase Order Item reference is missing in Subcontracting Receipt {0}").format(
- source_doc.name
- )
- )
-
- def update_item(obj, target, source_parent):
- sr_item_details = po_sr_item_dict.get(obj.name)
- ratio = flt(obj.qty) / flt(obj.fg_item_qty)
-
- target.update(
- {
- "qty": ratio * sr_item_details["qty"],
- "rejected_qty": ratio * sr_item_details["rejected_qty"],
- "warehouse": sr_item_details["warehouse"],
- "rejected_warehouse": sr_item_details["rejected_warehouse"],
- "subcontracting_receipt_item": sr_item_details["subcontracting_receipt_item"],
- }
- )
-
- def post_process(source, target):
- target.set_missing_values()
- target.update(
- {
- "posting_date": source_doc.posting_date,
- "posting_time": source_doc.posting_time,
- "subcontracting_receipt": source_doc.name,
- "supplier_warehouse": source_doc.supplier_warehouse,
- "is_subcontracted": 1,
- "currency": frappe.get_cached_value("Company", target.company, "default_currency"),
- }
- )
-
- target_doc = get_mapped_doc(
- "Purchase Order",
- po_name,
- {
- "Purchase Order": {
- "doctype": "Purchase Receipt",
- "field_map": {"supplier_warehouse": "supplier_warehouse"},
- "validation": {
- "docstatus": ["=", 1],
- },
- },
- "Purchase Order Item": {
- "doctype": "Purchase Receipt Item",
- "field_map": {
- "name": "purchase_order_item",
- "parent": "purchase_order",
- "bom": "bom",
- },
- "postprocess": update_item,
- "condition": lambda doc: doc.name in po_sr_item_dict,
- },
- "Purchase Taxes and Charges": {
- "doctype": "Purchase Taxes and Charges",
- "reset_value": True,
- },
- },
- postprocess=post_process,
- )
-
- if not target_doc.get("items"):
- add_po_items_to_pr(source_doc, target_doc)
-
- if (save or submit) and frappe.has_permission(target_doc.doctype, "create"):
- target_doc.save()
-
- if submit and frappe.has_permission(target_doc.doctype, "submit", target_doc):
- try:
- target_doc.submit()
- except Exception as e:
- target_doc.add_comment("Comment", _("Submit Action Failed") + "
" + str(e))
-
- if notify:
- frappe.msgprint(
- _("Purchase Receipt {0} created.").format(
- get_link_to_form(target_doc.doctype, target_doc.name)
- ),
- indicator="green",
- alert=True,
- )
-
- return target_doc
-
-
-def add_po_items_to_pr(scr_doc, target_doc):
- fg_items = {(item.item_code, item.purchase_order): item.qty for item in scr_doc.items}
-
- for (item_code, po_name), fg_qty in fg_items.items():
- po_doc = frappe.get_doc("Purchase Order", po_name)
- for item in po_doc.items:
- if item.fg_item != item_code:
- continue
-
- qty = (item.stock_qty - item.received_qty) * fg_qty / item.fg_item_qty
- if qty:
- target_doc.append(
- "items",
- {
- "item_code": item.item_code,
- "item_name": item.item_name,
- "description": item.description,
- "qty": qty,
- "rate": item.rate,
- "warehouse": item.warehouse,
- "purchase_order": item.parent,
- "purchase_order_item": item.name,
- },
- )
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
index a1d126fb7b5..b480a726b47 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
@@ -1311,11 +1311,11 @@ class TestSubcontractingReceipt(ERPNextTestSuite):
# Step - 8: Cancel Subcontracting Receipt
scr.cancel()
- self.assertTrue(scr.docstatus == 2)
+ self.assertEqual(scr.docstatus, 2)
def test_subcontract_return_from_rejected_warehouse(self):
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
- from erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt import (
+ from erpnext.subcontracting.doctype.subcontracting_receipt.mapper import (
make_subcontract_return_against_rejected_warehouse,
)
diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py
index 8351c40fa97..24665fff94a 100644
--- a/erpnext/support/doctype/issue/test_issue.py
+++ b/erpnext/support/doctype/issue/test_issue.py
@@ -245,7 +245,7 @@ class TestIssue(TestSetUp):
issue = make_issue(frappe.flags.current_time, index=1)
create_communication(issue.name, "test@example.com", "Received", frappe.flags.current_time)
- self.assertTrue(issue.status == "Open")
+ self.assertEqual(issue.status, "Open")
# send a reply within response SLA
frappe.flags.current_time = get_datetime("2021-11-02 11:00")
diff --git a/erpnext/templates/includes/announcement/announcement_row.html b/erpnext/templates/includes/announcement/announcement_row.html
index 3099441e344..1eda74784ca 100644
--- a/erpnext/templates/includes/announcement/announcement_row.html
+++ b/erpnext/templates/includes/announcement/announcement_row.html
@@ -24,10 +24,10 @@
if(content.length > show_char) {
var c = content.substr(0, show_char)
- var h = content.substr(show_char, content.length - show_char);
- html = c + ' ...'
- $(this).html(html);
+ // Set as text (not HTML) so DOM text isn't re-interpreted as
+ // markup (XSS). \u00a0 is a non-breaking space (same as ).
+ $(this).text(c + '\u00a0\u00a0...');
}
});
});
diff --git a/erpnext/templates/includes/projects/project_search_box.html b/erpnext/templates/includes/projects/project_search_box.html
index d7466873dda..8bebd0be244 100644
--- a/erpnext/templates/includes/projects/project_search_box.html
+++ b/erpnext/templates/includes/projects/project_search_box.html
@@ -18,7 +18,7 @@ frappe.ready(function() {
}
var thread = null;
function findResult(t) {
- window.location.href="/projects?project={{doc.name}}&q=" + t;
+ window.location.href="/projects?project={{doc.name}}&q=" + encodeURIComponent(t);
}
$("#project-search").keyup(function() {
diff --git a/erpnext/templates/includes/rfq.js b/erpnext/templates/includes/rfq.js
index cc998a90030..4570a7445ff 100644
--- a/erpnext/templates/includes/rfq.js
+++ b/erpnext/templates/includes/rfq.js
@@ -76,7 +76,7 @@ rfq = class rfq {
frappe.freeze();
frappe.call({
type: "POST",
- method: "erpnext.buying.doctype.request_for_quotation.request_for_quotation.create_supplier_quotation",
+ method: "erpnext.buying.doctype.request_for_quotation.mapper.create_supplier_quotation",
args: {
doc: doc
},
diff --git a/erpnext/templates/pages/order.html b/erpnext/templates/pages/order.html
index 5563a58b730..853c2566969 100644
--- a/erpnext/templates/pages/order.html
+++ b/erpnext/templates/pages/order.html
@@ -23,7 +23,7 @@