diff --git a/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json b/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json index 9fcbf5c6339..ebd309bfb67 100644 --- a/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json +++ b/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json @@ -14,7 +14,8 @@ "advance_amount", "allocated_amount", "exchange_gain_loss", - "ref_exchange_rate" + "ref_exchange_rate", + "difference_posting_date" ], "fields": [ { @@ -30,7 +31,7 @@ "width": "180px" }, { - "columns": 3, + "columns": 2, "fieldname": "reference_name", "fieldtype": "Dynamic Link", "in_list_view": 1, @@ -40,7 +41,7 @@ "read_only": 1 }, { - "columns": 3, + "columns": 2, "fieldname": "remarks", "fieldtype": "Text", "in_list_view": 1, @@ -111,13 +112,20 @@ "label": "Reference Exchange Rate", "non_negative": 1, "read_only": 1 + }, + { + "columns": 2, + "fieldname": "difference_posting_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Difference Posting Date" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-09-26 15:47:28.167371", + "modified": "2024-12-20 12:04:46.729972", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Advance", diff --git a/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json b/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json index f92b57a45e1..d4e3b9d896c 100644 --- a/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json +++ b/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json @@ -14,7 +14,8 @@ "advance_amount", "allocated_amount", "exchange_gain_loss", - "ref_exchange_rate" + "ref_exchange_rate", + "difference_posting_date" ], "fields": [ { @@ -30,7 +31,7 @@ "width": "250px" }, { - "columns": 3, + "columns": 2, "fieldname": "reference_name", "fieldtype": "Dynamic Link", "in_list_view": 1, @@ -41,7 +42,7 @@ "read_only": 1 }, { - "columns": 3, + "columns": 2, "fieldname": "remarks", "fieldtype": "Text", "in_list_view": 1, @@ -112,13 +113,20 @@ "label": "Reference Exchange Rate", "non_negative": 1, "read_only": 1 + }, + { + "columns": 2, + "fieldname": "difference_posting_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Difference Posting Date" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-09-26 15:47:46.911595", + "modified": "2024-12-20 11:58:28.962370", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Advance", 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 dc8a34647e2..2115d44322d 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -124,6 +124,9 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None): cost_center = get_cost_center(inv) tax_row.update({"cost_center": cost_center}) + if cint(tax_details.round_off_tax_amount): + inv.round_off_applicable_accounts_for_tax_withholding = tax_details.account_head + if inv.doctype == "Purchase Invoice": return tax_row, tax_deducted_on_advances, voucher_wise_amount else: diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index fa26fea3bf0..ecbf1177ee4 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -346,13 +346,14 @@ class AccountsController(TransactionBase): == 1 ) ).run() - frappe.db.sql( - "delete from `tabGL Entry` where voucher_type=%s and voucher_no=%s", (self.doctype, self.name) - ) - frappe.db.sql( - "delete from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s", - (self.doctype, self.name), - ) + gle = frappe.qb.DocType("GL Entry") + frappe.qb.from_(gle).delete().where( + (gle.voucher_type == self.doctype) & (gle.voucher_no == self.name) + ).run() + sle = frappe.qb.DocType("Stock Ledger Entry") + frappe.qb.from_(sle).delete().where( + (sle.voucher_type == self.doctype) & (sle.voucher_no == self.name) + ).run() def validate_return_against_account(self): if self.doctype in ["Sales Invoice", "Purchase Invoice"] and self.is_return and self.return_against: @@ -1027,11 +1028,12 @@ class AccountsController(TransactionBase): def clear_unallocated_advances(self, childtype, parentfield): self.set(parentfield, self.get(parentfield, {"allocated_amount": ["not in", [0, None, ""]]})) - frappe.db.sql( - """delete from `tab{}` where parentfield={} and parent = {} - and allocated_amount = 0""".format(childtype, "%s", "%s"), - (parentfield, self.name), - ) + doctype = frappe.qb.DocType(childtype) + frappe.qb.from_(doctype).delete().where( + (doctype.parentfield == parentfield) + & (doctype.parent == self.name) + & (doctype.allocated_amount == 0) + ).run() @frappe.whitelist() def apply_shipping_rule(self): @@ -1082,6 +1084,7 @@ class AccountsController(TransactionBase): "advance_amount": flt(d.amount), "allocated_amount": allocated_amount, "ref_exchange_rate": flt(d.exchange_rate), # exchange_rate of advance entry + "difference_posting_date": self.posting_date, } self.append("advances", advance_row) @@ -1332,7 +1335,6 @@ class AccountsController(TransactionBase): gain_loss_account = frappe.get_cached_value( "Company", self.company, "exchange_gain_loss_account" ) - je = create_gain_loss_journal( self.company, args.get("difference_posting_date") if args else self.posting_date, @@ -1445,6 +1447,7 @@ class AccountsController(TransactionBase): "Company", self.company, "exchange_gain_loss_account" ), "exchange_gain_loss": flt(d.get("exchange_gain_loss")), + "difference_posting_date": d.get("difference_posting_date"), } ) lst.append(args) @@ -1971,11 +1974,9 @@ class AccountsController(TransactionBase): for adv in self.advances: consider_for_total_advance = True if adv.reference_name == linked_doc_name: - frappe.db.sql( - f"""delete from `tab{self.doctype} Advance` - where name = %s""", - adv.name, - ) + doctype = frappe.qb.DocType(self.doctype + " Advance") + frappe.qb.from_(doctype).delete().where(doctype.name == adv.name).run() + consider_for_total_advance = False if consider_for_total_advance: diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index f661cc33c63..b2a4a4e0f7e 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -105,7 +105,7 @@ def validate_returned_items(doc): for d in doc.get("items"): key = d.item_code raise_exception = False - if doc.doctype in ["Purchase Receipt", "Purchase Invoice", "Sales Invoice"]: + if doc.doctype in ["Purchase Receipt", "Purchase Invoice", "Sales Invoice", "POS Invoice"]: field = frappe.scrub(doc.doctype) + "_item" if d.get(field): key = (d.item_code, d.get(field)) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 5b6a7b1506b..f98fdac3a6a 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -27,6 +27,11 @@ class calculate_taxes_and_totals: self.doc = doc frappe.flags.round_off_applicable_accounts = [] + if doc.get("round_off_applicable_accounts_for_tax_withholding"): + frappe.flags.round_off_applicable_accounts.append( + doc.round_off_applicable_accounts_for_tax_withholding + ) + self._items = self.filter_rows() if self.doc.doctype == "Quotation" else self.doc.get("items") get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 4ada8e60d9b..a184009b1e1 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -2,6 +2,8 @@ # For license information, please see license.txt +from datetime import datetime + import frappe from frappe import qb from frappe.query_builder.functions import Sum diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 29a288ef671..13fad07cb4a 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -8,7 +8,7 @@ from frappe import _, qb from frappe.desk.reportview import get_match_cond from frappe.model.document import Document from frappe.query_builder.functions import Sum -from frappe.utils import add_days, flt, get_datetime, get_time, get_url, nowtime, today +from frappe.utils import add_days, flt, get_datetime, get_link_to_form, get_time, nowtime, today from erpnext import get_default_company from erpnext.controllers.queries import get_filters_cond @@ -275,24 +275,19 @@ class Project(Document): frappe.db.set_value("Project", new_name, "copied_from", new_name) def send_welcome_email(self): - url = get_url(f"/project/?name={self.name}") - messages = ( - _("You have been invited to collaborate on the project: {0}").format(self.name), - url, - _("Join"), - ) + label = f"{self.project_name} ({self.name})" + url = get_link_to_form(self.doctype, self.name, label) - content = """ -

