Merge pull request #45358 from frappe/version-14-hotfix

chore: release v14
This commit is contained in:
ruthra kumar
2025-01-22 09:03:17 +05:30
committed by GitHub
13 changed files with 171 additions and 57 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -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:

View File

@@ -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:

View File

@@ -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))

View File

@@ -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)

View File

@@ -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

View File

@@ -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 = """
<p>{0}.</p>
<p><a href="{1}">{2}</a></p>
"""
content = "<p>{}</p>".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

View File

@@ -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):

View File

@@ -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);

View File

@@ -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"""<a href="{get_url_to_list("Customer")}?sales_person={self.name}">{"Customers"}</a>"""
)
)
)
def get_email_id(self):
if self.employee:
user = frappe.db.get_value("Employee", self.employee, "user_id")

View File

@@ -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(

View File

@@ -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)