Merge pull request #41854 from frappe/version-15-hotfix

chore: release v15
This commit is contained in:
rohitwaghchaure
2024-06-11 18:47:08 +05:30
committed by GitHub
20 changed files with 252 additions and 49 deletions

View File

@@ -89,6 +89,7 @@
"custom_remarks",
"remarks",
"base_in_words",
"is_opening",
"column_break_16",
"letter_head",
"print_heading",
@@ -777,6 +778,16 @@
"label": "Reconcile on Advance Payment Date",
"no_copy": 1,
"read_only": 1
},
{
"default": "No",
"depends_on": "eval: doc.book_advance_payments_in_separate_party_account == 1",
"fieldname": "is_opening",
"fieldtype": "Select",
"label": "Is Opening",
"options": "No\nYes",
"print_hide": 1,
"search_index": 1
}
],
"index_web_pages_for_search": 1,
@@ -790,7 +801,7 @@
"table_fieldname": "payment_entries"
}
],
"modified": "2024-05-17 10:21:11.199445",
"modified": "2024-05-31 17:07:06.197249",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",

View File

@@ -116,11 +116,13 @@ class PaymentEntry(AccountsController):
self.book_advance_payments_in_separate_party_account = False
if self.party_type not in ("Customer", "Supplier"):
self.is_opening = "No"
return
if not frappe.db.get_value(
"Company", self.company, "book_advance_payments_in_separate_party_account"
):
self.is_opening = "No"
return
# Important to set this flag for the gl building logic to work properly
@@ -132,6 +134,7 @@ class PaymentEntry(AccountsController):
if (account_type == "Payable" and self.party_type == "Customer") or (
account_type == "Receivable" and self.party_type == "Supplier"
):
self.is_opening = "No"
return
if self.references:
@@ -141,6 +144,7 @@ class PaymentEntry(AccountsController):
# If there are referencers other than `allowed_types`, treat this as a normal payment entry
if reference_types - allowed_types:
self.book_advance_payments_in_separate_party_account = False
self.is_opening = "No"
return
liability_account = get_party_account(

View File

@@ -1729,6 +1729,68 @@ class TestPaymentEntry(FrappeTestCase):
self.check_gl_entries()
self.check_pl_entries()
def test_opening_flag_for_advance_as_liability(self):
company = "_Test Company"
advance_account = create_account(
parent_account="Current Assets - _TC",
account_name="Advances Received",
company=company,
account_type="Receivable",
)
# Enable Advance in separate party account
frappe.db.set_value(
"Company",
company,
{
"book_advance_payments_in_separate_party_account": 1,
"default_advance_received_account": advance_account,
},
)
# Advance Payment
adv = create_payment_entry(
party_type="Customer",
party="_Test Customer",
payment_type="Receive",
paid_from="Debtors - _TC",
paid_to="_Test Cash - _TC",
)
adv.is_opening = "Yes"
adv.save() # use save() to trigger set_liability_account()
adv.submit()
gl_with_opening_set = frappe.db.get_all(
"GL Entry", filters={"voucher_no": adv.name, "is_opening": "Yes"}
)
# 'Is Opening' can be 'Yes' for Advances in separate party account
self.assertNotEqual(gl_with_opening_set, [])
# Disable Advance in separate party account
frappe.db.set_value(
"Company",
company,
{
"book_advance_payments_in_separate_party_account": 0,
"default_advance_received_account": None,
},
)
payment = create_payment_entry(
party_type="Customer",
party="_Test Customer",
payment_type="Receive",
paid_from="Debtors - _TC",
paid_to="_Test Cash - _TC",
)
payment.is_opening = "Yes"
payment.save()
payment.submit()
gl_with_opening_set = frappe.db.get_all(
"GL Entry", filters={"voucher_no": payment.name, "is_opening": "Yes"}
)
# 'Is Opening' should always be 'No' for normal advance payments
self.assertEqual(gl_with_opening_set, [])
def create_payment_entry(**args):
payment_entry = frappe.new_doc("Payment Entry")

View File

@@ -677,7 +677,7 @@ frappe.ui.form.on("Purchase Invoice", {
if (frm.doc.supplier) {
frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0;
}
if (!frm.doc.__onload.supplier_tds) {
if (!frm.doc.__onload.enable_apply_tds) {
frm.set_df_property("apply_tds", "read_only", 1);
}
}

View File

@@ -3,7 +3,7 @@
import frappe
from frappe import _, throw
from frappe import _, qb, throw
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder.functions import Sum
from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate
@@ -347,6 +347,22 @@ class PurchaseInvoice(BuyingController):
self.tax_withholding_category = tds_category
self.set_onload("supplier_tds", tds_category)
# If Linked Purchase Order has TDS applied, enable 'apply_tds' checkbox
if purchase_orders := [x.purchase_order for x in self.items if x.purchase_order]:
po = qb.DocType("Purchase Order")
po_with_tds = (
qb.from_(po)
.select(po.name)
.where(
po.docstatus.eq(1)
& (po.name.isin(purchase_orders))
& (po.apply_tds.eq(1))
& (po.tax_withholding_category.notnull())
)
.run()
)
self.set_onload("enable_apply_tds", True if po_with_tds else False)
super().set_missing_values(for_validate)
def validate_credit_to_acc(self):

View File

@@ -17,7 +17,7 @@
"in_create": 1,
"issingle": 1,
"links": [],
"modified": "2023-11-07 14:24:13.321522",
"modified": "2024-06-06 13:56:37.908879",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Repost Accounting Ledger Settings",
@@ -30,13 +30,17 @@
"print": 1,
"read": 1,
"role": "Administrator",
"select": 1,
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"read": 1,
"role": "System Manager",
"select": 1
"select": 1,
"write": 1
}
],
"sort_field": "modified",

View File

@@ -787,6 +787,7 @@
"hide_days": 1,
"hide_seconds": 1,
"label": "Time Sheets",
"no_copy": 1,
"options": "Sales Invoice Timesheet",
"print_hide": 1
},
@@ -2187,7 +2188,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2024-05-08 18:02:28.549041",
"modified": "2024-06-07 16:49:32.458402",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -69,48 +69,50 @@ def get_asset_categories_for_grouped_by_category(filters):
condition = ""
if filters.get("asset_category"):
condition += " and asset_category = %(asset_category)s"
# nosemgrep
return frappe.db.sql(
f"""
SELECT asset_category,
ifnull(sum(case when purchase_date < %(from_date)s then
case when ifnull(disposal_date, 0) = 0 or disposal_date >= %(from_date)s then
gross_purchase_amount
SELECT a.asset_category,
ifnull(sum(case when a.purchase_date < %(from_date)s then
case when ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s then
a.gross_purchase_amount
else
0
end
else
0
end), 0) as cost_as_on_from_date,
ifnull(sum(case when purchase_date >= %(from_date)s then
gross_purchase_amount
ifnull(sum(case when a.purchase_date >= %(from_date)s then
a.gross_purchase_amount
else
0
end), 0) as cost_of_new_purchase,
ifnull(sum(case when ifnull(disposal_date, 0) != 0
and disposal_date >= %(from_date)s
and disposal_date <= %(to_date)s then
case when status = "Sold" then
gross_purchase_amount
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s then
case when a.status = "Sold" then
a.gross_purchase_amount
else
0
end
else
0
end), 0) as cost_of_sold_asset,
ifnull(sum(case when ifnull(disposal_date, 0) != 0
and disposal_date >= %(from_date)s
and disposal_date <= %(to_date)s then
case when status = "Scrapped" then
gross_purchase_amount
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s then
case when a.status = "Scrapped" then
a.gross_purchase_amount
else
0
end
else
0
end), 0) as cost_of_scrapped_asset
from `tabAsset`
from `tabAsset` a
where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s {condition}
group by asset_category
and not exists(select name from `tabAsset Capitalization Asset Item` where asset = a.name)
group by a.asset_category
""",
{
"to_date": filters.to_date,

View File

@@ -715,6 +715,9 @@ class StockController(AccountsController):
row.db_set("rejected_serial_and_batch_bundle", None)
if row.get("current_serial_and_batch_bundle"):
row.db_set("current_serial_and_batch_bundle", None)
def set_serial_and_batch_bundle(self, table_name=None, ignore_validate=False):
if not table_name:
table_name = "items"

View File

@@ -452,6 +452,10 @@ class ProductionPlan(Document):
{"sales_order": data.parent, "sales_order_item": data.name, "qty": data.pending_qty}
)
bom_no = data.bom_no or item_details and item_details.bom_no or ""
if not bom_no:
continue
pi = self.append(
"po_items",
{
@@ -459,7 +463,7 @@ class ProductionPlan(Document):
"item_code": data.item_code,
"description": data.description or item_details.description,
"stock_uom": item_details and item_details.stock_uom or "",
"bom_no": data.bom_no or item_details and item_details.bom_no or "",
"bom_no": bom_no,
"planned_qty": data.pending_qty,
"pending_qty": data.pending_qty,
"planned_start_date": now_datetime(),

View File

@@ -328,6 +328,28 @@ class TestProductionPlan(FrappeTestCase):
self.assertEqual(pln2.po_items[0].bom_no, bom2.name)
def test_production_plan_with_non_active_bom_item(self):
item = make_item("Test Production Item 1 for Non Active BOM", {"is_stock_item": 1}).name
so1 = make_sales_order(item_code=item, qty=1)
pln = frappe.new_doc("Production Plan")
pln.company = so1.company
pln.get_items_from = "Sales Order"
pln.append(
"sales_orders",
{
"sales_order": so1.name,
"sales_order_date": so1.transaction_date,
"customer": so1.customer,
"grand_total": so1.grand_total,
},
)
pln.get_items()
self.assertFalse(pln.po_items)
def test_production_plan_combine_items(self):
"Test combining FG items in Production Plan."
item = "Test Production Item 1"

View File

@@ -83,7 +83,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
this.frm.doc.paid_amount = flt(this.frm.doc.grand_total, precision("grand_total"));
}
this.frm.refresh_field("taxes");
this.frm.refresh_fields();
}
calculate_discount_amount() {
@@ -841,7 +841,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
});
}
this.frm.refresh_field("taxes");
this.frm.refresh_fields();
}
set_default_payment(total_amount_to_pay, update_paid_amount) {

View File

@@ -1246,8 +1246,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
qty(doc, cdt, cdn) {
if (!this.frm.doc.__onload?.load_after_mapping) {
let item = frappe.get_doc(cdt, cdn);
let item = frappe.get_doc(cdt, cdn);
if (!this.is_a_mapped_document(item)) {
// item.pricing_rules = ''
frappe.run_serially([
() => this.remove_pricing_rule_for_item(item),

View File

@@ -794,6 +794,11 @@ def get_requested_item_qty(sales_order):
def make_material_request(source_name, target_doc=None):
requested_item_qty = get_requested_item_qty(source_name)
def postprocess(source, target):
if source.tc_name and frappe.db.get_value("Terms and Conditions", source.tc_name, "buying") != 1:
target.tc_name = None
target.terms = None
def get_remaining_qty(so_item):
return flt(
flt(so_item.qty)
@@ -849,6 +854,7 @@ def make_material_request(source_name, target_doc=None):
},
},
target_doc,
postprocess,
)
return doc

View File

@@ -42,7 +42,7 @@ class HolidayList(Document):
def validate(self):
self.validate_days()
self.total_holidays = len(self.holidays)
self.validate_dupliacte_date()
self.validate_duplicate_date()
@frappe.whitelist()
def get_weekly_off_dates(self):
@@ -148,7 +148,7 @@ class HolidayList(Document):
def clear_table(self):
self.set("holidays", [])
def validate_dupliacte_date(self):
def validate_duplicate_date(self):
unique_dates = []
for row in self.holidays:
if row.holiday_date in unique_dates:

View File

@@ -26,3 +26,29 @@ class TestVehicle(unittest.TestCase):
}
)
vehicle.insert()
def test_renaming_vehicle(self):
license_plate = random_string(10).upper()
vehicle = frappe.get_doc(
{
"doctype": "Vehicle",
"license_plate": license_plate,
"make": "Skoda",
"model": "Slavia",
"last_odometer": 5000,
"acquisition_date": frappe.utils.nowdate(),
"location": "Mumbai",
"chassis_no": "1234EFGH",
"uom": "Litre",
"vehicle_value": frappe.utils.flt(500000),
}
)
vehicle.insert()
new_license_plate = random_string(10).upper()
frappe.rename_doc("Vehicle", license_plate, new_license_plate)
self.assertEqual(
new_license_plate, frappe.db.get_value("Vehicle", new_license_plate, "license_plate")
)

View File

@@ -2,7 +2,8 @@
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"actions": [],
"allow_rename": 1,
"autoname": "field:license_plate",
"beta": 0,
"creation": "2016-09-03 03:33:27.680331",
@@ -834,7 +835,8 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2022-06-27 14:48:30.813359",
"links": [],
"modified": "2024-05-31 06:38:15.399283",
"modified_by": "Administrator",
"module": "Setup",
"name": "Vehicle",

View File

@@ -164,7 +164,11 @@ class StockReconciliation(StockController):
for item in self.items:
if not item.reconcile_all_serial_batch and item.serial_and_batch_bundle:
bundle = self.get_bundle_for_specific_serial_batch(item)
item.current_serial_and_batch_bundle = bundle
item.current_serial_and_batch_bundle = bundle.name
item.current_valuation_rate = abs(bundle.avg_rate)
if not item.valuation_rate:
item.valuation_rate = item.current_valuation_rate
continue
if not save and item.use_serial_batch_fields:
@@ -282,7 +286,12 @@ class StockReconciliation(StockController):
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
if row.current_serial_and_batch_bundle and not self.has_change_in_serial_batch(row):
return row.current_serial_and_batch_bundle
return frappe._dict(
{
"name": row.current_serial_and_batch_bundle,
"avg_rate": row.current_valuation_rate,
}
)
cls_obj = SerialBatchCreation(
{
@@ -316,12 +325,11 @@ class StockReconciliation(StockController):
total_current_qty += current_qty
entry.qty = current_qty * -1
reco_obj.flags.ignore_validate = True
reco_obj.save()
row.current_qty = total_current_qty
return reco_obj.name
return reco_obj
def has_change_in_serial_batch(self, row) -> bool:
bundles = {row.serial_and_batch_bundle: [], row.current_serial_and_batch_bundle: []}
@@ -721,7 +729,7 @@ class StockReconciliation(StockController):
for d in serial_nos:
frappe.db.set_value("Serial No", d, "purchase_rate", valuation_rate)
def get_sle_for_items(self, row, serial_nos=None):
def get_sle_for_items(self, row, serial_nos=None, current_bundle=True):
"""Insert Stock Ledger Entries"""
if not serial_nos and row.serial_no:
@@ -755,7 +763,7 @@ class StockReconciliation(StockController):
has_dimensions = True
if self.docstatus == 2 and (not row.batch_no or not row.serial_and_batch_bundle):
if row.current_qty:
if row.current_qty and current_bundle:
data.actual_qty = -1 * row.current_qty
data.qty_after_transaction = flt(row.current_qty)
data.previous_qty_after_transaction = flt(row.qty)
@@ -785,6 +793,8 @@ class StockReconciliation(StockController):
has_serial_no = False
for row in self.items:
sl_entries.append(self.get_sle_for_items(row))
if row.serial_and_batch_bundle and row.current_serial_and_batch_bundle:
sl_entries.append(self.get_sle_for_items(row, current_bundle=False))
if sl_entries:
if has_serial_no:

View File

@@ -1109,6 +1109,8 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
)
sr.reload()
self.assertTrue(sr.items[0].current_valuation_rate)
current_sabb = sr.items[0].current_serial_and_batch_bundle
doc = frappe.get_doc("Serial and Batch Bundle", current_sabb)
for row in doc.entries:
@@ -1118,6 +1120,18 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
batch_qty = get_batch_qty(batches[0].batch_no, warehouse, item.name)
self.assertEqual(batch_qty, 100)
for row in frappe.get_all("Repost Item Valuation", filters={"voucher_no": sr.name}):
rdoc = frappe.get_doc("Repost Item Valuation", row.name)
rdoc.cancel()
rdoc.delete()
sr.cancel()
for row in frappe.get_all(
"Serial and Batch Bundle", fields=["docstatus"], filters={"voucher_no": sr.name}
):
self.assertEqual(row.docstatus, 2)
def test_not_reconcile_all_serial_nos(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.stock.utils import get_incoming_rate

View File

@@ -36,6 +36,7 @@ from erpnext.stock.utils import (
get_incoming_outgoing_rate_for_cancel,
get_incoming_rate,
get_or_make_bin,
get_serial_nos_data,
get_stock_balance,
get_valuation_method,
)
@@ -811,9 +812,10 @@ class update_entries_after:
self.update_outgoing_rate_on_transaction(sle)
def get_serialized_values(self, sle):
from erpnext.stock.serial_batch_bundle import SerialNoValuation
incoming_rate = flt(sle.incoming_rate)
actual_qty = flt(sle.actual_qty)
serial_nos = cstr(sle.serial_no).split("\n")
if incoming_rate < 0:
# wrong incoming rate
@@ -826,8 +828,16 @@ class update_entries_after:
# In case of delivery/stock issue, get average purchase rate
# of serial nos of current entry
if not sle.is_cancelled:
outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
stock_value_change = -1 * outgoing_value
new_sle = copy.deepcopy(sle)
new_sle.qty = new_sle.actual_qty
new_sle.serial_nos = get_serial_nos_data(new_sle.get("serial_no"))
sn_obj = SerialNoValuation(
sle=new_sle, warehouse=new_sle.get("warehouse"), item_code=new_sle.get("item_code")
)
outgoing_value = sn_obj.get_incoming_rate()
stock_value_change = actual_qty * outgoing_value
else:
stock_value_change = actual_qty * sle.outgoing_rate
@@ -1272,6 +1282,8 @@ class update_entries_after:
self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
def update_batched_values(self, sle):
from erpnext.stock.serial_batch_bundle import BatchNoValuation
incoming_rate = flt(sle.incoming_rate)
actual_qty = flt(sle.actual_qty)
@@ -1282,21 +1294,25 @@ class update_entries_after:
if actual_qty > 0:
stock_value_difference = incoming_rate * actual_qty
else:
outgoing_rate = get_batch_incoming_rate(
item_code=sle.item_code,
warehouse=sle.warehouse,
batch_no=sle.batch_no,
posting_date=sle.posting_date,
posting_time=sle.posting_time,
creation=sle.creation,
new_sle = copy.deepcopy(sle)
new_sle.qty = new_sle.actual_qty
new_sle.batch_nos = frappe._dict({new_sle.batch_no: new_sle})
batch_obj = BatchNoValuation(
sle=new_sle,
warehouse=new_sle.get("warehouse"),
item_code=new_sle.get("item_code"),
)
outgoing_rate = batch_obj.get_incoming_rate()
if outgoing_rate is None:
# This can *only* happen if qty available for the batch is zero.
# in such case fall back various other rates.
# future entries will correct the overall accounting as each
# batch individually uses moving average rates.
outgoing_rate = self.get_fallback_rate(sle)
stock_value_difference = outgoing_rate * actual_qty
self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + stock_value_difference)