{0}.

-

{2}

- """ + content = "

{}

".format( + _("You have been invited to collaborate on the project: {0}").format(url) + ) for user in self.users: if user.welcome_email_sent == 0: frappe.sendmail( user.user, subject=_("Project Collaboration Invitation"), - content=content.format(*messages), + content=content, ) user.welcome_email_sent = 1 diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index bc94d3b706e..7db9faacca2 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -51,13 +51,18 @@ def search_by_term(search_term, warehouse, price_list): item_stock_qty = item_stock_qty // item.get("conversion_factor", 1) item.update({"actual_qty": item_stock_qty}) + price_filters = { + "price_list": price_list, + "item_code": item_code, + } + + if batch_no: + price_filters["batch_no"] = batch_no + price = frappe.get_list( doctype="Item Price", - filters={ - "price_list": price_list, - "item_code": item_code, - }, - fields=["uom", "currency", "price_list_rate"], + filters=price_filters, + fields=["uom", "currency", "price_list_rate", "batch_no"], ) def __sort(p): diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js index dba36bd2429..7021777d158 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -325,13 +325,16 @@ erpnext.PointOfSale.ItemSelector = class { } filter_items({ search_term = "" } = {}) { + const selling_price_list = this.events.get_frm().doc.selling_price_list; + if (search_term) { search_term = search_term.toLowerCase(); // memoize this.search_index = this.search_index || {}; - if (this.search_index[search_term]) { - const items = this.search_index[search_term]; + this.search_index[selling_price_list] = this.search_index[selling_price_list] || {}; + if (this.search_index[selling_price_list][search_term]) { + const items = this.search_index[selling_price_list][search_term]; this.items = items; this.render_item_list(items); this.auto_add_item && this.items.length == 1 && this.add_filtered_item_to_cart(); @@ -343,7 +346,7 @@ erpnext.PointOfSale.ItemSelector = class { // eslint-disable-next-line no-unused-vars const { items, serial_no, batch_no, barcode } = message; if (search_term && !barcode) { - this.search_index[search_term] = items; + this.search_index[selling_price_list][search_term] = items; } this.items = items; this.render_item_list(items); diff --git a/erpnext/setup/doctype/sales_person/sales_person.py b/erpnext/setup/doctype/sales_person/sales_person.py index 91a7008fabd..8af5e4aeb09 100644 --- a/erpnext/setup/doctype/sales_person/sales_person.py +++ b/erpnext/setup/doctype/sales_person/sales_person.py @@ -5,6 +5,7 @@ import frappe from frappe import _ from frappe.utils import flt +from frappe.utils.data import get_url_to_list from frappe.utils.nestedset import NestedSet, get_root_of from erpnext import get_default_currency @@ -14,6 +15,9 @@ class SalesPerson(NestedSet): nsm_parent_field = "parent_sales_person" def validate(self): + if not self.enabled: + self.validate_sales_person() + if not self.parent_sales_person: self.parent_sales_person = get_root_of("Sales Person") @@ -55,6 +59,25 @@ class SalesPerson(NestedSet): super().on_update() self.validate_one_root() + def validate_sales_person(self): + sales_team = frappe.qb.DocType("Sales Team") + + query = ( + frappe.qb.from_(sales_team) + .select(sales_team.sales_person) + .where((sales_team.sales_person == self.name) & (sales_team.parenttype == "Customer")) + .groupby(sales_team.sales_person) + ).run(as_dict=True) + + if query: + frappe.throw( + _("The Sales Person is linked with {0}").format( + frappe.bold( + f"""{"Customers"}""" + ) + ) + ) + def get_email_id(self): if self.employee: user = frappe.db.get_value("Employee", self.employee, "user_id") diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 2176d75ee97..a2a07e6d11e 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -26,9 +26,8 @@ from erpnext.stock.get_item_details import get_conversion_factor class PickList(Document): def validate(self): self.validate_for_qty() - if self.pick_manually and self.get("locations"): - self.validate_stock_qty() - self.check_serial_no_status() + self.validate_stock_qty() + self.check_serial_no_status() def before_save(self): self.update_status() @@ -42,14 +41,24 @@ class PickList(Document): from erpnext.stock.doctype.batch.batch import get_batch_qty for row in self.get("locations"): - if row.batch_no and not row.qty: + if not row.picked_qty: + continue + + if row.batch_no and row.picked_qty: batch_qty = get_batch_qty(row.batch_no, row.warehouse, row.item_code) - if row.qty > batch_qty: + if row.picked_qty > batch_qty: frappe.throw( _( - "At Row #{0}: The picked quantity {1} for the item {2} is greater than available stock {3} for the batch {4} in the warehouse {5}." - ).format(row.idx, row.item_code, batch_qty, row.batch_no, bold(row.warehouse)), + "At Row #{0}: The picked quantity {1} for the item {2} is greater than available stock {3} for the batch {4} in the warehouse {5}. Please restock the item." + ).format( + row.idx, + row.picked_qty, + row.item_code, + batch_qty, + row.batch_no, + bold(row.warehouse), + ), title=_("Insufficient Stock"), ) @@ -61,11 +70,11 @@ class PickList(Document): "actual_qty", ) - if row.qty > flt(bin_qty): + if row.picked_qty > flt(bin_qty): frappe.throw( _( "At Row #{0}: The picked quantity {1} for the item {2} is greater than available stock {3} in the warehouse {4}." - ).format(row.idx, row.qty, bold(row.item_code), bin_qty, bold(row.warehouse)), + ).format(row.idx, row.picked_qty, bold(row.item_code), bin_qty, bold(row.warehouse)), title=_("Insufficient Stock"), ) @@ -253,7 +262,14 @@ class PickList(Document): locations_replica = self.get("locations") # reset - self.delete_key("locations") + reset_rows = [] + for row in self.get("locations"): + if not row.picked_qty: + reset_rows.append(row) + + for row in reset_rows: + self.remove(row) + updated_locations = frappe._dict() for item_doc in items: item_code = item_doc.item_code @@ -323,6 +339,9 @@ class PickList(Document): # aggregate qty for same item item_map = OrderedDict() for item in locations: + if item.picked_qty: + continue + if not item.item_code: frappe.throw(f"Row #{item.idx}: Item Code is Mandatory") if not cint( diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 162fcadde7a..f679d28c2b2 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -842,5 +842,47 @@ class TestPickList(FrappeTestCase): for row in pl.locations: row.qty = row.qty + 10 + row.picked_qty = row.qty self.assertRaises(frappe.ValidationError, pl.save) + + def test_pick_list_not_reset_batch(self): + warehouse = "_Test Warehouse - _TC" + item = make_item( + "Test Do Not Reset Picked Item", + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BTH-PICKLT-.######", + }, + ).name + + se = make_stock_entry(item=item, to_warehouse=warehouse, qty=10) + se.reload() + batch1 = se.items[0].batch_no + se = make_stock_entry(item=item, to_warehouse=warehouse, qty=10) + se.reload() + batch2 = se.items[0].batch_no + + so = make_sales_order(item_code=item, qty=10, rate=100) + + pl = create_pick_list(so.name) + pl.save() + + for loc in pl.locations: + self.assertEqual(loc.batch_no, batch1) + loc.batch_no = batch2 + loc.picked_qty = 0.0 + + pl.save() + + for loc in pl.locations: + self.assertEqual(loc.batch_no, batch1) + loc.batch_no = batch2 + loc.picked_qty = 10.0 + + pl.save() + + for loc in pl.locations: + self.assertEqual(loc.batch_no, batch2)