diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 7af41f398ad..763e2e6992c 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -173,8 +173,8 @@ frappe.ui.form.on("Journal Entry", { var update_jv_details = function(doc, r) { $.each(r, function(i, d) { var row = frappe.model.add_child(doc, "Journal Entry Account", "accounts"); - row.account = d.account; - row.balance = d.balance; + frappe.model.set_value(row.doctype, row.name, "account", d.account) + frappe.model.set_value(row.doctype, row.name, "balance", d.balance) }); refresh_field("accounts"); } diff --git a/erpnext/accounts/doctype/payment_request/payment_request.json b/erpnext/accounts/doctype/payment_request/payment_request.json index 2ee356aaf40..2f3516e135a 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.json +++ b/erpnext/accounts/doctype/payment_request/payment_request.json @@ -186,8 +186,10 @@ { "fetch_from": "bank_account.bank", "fieldname": "bank", - "fieldtype": "Read Only", - "label": "Bank" + "fieldtype": "Link", + "label": "Bank", + "options": "Bank", + "read_only": 1 }, { "fetch_from": "bank_account.bank_account_no", @@ -366,10 +368,11 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-09-18 12:24:14.178853", + "modified": "2022-09-30 16:19:43.680025", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Request", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { @@ -401,5 +404,6 @@ } ], "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index 6f8b3822c2e..eedaaaf338b 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -343,7 +343,8 @@ "no_copy": 1, "options": "POS Invoice", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "default": "0", @@ -1553,7 +1554,7 @@ "icon": "fa fa-file-text", "is_submittable": 1, "links": [], - "modified": "2022-09-27 13:00:24.166684", + "modified": "2022-09-30 03:49:50.455199", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice", diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index fbe0ef39f83..54a3e934b2d 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -743,7 +743,3 @@ def add_return_modes(doc, pos_profile): ]: payment_mode = get_mode_of_payment_info(mode_of_payment, doc.company) append_payment(payment_mode[0]) - - -def on_doctype_update(): - frappe.db.add_index("POS Invoice", ["return_against"]) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 3020e6dc6e3..1e477776e2d 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -1430,6 +1430,7 @@ "fieldname": "tax_withheld_vouchers", "fieldtype": "Table", "label": "Tax Withheld Vouchers", + "no_copy": 1, "options": "Tax Withheld Vouchers", "read_only": 1 } @@ -1438,7 +1439,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2022-09-27 11:07:55.766844", + "modified": "2022-10-07 14:19:14.214157", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 782e08e33ba..ce44ae304b3 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -8,7 +8,7 @@ import frappe from frappe.model.dynamic_links import get_dynamic_link_map from frappe.model.naming import make_autoname from frappe.tests.utils import change_settings -from frappe.utils import add_days, flt, getdate, nowdate +from frappe.utils import add_days, flt, getdate, nowdate, today import erpnext from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account @@ -3196,6 +3196,37 @@ class TestSalesInvoice(unittest.TestCase): "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled ) + def test_batch_expiry_for_sales_invoice_return(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + from erpnext.stock.doctype.item.test_item import make_item + + item = make_item( + "_Test Batch Item For Return Check", + { + "is_purchase_item": 1, + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TBIRC.#####", + }, + ) + + pr = make_purchase_receipt(qty=1, item_code=item.name) + + batch_no = pr.items[0].batch_no + si = create_sales_invoice(qty=1, item_code=item.name, update_stock=1, batch_no=batch_no) + + si.load_from_db() + batch_no = si.items[0].batch_no + self.assertTrue(batch_no) + + frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(today(), -1)) + + return_si = make_return_doc(si.doctype, si.name) + return_si.save().submit() + + self.assertTrue(return_si.docstatus == 1) + def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() @@ -3289,6 +3320,7 @@ def create_sales_invoice(**args): "serial_no": args.serial_no, "conversion_factor": 1, "incoming_rate": args.incoming_rate or 0, + "batch_no": args.batch_no or None, }, ) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 84c2c9a3c3e..7eddd81ee03 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -249,6 +249,9 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N ) else: tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0 + + # once tds is deducted, not need to add vouchers in the invoice + voucher_wise_amount = {} else: tax_amount = get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers) @@ -425,7 +428,10 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers): ): # Get net total again as TDS is calculated on net total # Grand is used to just check for threshold breach - net_total = frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(net_total)") or 0.0 + net_total = 0 + if vouchers: + net_total = frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(net_total)") + net_total += inv.net_total supp_credit_amt = net_total - cumulative_threshold diff --git a/erpnext/e_commerce/doctype/website_item/website_item.json b/erpnext/e_commerce/doctype/website_item/website_item.json index c5775ee9075..6556eabf4ab 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.json +++ b/erpnext/e_commerce/doctype/website_item/website_item.json @@ -188,7 +188,8 @@ "in_list_view": 1, "label": "Item Group", "options": "Item Group", - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "default": "1", @@ -234,7 +235,8 @@ "fieldname": "brand", "fieldtype": "Link", "label": "Brand", - "options": "Brand" + "options": "Brand", + "search_index": 1 }, { "collapsible": 1, @@ -346,7 +348,7 @@ "index_web_pages_for_search": 1, "links": [], "make_attachments_public": 1, - "modified": "2022-09-13 04:05:11.614087", + "modified": "2022-09-30 04:01:52.090732", "modified_by": "Administrator", "module": "E-commerce", "name": "Website Item", diff --git a/erpnext/e_commerce/doctype/website_item/website_item.py b/erpnext/e_commerce/doctype/website_item/website_item.py index c0f8c79283d..3e5d5f768fa 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.py +++ b/erpnext/e_commerce/doctype/website_item/website_item.py @@ -403,9 +403,6 @@ def on_doctype_update(): # since route is a Text column, it needs a length for indexing frappe.db.add_index("Website Item", ["route(500)"]) - frappe.db.add_index("Website Item", ["item_group"]) - frappe.db.add_index("Website Item", ["brand"]) - def check_if_user_is_customer(user=None): from frappe.contacts.doctype.contact.contact import get_contact_name diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index a53c42c5ec8..804f03dc519 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -17,6 +17,7 @@ from erpnext.manufacturing.doctype.work_order.work_order import ( close_work_order, make_job_card, make_stock_entry, + make_stock_return_entry, stop_unstop, ) from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order @@ -1408,6 +1409,77 @@ class TestWorkOrder(FrappeTestCase): ) self.assertEqual(manufacture_ste_doc2.items[1].qty, 1) + def test_non_consumed_material_return_against_work_order(self): + frappe.db.set_value( + "Manufacturing Settings", + None, + "backflush_raw_materials_based_on", + "Material Transferred for Manufacture", + ) + + item = make_item( + "Test FG Item To Test Return Case", + { + "is_stock_item": 1, + }, + ) + + item_code = item.name + bom_doc = make_bom( + item=item_code, + source_warehouse="Stores - _TC", + raw_materials=["Test Batch MCC Keyboard", "Test Serial No BTT Headphone"], + ) + + # Create a work order + wo_doc = make_wo_order_test_record(production_item=item_code, qty=5) + wo_doc.save() + + self.assertEqual(wo_doc.bom_no, bom_doc.name) + + # Transfer material for manufacture + ste_doc = frappe.get_doc(make_stock_entry(wo_doc.name, "Material Transfer for Manufacture", 5)) + for row in ste_doc.items: + row.qty += 2 + row.transfer_qty += 2 + nste_doc = test_stock_entry.make_stock_entry( + item_code=row.item_code, target="Stores - _TC", qty=row.qty, basic_rate=100 + ) + + row.batch_no = nste_doc.items[0].batch_no + row.serial_no = nste_doc.items[0].serial_no + + ste_doc.save() + ste_doc.submit() + ste_doc.load_from_db() + + # Create a stock entry to manufacture the item + ste_doc = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 5)) + for row in ste_doc.items: + if row.s_warehouse and not row.t_warehouse: + row.qty -= 2 + row.transfer_qty -= 2 + + if row.serial_no: + serial_nos = get_serial_nos(row.serial_no) + row.serial_no = "\n".join(serial_nos[0:5]) + + ste_doc.save() + ste_doc.submit() + + wo_doc.load_from_db() + for row in wo_doc.required_items: + self.assertEqual(row.transferred_qty, 7) + self.assertEqual(row.consumed_qty, 5) + + self.assertEqual(wo_doc.status, "Completed") + return_ste_doc = make_stock_return_entry(wo_doc.name) + return_ste_doc.save() + + self.assertTrue(return_ste_doc.is_return) + for row in return_ste_doc.items: + self.assertEqual(row.qty, 2) + def prepare_data_for_backflush_based_on_materials_transferred(): batch_item_doc = make_item( diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index f3640b93b22..4aab3fa3734 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -180,6 +180,37 @@ frappe.ui.form.on("Work Order", { frm.trigger("make_bom"); }); } + + frm.trigger("add_custom_button_to_return_components"); + }, + + add_custom_button_to_return_components: function(frm) { + if (frm.doc.docstatus === 1 && in_list(["Closed", "Completed"], frm.doc.status)) { + let non_consumed_items = frm.doc.required_items.filter(d =>{ + return flt(d.consumed_qty) < flt(d.transferred_qty - d.returned_qty) + }); + + if (non_consumed_items && non_consumed_items.length) { + frm.add_custom_button(__("Return Components"), function() { + frm.trigger("create_stock_return_entry"); + }).addClass("btn-primary"); + } + } + }, + + create_stock_return_entry: function(frm) { + frappe.call({ + method: "erpnext.manufacturing.doctype.work_order.work_order.make_stock_return_entry", + args: { + "work_order": frm.doc.name, + }, + callback: function(r) { + if(!r.exc) { + let doc = frappe.model.sync(r.message); + frappe.set_route("Form", doc[0].doctype, doc[0].name); + } + } + }); }, make_job_card: function(frm) { @@ -517,7 +548,8 @@ frappe.ui.form.on("Work Order Operation", { erpnext.work_order = { set_custom_buttons: function(frm) { var doc = frm.doc; - if (doc.docstatus === 1 && doc.status != "Closed") { + + if (doc.status !== "Closed") { frm.add_custom_button(__('Close'), function() { frappe.confirm(__("Once the Work Order is Closed. It can't be resumed."), () => { @@ -525,7 +557,9 @@ erpnext.work_order = { } ); }, __("Status")); + } + if (doc.docstatus === 1 && !in_list(["Closed", "Completed"], doc.status)) { if (doc.status != 'Stopped' && doc.status != 'Completed') { frm.add_custom_button(__('Stop'), function() { erpnext.work_order.change_work_order_status(frm, "Stopped"); diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 7b8625372a8..1e6d982fc91 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -20,6 +20,7 @@ from frappe.utils import ( nowdate, time_diff_in_hours, ) +from pypika import functions as fn from erpnext.manufacturing.doctype.bom.bom import ( get_bom_item_rate, @@ -859,6 +860,7 @@ class WorkOrder(Document): if self.docstatus == 1: # calculate transferred qty based on submitted stock entries self.update_transferred_qty_for_required_items() + self.update_returned_qty() # update in bin self.update_reserved_qty_for_production() @@ -930,23 +932,62 @@ class WorkOrder(Document): self.set_available_qty() def update_transferred_qty_for_required_items(self): - """update transferred qty from submitted stock entries for that item against - the work order""" + ste = frappe.qb.DocType("Stock Entry") + ste_child = frappe.qb.DocType("Stock Entry Detail") - for d in self.required_items: - transferred_qty = frappe.db.sql( - """select sum(qty) - from `tabStock Entry` entry, `tabStock Entry Detail` detail - where - entry.work_order = %(name)s - and entry.purpose = 'Material Transfer for Manufacture' - and entry.docstatus = 1 - and detail.parent = entry.name - and (detail.item_code = %(item)s or detail.original_item = %(item)s)""", - {"name": self.name, "item": d.item_code}, - )[0][0] + query = ( + frappe.qb.from_(ste) + .inner_join(ste_child) + .on((ste_child.parent == ste.name)) + .select( + ste_child.item_code, + ste_child.original_item, + fn.Sum(ste_child.qty).as_("qty"), + ) + .where( + (ste.docstatus == 1) + & (ste.work_order == self.name) + & (ste.purpose == "Material Transfer for Manufacture") + & (ste.is_return == 0) + ) + .groupby(ste_child.item_code) + ) - d.db_set("transferred_qty", flt(transferred_qty), update_modified=False) + data = query.run(as_dict=1) or [] + transferred_items = frappe._dict({d.original_item or d.item_code: d.qty for d in data}) + + for row in self.required_items: + row.db_set( + "transferred_qty", (transferred_items.get(row.item_code) or 0.0), update_modified=False + ) + + def update_returned_qty(self): + ste = frappe.qb.DocType("Stock Entry") + ste_child = frappe.qb.DocType("Stock Entry Detail") + + query = ( + frappe.qb.from_(ste) + .inner_join(ste_child) + .on((ste_child.parent == ste.name)) + .select( + ste_child.item_code, + ste_child.original_item, + fn.Sum(ste_child.qty).as_("qty"), + ) + .where( + (ste.docstatus == 1) + & (ste.work_order == self.name) + & (ste.purpose == "Material Transfer for Manufacture") + & (ste.is_return == 1) + ) + .groupby(ste_child.item_code) + ) + + data = query.run(as_dict=1) or [] + returned_dict = frappe._dict({d.original_item or d.item_code: d.qty for d in data}) + + for row in self.required_items: + row.db_set("returned_qty", (returned_dict.get(row.item_code) or 0.0), update_modified=False) def update_consumed_qty_for_required_items(self): """ @@ -1470,3 +1511,25 @@ def get_reserved_qty_for_production(item_code: str, warehouse: str) -> float: ) ) ).run()[0][0] or 0.0 + + +@frappe.whitelist() +def make_stock_return_entry(work_order): + from erpnext.stock.doctype.stock_entry.stock_entry import get_available_materials + + non_consumed_items = get_available_materials(work_order) + if not non_consumed_items: + return + + wo_doc = frappe.get_cached_doc("Work Order", work_order) + + stock_entry = frappe.new_doc("Stock Entry") + stock_entry.from_bom = 1 + stock_entry.is_return = 1 + stock_entry.work_order = work_order + stock_entry.purpose = "Material Transfer for Manufacture" + stock_entry.bom_no = wo_doc.bom_no + stock_entry.add_transfered_raw_materials_in_items() + stock_entry.set_stock_entry_type() + + return stock_entry diff --git a/erpnext/manufacturing/doctype/work_order_item/work_order_item.json b/erpnext/manufacturing/doctype/work_order_item/work_order_item.json index 3acf5727d19..f354d45381c 100644 --- a/erpnext/manufacturing/doctype/work_order_item/work_order_item.json +++ b/erpnext/manufacturing/doctype/work_order_item/work_order_item.json @@ -20,6 +20,7 @@ "column_break_11", "transferred_qty", "consumed_qty", + "returned_qty", "available_qty_at_source_warehouse", "available_qty_at_wip_warehouse" ], @@ -97,6 +98,7 @@ "fieldtype": "Column Break" }, { + "columns": 1, "depends_on": "eval:!parent.skip_transfer", "fieldname": "consumed_qty", "fieldtype": "Float", @@ -127,11 +129,19 @@ "fieldtype": "Currency", "label": "Amount", "read_only": 1 + }, + { + "columns": 1, + "fieldname": "returned_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Returned Qty ", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2020-04-13 18:46:32.966416", + "modified": "2022-09-28 10:50:43.512562", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order Item", @@ -140,5 +150,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js index b2428e85b74..2fb4ec67913 100644 --- a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js +++ b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js @@ -50,7 +50,7 @@ frappe.query_reports["Work Order Consumed Materials"] = { label: __("Status"), fieldname: "status", fieldtype: "Select", - options: ["In Process", "Completed", "Stopped"] + options: ["", "In Process", "Completed", "Stopped"] }, { label: __("Excess Materials Consumed"), diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py index 8158bc9a02b..14e97d3dd78 100644 --- a/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py +++ b/erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py @@ -1,6 +1,8 @@ # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +from collections import defaultdict + import frappe from frappe import _ @@ -18,7 +20,11 @@ def get_data(report_filters): filters = get_filter_condition(report_filters) wo_items = {} - for d in frappe.get_all("Work Order", filters=filters, fields=fields): + + work_orders = frappe.get_all("Work Order", filters=filters, fields=fields) + returned_materials = get_returned_materials(work_orders) + + for d in work_orders: d.extra_consumed_qty = 0.0 if d.consumed_qty and d.consumed_qty > d.required_qty: d.extra_consumed_qty = d.consumed_qty - d.required_qty @@ -39,6 +45,28 @@ def get_data(report_filters): return data +def get_returned_materials(work_orders): + raw_materials_qty = defaultdict(float) + + raw_materials = frappe.get_all( + "Stock Entry", + fields=["`tabStock Entry Detail`.`item_code`", "`tabStock Entry Detail`.`qty`"], + filters=[ + ["Stock Entry", "is_return", "=", 1], + ["Stock Entry Detail", "docstatus", "=", 1], + ["Stock Entry", "work_order", "in", [d.name for d in work_orders]], + ], + ) + + for d in raw_materials: + raw_materials_qty[d.item_code] += d.qty + + for row in work_orders: + row.returned_qty = 0.0 + if raw_materials_qty.get(row.raw_material_item_code): + row.returned_qty = raw_materials_qty.get(row.raw_material_item_code) + + def get_fields(): return [ "`tabWork Order Item`.`parent`", @@ -65,7 +93,7 @@ def get_filter_condition(report_filters): for field in ["name", "production_item", "company", "status"]: value = report_filters.get(field) if value: - key = f"`{field}`" + key = f"{field}" filters.update({key: value}) return filters @@ -112,4 +140,10 @@ def get_columns(): "fieldtype": "Float", "width": 100, }, + { + "label": _("Returned Qty"), + "fieldname": "returned_qty", + "fieldtype": "Float", + "width": 100, + }, ] diff --git a/erpnext/patches/v14_0/migrate_remarks_from_gl_to_payment_ledger.py b/erpnext/patches/v14_0/migrate_remarks_from_gl_to_payment_ledger.py index 062d24b78bd..fd2a2a39cc6 100644 --- a/erpnext/patches/v14_0/migrate_remarks_from_gl_to_payment_ledger.py +++ b/erpnext/patches/v14_0/migrate_remarks_from_gl_to_payment_ledger.py @@ -3,6 +3,29 @@ from frappe import qb from frappe.utils import create_batch +def remove_duplicate_entries(pl_entries): + unique_vouchers = set() + for x in pl_entries: + unique_vouchers.add( + (x.company, x.account, x.party_type, x.party, x.voucher_type, x.voucher_no, x.gle_remarks) + ) + + entries = [] + for x in unique_vouchers: + entries.append( + frappe._dict( + company=x[0], + account=x[1], + party_type=x[2], + party=x[3], + voucher_type=x[4], + voucher_no=x[5], + gle_remarks=x[6], + ) + ) + return entries + + def execute(): if frappe.reload_doc("accounts", "doctype", "payment_ledger_entry"): @@ -34,6 +57,8 @@ def execute(): .run(as_dict=True) ) + pl_entries = remove_duplicate_entries(pl_entries) + if pl_entries: # split into multiple batches, update and commit for each batch batch_size = 1000 diff --git a/erpnext/public/scss/order-page.scss b/erpnext/public/scss/order-page.scss new file mode 100644 index 00000000000..6f5fe5d4d7a --- /dev/null +++ b/erpnext/public/scss/order-page.scss @@ -0,0 +1,115 @@ +#page-order { + .main-column { + .page-content-wrapper { + + .breadcrumb-container { + @media screen and (min-width: 567px) { + padding-left: var(--padding-sm); + } + } + + .container.my-4 { + background-color: var(--fg-color); + + @media screen and (min-width: 567px) { + padding: 1.25rem 1.5rem; + border-radius: var(--border-radius-md); + box-shadow: var(--card-shadow); + } + } + } + } +} + +.indicator-container { + @media screen and (max-width: 567px) { + padding-bottom: 0.8rem; + } +} + +.order-items { + padding: 1.5rem 0; + border-bottom: 1px solid var(--border-color); + color: var(--gray-700); + + @media screen and (max-width: 567px) { + align-items: flex-start !important; + } + .col-2 { + @media screen and (max-width: 567px) { + flex: auto; + max-width: 28%; + } + } + + .order-item-name { + font-size: var(--text-base); + font-weight: 500; + } + + .btn:focus, + .btn:hover { + background-color: var(--control-bg); + } + + + .col-6 { + @media screen and (max-width: 567px) { + max-width: 100%; + } + + &.order-item-name { + font-size: var(--text-base); + } + } +} + +.item-grand-total { + font-size: var(--text-base); +} + +.list-item-name, +.item-total, +.order-container, +.order-qty { + font-size: var(--text-md); +} + +.d-s-n { + @media screen and (max-width: 567px) { + display: none; + } +} + +.d-l-n { + @media screen and (min-width: 567px) { + display: none; + } +} + +.border-btm { + border-bottom: 1px solid var(--border-color); +} + +.order-taxes { + display: flex; + + @media screen and (min-width: 567px) { + justify-content: flex-end; + } + + .col-4 { + padding-right: 0; + + .col-8 { + padding-left: 0; + padding-right: 0; + } + + @media screen and (max-width: 567px) { + padding-left: 0; + flex: auto; + max-width: 100%; + } + } +} \ No newline at end of file diff --git a/erpnext/public/scss/website.scss b/erpnext/public/scss/website.scss index 9ea84160342..b5e97f1c34b 100644 --- a/erpnext/public/scss/website.scss +++ b/erpnext/public/scss/website.scss @@ -1,3 +1,4 @@ +@import './order-page'; .filter-options { max-height: 300px; @@ -32,19 +33,29 @@ height: 24px; } -.website-list .result { - margin-top: 2rem; -} +.website-list { + background-color: var(--fg-color); + padding: 0 var(--padding-lg); + border-radius: var(--border-radius-md); -.result { - border-bottom: 1px solid var(--border-color); + @media screen and (max-width: 567px) { + margin-left: -2rem; + } + + &.result { + border-bottom: 1px solid var(--border-color); + } } .transaction-list-item { padding: 1rem 0; - border-top: 1px solid var(--border-color); + border-bottom: 1px solid var(--border-color); position: relative; + &:only-child, &:last-child { + border: 0; + } + a.transaction-item-link { position: absolute; top: 0; @@ -68,3 +79,13 @@ line-height: 1.3; } } + +.list-item-name, .item-total { + font-size: var(--font-size-sm); +} + +.items-preview { + @media screen and (max-width: 567px) { + margin-top: 1rem; + } +} \ No newline at end of file diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index 548df318fac..c28f45aed41 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -37,8 +37,10 @@ class Bin(Document): self.set_projected_qty() - self.db_set("reserved_qty_for_production", flt(self.reserved_qty_for_production)) - self.db_set("projected_qty", self.projected_qty) + self.db_set( + "reserved_qty_for_production", flt(self.reserved_qty_for_production), update_modified=True + ) + self.db_set("projected_qty", self.projected_qty, update_modified=True) def update_reserved_qty_for_sub_contracting(self, subcontract_doctype="Subcontracting Order"): # reserved qty @@ -118,9 +120,9 @@ class Bin(Document): else: reserved_qty_for_sub_contract = 0 - self.db_set("reserved_qty_for_sub_contract", reserved_qty_for_sub_contract) + self.db_set("reserved_qty_for_sub_contract", reserved_qty_for_sub_contract, update_modified=True) self.set_projected_qty() - self.db_set("projected_qty", self.projected_qty) + self.db_set("projected_qty", self.projected_qty, update_modified=True) def on_doctype_update(): @@ -193,4 +195,5 @@ def update_qty(bin_name, args): "planned_qty": planned_qty, "projected_qty": projected_qty, }, + update_modified=True, ) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 6bcab737b37..1b9f16814c5 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -6,7 +6,7 @@ import json import frappe from frappe.tests.utils import FrappeTestCase -from frappe.utils import cstr, flt, nowdate, nowtime +from frappe.utils import add_days, cstr, flt, nowdate, nowtime, today from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.accounts.utils import get_balance_on @@ -1091,6 +1091,36 @@ class TestDeliveryNote(FrappeTestCase): frappe.db.exists("GL Entry", {"voucher_no": dn.name, "voucher_type": dn.doctype}) ) + def test_batch_expiry_for_delivery_note(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + + item = make_item( + "_Test Batch Item For Return Check", + { + "is_purchase_item": 1, + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TBIRC.#####", + }, + ) + + pi = make_purchase_receipt(qty=1, item_code=item.name) + + dn = create_delivery_note(qty=1, item_code=item.name, batch_no=pi.items[0].batch_no) + + dn.load_from_db() + batch_no = dn.items[0].batch_no + self.assertTrue(batch_no) + + frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(today(), -1)) + + return_dn = make_return_doc(dn.doctype, dn.name) + return_dn.save().submit() + + self.assertTrue(return_dn.docstatus == 1) + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") @@ -1117,6 +1147,7 @@ def create_delivery_note(**args): "expense_account": args.expense_account or "Cost of Goods Sold - _TC", "cost_center": args.cost_center or "_Test Cost Center - _TC", "serial_no": args.serial_no, + "batch_no": args.batch_no or None, "target_warehouse": args.target_warehouse, }, ) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index adddb413824..9c1c7e56796 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -183,7 +183,7 @@ class PickList(Document): frappe.throw("Row #{0}: Item Code is Mandatory".format(item.idx)) item_code = item.item_code reference = item.sales_order_item or item.material_request_item - key = (item_code, item.uom, item.warehouse, reference) + key = (item_code, item.uom, item.warehouse, item.batch_no, reference) item.idx = None item.name = None 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 c4705246b3c..d6f9bae5da2 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -128,6 +128,9 @@ def repost(doc): if not frappe.db.exists("Repost Item Valuation", doc.name): return + # This is to avoid TooManyWritesError in case of large reposts + frappe.db.MAX_WRITES_PER_TRANSACTION *= 4 + doc.set_status("In Progress") if not frappe.flags.in_test: frappe.db.commit() diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index abe98e2933e..7e9420d5035 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -148,19 +148,19 @@ "search_index": 1 }, { - "depends_on": "eval:doc.purpose==\"Send to Subcontractor\"", - "fieldname": "purchase_order", - "fieldtype": "Link", - "label": "Purchase Order", - "options": "Purchase Order" + "depends_on": "eval:doc.purpose==\"Send to Subcontractor\"", + "fieldname": "purchase_order", + "fieldtype": "Link", + "label": "Purchase Order", + "options": "Purchase Order" }, { - "depends_on": "eval:doc.purpose==\"Send to Subcontractor\"", - "fieldname": "subcontracting_order", - "fieldtype": "Link", - "label": "Subcontracting Order", - "options": "Subcontracting Order" - }, + "depends_on": "eval:doc.purpose==\"Send to Subcontractor\"", + "fieldname": "subcontracting_order", + "fieldtype": "Link", + "label": "Subcontracting Order", + "options": "Subcontracting Order" + }, { "depends_on": "eval:doc.purpose==\"Sales Return\"", "fieldname": "delivery_note_no", @@ -616,6 +616,7 @@ "fieldname": "is_return", "fieldtype": "Check", "hidden": 1, + "in_list_view": 1, "label": "Is Return", "no_copy": 1, "print_hide": 1, @@ -627,7 +628,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-05-02 05:21:39.060501", + "modified": "2022-10-07 14:39:51.943770", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 8bcd772d909..b1167351c48 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1212,13 +1212,19 @@ class StockEntry(StockController): def update_work_order(self): def _validate_work_order(pro_doc): + msg, title = "", "" if flt(pro_doc.docstatus) != 1: - frappe.throw(_("Work Order {0} must be submitted").format(self.work_order)) + msg = f"Work Order {self.work_order} must be submitted" if pro_doc.status == "Stopped": - frappe.throw( - _("Transaction not allowed against stopped Work Order {0}").format(self.work_order) - ) + msg = f"Transaction not allowed against stopped Work Order {self.work_order}" + + if self.is_return and pro_doc.status not in ["Completed", "Closed"]: + title = _("Stock Return") + msg = f"Work Order {self.work_order} must be completed or closed" + + if msg: + frappe.throw(_(msg), title=title) if self.job_card: job_doc = frappe.get_doc("Job Card", self.job_card) @@ -1754,10 +1760,12 @@ class StockEntry(StockController): for key, row in available_materials.items(): remaining_qty_to_produce = flt(wo_data.trans_qty) - flt(wo_data.produced_qty) - if remaining_qty_to_produce <= 0: + if remaining_qty_to_produce <= 0 and not self.is_return: continue - qty = (flt(row.qty) * flt(self.fg_completed_qty)) / remaining_qty_to_produce + qty = flt(row.qty) + if not self.is_return: + qty = (flt(row.qty) * flt(self.fg_completed_qty)) / remaining_qty_to_produce item = row.item_details if cint(frappe.get_cached_value("UOM", item.stock_uom, "must_be_whole_number")): @@ -1781,6 +1789,9 @@ class StockEntry(StockController): self.update_item_in_stock_entry_detail(row, item, qty) def update_item_in_stock_entry_detail(self, row, item, qty) -> None: + if not qty: + return + ste_item_details = { "from_warehouse": item.warehouse, "to_warehouse": "", @@ -1794,6 +1805,9 @@ class StockEntry(StockController): "original_item": item.original_item, } + if self.is_return: + ste_item_details["to_warehouse"] = item.s_warehouse + if row.serial_nos: serial_nos = row.serial_nos if item.batch_no: diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_list.js b/erpnext/stock/doctype/stock_entry/stock_entry_list.js index cbc3491eba4..4eb0da11d2f 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry_list.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry_list.js @@ -1,8 +1,13 @@ frappe.listview_settings['Stock Entry'] = { add_fields: ["`tabStock Entry`.`from_warehouse`", "`tabStock Entry`.`to_warehouse`", - "`tabStock Entry`.`purpose`", "`tabStock Entry`.`work_order`", "`tabStock Entry`.`bom_no`"], + "`tabStock Entry`.`purpose`", "`tabStock Entry`.`work_order`", "`tabStock Entry`.`bom_no`", + "`tabStock Entry`.`is_return`"], get_indicator: function (doc) { - if (doc.docstatus === 0) { + debugger + if(doc.is_return===1 && doc.purpose === "Material Transfer for Manufacture") { + return [__("Material Returned from WIP"), "orange", + "is_return,=,1|purpose,=,Material Transfer for Manufacture|docstatus,<,2"]; + } else if (doc.docstatus === 0) { return [__("Draft"), "red", "docstatus,=,0"]; } else if (doc.purpose === 'Send to Warehouse' && doc.per_transferred < 100) { diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index f7f8cbe4ee0..c64370dcdf2 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -153,7 +153,9 @@ class StockLedgerEntry(Document): def validate_batch(self): if self.batch_no and self.voucher_type != "Stock Entry": - if self.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and self.actual_qty < 0: + if (self.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and self.actual_qty < 0) or ( + self.voucher_type in ["Delivery Note", "Sales Invoice"] and self.actual_qty > 0 + ): return expiry_date = frappe.db.get_value("Batch", self.batch_no, "expiry_date") diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 50309647de0..9ca40c3675f 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1053,7 +1053,7 @@ class update_entries_after(object): updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value} if data.valuation_rate is not None: updated_values["valuation_rate"] = data.valuation_rate - frappe.db.set_value("Bin", bin_name, updated_values) + frappe.db.set_value("Bin", bin_name, updated_values, update_modified=True) def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False): diff --git a/erpnext/templates/includes/footer/footer_extension.html b/erpnext/templates/includes/footer/footer_extension.html index c7f0d06dff2..0072dc280c7 100644 --- a/erpnext/templates/includes/footer/footer_extension.html +++ b/erpnext/templates/includes/footer/footer_extension.html @@ -6,7 +6,7 @@ aria-label="{{ _('Your email address...') }}" aria-describedby="footer-subscribe-button">
Some functionality is disabled for the demo and the data will be cleared regularly.
- -+ {{ _("Valid Till") }}: {{ frappe.utils.format_date(doc.valid_till, 'medium') }} +
{% endif %} - +- {{ _("Valid Till") }}: {{ frappe.utils.format_date(doc.valid_till, 'medium') }} -
- {% endif %} -
- {%- set party_name = doc.supplier_name if doc.doctype in ['Supplier Quotation', 'Purchase Invoice', 'Purchase Order'] else doc.customer_name %}
- {{ party_name }}
-
- {% if doc.contact_display and doc.contact_display != party_name %}
-
- {{ doc.contact_display }}
- {% endif %}
-
| - {{ _("Item") }} - | -- {{ _("Quantity") }} - | -- {{ _("Amount") }} - | - - - {% for d in doc.items %} -
|---|---|---|
| - {{ item_name_and_description(d) }} - | -
- {{ d.qty }}
- {% if d.delivered_qty is defined and d.delivered_qty != None %}
- {{ _("Delivered") }} {{ d.delivered_qty }} +
+
+
+ {% if doc.doctype == "Quotation" and not doc.docstatus %}
+ {{ _("Pending") }}
+ {% else %}
+ {{ _(doc.get('indicator_title')) or _(doc.status) or _("Submitted") }}
{% endif %}
- |
-
- {{ d.get_formatted("amount") }}
- {{ _("Rate:") }} {{ d.get_formatted("rate") }} - |
-
Available Points: {{ available_loyalty_points }}
+Available Points: {{ + available_loyalty_points }}
{{ doc.terms }}
+{{ doc.terms }}