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

chore: release v15
This commit is contained in:
ruthra kumar
2025-03-19 16:46:43 +05:30
committed by GitHub
44 changed files with 381 additions and 126 deletions

View File

@@ -139,7 +139,7 @@ class PeriodClosingVoucher(AccountsController):
self.cancel_gl_entries()
def make_gl_entries(self):
if self.get_gle_count_in_selected_period() > 5000:
if frappe.db.estimate_count("GL Entry") > 100_000:
frappe.enqueue(
process_gl_and_closing_entries,
doc=self,
@@ -154,16 +154,6 @@ class PeriodClosingVoucher(AccountsController):
else:
process_gl_and_closing_entries(self)
def get_gle_count_in_selected_period(self):
return frappe.db.count(
"GL Entry",
{
"posting_date": ["between", [self.period_start_date, self.period_end_date]],
"company": self.company,
"is_cancelled": 0,
},
)
def get_pcv_gl_entries(self):
self.pl_accounts_reverse_gle = []
self.closing_account_gle = []

View File

@@ -1318,7 +1318,7 @@ class PurchaseInvoice(BuyingController):
"account": cost_of_goods_sold_account,
"against": item.expense_account,
"debit": stock_adjustment_amt,
"debit_in_transaction_currency": item.net_amount,
"debit_in_transaction_currency": stock_adjustment_amt / self.conversion_rate,
"remarks": self.get("remarks") or _("Stock Adjustment"),
"cost_center": item.cost_center,
"project": item.project or self.project,
@@ -1330,6 +1330,38 @@ class PurchaseInvoice(BuyingController):
warehouse_debit_amount = stock_amount
elif self.is_return and self.update_stock and self.is_internal_supplier and warehouse_debit_amount:
net_rate = item.base_net_amount
if item.sales_incoming_rate: # for internal transfer
net_rate = item.qty * item.sales_incoming_rate
stock_amount = (
net_rate
+ item.item_tax_amount
+ flt(item.landed_cost_voucher_amount)
+ flt(item.get("amount_difference_with_purchase_invoice"))
)
if flt(stock_amount, net_amt_precision) != flt(warehouse_debit_amount, net_amt_precision):
cost_of_goods_sold_account = self.get_company_default("default_expense_account")
stock_adjustment_amt = stock_amount - warehouse_debit_amount
gl_entries.append(
self.get_gl_dict(
{
"account": cost_of_goods_sold_account,
"against": item.expense_account,
"debit": stock_adjustment_amt,
"debit_in_transaction_currency": stock_adjustment_amt / self.conversion_rate,
"remarks": self.get("remarks") or _("Stock Adjustment"),
"cost_center": item.cost_center,
"project": item.project or self.project,
},
account_currency,
item=item,
)
)
return warehouse_debit_amount
def make_tax_gl_entries(self, gl_entries):

View File

@@ -3,7 +3,6 @@
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2022-01-25 10:29:57.771398",
"default_print_format": "Sales Invoice Print",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
@@ -2189,7 +2188,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2025-03-05 17:06:59.720616",
"modified": "2025-03-17 19:32:31.809658",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
@@ -2245,4 +2244,4 @@
"title_field": "title",
"track_changes": 1,
"track_seen": 1
}
}

View File

@@ -81,6 +81,10 @@ def make_acc_dimensions_offsetting_entry(gl_map):
"credit_in_account_currency": credit,
"remarks": _("Offsetting for Accounting Dimension") + f" - {dimension.name}",
"against_voucher": None,
"account_currency": dimension.account_currency,
# Party Type and Party are restricted to Receivable and Payable accounts
"party_type": None,
"party": None,
}
)
offsetting_entry["against_voucher_type"] = None
@@ -108,6 +112,9 @@ def get_accounting_dimensions_for_offsetting_entry(gl_map, company):
accounting_dimensions_to_offset = []
for acc_dimension in acc_dimensions:
values = set([entry.get(acc_dimension.fieldname) for entry in gl_map])
acc_dimension.account_currency = frappe.get_cached_value(
"Account", acc_dimension.offsetting_account, "account_currency"
)
if len(values) > 1:
accounting_dimensions_to_offset.append(acc_dimension)

View File

