mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-31 18:59:08 +00:00
Merge pull request #41854 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -89,6 +89,7 @@
|
|||||||
"custom_remarks",
|
"custom_remarks",
|
||||||
"remarks",
|
"remarks",
|
||||||
"base_in_words",
|
"base_in_words",
|
||||||
|
"is_opening",
|
||||||
"column_break_16",
|
"column_break_16",
|
||||||
"letter_head",
|
"letter_head",
|
||||||
"print_heading",
|
"print_heading",
|
||||||
@@ -777,6 +778,16 @@
|
|||||||
"label": "Reconcile on Advance Payment Date",
|
"label": "Reconcile on Advance Payment Date",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"read_only": 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,
|
"index_web_pages_for_search": 1,
|
||||||
@@ -790,7 +801,7 @@
|
|||||||
"table_fieldname": "payment_entries"
|
"table_fieldname": "payment_entries"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2024-05-17 10:21:11.199445",
|
"modified": "2024-05-31 17:07:06.197249",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Entry",
|
"name": "Payment Entry",
|
||||||
|
|||||||
@@ -116,11 +116,13 @@ class PaymentEntry(AccountsController):
|
|||||||
|
|
||||||
self.book_advance_payments_in_separate_party_account = False
|
self.book_advance_payments_in_separate_party_account = False
|
||||||
if self.party_type not in ("Customer", "Supplier"):
|
if self.party_type not in ("Customer", "Supplier"):
|
||||||
|
self.is_opening = "No"
|
||||||
return
|
return
|
||||||
|
|
||||||
if not frappe.db.get_value(
|
if not frappe.db.get_value(
|
||||||
"Company", self.company, "book_advance_payments_in_separate_party_account"
|
"Company", self.company, "book_advance_payments_in_separate_party_account"
|
||||||
):
|
):
|
||||||
|
self.is_opening = "No"
|
||||||
return
|
return
|
||||||
|
|
||||||
# Important to set this flag for the gl building logic to work properly
|
# 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 (
|
if (account_type == "Payable" and self.party_type == "Customer") or (
|
||||||
account_type == "Receivable" and self.party_type == "Supplier"
|
account_type == "Receivable" and self.party_type == "Supplier"
|
||||||
):
|
):
|
||||||
|
self.is_opening = "No"
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.references:
|
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 there are referencers other than `allowed_types`, treat this as a normal payment entry
|
||||||
if reference_types - allowed_types:
|
if reference_types - allowed_types:
|
||||||
self.book_advance_payments_in_separate_party_account = False
|
self.book_advance_payments_in_separate_party_account = False
|
||||||
|
self.is_opening = "No"
|
||||||
return
|
return
|
||||||
|
|
||||||
liability_account = get_party_account(
|
liability_account = get_party_account(
|
||||||
|
|||||||
@@ -1729,6 +1729,68 @@ class TestPaymentEntry(FrappeTestCase):
|
|||||||
self.check_gl_entries()
|
self.check_gl_entries()
|
||||||
self.check_pl_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):
|
def create_payment_entry(**args):
|
||||||
payment_entry = frappe.new_doc("Payment Entry")
|
payment_entry = frappe.new_doc("Payment Entry")
|
||||||
|
|||||||
@@ -677,7 +677,7 @@ frappe.ui.form.on("Purchase Invoice", {
|
|||||||
if (frm.doc.supplier) {
|
if (frm.doc.supplier) {
|
||||||
frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0;
|
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);
|
frm.set_df_property("apply_tds", "read_only", 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, throw
|
from frappe import _, qb, throw
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
from frappe.query_builder.functions import Sum
|
from frappe.query_builder.functions import Sum
|
||||||
from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate
|
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.tax_withholding_category = tds_category
|
||||||
self.set_onload("supplier_tds", 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)
|
super().set_missing_values(for_validate)
|
||||||
|
|
||||||
def validate_credit_to_acc(self):
|
def validate_credit_to_acc(self):
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"in_create": 1,
|
"in_create": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-11-07 14:24:13.321522",
|
"modified": "2024-06-06 13:56:37.908879",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Repost Accounting Ledger Settings",
|
"name": "Repost Accounting Ledger Settings",
|
||||||
@@ -30,13 +30,17 @@
|
|||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"role": "Administrator",
|
"role": "Administrator",
|
||||||
|
"select": 1,
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"role": "System Manager",
|
"role": "System Manager",
|
||||||
"select": 1
|
"select": 1,
|
||||||
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
|
|||||||
@@ -787,6 +787,7 @@
|
|||||||
"hide_days": 1,
|
"hide_days": 1,
|
||||||
"hide_seconds": 1,
|
"hide_seconds": 1,
|
||||||
"label": "Time Sheets",
|
"label": "Time Sheets",
|
||||||
|
"no_copy": 1,
|
||||||
"options": "Sales Invoice Timesheet",
|
"options": "Sales Invoice Timesheet",
|
||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
@@ -2187,7 +2188,7 @@
|
|||||||
"link_fieldname": "consolidated_invoice"
|
"link_fieldname": "consolidated_invoice"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2024-05-08 18:02:28.549041",
|
"modified": "2024-06-07 16:49:32.458402",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice",
|
"name": "Sales Invoice",
|
||||||
|
|||||||
@@ -69,48 +69,50 @@ def get_asset_categories_for_grouped_by_category(filters):
|
|||||||
condition = ""
|
condition = ""
|
||||||
if filters.get("asset_category"):
|
if filters.get("asset_category"):
|
||||||
condition += " and asset_category = %(asset_category)s"
|
condition += " and asset_category = %(asset_category)s"
|
||||||
|
# nosemgrep
|
||||||
return frappe.db.sql(
|
return frappe.db.sql(
|
||||||
f"""
|
f"""
|
||||||
SELECT asset_category,
|
SELECT a.asset_category,
|
||||||
ifnull(sum(case when purchase_date < %(from_date)s then
|
ifnull(sum(case when a.purchase_date < %(from_date)s then
|
||||||
case when ifnull(disposal_date, 0) = 0 or disposal_date >= %(from_date)s then
|
case when ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s then
|
||||||
gross_purchase_amount
|
a.gross_purchase_amount
|
||||||
else
|
else
|
||||||
0
|
0
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
0
|
0
|
||||||
end), 0) as cost_as_on_from_date,
|
end), 0) as cost_as_on_from_date,
|
||||||
ifnull(sum(case when purchase_date >= %(from_date)s then
|
ifnull(sum(case when a.purchase_date >= %(from_date)s then
|
||||||
gross_purchase_amount
|
a.gross_purchase_amount
|
||||||
else
|
else
|
||||||
0
|
0
|
||||||
end), 0) as cost_of_new_purchase,
|
end), 0) as cost_of_new_purchase,
|
||||||
ifnull(sum(case when ifnull(disposal_date, 0) != 0
|
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||||
and disposal_date >= %(from_date)s
|
and a.disposal_date >= %(from_date)s
|
||||||
and disposal_date <= %(to_date)s then
|
and a.disposal_date <= %(to_date)s then
|
||||||
case when status = "Sold" then
|
case when a.status = "Sold" then
|
||||||
gross_purchase_amount
|
a.gross_purchase_amount
|
||||||
else
|
else
|
||||||
0
|
0
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
0
|
0
|
||||||
end), 0) as cost_of_sold_asset,
|
end), 0) as cost_of_sold_asset,
|
||||||
ifnull(sum(case when ifnull(disposal_date, 0) != 0
|
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
|
||||||
and disposal_date >= %(from_date)s
|
and a.disposal_date >= %(from_date)s
|
||||||
and disposal_date <= %(to_date)s then
|
and a.disposal_date <= %(to_date)s then
|
||||||
case when status = "Scrapped" then
|
case when a.status = "Scrapped" then
|
||||||
gross_purchase_amount
|
a.gross_purchase_amount
|
||||||
else
|
else
|
||||||
0
|
0
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
0
|
0
|
||||||
end), 0) as cost_of_scrapped_asset
|
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}
|
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,
|
"to_date": filters.to_date,
|
||||||
|
|||||||
@@ -715,6 +715,9 @@ class StockController(AccountsController):
|
|||||||
|
|
||||||
row.db_set("rejected_serial_and_batch_bundle", None)
|
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):
|
def set_serial_and_batch_bundle(self, table_name=None, ignore_validate=False):
|
||||||
if not table_name:
|
if not table_name:
|
||||||
table_name = "items"
|
table_name = "items"
|
||||||
|
|||||||
@@ -452,6 +452,10 @@ class ProductionPlan(Document):
|
|||||||
{"sales_order": data.parent, "sales_order_item": data.name, "qty": data.pending_qty}
|
{"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(
|
pi = self.append(
|
||||||
"po_items",
|
"po_items",
|
||||||
{
|
{
|
||||||
@@ -459,7 +463,7 @@ class ProductionPlan(Document):
|
|||||||
"item_code": data.item_code,
|
"item_code": data.item_code,
|
||||||
"description": data.description or item_details.description,
|
"description": data.description or item_details.description,
|
||||||
"stock_uom": item_details and item_details.stock_uom or "",
|
"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,
|
"planned_qty": data.pending_qty,
|
||||||
"pending_qty": data.pending_qty,
|
"pending_qty": data.pending_qty,
|
||||||
"planned_start_date": now_datetime(),
|
"planned_start_date": now_datetime(),
|
||||||
|
|||||||
@@ -328,6 +328,28 @@ class TestProductionPlan(FrappeTestCase):
|
|||||||
|
|
||||||
self.assertEqual(pln2.po_items[0].bom_no, bom2.name)
|
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):
|
def test_production_plan_combine_items(self):
|
||||||
"Test combining FG items in Production Plan."
|
"Test combining FG items in Production Plan."
|
||||||
item = "Test Production Item 1"
|
item = "Test Production Item 1"
|
||||||
|
|||||||
@@ -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.doc.paid_amount = flt(this.frm.doc.grand_total, precision("grand_total"));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.frm.refresh_field("taxes");
|
this.frm.refresh_fields();
|
||||||
}
|
}
|
||||||
|
|
||||||
calculate_discount_amount() {
|
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) {
|
set_default_payment(total_amount_to_pay, update_paid_amount) {
|
||||||
|
|||||||
@@ -1246,8 +1246,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
}
|
}
|
||||||
|
|
||||||
qty(doc, cdt, cdn) {
|
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 = ''
|
// item.pricing_rules = ''
|
||||||
frappe.run_serially([
|
frappe.run_serially([
|
||||||
() => this.remove_pricing_rule_for_item(item),
|
() => this.remove_pricing_rule_for_item(item),
|
||||||
|
|||||||
@@ -794,6 +794,11 @@ def get_requested_item_qty(sales_order):
|
|||||||
def make_material_request(source_name, target_doc=None):
|
def make_material_request(source_name, target_doc=None):
|
||||||
requested_item_qty = get_requested_item_qty(source_name)
|
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):
|
def get_remaining_qty(so_item):
|
||||||
return flt(
|
return flt(
|
||||||
flt(so_item.qty)
|
flt(so_item.qty)
|
||||||
@@ -849,6 +854,7 @@ def make_material_request(source_name, target_doc=None):
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
target_doc,
|
target_doc,
|
||||||
|
postprocess,
|
||||||
)
|
)
|
||||||
|
|
||||||
return doc
|
return doc
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class HolidayList(Document):
|
|||||||
def validate(self):
|
def validate(self):
|
||||||
self.validate_days()
|
self.validate_days()
|
||||||
self.total_holidays = len(self.holidays)
|
self.total_holidays = len(self.holidays)
|
||||||
self.validate_dupliacte_date()
|
self.validate_duplicate_date()
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_weekly_off_dates(self):
|
def get_weekly_off_dates(self):
|
||||||
@@ -148,7 +148,7 @@ class HolidayList(Document):
|
|||||||
def clear_table(self):
|
def clear_table(self):
|
||||||
self.set("holidays", [])
|
self.set("holidays", [])
|
||||||
|
|
||||||
def validate_dupliacte_date(self):
|
def validate_duplicate_date(self):
|
||||||
unique_dates = []
|
unique_dates = []
|
||||||
for row in self.holidays:
|
for row in self.holidays:
|
||||||
if row.holiday_date in unique_dates:
|
if row.holiday_date in unique_dates:
|
||||||
|
|||||||
@@ -26,3 +26,29 @@ class TestVehicle(unittest.TestCase):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
vehicle.insert()
|
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")
|
||||||
|
)
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
"allow_copy": 0,
|
"allow_copy": 0,
|
||||||
"allow_guest_to_view": 0,
|
"allow_guest_to_view": 0,
|
||||||
"allow_import": 0,
|
"allow_import": 0,
|
||||||
"allow_rename": 0,
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
"autoname": "field:license_plate",
|
"autoname": "field:license_plate",
|
||||||
"beta": 0,
|
"beta": 0,
|
||||||
"creation": "2016-09-03 03:33:27.680331",
|
"creation": "2016-09-03 03:33:27.680331",
|
||||||
@@ -834,7 +835,8 @@
|
|||||||
"issingle": 0,
|
"issingle": 0,
|
||||||
"istable": 0,
|
"istable": 0,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"modified": "2022-06-27 14:48:30.813359",
|
"links": [],
|
||||||
|
"modified": "2024-05-31 06:38:15.399283",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Setup",
|
"module": "Setup",
|
||||||
"name": "Vehicle",
|
"name": "Vehicle",
|
||||||
|
|||||||
@@ -164,7 +164,11 @@ class StockReconciliation(StockController):
|
|||||||
for item in self.items:
|
for item in self.items:
|
||||||
if not item.reconcile_all_serial_batch and item.serial_and_batch_bundle:
|
if not item.reconcile_all_serial_batch and item.serial_and_batch_bundle:
|
||||||
bundle = self.get_bundle_for_specific_serial_batch(item)
|
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
|
continue
|
||||||
|
|
||||||
if not save and item.use_serial_batch_fields:
|
if not save and item.use_serial_batch_fields:
|
||||||
@@ -282,7 +286,12 @@ class StockReconciliation(StockController):
|
|||||||
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
||||||
|
|
||||||
if row.current_serial_and_batch_bundle and not self.has_change_in_serial_batch(row):
|
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(
|
cls_obj = SerialBatchCreation(
|
||||||
{
|
{
|
||||||
@@ -316,12 +325,11 @@ class StockReconciliation(StockController):
|
|||||||
total_current_qty += current_qty
|
total_current_qty += current_qty
|
||||||
entry.qty = current_qty * -1
|
entry.qty = current_qty * -1
|
||||||
|
|
||||||
reco_obj.flags.ignore_validate = True
|
|
||||||
reco_obj.save()
|
reco_obj.save()
|
||||||
|
|
||||||
row.current_qty = total_current_qty
|
row.current_qty = total_current_qty
|
||||||
|
|
||||||
return reco_obj.name
|
return reco_obj
|
||||||
|
|
||||||
def has_change_in_serial_batch(self, row) -> bool:
|
def has_change_in_serial_batch(self, row) -> bool:
|
||||||
bundles = {row.serial_and_batch_bundle: [], row.current_serial_and_batch_bundle: []}
|
bundles = {row.serial_and_batch_bundle: [], row.current_serial_and_batch_bundle: []}
|
||||||
@@ -721,7 +729,7 @@ class StockReconciliation(StockController):
|
|||||||
for d in serial_nos:
|
for d in serial_nos:
|
||||||
frappe.db.set_value("Serial No", d, "purchase_rate", valuation_rate)
|
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"""
|
"""Insert Stock Ledger Entries"""
|
||||||
|
|
||||||
if not serial_nos and row.serial_no:
|
if not serial_nos and row.serial_no:
|
||||||
@@ -755,7 +763,7 @@ class StockReconciliation(StockController):
|
|||||||
has_dimensions = True
|
has_dimensions = True
|
||||||
|
|
||||||
if self.docstatus == 2 and (not row.batch_no or not row.serial_and_batch_bundle):
|
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.actual_qty = -1 * row.current_qty
|
||||||
data.qty_after_transaction = flt(row.current_qty)
|
data.qty_after_transaction = flt(row.current_qty)
|
||||||
data.previous_qty_after_transaction = flt(row.qty)
|
data.previous_qty_after_transaction = flt(row.qty)
|
||||||
@@ -785,6 +793,8 @@ class StockReconciliation(StockController):
|
|||||||
has_serial_no = False
|
has_serial_no = False
|
||||||
for row in self.items:
|
for row in self.items:
|
||||||
sl_entries.append(self.get_sle_for_items(row))
|
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 sl_entries:
|
||||||
if has_serial_no:
|
if has_serial_no:
|
||||||
|
|||||||
@@ -1109,6 +1109,8 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
sr.reload()
|
sr.reload()
|
||||||
|
|
||||||
|
self.assertTrue(sr.items[0].current_valuation_rate)
|
||||||
current_sabb = sr.items[0].current_serial_and_batch_bundle
|
current_sabb = sr.items[0].current_serial_and_batch_bundle
|
||||||
doc = frappe.get_doc("Serial and Batch Bundle", current_sabb)
|
doc = frappe.get_doc("Serial and Batch Bundle", current_sabb)
|
||||||
for row in doc.entries:
|
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)
|
batch_qty = get_batch_qty(batches[0].batch_no, warehouse, item.name)
|
||||||
self.assertEqual(batch_qty, 100)
|
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):
|
def test_not_reconcile_all_serial_nos(self):
|
||||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||||
from erpnext.stock.utils import get_incoming_rate
|
from erpnext.stock.utils import get_incoming_rate
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ from erpnext.stock.utils import (
|
|||||||
get_incoming_outgoing_rate_for_cancel,
|
get_incoming_outgoing_rate_for_cancel,
|
||||||
get_incoming_rate,
|
get_incoming_rate,
|
||||||
get_or_make_bin,
|
get_or_make_bin,
|
||||||
|
get_serial_nos_data,
|
||||||
get_stock_balance,
|
get_stock_balance,
|
||||||
get_valuation_method,
|
get_valuation_method,
|
||||||
)
|
)
|
||||||
@@ -811,9 +812,10 @@ class update_entries_after:
|
|||||||
self.update_outgoing_rate_on_transaction(sle)
|
self.update_outgoing_rate_on_transaction(sle)
|
||||||
|
|
||||||
def get_serialized_values(self, sle):
|
def get_serialized_values(self, sle):
|
||||||
|
from erpnext.stock.serial_batch_bundle import SerialNoValuation
|
||||||
|
|
||||||
incoming_rate = flt(sle.incoming_rate)
|
incoming_rate = flt(sle.incoming_rate)
|
||||||
actual_qty = flt(sle.actual_qty)
|
actual_qty = flt(sle.actual_qty)
|
||||||
serial_nos = cstr(sle.serial_no).split("\n")
|
|
||||||
|
|
||||||
if incoming_rate < 0:
|
if incoming_rate < 0:
|
||||||
# wrong incoming rate
|
# wrong incoming rate
|
||||||
@@ -826,8 +828,16 @@ class update_entries_after:
|
|||||||
# In case of delivery/stock issue, get average purchase rate
|
# In case of delivery/stock issue, get average purchase rate
|
||||||
# of serial nos of current entry
|
# of serial nos of current entry
|
||||||
if not sle.is_cancelled:
|
if not sle.is_cancelled:
|
||||||
outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
|
new_sle = copy.deepcopy(sle)
|
||||||
stock_value_change = -1 * outgoing_value
|
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:
|
else:
|
||||||
stock_value_change = actual_qty * sle.outgoing_rate
|
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
|
self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
|
||||||
|
|
||||||
def update_batched_values(self, sle):
|
def update_batched_values(self, sle):
|
||||||
|
from erpnext.stock.serial_batch_bundle import BatchNoValuation
|
||||||
|
|
||||||
incoming_rate = flt(sle.incoming_rate)
|
incoming_rate = flt(sle.incoming_rate)
|
||||||
actual_qty = flt(sle.actual_qty)
|
actual_qty = flt(sle.actual_qty)
|
||||||
|
|
||||||
@@ -1282,21 +1294,25 @@ class update_entries_after:
|
|||||||
if actual_qty > 0:
|
if actual_qty > 0:
|
||||||
stock_value_difference = incoming_rate * actual_qty
|
stock_value_difference = incoming_rate * actual_qty
|
||||||
else:
|
else:
|
||||||
outgoing_rate = get_batch_incoming_rate(
|
new_sle = copy.deepcopy(sle)
|
||||||
item_code=sle.item_code,
|
|
||||||
warehouse=sle.warehouse,
|
new_sle.qty = new_sle.actual_qty
|
||||||
batch_no=sle.batch_no,
|
new_sle.batch_nos = frappe._dict({new_sle.batch_no: new_sle})
|
||||||
posting_date=sle.posting_date,
|
batch_obj = BatchNoValuation(
|
||||||
posting_time=sle.posting_time,
|
sle=new_sle,
|
||||||
creation=sle.creation,
|
warehouse=new_sle.get("warehouse"),
|
||||||
|
item_code=new_sle.get("item_code"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
outgoing_rate = batch_obj.get_incoming_rate()
|
||||||
|
|
||||||
if outgoing_rate is None:
|
if outgoing_rate is None:
|
||||||
# This can *only* happen if qty available for the batch is zero.
|
# This can *only* happen if qty available for the batch is zero.
|
||||||
# in such case fall back various other rates.
|
# in such case fall back various other rates.
|
||||||
# future entries will correct the overall accounting as each
|
# future entries will correct the overall accounting as each
|
||||||
# batch individually uses moving average rates.
|
# batch individually uses moving average rates.
|
||||||
outgoing_rate = self.get_fallback_rate(sle)
|
outgoing_rate = self.get_fallback_rate(sle)
|
||||||
|
|
||||||
stock_value_difference = outgoing_rate * actual_qty
|
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)
|
self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + stock_value_difference)
|
||||||
|
|||||||
Reference in New Issue
Block a user