mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-04 05:58:27 +00:00
Merge pull request #45358 from frappe/version-14-hotfix
chore: release v14
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user