@@ -520,7 +520,7 @@ class ReceivablePayableReport:
ps.description, ps.paid_amount, ps.discounted_amount
from `tab{row.voucher_type}` si, `tabPayment Schedule` ps
where
si.name = ps.parent and
si.name = ps.parent and ps.parenttype = '{row.voucher_type}' and
si.name = %s and
si.is_return = 0
order by ps.paid_amount desc, due_date
@@ -729,11 +729,13 @@ class ReceivablePayableReport:
"company": self.filters.company,
"update_outstanding_for_self": 0,
}
or_filters = {}
for party_type in self.party_type:
if party_type := self.filters.party_type:
party_field = scrub(party_type)
if self.filters.get(party_field):
or_filters.update({party_field: self.filters.get(party_field)})
if parties := self.filters.get("party"):
or_filters.update({party_field: ["in", parties]})
self.return_entries = frappe._dict(
frappe.get_all(
doctype, filters=filters, or_filters=or_filters, fields=["name", "return_against"], as_list=1

View File

@@ -123,6 +123,7 @@ class AssetCapitalization(StockController):
self.make_bundle_using_old_serial_batch_fields()
self.update_stock_ledger()
self.make_gl_entries()
self.repost_future_sle_and_gle()
self.update_target_asset()
def on_cancel(self):
@@ -136,6 +137,7 @@ class AssetCapitalization(StockController):
)
self.update_stock_ledger()
self.make_gl_entries()
self.repost_future_sle_and_gle()
self.restore_consumed_asset_items()
def set_title(self):

View File

@@ -913,6 +913,7 @@
},
{
"allow_on_submit": 1,
"depends_on": "eval:parent.is_subcontracted && !parent.is_old_subcontracting_flow",
"fieldname": "subcontracted_quantity",
"fieldtype": "Float",
"label": "Subcontracted Quantity",
@@ -926,7 +927,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-03-02 16:58:26.059601",
"modified": "2025-03-13 17:27:43.468602",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",
@@ -940,4 +941,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -271,6 +271,7 @@ class AccountsController(TransactionBase):
self.set_total_in_words()
self.set_default_letter_head()
self.validate_company_in_accounting_dimension()
self.validate_party_address_and_contact()
def set_default_letter_head(self):
if hasattr(self, "letter_head") and not self.letter_head:
@@ -441,6 +442,45 @@ class AccountsController(TransactionBase):
)
)
def validate_party_address_and_contact(self):
party, party_type = None, None
if self.get("customer"):
party, party_type = self.customer, "Customer"
billing_address, shipping_address = (
self.get("customer_address"),
self.get("shipping_address_name"),
)
self.validate_party_address(party, party_type, billing_address, shipping_address)
elif self.get("supplier"):
party, party_type = self.supplier, "Supplier"
billing_address = self.get("supplier_address")
self.validate_party_address(party, party_type, billing_address)
if party and party_type:
self.validate_party_contact(party, party_type)
def validate_party_address(self, party, party_type, billing_address, shipping_address=None):
if billing_address or shipping_address:
party_address = frappe.get_all(
"Dynamic Link",
{"link_doctype": party_type, "link_name": party, "parenttype": "Address"},
pluck="parent",
)
if billing_address and billing_address not in party_address:
frappe.throw(_("Billing Address does not belong to the {0}").format(party))
elif shipping_address and shipping_address not in party_address:
frappe.throw(_("Shipping Address does not belong to the {0}").format(party))
def validate_party_contact(self, party, party_type):
if self.get("contact_person"):
contact = frappe.get_all(
"Dynamic Link",
{"link_doctype": party_type, "link_name": party, "parenttype": "Contact"},
pluck="parent",
)
if self.contact_person and self.contact_person not in contact:
frappe.throw(_("Contact Person does not belong to the {0}").format(party))
def validate_return_against_account(self):
if self.doctype in ["Sales Invoice", "Purchase Invoice"] and self.is_return and self.return_against:
cr_dr_account_field = "debit_to" if self.doctype == "Sales Invoice" else "credit_to"
@@ -3666,6 +3706,9 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
if d.get("schedule_date") and parent_doctype == "Purchase Order":
child_item.schedule_date = d.get("schedule_date")
if d.get("bom_no") and parent_doctype == "Sales Order":
child_item.bom_no = d.get("bom_no")
if flt(child_item.price_list_rate):
if flt(child_item.rate) > flt(child_item.price_list_rate):
# if rate is greater than price_list_rate, set margin

View File

@@ -2175,3 +2175,59 @@ class TestAccountsController(FrappeTestCase):
si_1 = create_sales_invoice(do_not_submit=True)
si_1.items[0].project = project.name
self.assertRaises(frappe.ValidationError, si_1.save)
def test_party_billing_and_shipping_address(self):
from erpnext.crm.doctype.prospect.test_prospect import make_address
customer_billing = make_address(address_title="Customer")
customer_billing.append("links", {"link_doctype": "Customer", "link_name": "_Test Customer"})
customer_billing.save()
supplier_billing = make_address(address_title="Supplier", address_line1="2", city="Ahmedabad")
supplier_billing.append("links", {"link_doctype": "Supplier", "link_name": "_Test Supplier"})
supplier_billing.save()
customer_shipping = make_address(
address_title="Customer", address_type="Shipping", address_line1="10"
)
customer_shipping.append("links", {"link_doctype": "Customer", "link_name": "_Test Customer"})
customer_shipping.save()
supplier_shipping = make_address(
address_title="Supplier", address_type="Shipping", address_line1="20", city="Ahmedabad"
)
supplier_shipping.append("links", {"link_doctype": "Supplier", "link_name": "_Test Supplier"})
supplier_shipping.save()
si = create_sales_invoice(do_not_save=True)
si.customer_address = supplier_billing.name
self.assertRaises(frappe.ValidationError, si.save)
si.customer_address = customer_billing.name
si.save()
si.shipping_address_name = supplier_shipping.name
self.assertRaises(frappe.ValidationError, si.save)
si.shipping_address_name = customer_shipping.name
si.reload()
si.save()
pi = make_purchase_invoice(do_not_save=True)
pi.supplier_address = customer_shipping.name
self.assertRaises(frappe.ValidationError, pi.save)
pi.supplier_address = supplier_shipping.name
pi.save()
def test_party_contact(self):
from frappe.contacts.doctype.contact.test_contact import create_contact
customer_contact = create_contact(name="Customer", salutation="Mr", save=False)
customer_contact.append("links", {"link_doctype": "Customer", "link_name": "_Test Customer"})
customer_contact.save()
supplier_contact = create_contact(name="Supplier", salutation="Mr", save=False)
supplier_contact.append("links", {"link_doctype": "Supplier", "link_name": "_Test Supplier"})
supplier_contact.save()
si = create_sales_invoice(do_not_save=True)
si.contact_person = supplier_contact.name
self.assertRaises(frappe.ValidationError, si.save)
si.contact_person = customer_contact.name
si.save()

View File

@@ -440,8 +440,8 @@ class BOM(WebsiteGenerator):
"description": item and args["description"] or "",
"image": item and args["image"] or "",
"stock_uom": item and args["stock_uom"] or "",
"uom": args["uom"] if hasattr(args, "uom") else item and args["stock_uom"] or "",
"conversion_factor": args["conversion_factor"] if hasattr(args, "conversion_factor") else 1,
"uom": args["uom"] if args.get("uom") else item and args["stock_uom"] or "",
"conversion_factor": args["conversion_factor"] if args.get("conversion_factor") else 1,
"bom_no": args["bom_no"],
"rate": rate,
"qty": args.get("qty") or args.get("stock_qty") or 1,

View File

@@ -86,7 +86,6 @@
"search_index": 1
},
{
"fetch_from": "work_order.bom_no",
"fieldname": "bom_no",
"fieldtype": "Link",
"label": "BOM No",
@@ -281,7 +280,7 @@
"fieldtype": "Column Break"
},
{
"fetch_from": "work_order.production_item",
"fetch_from": "bom_no.item",
"fieldname": "production_item",
"fieldtype": "Link",
"label": "Production Item",
@@ -511,7 +510,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2023-06-28 19:23:14.345214",
"modified": "2025-03-17 15:55:11.143456",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card",

View File

@@ -64,9 +64,7 @@ class JobCard(Document):
from erpnext.manufacturing.doctype.job_card_scheduled_time.job_card_scheduled_time import (
JobCardScheduledTime,
)
from erpnext.manufacturing.doctype.job_card_scrap_item.job_card_scrap_item import (
JobCardScrapItem,
)
from erpnext.manufacturing.doctype.job_card_scrap_item.job_card_scrap_item import JobCardScrapItem
from erpnext.manufacturing.doctype.job_card_time_log.job_card_time_log import JobCardTimeLog
actual_end_date: DF.Datetime | None
@@ -91,7 +89,7 @@ class JobCard(Document):
naming_series: DF.Literal["PO-JOB.#####"]
operation: DF.Link
operation_id: DF.Data | None
operation_row_number: DF.Literal
operation_row_number: DF.Literal[None]
posting_date: DF.Date | None
process_loss_qty: DF.Float
production_item: DF.Link | None

View File

@@ -363,8 +363,8 @@ class ProductionPlan(Document):
for item in items:
item.pending_qty = (
flt(item.qty) - max(item.work_order_qty, item.delivered_qty, 0) * item.conversion_factor
)
flt(item.qty) - max(item.work_order_qty, item.delivered_qty, 0)
) * item.conversion_factor
pi = frappe.qb.DocType("Packed Item")
@@ -425,6 +425,7 @@ class ProductionPlan(Document):
mr_item.item_code,
mr_item.warehouse,
mr_item.description,
mr_item.bom_no,
((mr_item.qty - mr_item.ordered_qty) * mr_item.conversion_factor).as_("pending_qty"),
)
.distinct()

View File

@@ -42,6 +42,7 @@ class TestWorkOrder(FrappeTestCase):
prepare_data_for_backflush_based_on_materials_transferred()
def tearDown(self):
frappe.local.future_sle = {}
frappe.db.rollback()
def check_planned_qty(self):

View File

@@ -301,6 +301,12 @@ frappe.ui.form.on("Work Order", {
label: __("Sequence Id"),
read_only: 1,
},
{
fieldtype: "Link",
fieldname: "bom",
label: __("BOM"),
read_only: 1,
},
],
data: operations_data,
in_place_edit: true,
@@ -341,6 +347,7 @@ frappe.ui.form.on("Work Order", {
qty: pending_qty,
pending_qty: pending_qty,
sequence_id: data.sequence_id,
bom: data.bom,
});
}
}

View File

@@ -1616,7 +1616,7 @@ def create_job_card(work_order, row, enable_capacity_planning=False, auto_create
"posting_date": nowdate(),
"for_quantity": row.job_card_qty or work_order.get("qty", 0),
"operation_id": row.get("name"),
"bom_no": work_order.bom_no,
"bom_no": row.get("bom"),
"project": work_order.project,
"company": work_order.company,
"sequence_id": row.get("sequence_id"),

View File

@@ -397,7 +397,7 @@ erpnext.patches.v15_0.set_difference_amount_in_asset_value_adjustment
erpnext.patches.v14_0.update_posting_datetime
erpnext.stock.doctype.stock_ledger_entry.patches.ensure_sle_indexes
erpnext.patches.v15_0.update_query_report
erpnext.patches.v15_0.rename_field_from_rate_difference_to_amount_difference
erpnext.patches.v15_0.rename_field_from_rate_difference_to_amount_difference #2025-03-18
erpnext.patches.v15_0.recalculate_amount_difference_field
erpnext.patches.v15_0.rename_sla_fields #2025-03-12
erpnext.patches.v15_0.set_purchase_receipt_row_item_to_capitalization_stock_item

View File

@@ -27,11 +27,7 @@ def execute():
table.qty,
parent.conversion_rate,
)
.where(
(table.amount_difference_with_purchase_invoice != 0)
& (table.docstatus == 1)
& (parent.company == company)
)
.where((table.docstatus == 1) & (parent.company == company))
)
posting_date = "2024-04-01"
@@ -121,6 +117,7 @@ def get_billed_qty_against_purchase_receipt(pr_names):
frappe.qb.from_(table)
.select(table.pr_detail, Sum(table.qty).as_("qty"))
.where((table.pr_detail.isin(pr_names)) & (table.docstatus == 1))
.groupby(table.pr_detail)
)
invoice_data = query.run(as_list=1)

View File

@@ -3,5 +3,12 @@ from frappe.model.utils.rename_field import rename_field
def execute():
rename_field("Purchase Order Item", "sco_qty", "subcontracted_quantity")
rename_field("Subcontracting Order Item", "sc_conversion_factor", "subcontracting_conversion_factor")
if frappe.db.table_exists("Purchase Order Item") and frappe.db.has_column(
"Purchase Order Item", "sco_qty"
):
rename_field("Purchase Order Item", "sco_qty", "subcontracted_quantity")
if frappe.db.table_exists("Subcontracting Order Item") and frappe.db.has_column(
"Subcontracting Order Item", "sc_conversion_factor"
):
rename_field("Subcontracting Order Item", "sc_conversion_factor", "subcontracting_conversion_factor")

View File

@@ -194,7 +194,7 @@ erpnext.buying = {
}
qty(doc, cdt, cdn) {
if ((doc.doctype == "Purchase Receipt") || (doc.doctype == "Purchase Invoice" && (doc.update_stock || doc.is_return))) {
if ((doc.doctype == "Purchase Receipt") || (doc.doctype == "Purchase Invoice" && doc.update_stock)) {
this.calculate_received_qty(doc, cdt, cdn)
}
super.qty(doc, cdt, cdn);

View File

@@ -335,7 +335,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
let d = locals[cdt][cdn];
return {
filters: {
docstatus: 1,
docstatus: ("<", 2),
inspection_type: inspection_type,
reference_name: doc.name,
item_code: d.item_code

View File

@@ -692,7 +692,7 @@ erpnext.utils.update_child_items = function (opts) {
},
callback: function (r) {
if (r.message) {
const { qty, price_list_rate: rate, uom, conversion_factor } = r.message;
const { qty, price_list_rate: rate, uom, conversion_factor, bom_no } = r.message;
const row = dialog.fields_dict.trans_items.df.data.find(
(doc) => doc.idx == me.doc.idx
@@ -703,6 +703,7 @@ erpnext.utils.update_child_items = function (opts) {
uom: me.doc.uom || uom,
qty: me.doc.qty || qty,
rate: me.doc.rate || rate,
bom_no: bom_no,
});
dialog.fields_dict.trans_items.grid.refresh();
}

View File

@@ -17,7 +17,7 @@ erpnext.utils.get_party_details = function (frm, method, args, callback) {
(frm.doc.party_name && ["Quotation", "Opportunity"].includes(frm.doc.doctype))
) {
let party_type = "Customer";
if (frm.doc.quotation_to && ["Lead", "Prospect"].includes(frm.doc.quotation_to)) {
if (frm.doc.quotation_to && ["Lead", "Prospect", "CRM Deal"].includes(frm.doc.quotation_to)) {
party_type = frm.doc.quotation_to;
}

View File

@@ -39,11 +39,10 @@ class TestUaeVat201(TestCase):
make_item("_Test UAE VAT Zero Rated Item", properties={"is_zero_rated": 1, "is_exempt": 0})
make_item("_Test UAE VAT Exempt Item", properties={"is_zero_rated": 0, "is_exempt": 1})
def test_uae_vat_201_report(self):
make_sales_invoices()
create_purchase_invoices()
def test_uae_vat_201_report(self):
filters = {"company": "_Test Company UAE VAT"}
total_emiratewise = get_total_emiratewise(filters)
amounts_by_emirate = {}
@@ -64,6 +63,37 @@ class TestUaeVat201(TestCase):
self.assertEqual(get_standard_rated_expenses_total(filters), 250)
self.assertEqual(get_standard_rated_expenses_tax(filters), 1)
def test_uae_vat_201_report_with_foreign_transaction(self):
pi = make_purchase_invoice(
company="_Test Company UAE VAT",
supplier="_Test UAE Supplier",
supplier_warehouse="_Test UAE VAT Supplier Warehouse - _TCUV",
warehouse="_Test UAE VAT Supplier Warehouse - _TCUV",
currency="USD",
conversion_rate=3.67,
cost_center="Main - _TCUV",
expense_account="Cost of Goods Sold - _TCUV",
item="_Test UAE VAT Item",
do_not_save=1,
uom="Nos",
)
pi.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "VAT 5% - _TCUV",
"cost_center": "Main - _TCUV",
"description": "VAT 5% @ 5.0",
"rate": 5.0,
},
)
pi.recoverable_standard_rated_expenses = 50
pi.save().submit()
filters = {"company": "_Test Company UAE VAT"}
self.assertEqual(get_standard_rated_expenses_total(filters), 917.5)
self.assertEqual(get_standard_rated_expenses_tax(filters), 50)
def make_company(company_name, abbr):
if not frappe.db.exists("Company", company_name):

View File

@@ -179,7 +179,7 @@ def get_reverse_charge_total(filters):
try:
return (
frappe.db.get_all(
"Purchase Invoice", filters=query_filters, fields=["sum(total)"], as_list=True, limit=1
"Purchase Invoice", filters=query_filters, fields=["sum(base_total)"], as_list=True, limit=1
)[0][0]
or 0
)
@@ -219,7 +219,7 @@ def get_reverse_charge_recoverable_total(filters):
try:
return (
frappe.db.get_all(
"Purchase Invoice", filters=query_filters, fields=["sum(total)"], as_list=True, limit=1
"Purchase Invoice", filters=query_filters, fields=["sum(base_total)"], as_list=True, limit=1
)[0][0]
or 0
)
@@ -274,7 +274,7 @@ def get_standard_rated_expenses_total(filters):
try:
return (
frappe.db.get_all(
"Purchase Invoice", filters=query_filters, fields=["sum(total)"], as_list=True, limit=1
"Purchase Invoice", filters=query_filters, fields=["sum(base_total)"], as_list=True, limit=1
)[0][0]
or 0
)
@@ -310,7 +310,7 @@ def get_tourist_tax_return_total(filters):
try:
return (
frappe.db.get_all(
"Sales Invoice", filters=query_filters, fields=["sum(total)"], as_list=True, limit=1
"Sales Invoice", filters=query_filters, fields=["sum(base_total)"], as_list=True, limit=1
)[0][0]
or 0
)

View File

@@ -876,6 +876,7 @@ def make_material_request(source_name, target_doc=None):
"name": "sales_order_item",
"parent": "sales_order",
"delivery_date": "required_by",
"bom_no": "bom_no",
},
"condition": lambda item: not frappe.db.exists(
"Product Bundle", {"name": item.item_code, "disabled": 0}

View File

@@ -2136,7 +2136,7 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
self.assertEqual(dn.items[0].rate, 90)
def test_credit_limit_on_so_reopning(self):
def test_credit_limit_on_so_reopening(self):
# set credit limit
company = "_Test Company"
customer = frappe.get_doc("Customer", self.customer)
@@ -2148,12 +2148,14 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
so1 = make_sales_order(qty=9, rate=100, do_not_submit=True)
so1.customer = self.customer
so1.customer_address = so1.shipping_address_name = None
so1.save().submit()
so1.update_status("Closed")
so2 = make_sales_order(qty=9, rate=100, do_not_submit=True)
so2.customer = self.customer
so2.customer_address = so2.shipping_address_name = None
so2.save().submit()
self.assertRaises(frappe.ValidationError, so1.update_status, "Draft")

View File

@@ -449,6 +449,7 @@ erpnext.PointOfSale.Controller = class {
init_order_summary() {
this.order_summary = new erpnext.PointOfSale.PastOrderSummary({
wrapper: this.$components_wrapper,
settings: this.settings,
events: {
get_frm: () => this.frm,
@@ -485,7 +486,6 @@ erpnext.PointOfSale.Controller = class {
]);
},
},
pos_profile: this.pos_profile,
});
}

View File

@@ -1,8 +1,8 @@
erpnext.PointOfSale.PastOrderSummary = class {
constructor({ wrapper, events, pos_profile }) {
constructor({ wrapper, settings, events }) {
this.wrapper = wrapper;
this.events = events;
this.pos_profile = pos_profile;
this.print_receipt_on_order_complete = settings.print_receipt_on_order_complete;
this.init_component();
}
@@ -357,8 +357,8 @@ erpnext.PointOfSale.PastOrderSummary = class {
this.add_summary_btns(condition_btns_map);
if (after_submission) {
this.print_receipt_on_order_complete();
if (after_submission && this.print_receipt_on_order_complete) {
this.print_receipt();
}
}
@@ -426,16 +426,4 @@ erpnext.PointOfSale.PastOrderSummary = class {
toggle_component(show) {
show ? this.$component.css("display", "flex") : this.$component.css("display", "none");
}
async print_receipt_on_order_complete() {
const res = await frappe.db.get_value(
"POS Profile",
this.pos_profile,
"print_receipt_on_order_complete"
);
if (res.message.print_receipt_on_order_complete) {
this.print_receipt();
}
}
};

View File

@@ -220,35 +220,42 @@ class TransactionDeletionRecord(Document):
"""Delete addresses to which leads are linked"""
self.validate_doc_status()
if not self.delete_leads_and_addresses:
leads = frappe.get_all("Lead", filters={"company": self.company})
leads = ["'%s'" % row.get("name") for row in leads]
leads = frappe.db.get_all("Lead", filters={"company": self.company}, pluck="name")
addresses = []
if leads:
addresses = frappe.db.sql_list(
"""select parent from `tabDynamic Link` where link_name
in ({leads})""".format(leads=",".join(leads))
addresses = frappe.db.get_all(
"Dynamic Link", filters={"link_name": ("in", leads)}, pluck="parent"
)
if addresses:
addresses = ["%s" % frappe.db.escape(addr) for addr in addresses]
frappe.db.sql(
"""delete from `tabAddress` where name in ({addresses}) and
name not in (select distinct dl1.parent from `tabDynamic Link` dl1
inner join `tabDynamic Link` dl2 on dl1.parent=dl2.parent
and dl1.link_doctype<>dl2.link_doctype)""".format(addresses=",".join(addresses))
)
address = qb.DocType("Address")
dl1 = qb.DocType("Dynamic Link")
dl2 = qb.DocType("Dynamic Link")
frappe.db.sql(
"""delete from `tabDynamic Link` where link_doctype='Lead'
and parenttype='Address' and link_name in ({leads})""".format(leads=",".join(leads))
)
qb.from_(address).delete().where(
(address.name.isin(addresses))
& (
address.name.notin(
qb.from_(dl1)
.join(dl2)
.on((dl1.parent == dl2.parent) & (dl1.link_doctype != dl2.link_doctype))
.select(dl1.parent)
.distinct()
)
)
).run()
dynamic_link = qb.DocType("Dynamic Link")
qb.from_(dynamic_link).delete().where(
(dynamic_link.link_doctype == "Lead")
& (dynamic_link.parenttype == "Address")
& (dynamic_link.link_name.isin(leads))
).run()
customer = qb.DocType("Customer")
qb.update(customer).set(customer.lead_name, None).where(customer.lead_name.isin(leads)).run()
frappe.db.sql(
"""update `tabCustomer` set lead_name=NULL where lead_name in ({leads})""".format(
leads=",".join(leads)
)
)
self.db_set("delete_leads_and_addresses", 1)
self.enqueue_task(task="Reset Company Values")

View File

@@ -228,7 +228,6 @@ class DeprecatedBatchNoValuation:
(sle.item_code == self.sle.item_code)
& (sle.warehouse == self.sle.warehouse)
& (sle.batch_no.isnotnull())
& (batch.use_batchwise_valuation == 0)
& (sle.is_cancelled == 0)
& (sle.batch_no.isin(self.non_batchwise_valuation_batches))
)
@@ -278,7 +277,6 @@ class DeprecatedBatchNoValuation:
(sle.item_code == self.sle.item_code)
& (sle.warehouse == self.sle.warehouse)
& (sle.batch_no.isnotnull())
& (batch.use_batchwise_valuation == 0)
& (sle.is_cancelled == 0)
)
.where(timestamp_condition)
@@ -318,7 +316,6 @@ class DeprecatedBatchNoValuation:
(sabb.item_code == self.sle.item_code)
& (sabb.warehouse == self.sle.warehouse)
& (sabb_entry.batch_no.isnotnull())
& (batch.use_batchwise_valuation == 0)
& (sabb.is_cancelled == 0)
& (sabb.docstatus == 1)
)
@@ -378,7 +375,6 @@ class DeprecatedBatchNoValuation:
(bundle.item_code == self.sle.item_code)
& (bundle.warehouse == self.sle.warehouse)
& (bundle_child.batch_no.isnotnull())
& (batch.use_batchwise_valuation == 0)
& (bundle.is_cancelled == 0)
& (bundle.docstatus == 1)
& (bundle.type_of_transaction.isin(["Inward", "Outward"]))

View File

@@ -157,7 +157,13 @@ class Batch(Document):
frappe.throw(_("The selected item cannot have Batch"))
def set_batchwise_valuation(self):
from erpnext.stock.utils import get_valuation_method
if self.is_new():
if get_valuation_method(self.item) == "Moving Average":
self.use_batchwise_valuation = 0
return
if frappe.db.get_single_value("Stock Settings", "do_not_use_batchwise_valuation"):
self.use_batchwise_valuation = 0
return

View File

@@ -40,7 +40,8 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Default Warehouse",
"options": "Warehouse"
"options": "Warehouse",
"search_index": 1
},
{
"fieldname": "column_break_3",
@@ -139,7 +140,7 @@
],
"istable": 1,
"links": [],
"modified": "2023-09-04 12:33:14.607267",
"modified": "2025-03-17 13:46:09.719105",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Default",
@@ -150,4 +151,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -813,7 +813,7 @@ def create_pick_list(source_name, target_doc=None):
},
"Material Request Item": {
"doctype": "Pick List Item",
"field_map": {"name": "material_request_item", "qty": "stock_qty"},
"field_map": {"name": "material_request_item", "stock_qty": "stock_qty"},
},
},
target_doc,

View File

@@ -1189,6 +1189,7 @@ def create_delivery_note(source_name, target_doc=None):
if not all(item.sales_order for item in pick_list.locations):
delivery_note = create_dn_wo_so(pick_list)
frappe.msgprint(_("Delivery Note(s) created for the Pick List"))
return delivery_note
@@ -1205,6 +1206,7 @@ def create_dn_wo_so(pick_list):
},
}
map_pl_locations(pick_list, item_table_mapper_without_so, delivery_note)
delivery_note.insert(ignore_mandatory=True)
return delivery_note
@@ -1232,7 +1234,10 @@ def create_dn_with_so(sales_dict, pick_list):
# map all items of all sales orders of that customer
for so in sales_dict[customer]:
map_pl_locations(pick_list, item_table_mapper, delivery_note, so)
delivery_note.flags.ignore_mandatory = True
delivery_note.insert()
update_packed_item_details(pick_list, delivery_note)
delivery_note.save()
return delivery_note

View File

@@ -1083,6 +1083,9 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate
total_amount, total_billed_amount = 0, 0
item_wise_returned_qty = get_item_wise_returned_qty(pr_doc)
if adjust_incoming_rate:
item_wise_billed_qty = get_billed_qty_against_purchase_receipt(pr_doc)
for item in pr_doc.items:
returned_qty = flt(item_wise_returned_qty.get(item.name))
returned_amount = flt(returned_qty) * flt(item.rate)
@@ -1102,7 +1105,6 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate
if adjust_incoming_rate:
adjusted_amt = 0.0
item_wise_billed_qty = get_billed_qty_against_purchase_receipt(pr_doc)
if (
item.billed_amt is not None
@@ -1134,6 +1136,7 @@ def get_billed_qty_against_purchase_receipt(pr_doc):
frappe.qb.from_(table)
.select(table.pr_detail, fn.Sum(table.qty).as_("qty"))
.where((table.pr_detail.isin(pr_names)) & (table.docstatus == 1))
.groupby(table.pr_detail)
)
invoice_data = query.run(as_list=1)

View File

@@ -11,12 +11,12 @@ def get_data():
"Auto Repeat": "reference_document",
"Purchase Receipt": "return_against",
"Stock Reservation Entry": "from_voucher_no",
"Quality Inspection": "reference_name",
},
"internal_links": {
"Material Request": ["items", "material_request"],
"Purchase Order": ["items", "purchase_order"],
"Project": ["items", "project"],
"Quality Inspection": ["items", "quality_inspection"],
},
"transactions": [
{

View File

@@ -3327,7 +3327,7 @@ class TestPurchaseReceipt(FrappeTestCase):
bundle = dn.items[0].serial_and_batch_bundle
valuation_rate = frappe.db.get_value("Serial and Batch Bundle", bundle, "avg_rate")
self.assertEqual(valuation_rate, 100)
self.assertEqual(valuation_rate, 150)
doc = frappe.get_doc("Stock Settings")
doc.do_not_use_batchwise_valuation = 1

View File

@@ -181,6 +181,9 @@ class QualityInspection(Document):
child = self.append("readings", {})
child.update(d)
child.status = "Accepted"
child.parameter_group = frappe.get_value(
"Quality Inspection Parameter", d.specification, "parameter_group"
)
@frappe.whitelist()
def get_quality_inspection_template(self):

View File

@@ -145,6 +145,11 @@ class SerialandBatchBundle(Document):
)
elif not frappe.db.exists("Stock Ledger Entry", {"voucher_detail_no": self.voucher_detail_no}):
if self.voucher_type == "Delivery Note" and frappe.db.exists(
"Packed Item", self.voucher_detail_no
):
return
frappe.throw(
_("The serial and batch bundle {0} not linked to {1} {2}").format(
bold(self.name), self.voucher_type, bold(self.voucher_no)

View File

@@ -913,7 +913,12 @@ class StockEntry(StockController):
if frappe.db.exists(
"Stock Entry",
{"docstatus": 1, "work_order": self.work_order, "purpose": "Manufacture"},
{
"docstatus": 1,
"work_order": self.work_order,
"purpose": "Manufacture",
"name": ("!=", self.name),
},
):
frappe.throw(
_("Only one {0} entry can be created against the Work Order {1}").format(
@@ -1593,17 +1598,38 @@ class StockEntry(StockController):
@frappe.whitelist()
def get_item_details(self, args=None, for_update=False):
item = frappe.db.sql(
"""select i.name, i.stock_uom, i.description, i.image, i.item_name, i.item_group,
i.has_batch_no, i.sample_quantity, i.has_serial_no, i.allow_alternative_item,
id.expense_account, id.buying_cost_center
from `tabItem` i LEFT JOIN `tabItem Default` id ON i.name=id.parent and id.company=%s
where i.name=%s
and i.disabled=0
and (i.end_of_life is null or i.end_of_life<'1900-01-01' or i.end_of_life > %s)""",
(self.company, args.get("item_code"), nowdate()),
as_dict=1,
item = frappe.qb.DocType("Item")
item_default = frappe.qb.DocType("Item Default")
query = (
frappe.qb.from_(item)
.left_join(item_default)
.on((item.name == item_default.parent) & (item_default.company == self.company))
.select(
item.name,
item.stock_uom,
item.description,
item.image,
item.item_name,
item.item_group,
item.has_batch_no,
item.sample_quantity,
item.has_serial_no,
item.allow_alternative_item,
item_default.expense_account,
item_default.buying_cost_center,
)
.where(
(item.name == args.get("item_code"))
& (item.disabled == 0)
& (
(item.end_of_life.isnull())
| (item.end_of_life < "1900-01-01")
| (item.end_of_life > nowdate())
)
)
)
item = query.run(as_dict=True)
if not item:
frappe.throw(
@@ -1646,6 +1672,11 @@ class StockEntry(StockController):
if self.purpose == "Material Issue":
ret["expense_account"] = item.get("expense_account") or item_group_defaults.get("expense_account")
if self.purpose == "Manufacture":
ret["expense_account"] = frappe.get_cached_value(
"Company", self.company, "stock_adjustment_account"
)
for company_field, field in {
"stock_adjustment_account": "expense_account",
"cost_center": "cost_center",

View File

@@ -456,6 +456,45 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
frappe.set_user("Administrator")
user.remove_roles("Stock Manager")
def test_batchwise_item_valuation_fifo(self):
item, warehouses, batches = setup_item_valuation_test(valuation_method="FIFO")
# Incoming Entries for Stock Value check
pr_entry_list = [
(item, warehouses[0], batches[0], 1, 100),
(item, warehouses[0], batches[1], 1, 50),
(item, warehouses[0], batches[0], 1, 150),
(item, warehouses[0], batches[1], 1, 100),
]
prs = create_purchase_receipt_entries_for_batchwise_item_valuation_test(pr_entry_list)
sle_details = fetch_sle_details_for_doc_list(prs, ["stock_value"])
sv_list = [d["stock_value"] for d in sle_details]
expected_sv = [100, 150, 300, 400]
self.assertEqual(expected_sv, sv_list, "Incorrect 'Stock Value' values")
# Outgoing Entries for Stock Value Difference check
dn_entry_list = [
(item, warehouses[0], batches[1], 1, 200),
(item, warehouses[0], batches[0], 1, 200),
(item, warehouses[0], batches[1], 1, 200),
(item, warehouses[0], batches[0], 1, 200),
]
frappe.flags.use_serial_and_batch_fields = True
dns = create_delivery_note_entries_for_batchwise_item_valuation_test(dn_entry_list)
sle_details = fetch_sle_details_for_doc_list(dns, ["stock_value_difference"])
svd_list = [-1 * d["stock_value_difference"] for d in sle_details]
expected_incoming_rates = expected_abs_svd = [75.0, 125.0, 75.0, 125.0]
self.assertEqual(expected_abs_svd, svd_list, "Incorrect 'Stock Value Difference' values")
for dn, _incoming_rate in zip(dns, expected_incoming_rates, strict=False):
self.assertTrue(
dn.items[0].incoming_rate in expected_abs_svd,
"Incorrect 'Incoming Rate' values fetched for DN items",
)
frappe.flags.use_serial_and_batch_fields = False
def test_batchwise_item_valuation_moving_average(self):
item, warehouses, batches = setup_item_valuation_test(valuation_method="Moving Average")
@@ -484,7 +523,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
dns = create_delivery_note_entries_for_batchwise_item_valuation_test(dn_entry_list)
sle_details = fetch_sle_details_for_doc_list(dns, ["stock_value_difference"])
svd_list = [-1 * d["stock_value_difference"] for d in sle_details]
expected_incoming_rates = expected_abs_svd = [75.0, 125.0, 75.0, 125.0]
expected_incoming_rates = expected_abs_svd = [100.0, 100.0, 100.0, 100.0]
self.assertEqual(expected_abs_svd, svd_list, "Incorrect 'Stock Value Difference' values")
for dn, _incoming_rate in zip(dns, expected_incoming_rates, strict=False):

View File

@@ -10,25 +10,14 @@ from pypika import functions as fn
from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter
SLE_COUNT_LIMIT = 10_000
def _estimate_table_row_count(doctype: str):
table = get_table_name(doctype)
return cint(
frappe.db.sql(
f"""select table_rows
from information_schema.tables
where table_name = '{table}' ;"""
)[0][0]
)
SLE_COUNT_LIMIT = 100_000
def execute(filters=None):
if not filters:
filters = {}
sle_count = _estimate_table_row_count("Stock Ledger Entry")
sle_count = frappe.db.estimate_count("Stock Ledger Entry")
if (
sle_count > SLE_COUNT_LIMIT

View File

@@ -683,6 +683,8 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
return query.run(as_dict=True)
def prepare_batches(self):
from erpnext.stock.utils import get_valuation_method
self.batches = self.batch_nos
if isinstance(self.batch_nos, dict):
self.batches = list(self.batch_nos.keys())
@@ -690,6 +692,10 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
self.batchwise_valuation_batches = []
self.non_batchwise_valuation_batches = []
if get_valuation_method(self.sle.item_code) == "Moving Average":
self.non_batchwise_valuation_batches = self.batches
return
batches = frappe.get_all(
"Batch", filters={"name": ("in", self.batches), "use_batchwise_valuation": 1}, fields=["name"]
)