mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-19 12:44:03 +00:00
Compare commits
23 Commits
v15.112.0
...
version-15
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b6dd42aa1 | ||
|
|
e556cbbe6a | ||
|
|
ad9c16073e | ||
|
|
ce2560e2fe | ||
|
|
37dffa7273 | ||
|
|
3ef6475249 | ||
|
|
6deff470d8 | ||
|
|
03cda066a5 | ||
|
|
90fd057fb3 | ||
|
|
e4370ab332 | ||
|
|
ce8fce78f1 | ||
|
|
f3334eb2d3 | ||
|
|
5c4f19ebdc | ||
|
|
98caefea88 | ||
|
|
c7acd88742 | ||
|
|
60f5de7ab8 | ||
|
|
27d574dad5 | ||
|
|
5c4220ee77 | ||
|
|
c13567228e | ||
|
|
3f9a88a5e2 | ||
|
|
8b2204ce69 | ||
|
|
19913127a7 | ||
|
|
690adf1051 |
@@ -4,7 +4,7 @@ import inspect
|
||||
import frappe
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "15.112.0"
|
||||
__version__ = "15.108.3"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -9,6 +9,13 @@ cur_frm.add_fetch("bank", "swift_number", "swift_number");
|
||||
|
||||
frappe.ui.form.on("Bank Guarantee", {
|
||||
setup: function (frm) {
|
||||
frm.set_query("reference_doctype", function () {
|
||||
return {
|
||||
filters: {
|
||||
name: ["in", ["Sales Order", "Purchase Order"]],
|
||||
},
|
||||
};
|
||||
});
|
||||
frm.set_query("bank_account", function () {
|
||||
return {
|
||||
filters: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_bulk_edit": 1,
|
||||
"autoname": "ACC-BG-.YYYY.-.#####",
|
||||
"creation": "2016-12-17 10:43:35.731631",
|
||||
"doctype": "DocType",
|
||||
@@ -50,8 +51,7 @@
|
||||
"fieldname": "reference_doctype",
|
||||
"fieldtype": "Link",
|
||||
"label": "Reference Document Type",
|
||||
"options": "DocType",
|
||||
"read_only": 1
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_docname",
|
||||
@@ -60,14 +60,14 @@
|
||||
"options": "reference_doctype"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.bg_type == \"Receiving\"",
|
||||
"depends_on": "eval: doc.reference_doctype == \"Sales Order\"",
|
||||
"fieldname": "customer",
|
||||
"fieldtype": "Link",
|
||||
"label": "Customer",
|
||||
"options": "Customer"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.bg_type == \"Providing\"",
|
||||
"depends_on": "eval: doc.reference_doctype == \"Purchase Order\"",
|
||||
"fieldname": "supplier",
|
||||
"fieldtype": "Link",
|
||||
"label": "Supplier",
|
||||
@@ -217,11 +217,11 @@
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-09-26 00:38:17.584694",
|
||||
"modified": "2026-05-25 18:12:10.768835",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Guarantee",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
||||
@@ -425,11 +425,11 @@ def get_ordered_amount(args):
|
||||
|
||||
|
||||
def get_other_condition(args, for_doc):
|
||||
condition = "expense_account = '%s'" % (args.expense_account)
|
||||
condition = f"expense_account = {frappe.db.escape(args.expense_account)}"
|
||||
budget_against_field = args.get("budget_against_field")
|
||||
|
||||
if budget_against_field and args.get(budget_against_field):
|
||||
condition += f" and child.{budget_against_field} = '{args.get(budget_against_field)}'"
|
||||
condition += f" and child.{budget_against_field} = {frappe.db.escape(args.get(budget_against_field))}"
|
||||
|
||||
if args.get("fiscal_year"):
|
||||
date_field = "schedule_date" if for_doc == "Material Request" else "transaction_date"
|
||||
@@ -437,8 +437,7 @@ def get_other_condition(args, for_doc):
|
||||
"Fiscal Year", args.get("fiscal_year"), ["year_start_date", "year_end_date"]
|
||||
)
|
||||
|
||||
condition += f""" and parent.{date_field}
|
||||
between '{start_date}' and '{end_date}' """
|
||||
condition += f" and parent.{date_field} between {frappe.db.escape(str(start_date))} and {frappe.db.escape(str(end_date))}"
|
||||
|
||||
return condition
|
||||
|
||||
|
||||
@@ -1184,7 +1184,11 @@ class JournalEntry(AccountsController):
|
||||
self.validate_total_debit_and_credit()
|
||||
|
||||
def get_values(self):
|
||||
cond = f" and outstanding_amount <= {self.write_off_amount}" if flt(self.write_off_amount) > 0 else ""
|
||||
cond = (
|
||||
f" and outstanding_amount <= {flt(self.write_off_amount)}"
|
||||
if flt(self.write_off_amount) > 0
|
||||
else ""
|
||||
)
|
||||
|
||||
if self.write_off_based_on == "Accounts Receivable":
|
||||
return frappe.db.sql(
|
||||
|
||||
@@ -1197,9 +1197,9 @@ class PaymentEntry(AccountsController):
|
||||
continue
|
||||
|
||||
if tax.add_deduct_tax == "Add":
|
||||
included_taxes += tax.base_tax_amount
|
||||
included_taxes += flt(tax.base_tax_amount)
|
||||
else:
|
||||
included_taxes -= tax.base_tax_amount
|
||||
included_taxes -= flt(tax.base_tax_amount)
|
||||
|
||||
return included_taxes
|
||||
|
||||
|
||||
@@ -1118,6 +1118,27 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
|
||||
self.assertEqual(gl_entries, expected_gl_entries)
|
||||
|
||||
def test_payment_entry_with_inclusive_tax(self):
|
||||
# inclusive tax built server-side: base_tax_amount is None until apply_taxes()
|
||||
payment_entry = create_payment_entry(paid_amount=1180)
|
||||
payment_entry.append(
|
||||
"taxes",
|
||||
{
|
||||
"account_head": "_Test Account Service Tax - _TC",
|
||||
"charge_type": "On Paid Amount",
|
||||
"rate": 18,
|
||||
"included_in_paid_amount": 1,
|
||||
"add_deduct_tax": "Add",
|
||||
"description": "Service Tax",
|
||||
},
|
||||
)
|
||||
payment_entry.save()
|
||||
payment_entry.submit()
|
||||
|
||||
# 1180 incl 18% => 1000 base + 180 tax
|
||||
self.assertEqual(flt(payment_entry.total_taxes_and_charges, 2), 180.0)
|
||||
self.assertEqual(flt(payment_entry.unallocated_amount, 2), 1000.0)
|
||||
|
||||
def test_payment_entry_against_onhold_purchase_invoice(self):
|
||||
pi = make_purchase_invoice()
|
||||
|
||||
|
||||
@@ -31,7 +31,8 @@ class BulkTransactionLog(Document):
|
||||
log_detail = qb.DocType("Bulk Transaction Log Detail")
|
||||
|
||||
has_records = frappe.db.sql(
|
||||
f"select exists (select * from `tabBulk Transaction Log Detail` where date = '{self.name}');"
|
||||
"select exists (select * from `tabBulk Transaction Log Detail` where date = %s);",
|
||||
(self.name,),
|
||||
)[0][0]
|
||||
if not has_records:
|
||||
raise frappe.DoesNotExistError
|
||||
|
||||
@@ -5,6 +5,7 @@ def get_data():
|
||||
return {
|
||||
"fieldname": "supplier",
|
||||
"non_standard_fieldnames": {"Payment Entry": "party", "Bank Account": "party"},
|
||||
"dynamic_links": {"party": ["Supplier", "party_type"]},
|
||||
"transactions": [
|
||||
{"label": _("Procurement"), "items": ["Request for Quotation", "Supplier Quotation"]},
|
||||
{"label": _("Orders"), "items": ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]},
|
||||
|
||||
@@ -7,6 +7,7 @@ import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import Case
|
||||
from frappe.utils import cstr, flt
|
||||
|
||||
from erpnext.utilities.product import get_item_codes_by_attributes
|
||||
@@ -129,6 +130,53 @@ def validate_is_incremental(numeric_attribute, attribute, value, item):
|
||||
)
|
||||
|
||||
|
||||
def get_attribute_value_renames(item_attribute):
|
||||
"""Return old to new attribute value mappings for renamed Item Attribute Value rows."""
|
||||
if item_attribute.numeric_values:
|
||||
return {}
|
||||
|
||||
db_value = item_attribute.get_doc_before_save()
|
||||
if not db_value:
|
||||
return {}
|
||||
|
||||
old_values = {d.name: d.attribute_value for d in db_value.item_attribute_values}
|
||||
renames = {}
|
||||
|
||||
for row in item_attribute.item_attribute_values:
|
||||
if row.name in old_values and old_values[row.name] != row.attribute_value:
|
||||
renames[old_values[row.name]] = row.attribute_value
|
||||
|
||||
return renames
|
||||
|
||||
|
||||
def update_variant_attribute_values(item_attribute):
|
||||
"""Propagate renamed Item Attribute Values to Item Variant Attribute on variant items."""
|
||||
value_map = get_attribute_value_renames(item_attribute)
|
||||
if not value_map:
|
||||
return
|
||||
|
||||
item_variant_table = frappe.qb.DocType("Item Variant Attribute")
|
||||
item_table = frappe.qb.DocType("Item")
|
||||
attribute_value = item_variant_table.attribute_value
|
||||
attribute_value_case = Case()
|
||||
|
||||
for old_value, new_value in value_map.items():
|
||||
attribute_value_case = attribute_value_case.when(attribute_value == old_value, new_value)
|
||||
|
||||
(
|
||||
frappe.qb.update(item_variant_table)
|
||||
.join(item_table)
|
||||
.on(item_table.name == item_variant_table.parent)
|
||||
.set(attribute_value, attribute_value_case.else_(attribute_value))
|
||||
.where(item_table.variant_of.isnotnull())
|
||||
.where(item_table.variant_of != "")
|
||||
.where(item_variant_table.attribute == item_attribute.name)
|
||||
.where(attribute_value.isin(list(value_map)))
|
||||
).run()
|
||||
|
||||
frappe.flags.attribute_values = None
|
||||
|
||||
|
||||
def validate_item_attribute_value(attributes_list, attribute, attribute_value, item, from_variant=True):
|
||||
allow_rename_attribute_value = frappe.db.get_single_value(
|
||||
"Item Variant Settings", "allow_rename_attribute_value"
|
||||
|
||||
@@ -11,7 +11,7 @@ from frappe.utils import cint, flt, format_datetime, get_datetime
|
||||
import erpnext
|
||||
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
|
||||
from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle
|
||||
from erpnext.stock.utils import get_incoming_rate, get_valuation_method, getdate
|
||||
from erpnext.stock.utils import get_combine_datetime, get_incoming_rate, get_valuation_method, getdate
|
||||
|
||||
|
||||
class StockOverReturnError(frappe.ValidationError):
|
||||
@@ -380,6 +380,8 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
doc.pricing_rules = []
|
||||
doc.return_against = source.name
|
||||
doc.set_warehouse = ""
|
||||
if doctype == "Sales Invoice":
|
||||
doc.is_debit_note = 0
|
||||
if doctype == "Sales Invoice" or doctype == "POS Invoice":
|
||||
doc.is_pos = source.is_pos
|
||||
|
||||
@@ -1180,8 +1182,7 @@ def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_f
|
||||
"batches": data.get("batches"),
|
||||
"serial_nos_valuation": data.get("serial_nos_valuation"),
|
||||
"batches_valuation": data.get("batches_valuation"),
|
||||
"posting_date": parent_doc.posting_date,
|
||||
"posting_time": parent_doc.posting_time,
|
||||
"posting_datetime": get_combine_datetime(parent_doc.posting_date, parent_doc.posting_time),
|
||||
"voucher_type": parent_doc.doctype,
|
||||
"voucher_no": parent_doc.name,
|
||||
"voucher_detail_no": child_doc.name,
|
||||
|
||||
@@ -12,7 +12,7 @@ from erpnext.controllers.sales_and_purchase_return import get_rate_for_return, i
|
||||
from erpnext.controllers.stock_controller import StockController
|
||||
from erpnext.stock.doctype.item.item import set_item_default
|
||||
from erpnext.stock.get_item_details import get_bin_details, get_conversion_factor
|
||||
from erpnext.stock.utils import get_incoming_rate, get_valuation_method
|
||||
from erpnext.stock.utils import get_combine_datetime, get_incoming_rate, get_valuation_method
|
||||
|
||||
|
||||
class SellingController(StockController):
|
||||
@@ -1084,8 +1084,7 @@ def get_serial_and_batch_bundle(child, parent, delivery_note_child=None):
|
||||
"voucher_type": parent.doctype,
|
||||
"voucher_no": parent.name if parent.docstatus < 2 else None,
|
||||
"voucher_detail_no": delivery_note_child.name if delivery_note_child else child.name,
|
||||
"posting_date": parent.posting_date,
|
||||
"posting_time": parent.posting_time,
|
||||
"posting_datetime": get_combine_datetime(parent.posting_date, parent.posting_time),
|
||||
"qty": child.qty,
|
||||
"type_of_transaction": "Outward" if child.qty > 0 and parent.docstatus < 2 else "Inward",
|
||||
"company": parent.company,
|
||||
|
||||
@@ -394,9 +394,9 @@ class StatusUpdater(Document):
|
||||
for args in self.status_updater:
|
||||
# condition to include current record (if submit or no if cancel)
|
||||
if self.docstatus == 1:
|
||||
args["cond"] = " or parent='%s'" % self.name.replace('"', '"')
|
||||
args["cond"] = " or parent=%s" % frappe.db.escape(self.name)
|
||||
else:
|
||||
args["cond"] = " and parent!='%s'" % self.name.replace('"', '"')
|
||||
args["cond"] = " and parent!=%s" % frappe.db.escape(self.name)
|
||||
|
||||
self._update_children(args, update_modified)
|
||||
|
||||
@@ -426,9 +426,10 @@ class StatusUpdater(Document):
|
||||
args["second_source_condition"] = frappe.db.sql(
|
||||
""" select ifnull((select sum({second_source_field})
|
||||
from `tab{second_source_dt}`
|
||||
where `{second_join_field}`='{detail_id}'
|
||||
where `{second_join_field}`=%(detail_id)s
|
||||
and (`tab{second_source_dt}`.docstatus=1)
|
||||
{second_source_extra_cond}), 0) """.format(**args)
|
||||
{second_source_extra_cond}), 0) """.format(**args),
|
||||
{"detail_id": args["detail_id"]},
|
||||
)[0][0]
|
||||
|
||||
if args["detail_id"]:
|
||||
@@ -439,9 +440,10 @@ class StatusUpdater(Document):
|
||||
frappe.db.sql(
|
||||
"""
|
||||
(select ifnull(sum({source_field}), 0)
|
||||
from `tab{source_dt}` where `{join_field}`='{detail_id}'
|
||||
from `tab{source_dt}` where `{join_field}`=%(detail_id)s
|
||||
and (docstatus=1 {cond}) {extra_cond})
|
||||
""".format(**args)
|
||||
""".format(**args),
|
||||
{"detail_id": args["detail_id"]},
|
||||
)[0][0]
|
||||
or 0.0
|
||||
)
|
||||
@@ -452,7 +454,8 @@ class StatusUpdater(Document):
|
||||
frappe.db.sql(
|
||||
"""update `tab{target_dt}`
|
||||
set {target_field} = {source_dt_value} {update_modified}
|
||||
where name='{detail_id}'""".format(**args)
|
||||
where name=%(detail_id)s""".format(**args),
|
||||
{"detail_id": args["detail_id"]},
|
||||
)
|
||||
|
||||
def _update_percent_field_in_targets(self, args, update_modified=True):
|
||||
|
||||
@@ -26,6 +26,7 @@ from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
|
||||
get_evaluated_inventory_dimension,
|
||||
)
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
combine_datetime,
|
||||
get_type_of_transaction,
|
||||
)
|
||||
from erpnext.stock.stock_ledger import get_items_to_be_repost
|
||||
@@ -282,8 +283,7 @@ class StockController(AccountsController):
|
||||
):
|
||||
bundle_details = {
|
||||
"item_code": row.get("rm_item_code") or row.item_code,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
"posting_datetime": combine_datetime(self.posting_date, self.posting_time),
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"voucher_detail_no": row.name,
|
||||
|
||||
@@ -13,6 +13,7 @@ from frappe.utils import cint, flt, get_link_to_form
|
||||
from erpnext.controllers.stock_controller import StockController
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
combine_datetime,
|
||||
get_auto_batch_nos,
|
||||
get_available_serial_nos,
|
||||
get_voucher_wise_serial_batch_from_bundle,
|
||||
@@ -570,8 +571,7 @@ class SubcontractingController(StockController):
|
||||
"qty": qty,
|
||||
"serial_nos": serial_nos,
|
||||
"batches": batches,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
"posting_datetime": combine_datetime(self.posting_date, self.posting_time),
|
||||
"voucher_type": "Subcontracting Receipt",
|
||||
"do_not_submit": True,
|
||||
"type_of_transaction": "Outward" if qty > 0 else "Inward",
|
||||
|
||||
@@ -7,7 +7,7 @@ import json
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.modules.utils import get_module_app
|
||||
from frappe.utils import flt, has_common
|
||||
from frappe.utils import cint, flt, has_common
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
|
||||
|
||||
@@ -436,3 +436,4 @@ erpnext.patches.v16_0.depends_on_inv_dimensions
|
||||
erpnext.patches.v16_0.clear_procedures_from_receivable_report
|
||||
erpnext.patches.v16_0.migrate_address_contact_custom_fields
|
||||
erpnext.patches.v15_0.set_main_item_code_in_material_request_plan_item
|
||||
erpnext.patches.v16_0.set_posting_datetime_for_sabb_and_drop_indexes
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import click
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
frappe.db.sql(
|
||||
"""
|
||||
UPDATE `tabSerial and Batch Bundle`
|
||||
JOIN `tabStock Ledger Entry`
|
||||
ON `tabSerial and Batch Bundle`.`name` = `tabStock Ledger Entry`.`serial_and_batch_bundle`
|
||||
SET `tabSerial and Batch Bundle`.`posting_datetime` = `tabStock Ledger Entry`.`posting_datetime`
|
||||
WHERE `tabStock Ledger Entry`.`is_cancelled` = 0
|
||||
"""
|
||||
)
|
||||
|
||||
drop_indexes()
|
||||
|
||||
|
||||
def drop_indexes():
|
||||
table = "tabSerial and Batch Bundle"
|
||||
index_list = ["voucher_no_index", "item_code_index", "warehouse_index", "company_index"]
|
||||
|
||||
for index in index_list:
|
||||
if not frappe.db.has_index(table, index):
|
||||
continue
|
||||
|
||||
try:
|
||||
frappe.db.sql_ddl(f"ALTER TABLE `{table}` DROP INDEX `{index}`")
|
||||
click.echo(f"✓ dropped {index} index from {table}")
|
||||
except Exception:
|
||||
frappe.log_error("Failed to drop index")
|
||||
@@ -11,7 +11,10 @@ def get_data():
|
||||
"Bank Account": "party",
|
||||
"Subscription": "party",
|
||||
},
|
||||
"dynamic_links": {"party_name": ["Customer", "quotation_to"]},
|
||||
"dynamic_links": {
|
||||
"party_name": ["Customer", "quotation_to"],
|
||||
"party": ["Customer", "party_type"],
|
||||
},
|
||||
"transactions": [
|
||||
{"label": _("Pre Sales"), "items": ["Opportunity", "Quotation"]},
|
||||
{"label": _("Orders"), "items": ["Sales Order", "Delivery Note", "Sales Invoice"]},
|
||||
|
||||
@@ -14,6 +14,9 @@ def execute(filters=None):
|
||||
days_since_last_order = filters.get("days_since_last_order")
|
||||
doctype = filters.get("doctype")
|
||||
|
||||
if doctype not in ("Sales Order", "Sales Invoice"):
|
||||
frappe.throw(_("Invalid value {0} for 'Doctype'").format(doctype))
|
||||
|
||||
if cint(days_since_last_order) <= 0:
|
||||
frappe.throw(_("'Days Since Last Order' must be greater than or equal to zero"))
|
||||
|
||||
|
||||
@@ -427,14 +427,16 @@ class Analytics:
|
||||
break
|
||||
|
||||
def get_groups(self):
|
||||
if self.filters.tree_type == "Territory":
|
||||
parent = "parent_territory"
|
||||
if self.filters.tree_type == "Customer Group":
|
||||
parent = "parent_customer_group"
|
||||
if self.filters.tree_type == "Item Group":
|
||||
parent = "parent_item_group"
|
||||
if self.filters.tree_type == "Supplier Group":
|
||||
parent = "parent_supplier_group"
|
||||
parent_field_map = {
|
||||
"Territory": "parent_territory",
|
||||
"Customer Group": "parent_customer_group",
|
||||
"Item Group": "parent_item_group",
|
||||
"Supplier Group": "parent_supplier_group",
|
||||
}
|
||||
if self.filters.tree_type not in parent_field_map:
|
||||
frappe.throw(_("Invalid Tree Type {0}").format(self.filters.tree_type))
|
||||
|
||||
parent = parent_field_map[self.filters.tree_type]
|
||||
|
||||
self.depth_map = frappe._dict()
|
||||
|
||||
@@ -453,6 +455,9 @@ class Analytics:
|
||||
def get_teams(self):
|
||||
self.depth_map = frappe._dict()
|
||||
|
||||
if not frappe.db.exists("DocType", self.filters.doc_type):
|
||||
frappe.throw(_("Invalid Document Type {0}").format(self.filters.doc_type))
|
||||
|
||||
self.group_entries = frappe.db.sql(
|
||||
f""" select * from (select "Order Types" as name, 0 as lft,
|
||||
2 as rgt, '' as parent union select distinct order_type as name, 1 as lft, 1 as rgt, "Order Types" as parent
|
||||
|
||||
@@ -120,7 +120,9 @@ class AuthorizationControl(TransactionBase):
|
||||
if val == 1:
|
||||
add_cond += " and system_user = {}".format(frappe.db.escape(session["user"]))
|
||||
elif val == 2:
|
||||
add_cond += " and system_role IN %s" % ("('" + "','".join(frappe.get_roles()) + "')")
|
||||
add_cond += " and system_role IN (%s)" % ", ".join(
|
||||
frappe.db.escape(r) for r in frappe.get_roles()
|
||||
)
|
||||
else:
|
||||
add_cond += " and ifnull(system_user,'') = '' and ifnull(system_role,'') = ''"
|
||||
|
||||
@@ -203,8 +205,8 @@ class AuthorizationControl(TransactionBase):
|
||||
and docstatus != 2
|
||||
""".format(
|
||||
"%s",
|
||||
"'" + "','".join(frappe.get_roles()) + "'",
|
||||
"'" + "','".join(final_based_on) + "'",
|
||||
", ".join(frappe.db.escape(r) for r in frappe.get_roles()),
|
||||
", ".join(frappe.db.escape(b) for b in final_based_on),
|
||||
"%s",
|
||||
),
|
||||
(doctype_name, company),
|
||||
|
||||
@@ -36,6 +36,11 @@ class DeprecatedSerialNoValuation:
|
||||
|
||||
# get rate from serial nos within same company
|
||||
incoming_values = 0.0
|
||||
posting_datetime = self.sle.posting_datetime
|
||||
|
||||
if not posting_datetime and self.sle.posting_date:
|
||||
posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time)
|
||||
|
||||
for serial_no in serial_nos:
|
||||
sn_details = frappe.db.get_value("Serial No", serial_no, ["purchase_rate", "company"], as_dict=1)
|
||||
if (
|
||||
@@ -64,10 +69,7 @@ class DeprecatedSerialNoValuation:
|
||||
& (table.serial_and_batch_bundle.isnull())
|
||||
& (table.actual_qty > 0)
|
||||
& (table.is_cancelled == 0)
|
||||
& (
|
||||
table.posting_datetime
|
||||
<= get_combine_datetime(self.sle.posting_date, self.sle.posting_time)
|
||||
)
|
||||
& (table.posting_datetime <= posting_datetime)
|
||||
)
|
||||
.orderby(table.posting_datetime, order=Order.desc)
|
||||
.limit(1)
|
||||
@@ -98,11 +100,8 @@ class DeprecatedBatchNoValuation:
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
|
||||
timestamp_condition = None
|
||||
if self.sle.posting_date:
|
||||
if self.sle.posting_time is None:
|
||||
self.sle.posting_time = nowtime()
|
||||
|
||||
posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time)
|
||||
if self.sle.posting_datetime:
|
||||
posting_datetime = self.sle.posting_datetime
|
||||
if not self.sle.creation:
|
||||
posting_datetime = posting_datetime + datetime.timedelta(milliseconds=1)
|
||||
|
||||
@@ -202,7 +201,11 @@ class DeprecatedBatchNoValuation:
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
batch = frappe.qb.DocType("Batch")
|
||||
|
||||
posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time)
|
||||
posting_datetime = self.sle.posting_datetime
|
||||
|
||||
if not posting_datetime and self.sle.posting_date:
|
||||
posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time)
|
||||
|
||||
if not self.sle.creation:
|
||||
posting_datetime = posting_datetime + datetime.timedelta(milliseconds=1)
|
||||
|
||||
@@ -266,7 +269,10 @@ class DeprecatedBatchNoValuation:
|
||||
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
|
||||
posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time)
|
||||
posting_datetime = self.sle.posting_datetime
|
||||
if not posting_datetime and self.sle.posting_date:
|
||||
posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time)
|
||||
|
||||
if not self.sle.creation:
|
||||
posting_datetime = posting_datetime + datetime.timedelta(milliseconds=1)
|
||||
|
||||
@@ -308,19 +314,22 @@ class DeprecatedBatchNoValuation:
|
||||
|
||||
@deprecated
|
||||
def set_balance_value_from_bundle(self) -> None:
|
||||
from erpnext.stock.utils import get_combine_datetime
|
||||
|
||||
bundle = frappe.qb.DocType("Serial and Batch Bundle")
|
||||
bundle_child = frappe.qb.DocType("Serial and Batch Entry")
|
||||
batch = frappe.qb.DocType("Batch")
|
||||
|
||||
timestamp_condition = CombineDatetime(bundle.posting_date, bundle.posting_time) < CombineDatetime(
|
||||
self.sle.posting_date, self.sle.posting_time
|
||||
)
|
||||
posting_datetime = self.sle.posting_datetime
|
||||
if not posting_datetime and self.sle.posting_date:
|
||||
posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time)
|
||||
|
||||
timestamp_condition = bundle.posting_datetime < posting_datetime
|
||||
|
||||
if self.sle.creation:
|
||||
timestamp_condition |= (
|
||||
CombineDatetime(bundle.posting_date, bundle.posting_time)
|
||||
== CombineDatetime(self.sle.posting_date, self.sle.posting_time)
|
||||
) & (bundle.creation < self.sle.creation)
|
||||
timestamp_condition |= (bundle.posting_datetime == posting_datetime) & (
|
||||
bundle.creation < self.sle.creation
|
||||
)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(bundle)
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"disabled",
|
||||
"column_break_24",
|
||||
"use_batchwise_valuation",
|
||||
"allow_negative_stock_for_batch",
|
||||
"sb_batch",
|
||||
"batch_id",
|
||||
"item",
|
||||
@@ -202,6 +203,14 @@
|
||||
"label": "Use Batch-wise Valuation",
|
||||
"read_only": 1,
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If enabled, the system will allow negative stock entries for this batch, overriding the 'Allow negative stock for Batch' setting in Stock Settings. This may lead to incorrect valuation rates, so it is recommended to avoid using this option.",
|
||||
"fieldname": "allow_negative_stock_for_batch",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Negative Stock for Batch",
|
||||
"no_copy": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-archive",
|
||||
@@ -209,7 +218,7 @@
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"max_attachments": 5,
|
||||
"modified": "2026-06-16 16:01:26.556324",
|
||||
"modified": "2026-06-17 12:17:28.339975",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Batch",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import datetime
|
||||
from collections import OrderedDict, defaultdict
|
||||
|
||||
import frappe
|
||||
@@ -10,7 +11,7 @@ from frappe.model.document import Document
|
||||
from frappe.model.naming import make_autoname, revert_series_if_last
|
||||
from frappe.query_builder.functions import CurDate, Sum
|
||||
from frappe.utils import cint, flt, get_link_to_form
|
||||
from frappe.utils.data import add_days
|
||||
from frappe.utils.data import DateTimeLikeObject, add_days
|
||||
|
||||
|
||||
class UnableToSelectBatchError(frappe.ValidationError):
|
||||
@@ -94,6 +95,7 @@ class Batch(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
allow_negative_stock_for_batch: DF.Check
|
||||
batch_id: DF.Data
|
||||
batch_qty: DF.Float
|
||||
description: DF.SmallText | None
|
||||
@@ -232,17 +234,18 @@ class Batch(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_batch_qty(
|
||||
batch_no=None,
|
||||
warehouse=None,
|
||||
item_code=None,
|
||||
creation=None,
|
||||
posting_date=None,
|
||||
posting_time=None,
|
||||
ignore_voucher_nos=None,
|
||||
for_stock_levels=False,
|
||||
consider_negative_batches=False,
|
||||
do_not_check_future_batches=False,
|
||||
ignore_reserved_stock=False,
|
||||
batch_no: str | None = None,
|
||||
warehouse: str | None = None,
|
||||
item_code: str | None = None,
|
||||
creation: DateTimeLikeObject | None = None,
|
||||
posting_datetime: DateTimeLikeObject | None = None,
|
||||
posting_date: DateTimeLikeObject | None = None,
|
||||
posting_time: datetime.timedelta | None = None,
|
||||
ignore_voucher_nos: list | None = None,
|
||||
for_stock_levels: bool = False,
|
||||
consider_negative_batches: bool = False,
|
||||
do_not_check_future_batches: bool = False,
|
||||
ignore_reserved_stock: bool = False,
|
||||
):
|
||||
"""Returns batch actual qty if warehouse is passed,
|
||||
or returns dict of qty by warehouse if warehouse is None
|
||||
@@ -255,6 +258,7 @@ def get_batch_qty(
|
||||
:param for_stock_levels: True consider expired batches"""
|
||||
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
combine_datetime,
|
||||
get_auto_batch_nos,
|
||||
)
|
||||
|
||||
@@ -264,8 +268,6 @@ def get_batch_qty(
|
||||
"item_code": item_code,
|
||||
"warehouse": warehouse,
|
||||
"creation": creation,
|
||||
"posting_date": posting_date,
|
||||
"posting_time": posting_time,
|
||||
"batch_no": batch_no,
|
||||
"based_on": frappe.get_single_value("Stock Settings", "pick_serial_and_batch_based_on"),
|
||||
"ignore_voucher_nos": ignore_voucher_nos,
|
||||
@@ -276,6 +278,10 @@ def get_batch_qty(
|
||||
}
|
||||
)
|
||||
|
||||
kwargs["posting_datetime"] = posting_datetime
|
||||
if not kwargs.get("posting_datetime") and posting_date:
|
||||
kwargs["posting_datetime"] = combine_datetime(posting_date, posting_time)
|
||||
|
||||
batches = get_auto_batch_nos(kwargs)
|
||||
|
||||
if not (batch_no and warehouse):
|
||||
@@ -357,6 +363,7 @@ def make_batch_bundle(
|
||||
):
|
||||
from frappe.utils import nowtime, today
|
||||
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import combine_datetime
|
||||
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
||||
|
||||
return (
|
||||
@@ -364,8 +371,7 @@ def make_batch_bundle(
|
||||
{
|
||||
"item_code": item_code,
|
||||
"warehouse": warehouse,
|
||||
"posting_date": today(),
|
||||
"posting_time": nowtime(),
|
||||
"posting_datetime": combine_datetime(today(), nowtime()),
|
||||
"voucher_type": "Stock Entry",
|
||||
"qty": qty,
|
||||
"type_of_transaction": type_of_transaction,
|
||||
@@ -476,9 +482,13 @@ def get_pos_reserved_batch_qty(filters):
|
||||
|
||||
def get_available_batches(kwargs):
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
combine_datetime,
|
||||
get_auto_batch_nos,
|
||||
)
|
||||
|
||||
if kwargs.get("posting_date"):
|
||||
kwargs["posting_datetime"] = combine_datetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
|
||||
|
||||
batchwise_qty = OrderedDict()
|
||||
|
||||
batches = get_auto_batch_nos(kwargs)
|
||||
|
||||
@@ -360,6 +360,89 @@ class TestItem(FrappeTestCase):
|
||||
self.assertRaises(InvalidItemAttributeValueError, attribute.save)
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_rename_attribute_value_updates_variants(self):
|
||||
frappe.delete_doc_if_exists("Item", "_Test Variant Item-L", force=1)
|
||||
|
||||
variant = create_variant("_Test Variant Item", {"Test Size": "Large"})
|
||||
variant.save()
|
||||
|
||||
attribute = frappe.get_doc("Item Attribute", "Test Size")
|
||||
for row in attribute.item_attribute_values:
|
||||
if row.attribute_value == "Large":
|
||||
row.attribute_value = "Larger"
|
||||
break
|
||||
|
||||
def restore_test_size_large():
|
||||
doc = frappe.get_doc("Item Attribute", "Test Size")
|
||||
for row in doc.item_attribute_values:
|
||||
if row.attribute_value == "Larger":
|
||||
row.attribute_value = "Large"
|
||||
break
|
||||
frappe.flags.attribute_values = None
|
||||
doc.save()
|
||||
|
||||
self.addCleanup(restore_test_size_large)
|
||||
|
||||
frappe.flags.attribute_values = None
|
||||
attribute.save()
|
||||
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(
|
||||
"Item Variant Attribute",
|
||||
{"parent": variant.name, "attribute": "Test Size"},
|
||||
"attribute_value",
|
||||
),
|
||||
"Larger",
|
||||
)
|
||||
|
||||
def test_swapped_attribute_value_renames_update_variants(self):
|
||||
frappe.delete_doc_if_exists("Item", "_Test Variant Item-L", force=1)
|
||||
frappe.delete_doc_if_exists("Item", "_Test Variant Item-S", force=1)
|
||||
|
||||
large_variant = create_variant("_Test Variant Item", {"Test Size": "Large"})
|
||||
large_variant.save()
|
||||
|
||||
small_variant = create_variant("_Test Variant Item", {"Test Size": "Small"})
|
||||
small_variant.save()
|
||||
|
||||
attribute = frappe.get_doc("Item Attribute", "Test Size")
|
||||
original_values = {row.name: row.attribute_value for row in attribute.item_attribute_values}
|
||||
|
||||
def restore_test_size_values():
|
||||
doc = frappe.get_doc("Item Attribute", "Test Size")
|
||||
for row in doc.item_attribute_values:
|
||||
row.attribute_value = original_values[row.name]
|
||||
frappe.flags.attribute_values = None
|
||||
doc.save()
|
||||
|
||||
self.addCleanup(restore_test_size_values)
|
||||
|
||||
for row in attribute.item_attribute_values:
|
||||
if row.attribute_value == "Large":
|
||||
row.attribute_value = "Small"
|
||||
elif row.attribute_value == "Small":
|
||||
row.attribute_value = "Large"
|
||||
|
||||
frappe.flags.attribute_values = None
|
||||
attribute.save()
|
||||
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(
|
||||
"Item Variant Attribute",
|
||||
{"parent": large_variant.name, "attribute": "Test Size"},
|
||||
"attribute_value",
|
||||
),
|
||||
"Small",
|
||||
)
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(
|
||||
"Item Variant Attribute",
|
||||
{"parent": small_variant.name, "attribute": "Test Size"},
|
||||
"attribute_value",
|
||||
),
|
||||
"Large",
|
||||
)
|
||||
|
||||
def test_make_item_variant(self):
|
||||
frappe.delete_doc_if_exists("Item", "_Test Variant Item-L", force=1)
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from frappe.utils import flt
|
||||
|
||||
from erpnext.controllers.item_variant import (
|
||||
InvalidItemAttributeValueError,
|
||||
update_variant_attribute_values,
|
||||
validate_is_incremental,
|
||||
validate_item_attribute_value,
|
||||
)
|
||||
@@ -47,6 +48,7 @@ class ItemAttribute(Document):
|
||||
self.validate_duplication()
|
||||
|
||||
def on_update(self):
|
||||
update_variant_attribute_values(self)
|
||||
self.validate_exising_items()
|
||||
self.set_enabled_disabled_in_items()
|
||||
|
||||
|
||||
@@ -209,7 +209,7 @@ class MaterialRequest(BuyingController):
|
||||
|
||||
def check_modified_date(self):
|
||||
mod_db = frappe.db.sql("""select modified from `tabMaterial Request` where name = %s""", self.name)
|
||||
date_diff = frappe.db.sql(f"""select TIMEDIFF('{mod_db[0][0]}', '{cstr(self.modified)}')""")
|
||||
date_diff = frappe.db.sql("""select TIMEDIFF(%s, %s)""", (mod_db[0][0], cstr(self.modified)))
|
||||
|
||||
if date_diff and date_diff[0][0]:
|
||||
frappe.throw(_("{0} {1} has been modified. Please refresh.").format(_(self.doctype), self.name))
|
||||
|
||||
@@ -995,6 +995,52 @@ class TestMaterialRequest(FrappeTestCase):
|
||||
se.save()
|
||||
se.submit()
|
||||
|
||||
def test_mr_status_for_mixed_direct_and_transit_transfer(self):
|
||||
material_request = make_material_request(
|
||||
material_request_type="Material Transfer",
|
||||
item_code="_Test Item Home Desktop 100",
|
||||
qty=5,
|
||||
)
|
||||
|
||||
in_transit_wh = get_in_transit_warehouse(material_request.company)
|
||||
|
||||
# Make stock available
|
||||
self._insert_stock_entry(20.0, 20.0)
|
||||
|
||||
# Direct Transfer for 3 Qty
|
||||
direct_transfer = make_stock_entry(material_request.name)
|
||||
direct_transfer.items[0].update(
|
||||
{
|
||||
"qty": 3,
|
||||
"transfer_qty": 3,
|
||||
"s_warehouse": "_Test Warehouse 1 - _TC",
|
||||
}
|
||||
)
|
||||
direct_transfer.save()
|
||||
direct_transfer.submit()
|
||||
|
||||
# In Transit Transfer for remaining 2 Qty
|
||||
transit_transfer = make_in_transit_stock_entry(material_request.name, in_transit_wh)
|
||||
transit_transfer.items[0].update(
|
||||
{
|
||||
"qty": 2,
|
||||
"s_warehouse": "_Test Warehouse 1 - _TC",
|
||||
}
|
||||
)
|
||||
transit_transfer.save()
|
||||
transit_transfer.submit()
|
||||
|
||||
# Complete End Transit
|
||||
end_transit = make_stock_in_entry(transit_transfer.name)
|
||||
end_transit.save()
|
||||
end_transit.submit()
|
||||
|
||||
material_request.reload()
|
||||
|
||||
self.assertEqual(material_request.per_ordered, 100)
|
||||
self.assertEqual(material_request.status, "Transferred")
|
||||
self.assertEqual(material_request.transfer_status, "Completed")
|
||||
|
||||
|
||||
def get_in_transit_warehouse(company):
|
||||
if not frappe.db.exists("Warehouse Type", "Transit"):
|
||||
|
||||
@@ -29,8 +29,7 @@
|
||||
"voucher_no",
|
||||
"voucher_detail_no",
|
||||
"column_break_aouy",
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
"posting_datetime",
|
||||
"returned_against",
|
||||
"section_break_wzou",
|
||||
"is_cancelled",
|
||||
@@ -49,8 +48,7 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.item_group",
|
||||
@@ -79,8 +77,7 @@
|
||||
"in_standard_filter": 1,
|
||||
"label": "Item Code",
|
||||
"options": "Item",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.item_name",
|
||||
@@ -117,8 +114,7 @@
|
||||
"in_standard_filter": 1,
|
||||
"label": "Voucher No",
|
||||
"no_copy": 1,
|
||||
"options": "voucher_type",
|
||||
"search_index": 1
|
||||
"options": "voucher_type"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -188,8 +184,7 @@
|
||||
"in_standard_filter": 1,
|
||||
"label": "Warehouse",
|
||||
"mandatory_depends_on": "eval:doc.type_of_transaction != \"Maintenance\"",
|
||||
"options": "Warehouse",
|
||||
"search_index": 1
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
"fieldname": "type_of_transaction",
|
||||
@@ -211,18 +206,6 @@
|
||||
"fieldname": "section_break_wzou",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Posting Date",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "posting_time",
|
||||
"fieldtype": "Time",
|
||||
"label": "Posting Time",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_detail_no",
|
||||
"fieldtype": "Data",
|
||||
@@ -251,12 +234,17 @@
|
||||
"label": "Naming Series",
|
||||
"options": "\nSABB-.########",
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "posting_datetime",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Posting Datetime"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-09-15 14:37:26.441742",
|
||||
"modified": "2025-09-24 16:24:48.154853",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Serial and Batch Bundle",
|
||||
|
||||
@@ -74,8 +74,7 @@ class SerialandBatchBundle(Document):
|
||||
item_group: DF.Link | None
|
||||
item_name: DF.Data | None
|
||||
naming_series: DF.Literal["", "SABB-.########"]
|
||||
posting_date: DF.Date | None
|
||||
posting_time: DF.Time | None
|
||||
posting_datetime: DF.Datetime | None
|
||||
returned_against: DF.Data | None
|
||||
total_amount: DF.Float
|
||||
total_qty: DF.Float
|
||||
@@ -280,8 +279,7 @@ class SerialandBatchBundle(Document):
|
||||
kwargs.update(
|
||||
{
|
||||
"voucher_no": self.voucher_no,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
"posting_datetime": self.posting_datetime,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -332,8 +330,7 @@ class SerialandBatchBundle(Document):
|
||||
kwargs = frappe._dict(
|
||||
{
|
||||
"item_code": self.item_code,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
"posting_datetime": self.posting_datetime,
|
||||
"serial_nos": serial_nos,
|
||||
"check_serial_nos": True,
|
||||
}
|
||||
@@ -669,8 +666,7 @@ class SerialandBatchBundle(Document):
|
||||
def get_sle_for_outward_transaction(self):
|
||||
sle = frappe._dict(
|
||||
{
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
"posting_datetime": self.posting_datetime,
|
||||
"item_code": self.item_code,
|
||||
"warehouse": self.warehouse,
|
||||
"serial_and_batch_bundle": self.name,
|
||||
@@ -829,11 +825,10 @@ class SerialandBatchBundle(Document):
|
||||
if not self.voucher_detail_no or self.voucher_detail_no != row.name:
|
||||
values_to_set["voucher_detail_no"] = row.name
|
||||
|
||||
if parent.get("posting_date") and (not self.posting_date or self.posting_date != parent.posting_date):
|
||||
values_to_set["posting_date"] = parent.posting_date or today()
|
||||
|
||||
if parent.get("posting_time") and (not self.posting_time or self.posting_time != parent.posting_time):
|
||||
values_to_set["posting_time"] = parent.posting_time
|
||||
if parent.get("posting_date") and parent.get("posting_time"):
|
||||
posting_datetime = combine_datetime(parent.posting_date, parent.posting_time)
|
||||
if not self.posting_datetime or self.posting_datetime != posting_datetime:
|
||||
values_to_set["posting_datetime"] = posting_datetime
|
||||
|
||||
if row.get("doctype") == "Packed Item" and row.get("parent_detail_docname"):
|
||||
values_to_set["voucher_detail_no"] = row.get("parent_detail_docname")
|
||||
@@ -911,9 +906,7 @@ class SerialandBatchBundle(Document):
|
||||
parent = frappe.qb.DocType("Serial and Batch Bundle")
|
||||
child = frappe.qb.DocType("Serial and Batch Entry")
|
||||
|
||||
timestamp_condition = CombineDatetime(parent.posting_date, parent.posting_time) > CombineDatetime(
|
||||
self.posting_date, self.posting_time
|
||||
)
|
||||
timestamp_condition = parent.posting_datetime > self.posting_datetime
|
||||
|
||||
future_entries = (
|
||||
frappe.qb.from_(parent)
|
||||
@@ -1515,7 +1508,7 @@ class SerialandBatchBundle(Document):
|
||||
def throw_negative_batch(self, batch_no, available_qty, precision, posting_datetime=None):
|
||||
from erpnext.stock.stock_ledger import NegativeStockError
|
||||
|
||||
if frappe.db.get_single_value("Stock Settings", "allow_negative_stock_for_batch"):
|
||||
if allow_negative_stock_for_batch(batch_no):
|
||||
return
|
||||
|
||||
date_msg = ""
|
||||
@@ -1526,7 +1519,7 @@ class SerialandBatchBundle(Document):
|
||||
"""
|
||||
The Batch {0} of an item {1} has negative stock in the warehouse {2}{3}.
|
||||
Please add a stock quantity of {4} to proceed with this entry.
|
||||
If it is not possible to make an adjustment entry, please enable 'Allow Negative Stock for Batch' in Stock Settings to proceed.
|
||||
If it is not possible to make an adjustment entry, please enable 'Allow Negative Stock for Batch' in the batch {0} or Stock Settings to proceed.
|
||||
However, enabling this setting may lead to negative stock in the system.
|
||||
So please ensure the stock levels are adjusted as soon as possible to maintain the correct valuation rate."""
|
||||
).format(
|
||||
@@ -2083,6 +2076,8 @@ def create_serial_batch_no_ledgers(
|
||||
if parent_doc.get("doctype") == "Stock Entry":
|
||||
warehouse = warehouse or child_row.s_warehouse or child_row.t_warehouse
|
||||
|
||||
posting_datetime = combine_datetime(parent_doc.get("posting_date"), parent_doc.get("posting_time"))
|
||||
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Serial and Batch Bundle",
|
||||
@@ -2091,8 +2086,7 @@ def create_serial_batch_no_ledgers(
|
||||
"warehouse": warehouse,
|
||||
"is_rejected": child_row.is_rejected,
|
||||
"type_of_transaction": type_of_transaction,
|
||||
"posting_date": parent_doc.get("posting_date"),
|
||||
"posting_time": parent_doc.get("posting_time"),
|
||||
"posting_datetime": posting_datetime,
|
||||
"company": parent_doc.get("company"),
|
||||
}
|
||||
)
|
||||
@@ -2128,6 +2122,25 @@ def create_serial_batch_no_ledgers(
|
||||
return doc
|
||||
|
||||
|
||||
def combine_datetime(date, time=None):
|
||||
from erpnext.stock.utils import get_combine_datetime
|
||||
|
||||
return get_combine_datetime(date, time)
|
||||
|
||||
|
||||
def allow_negative_stock_for_batch(batch_no):
|
||||
"""Return whether negative stock is allowed for the given batch.
|
||||
|
||||
The batch-level setting takes priority: if `allow_negative_stock_for_batch`
|
||||
is enabled on the Batch, negative stock is allowed regardless of Stock Settings.
|
||||
Otherwise, fall back to the `allow_negative_stock_for_batch` Stock Setting.
|
||||
"""
|
||||
if batch_no and frappe.db.get_value("Batch", batch_no, "allow_negative_stock_for_batch"):
|
||||
return True
|
||||
|
||||
return bool(frappe.db.get_single_value("Stock Settings", "allow_negative_stock_for_batch"))
|
||||
|
||||
|
||||
def get_batch(item_code):
|
||||
from erpnext.stock.doctype.batch.batch import make_batch
|
||||
|
||||
@@ -2180,8 +2193,8 @@ def update_serial_batch_no_ledgers(bundle, entries, child_row, parent_doc, wareh
|
||||
)
|
||||
|
||||
doc.voucher_detail_no = child_row.name
|
||||
doc.posting_date = parent_doc.posting_date
|
||||
doc.posting_time = parent_doc.posting_time
|
||||
doc.posting_datetime = combine_datetime(parent_doc.get("posting_date"), parent_doc.get("posting_time"))
|
||||
|
||||
doc.warehouse = warehouse or doc.warehouse
|
||||
doc.set("entries", [])
|
||||
|
||||
@@ -2269,6 +2282,9 @@ def get_available_serial_nos(kwargs):
|
||||
elif kwargs.based_on == "Expiry":
|
||||
order_by = "amc_expiry_date"
|
||||
|
||||
if not kwargs.get("posting_datetime") and kwargs.get("posting_date"):
|
||||
kwargs["posting_datetime"] = combine_datetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
|
||||
|
||||
filters = {"item_code": kwargs.item_code}
|
||||
|
||||
# ignore_warehouse is used for backdated stock transactions
|
||||
@@ -2285,10 +2301,8 @@ def get_available_serial_nos(kwargs):
|
||||
if kwargs.get("ignore_serial_nos"):
|
||||
ignore_serial_nos.extend(kwargs.get("ignore_serial_nos"))
|
||||
|
||||
if kwargs.get("posting_date"):
|
||||
if kwargs.get("posting_time") is None:
|
||||
kwargs.posting_time = nowtime()
|
||||
|
||||
ignore_serial_nos = list(set(ignore_serial_nos))
|
||||
if kwargs.get("posting_datetime"):
|
||||
time_based_serial_nos = get_serial_nos_based_on_posting_date(kwargs, ignore_serial_nos)
|
||||
|
||||
if not time_based_serial_nos:
|
||||
@@ -2692,6 +2706,9 @@ def get_reserved_batches_for_sre(kwargs) -> dict:
|
||||
|
||||
|
||||
def get_auto_batch_nos(kwargs):
|
||||
if not kwargs.get("posting_datetime") and kwargs.get("posting_date"):
|
||||
kwargs["posting_datetime"] = combine_datetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
|
||||
|
||||
available_batches = get_available_batches(kwargs)
|
||||
qty = flt(kwargs.qty)
|
||||
|
||||
@@ -2721,7 +2738,7 @@ def get_auto_batch_nos(kwargs):
|
||||
if kwargs.based_on == "Expiry":
|
||||
available_batches = sorted(available_batches, key=lambda x: x.expiry_date or getdate("9999-12-31"))
|
||||
|
||||
if not kwargs.get("do_not_check_future_batches") and available_batches and kwargs.get("posting_date"):
|
||||
if not kwargs.get("do_not_check_future_batches") and available_batches and kwargs.get("posting_datetime"):
|
||||
filter_zero_near_batches(available_batches, kwargs)
|
||||
|
||||
if not kwargs.consider_negative_batches:
|
||||
@@ -2737,8 +2754,7 @@ def get_auto_batch_nos(kwargs):
|
||||
def filter_zero_near_batches(available_batches, kwargs):
|
||||
kwargs.batch_no = [d.batch_no for d in available_batches]
|
||||
|
||||
del kwargs["posting_date"]
|
||||
del kwargs["posting_time"]
|
||||
del kwargs["posting_datetime"]
|
||||
|
||||
kwargs.do_not_check_future_batches = 1
|
||||
available_batches_in_future = get_auto_batch_nos(kwargs)
|
||||
@@ -2804,8 +2820,6 @@ def update_available_batches(available_batches, *reserved_batches) -> None:
|
||||
|
||||
|
||||
def get_available_batches(kwargs):
|
||||
from erpnext.stock.utils import get_combine_datetime
|
||||
|
||||
stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
|
||||
batch_ledger = frappe.qb.DocType("Serial and Batch Entry")
|
||||
batch_table = frappe.qb.DocType("Batch")
|
||||
@@ -2833,23 +2847,15 @@ def get_available_batches(kwargs):
|
||||
if not kwargs.get("for_stock_levels"):
|
||||
query = query.where((batch_table.expiry_date >= today()) | (batch_table.expiry_date.isnull()))
|
||||
|
||||
if kwargs.get("posting_date"):
|
||||
if kwargs.get("posting_time") is None:
|
||||
kwargs.posting_time = nowtime()
|
||||
|
||||
timestamp_condition = stock_ledger_entry.posting_datetime <= get_combine_datetime(
|
||||
kwargs.posting_date, kwargs.posting_time
|
||||
)
|
||||
if kwargs.get("posting_datetime"):
|
||||
timestamp_condition = stock_ledger_entry.posting_datetime <= kwargs.posting_datetime
|
||||
|
||||
if kwargs.get("creation"):
|
||||
timestamp_condition = stock_ledger_entry.posting_datetime < get_combine_datetime(
|
||||
kwargs.posting_date, kwargs.posting_time
|
||||
)
|
||||
timestamp_condition = stock_ledger_entry.posting_datetime < kwargs.posting_datetime
|
||||
|
||||
timestamp_condition |= (
|
||||
stock_ledger_entry.posting_datetime
|
||||
== get_combine_datetime(kwargs.posting_date, kwargs.posting_time)
|
||||
) & (stock_ledger_entry.creation < kwargs.creation)
|
||||
timestamp_condition |= (stock_ledger_entry.posting_datetime == kwargs.posting_datetime) & (
|
||||
stock_ledger_entry.creation < kwargs.creation
|
||||
)
|
||||
|
||||
query = query.where(timestamp_condition)
|
||||
|
||||
@@ -3035,15 +3041,14 @@ def get_ledgers_from_serial_batch_bundle(**kwargs) -> list[frappe._dict]:
|
||||
serial_batch_table.incoming_rate,
|
||||
bundle_table.voucher_detail_no,
|
||||
bundle_table.voucher_no,
|
||||
bundle_table.posting_date,
|
||||
bundle_table.posting_time,
|
||||
bundle_table.posting_datetime,
|
||||
)
|
||||
.where(
|
||||
(bundle_table.docstatus == 1)
|
||||
& (bundle_table.is_cancelled == 0)
|
||||
& (bundle_table.type_of_transaction.isin(["Inward", "Outward"]))
|
||||
)
|
||||
.orderby(bundle_table.posting_date, bundle_table.posting_time)
|
||||
.orderby(bundle_table.posting_datetime)
|
||||
)
|
||||
|
||||
for key, val in kwargs.items():
|
||||
@@ -3061,7 +3066,7 @@ def get_ledgers_from_serial_batch_bundle(**kwargs) -> list[frappe._dict]:
|
||||
query = query.where(bundle_table[key].isin(val))
|
||||
else:
|
||||
query = query.where(bundle_table[key] == val)
|
||||
elif key in ["posting_date", "posting_time"]:
|
||||
elif key in ["posting_datetime"]:
|
||||
query = query.where(bundle_table[key] >= val)
|
||||
else:
|
||||
if isinstance(val, list):
|
||||
@@ -3073,8 +3078,6 @@ def get_ledgers_from_serial_batch_bundle(**kwargs) -> list[frappe._dict]:
|
||||
|
||||
|
||||
def get_stock_ledgers_for_serial_nos(kwargs):
|
||||
from erpnext.stock.utils import get_combine_datetime
|
||||
|
||||
stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
|
||||
|
||||
query = (
|
||||
@@ -3090,23 +3093,15 @@ def get_stock_ledgers_for_serial_nos(kwargs):
|
||||
.orderby(stock_ledger_entry.creation)
|
||||
)
|
||||
|
||||
if kwargs.get("posting_date"):
|
||||
if kwargs.get("posting_time") is None:
|
||||
kwargs.posting_time = nowtime()
|
||||
|
||||
timestamp_condition = stock_ledger_entry.posting_datetime <= get_combine_datetime(
|
||||
kwargs.posting_date, kwargs.posting_time
|
||||
)
|
||||
if kwargs.get("posting_datetime"):
|
||||
timestamp_condition = stock_ledger_entry.posting_datetime <= kwargs.posting_datetime
|
||||
|
||||
if kwargs.get("creation"):
|
||||
timestamp_condition = stock_ledger_entry.posting_datetime < get_combine_datetime(
|
||||
kwargs.posting_date, kwargs.posting_time
|
||||
)
|
||||
timestamp_condition = stock_ledger_entry.posting_datetime < kwargs.posting_datetime
|
||||
|
||||
timestamp_condition |= (
|
||||
stock_ledger_entry.posting_datetime
|
||||
== get_combine_datetime(kwargs.posting_date, kwargs.posting_time)
|
||||
) & (stock_ledger_entry.creation < kwargs.creation)
|
||||
timestamp_condition |= (stock_ledger_entry.posting_datetime == kwargs.posting_datetime) & (
|
||||
stock_ledger_entry.creation < kwargs.creation
|
||||
)
|
||||
|
||||
query = query.where(timestamp_condition)
|
||||
|
||||
@@ -3129,8 +3124,6 @@ def get_stock_ledgers_for_serial_nos(kwargs):
|
||||
|
||||
|
||||
def get_stock_ledgers_batches(kwargs):
|
||||
from erpnext.stock.utils import get_combine_datetime
|
||||
|
||||
stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
|
||||
batch_table = frappe.qb.DocType("Batch")
|
||||
|
||||
@@ -3164,23 +3157,15 @@ def get_stock_ledgers_batches(kwargs):
|
||||
if not kwargs.get("for_stock_levels"):
|
||||
query = query.where((batch_table.expiry_date >= today()) | (batch_table.expiry_date.isnull()))
|
||||
|
||||
if kwargs.get("posting_date"):
|
||||
if kwargs.get("posting_time") is None:
|
||||
kwargs.posting_time = nowtime()
|
||||
|
||||
timestamp_condition = stock_ledger_entry.posting_datetime <= get_combine_datetime(
|
||||
kwargs.posting_date, kwargs.posting_time
|
||||
)
|
||||
if kwargs.get("posting_datetime"):
|
||||
timestamp_condition = stock_ledger_entry.posting_datetime <= kwargs.posting_datetime
|
||||
|
||||
if kwargs.get("creation"):
|
||||
timestamp_condition = stock_ledger_entry.posting_datetime < get_combine_datetime(
|
||||
kwargs.posting_date, kwargs.posting_time
|
||||
)
|
||||
timestamp_condition = stock_ledger_entry.posting_datetime < kwargs.posting_datetime
|
||||
|
||||
timestamp_condition |= (
|
||||
stock_ledger_entry.posting_datetime
|
||||
== get_combine_datetime(kwargs.posting_date, kwargs.posting_time)
|
||||
) & (stock_ledger_entry.creation < kwargs.creation)
|
||||
timestamp_condition |= (stock_ledger_entry.posting_datetime == kwargs.posting_datetime) & (
|
||||
stock_ledger_entry.creation < kwargs.creation
|
||||
)
|
||||
|
||||
query = query.where(timestamp_condition)
|
||||
|
||||
@@ -3268,3 +3253,7 @@ def get_stock_reco_details(voucher_detail_no):
|
||||
],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("Serial and Batch Bundle", ["item_code", "warehouse", "posting_datetime", "creation"])
|
||||
|
||||
@@ -10,6 +10,7 @@ from frappe.utils import flt, nowtime, today
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
add_serial_batch_ledgers,
|
||||
combine_datetime,
|
||||
make_batch_nos,
|
||||
make_serial_nos,
|
||||
)
|
||||
@@ -1271,14 +1272,17 @@ def make_serial_batch_bundle(kwargs):
|
||||
if kwargs.get("type_of_transaction"):
|
||||
type_of_transaction = kwargs.get("type_of_transaction")
|
||||
|
||||
posting_datetime = None
|
||||
if kwargs.get("posting_date"):
|
||||
posting_datetime = combine_datetime(kwargs.posting_date, kwargs.posting_time or nowtime())
|
||||
|
||||
sb = SerialBatchCreation(
|
||||
{
|
||||
"item_code": kwargs.item_code,
|
||||
"warehouse": kwargs.warehouse,
|
||||
"voucher_type": kwargs.voucher_type,
|
||||
"voucher_no": kwargs.voucher_no,
|
||||
"posting_date": kwargs.posting_date,
|
||||
"posting_time": kwargs.posting_time,
|
||||
"posting_datetime": posting_datetime,
|
||||
"qty": kwargs.qty,
|
||||
"avg_rate": kwargs.rate,
|
||||
"batches": kwargs.batches,
|
||||
|
||||
@@ -1,26 +1,30 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
cur_frm.add_fetch("customer", "customer_name", "customer_name");
|
||||
cur_frm.add_fetch("supplier", "supplier_name", "supplier_name");
|
||||
|
||||
cur_frm.add_fetch("item_code", "item_name", "item_name");
|
||||
cur_frm.add_fetch("item_code", "description", "description");
|
||||
cur_frm.add_fetch("item_code", "item_group", "item_group");
|
||||
cur_frm.add_fetch("item_code", "brand", "brand");
|
||||
|
||||
cur_frm.cscript.onload = function () {
|
||||
cur_frm.set_query("item_code", function () {
|
||||
return erpnext.queries.item({ is_stock_item: 1, has_serial_no: 1 });
|
||||
});
|
||||
};
|
||||
|
||||
frappe.ui.form.on("Serial No", "refresh", function (frm) {
|
||||
frm.toggle_enable("item_code", frm.doc.__islocal);
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Serial No", {
|
||||
setup(frm) {
|
||||
frm.add_fetch("customer", "customer_name", "customer_name");
|
||||
frm.add_fetch("supplier", "supplier_name", "supplier_name");
|
||||
frm.add_fetch("item_code", "item_name", "item_name");
|
||||
frm.add_fetch("item_code", "description", "description");
|
||||
frm.add_fetch("item_code", "item_group", "item_group");
|
||||
frm.add_fetch("item_code", "brand", "brand");
|
||||
|
||||
frm.set_query("item_code", function () {
|
||||
return erpnext.queries.item({ is_stock_item: 1, has_serial_no: 1 });
|
||||
});
|
||||
|
||||
frm.set_query("work_order", () => {
|
||||
return {
|
||||
filters: {
|
||||
docstatus: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
refresh(frm) {
|
||||
frm.toggle_enable("item_code", frm.doc.__islocal);
|
||||
frm.trigger("view_ledgers");
|
||||
},
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ from erpnext.stock.serial_batch_bundle import (
|
||||
get_serial_or_batch_items,
|
||||
)
|
||||
from erpnext.stock.stock_ledger import NegativeStockError, get_previous_sle, get_valuation_rate
|
||||
from erpnext.stock.utils import get_bin, get_incoming_rate
|
||||
from erpnext.stock.utils import get_bin, get_combine_datetime, get_incoming_rate
|
||||
|
||||
|
||||
class FinishedGoodError(frappe.ValidationError):
|
||||
@@ -1527,8 +1527,7 @@ class StockEntry(StockController):
|
||||
{
|
||||
"item_code": row.item_code,
|
||||
"warehouse": row.s_warehouse,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
"posting_datetime": get_combine_datetime(self.posting_date, self.posting_time),
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_detail_no": row.name,
|
||||
"qty": row.transfer_qty * -1,
|
||||
@@ -4058,8 +4057,7 @@ def create_serial_and_batch_bundle(parent_doc, row, child, type_of_transaction=N
|
||||
"item_code": child.item_code,
|
||||
"warehouse": child.warehouse,
|
||||
"type_of_transaction": type_of_transaction,
|
||||
"posting_date": parent_doc.posting_date,
|
||||
"posting_time": parent_doc.posting_time,
|
||||
"posting_datetime": get_combine_datetime(parent_doc.posting_date, parent_doc.posting_time),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -4116,13 +4114,19 @@ def get_batchwise_serial_nos(item_code, row):
|
||||
|
||||
|
||||
def get_transferred_qty(material_request):
|
||||
sed = DocType("Stock Entry Detail")
|
||||
from pypika import Case
|
||||
|
||||
se = frappe.qb.DocType("Stock Entry")
|
||||
sed = frappe.qb.DocType("Stock Entry Detail")
|
||||
completed_qty = Case().when(se.add_to_transit == 1, sed.transferred_qty).else_(sed.transfer_qty)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(sed)
|
||||
.inner_join(se)
|
||||
.on(se.name == sed.parent)
|
||||
.select(
|
||||
Sum(sed.transfer_qty).as_("transfer_qty"),
|
||||
Sum(sed.transferred_qty).as_("transferred_qty"),
|
||||
Sum(completed_qty).as_("transferred_qty"),
|
||||
)
|
||||
.where((sed.material_request == material_request) & (sed.docstatus == 1))
|
||||
).run(as_dict=True)
|
||||
|
||||
@@ -8,6 +8,7 @@ import frappe
|
||||
from frappe.utils import cint, flt
|
||||
|
||||
import erpnext
|
||||
from erpnext.stock.utils import get_combine_datetime
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry import StockEntry
|
||||
@@ -140,6 +141,10 @@ def make_stock_entry(**args):
|
||||
elif args.batches:
|
||||
batches = args.batches
|
||||
|
||||
posting_datetime = None
|
||||
if args.posting_date and args.posting_time:
|
||||
posting_datetime = get_combine_datetime(args.posting_date, args.posting_time)
|
||||
|
||||
bundle_id = (
|
||||
SerialBatchCreation(
|
||||
{
|
||||
@@ -151,8 +156,7 @@ def make_stock_entry(**args):
|
||||
"serial_nos": args.serial_no,
|
||||
"type_of_transaction": "Outward" if args.source else "Inward",
|
||||
"company": s.company,
|
||||
"posting_date": s.posting_date,
|
||||
"posting_time": s.posting_time,
|
||||
"posting_datetime": posting_datetime,
|
||||
"rate": args.rate or args.basic_rate,
|
||||
"do_not_submit": True,
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ from erpnext.controllers.stock_controller import StockController
|
||||
from erpnext.stock.doctype.batch.batch import get_available_batches, get_batch_qty
|
||||
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
combine_datetime,
|
||||
get_available_serial_nos,
|
||||
)
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
@@ -136,8 +137,7 @@ class StockReconciliation(StockController):
|
||||
{
|
||||
"item_code": row.item_code,
|
||||
"warehouse": row.warehouse,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
"posting_datetime": combine_datetime(self.posting_date, self.posting_time),
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"voucher_detail_no": row.name,
|
||||
@@ -243,8 +243,7 @@ class StockReconciliation(StockController):
|
||||
"doctype": "Serial and Batch Bundle",
|
||||
"item_code": item.item_code,
|
||||
"warehouse": item.warehouse,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
"posting_datetime": combine_datetime(self.posting_date, self.posting_time),
|
||||
"voucher_type": self.doctype,
|
||||
"type_of_transaction": "Outward",
|
||||
}
|
||||
@@ -262,8 +261,7 @@ class StockReconciliation(StockController):
|
||||
{
|
||||
"item_code": item.item_code,
|
||||
"warehouse": item.warehouse,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
"posting_datetime": combine_datetime(self.posting_date, self.posting_time),
|
||||
"ignore_warehouse": 1,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -19,7 +19,7 @@ def get_data(filters):
|
||||
"Serial and Batch Bundle",
|
||||
fields=[
|
||||
"`tabSerial and Batch Bundle`.`voucher_type`",
|
||||
"`tabSerial and Batch Bundle`.`posting_date`",
|
||||
"`tabSerial and Batch Bundle`.`posting_datetime` as posting_date",
|
||||
"`tabSerial and Batch Bundle`.`name`",
|
||||
"`tabSerial and Batch Bundle`.`company`",
|
||||
"`tabSerial and Batch Bundle`.`voucher_no`",
|
||||
@@ -33,7 +33,7 @@ def get_data(filters):
|
||||
"`tabSerial and Batch Entry`.`qty`",
|
||||
],
|
||||
filters=filter_conditions,
|
||||
order_by="posting_date",
|
||||
order_by="posting_datetime",
|
||||
)
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ def get_filter_conditions(filters):
|
||||
filter_conditions.append(
|
||||
[
|
||||
"Serial and Batch Bundle",
|
||||
"posting_date",
|
||||
"posting_datetime",
|
||||
"between",
|
||||
[filters.get("from_date"), filters.get("to_date")],
|
||||
]
|
||||
|
||||
@@ -358,7 +358,7 @@ class FIFOSlots:
|
||||
if row.voucher_type != "Stock Reconciliation":
|
||||
return
|
||||
|
||||
if not row.batch_no or row.serial_no or row.serial_and_batch_bundle:
|
||||
if row.has_serial_no and (not row.batch_no or row.serial_no or row.serial_and_batch_bundle):
|
||||
if row.voucher_detail_no in self.stock_reco_voucher_wise_count:
|
||||
# Legacy reconciliation with a single SLE has qty_after_transaction and
|
||||
# stock_value_difference without an outward entry, so reset the queue first.
|
||||
@@ -1083,6 +1083,7 @@ class FIFOSlots:
|
||||
(doctype.voucher_type == "Stock Reconciliation")
|
||||
& (doctype.docstatus < 2)
|
||||
& (doctype.is_cancelled == 0)
|
||||
& (item.has_serial_no == 1)
|
||||
)
|
||||
.groupby(doctype.voucher_detail_no)
|
||||
)
|
||||
|
||||
@@ -195,6 +195,67 @@ class TestStockAgeing(FrappeTestCase):
|
||||
self.assertEqual(queue[0][0], 20.0)
|
||||
self.assertEqual(queue[1][0], 20.0)
|
||||
|
||||
def test_non_serial_stock_reco_decrease_preserves_ageing(self):
|
||||
"""
|
||||
Non-serial stock reconciliation should adjust FIFO by the balance delta.
|
||||
Decreasing stock consumes old slots; increasing stock adds only the new qty.
|
||||
"""
|
||||
|
||||
def make_sle(
|
||||
posting_date,
|
||||
voucher_type,
|
||||
voucher_no,
|
||||
actual_qty,
|
||||
qty_after,
|
||||
voucher_detail_no=None,
|
||||
stock_value_difference=None,
|
||||
):
|
||||
stock_value_difference = actual_qty if stock_value_difference is None else stock_value_difference
|
||||
|
||||
return frappe._dict(
|
||||
name="Flask Item",
|
||||
item_name="Flask Item",
|
||||
description="Flask Item",
|
||||
item_group=None,
|
||||
brand=None,
|
||||
stock_uom="Nos",
|
||||
actual_qty=actual_qty,
|
||||
qty_after_transaction=qty_after,
|
||||
stock_value_difference=stock_value_difference,
|
||||
valuation_rate=1,
|
||||
warehouse="WH 1",
|
||||
posting_date=posting_date,
|
||||
voucher_type=voucher_type,
|
||||
voucher_no=voucher_no,
|
||||
voucher_detail_no=voucher_detail_no,
|
||||
has_serial_no=False,
|
||||
has_batch_no=False,
|
||||
serial_no=None,
|
||||
batch_no=None,
|
||||
serial_and_batch_bundle=None,
|
||||
)
|
||||
|
||||
filters = frappe._dict(company="_Test Company", to_date="2026-02-15", ranges=["30", "60", "90"])
|
||||
sle = [
|
||||
make_sle("2025-11-30", "Stock Entry", "001", 100, 100),
|
||||
make_sle("2025-12-31", "Stock Reconciliation", "002", 0, 60, "SRI-DECREASE", -40),
|
||||
make_sle("2026-01-31", "Stock Reconciliation", "003", 0, 90, "SRI-INCREASE", 30),
|
||||
]
|
||||
|
||||
fifo_slots = FIFOSlots(filters, sle)
|
||||
|
||||
def prepare_stock_reco_voucher_wise_count():
|
||||
fifo_slots.stock_reco_voucher_wise_count = frappe._dict({"SRI-DECREASE": 100, "SRI-INCREASE": 60})
|
||||
|
||||
fifo_slots.prepare_stock_reco_voucher_wise_count = prepare_stock_reco_voucher_wise_count
|
||||
|
||||
slots = fifo_slots.generate()
|
||||
queue = slots["Flask Item"]["fifo_queue"]
|
||||
report_data = format_report_data(filters, slots, filters.to_date)
|
||||
|
||||
self.assertEqual(queue, [[60.0, "2025-11-30", 60.0], [30.0, "2026-01-31", 30.0]])
|
||||
self.assertEqual(report_data[0][7:15], [30.0, 30.0, 0.0, 0.0, 60.0, 60.0, 0.0, 0.0])
|
||||
|
||||
def test_sequential_stock_reco_same_warehouse(self):
|
||||
"""
|
||||
Test back to back stock recos (same warehouse).
|
||||
|
||||
@@ -5,7 +5,7 @@ from frappe import _, bold
|
||||
from frappe.model.naming import NamingSeries, make_autoname, parse_naming_series
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder.functions import CombineDatetime, Max, Sum, Timestamp
|
||||
from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, now, nowtime, today
|
||||
from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, getdate, now, nowtime, today
|
||||
from pypika import Order
|
||||
from pypika.terms import ExistsCriterion
|
||||
|
||||
@@ -140,8 +140,7 @@ class SerialBatchBundle:
|
||||
{
|
||||
"item_code": self.item_code,
|
||||
"warehouse": self.warehouse,
|
||||
"posting_date": self.sle.posting_date,
|
||||
"posting_time": self.sle.posting_time,
|
||||
"posting_datetime": self.sle.posting_datetime,
|
||||
"voucher_type": self.sle.voucher_type,
|
||||
"voucher_no": self.sle.voucher_no,
|
||||
"voucher_detail_no": self.sle.voucher_detail_no,
|
||||
@@ -484,7 +483,7 @@ class SerialBatchBundle:
|
||||
if status == "Delivered":
|
||||
warranty_period = frappe.get_cached_value("Item", sle.item_code, "warranty_period")
|
||||
if warranty_period:
|
||||
warranty_expiry_date = add_days(sle.posting_date, cint(warranty_period))
|
||||
warranty_expiry_date = add_days(getdate(sle.posting_datetime), cint(warranty_period))
|
||||
query = query.set(sn_table.warranty_expiry_date, warranty_expiry_date)
|
||||
query = query.set(sn_table.warranty_period, warranty_period)
|
||||
else:
|
||||
@@ -509,7 +508,7 @@ class SerialBatchBundle:
|
||||
sle_doctype.voucher_no,
|
||||
sle_doctype.is_cancelled,
|
||||
sle_doctype.item_code,
|
||||
sle_doctype.posting_date,
|
||||
sle_doctype.posting_datetime,
|
||||
sle_doctype.company,
|
||||
)
|
||||
.where(
|
||||
@@ -663,7 +662,7 @@ class SerialNoValuation(DeprecatedSerialNoValuation):
|
||||
.on(bundle.name == bundle_child.parent)
|
||||
.select(
|
||||
bundle_child.serial_no,
|
||||
Max(CombineDatetime(bundle.posting_date, bundle.posting_time)).as_("max_posting_dt"),
|
||||
Max(bundle.posting_datetime).as_("max_posting_dt"),
|
||||
)
|
||||
.where(
|
||||
(bundle.is_cancelled == 0)
|
||||
@@ -681,13 +680,8 @@ class SerialNoValuation(DeprecatedSerialNoValuation):
|
||||
if self.sle.voucher_no:
|
||||
latest_posting = latest_posting.where(bundle.voucher_no != self.sle.voucher_no)
|
||||
|
||||
if self.sle.posting_date:
|
||||
if self.sle.posting_time is None:
|
||||
self.sle.posting_time = nowtime()
|
||||
|
||||
timestamp_condition = CombineDatetime(
|
||||
bundle.posting_date, bundle.posting_time
|
||||
) <= CombineDatetime(self.sle.posting_date, self.sle.posting_time)
|
||||
if self.sle.posting_datetime:
|
||||
timestamp_condition = bundle.posting_datetime <= self.sle.posting_datetime
|
||||
|
||||
latest_posting = latest_posting.where(timestamp_condition)
|
||||
|
||||
@@ -704,10 +698,7 @@ class SerialNoValuation(DeprecatedSerialNoValuation):
|
||||
.join(latest_posting)
|
||||
.on(
|
||||
(latest_posting.serial_no == bundle_child.serial_no)
|
||||
& (
|
||||
latest_posting.max_posting_dt
|
||||
== CombineDatetime(bundle.posting_date, bundle.posting_time)
|
||||
)
|
||||
& (latest_posting.max_posting_dt == bundle.posting_datetime)
|
||||
)
|
||||
.select(
|
||||
bundle_child.serial_no,
|
||||
@@ -839,19 +830,13 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
|
||||
child = frappe.qb.DocType("Serial and Batch Entry")
|
||||
|
||||
timestamp_condition = ""
|
||||
if self.sle.posting_date:
|
||||
if self.sle.posting_time is None:
|
||||
self.sle.posting_time = nowtime()
|
||||
|
||||
timestamp_condition = CombineDatetime(parent.posting_date, parent.posting_time) < CombineDatetime(
|
||||
self.sle.posting_date, self.sle.posting_time
|
||||
)
|
||||
if self.sle.posting_datetime:
|
||||
timestamp_condition = parent.posting_datetime < self.sle.posting_datetime
|
||||
|
||||
if self.sle.creation:
|
||||
timestamp_condition |= (
|
||||
CombineDatetime(parent.posting_date, parent.posting_time)
|
||||
== CombineDatetime(self.sle.posting_date, self.sle.posting_time)
|
||||
) & (parent.creation < self.sle.creation)
|
||||
timestamp_condition |= (parent.posting_datetime == self.sle.posting_datetime) & (
|
||||
parent.creation < self.sle.creation
|
||||
)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(parent)
|
||||
@@ -1074,9 +1059,9 @@ class SerialBatchCreation:
|
||||
self.__dict__.update(item_details)
|
||||
|
||||
def set_other_details(self):
|
||||
if not self.get("posting_date"):
|
||||
self.posting_date = today()
|
||||
self.__dict__["posting_date"] = self.posting_date
|
||||
if not self.get("posting_datetime"):
|
||||
self.posting_datetime = now()
|
||||
self.__dict__["posting_datetime"] = self.posting_datetime
|
||||
|
||||
if not self.get("actual_qty"):
|
||||
qty = self.get("qty") or self.get("total_qty")
|
||||
@@ -1101,8 +1086,7 @@ class SerialBatchCreation:
|
||||
new_package.docstatus = 0
|
||||
new_package.warehouse = self.warehouse
|
||||
new_package.voucher_no = ""
|
||||
new_package.posting_date = self.posting_date if hasattr(self, "posting_date") else today()
|
||||
new_package.posting_time = self.posting_time if hasattr(self, "posting_time") else nowtime()
|
||||
new_package.posting_datetime = self.posting_datetime if hasattr(self, "posting_datetime") else now()
|
||||
new_package.type_of_transaction = self.type_of_transaction
|
||||
new_package.returned_against = self.get("returned_against")
|
||||
|
||||
@@ -1242,9 +1226,8 @@ class SerialBatchCreation:
|
||||
elif self.has_serial_no and not self.get("serial_nos"):
|
||||
self.serial_nos = get_serial_nos_for_outward(kwargs)
|
||||
elif not self.has_serial_no and self.has_batch_no and not self.get("batches"):
|
||||
if self.get("posting_date"):
|
||||
kwargs["posting_date"] = self.get("posting_date")
|
||||
kwargs["posting_time"] = self.get("posting_time")
|
||||
if self.get("posting_datetime"):
|
||||
kwargs["posting_datetime"] = self.get("posting_datetime")
|
||||
|
||||
self.batches = get_available_batches(kwargs)
|
||||
|
||||
|
||||
@@ -283,7 +283,7 @@ def set_stock_balance_as_per_serial_no(
|
||||
if not posting_time:
|
||||
posting_time = nowtime()
|
||||
|
||||
condition = " and item.name='%s'" % item_code.replace("'", "'") if item_code else ""
|
||||
condition = " and item.name=%s" % frappe.db.escape(item_code, percent=False) if item_code else ""
|
||||
|
||||
bin = frappe.db.sql(
|
||||
"""select bin.item_code, bin.warehouse, bin.actual_qty, item.stock_uom
|
||||
|
||||
@@ -1975,9 +1975,6 @@ def get_valuation_rate(
|
||||
|
||||
# Get moving average rate of a specific batch number
|
||||
if warehouse and serial_and_batch_bundle:
|
||||
sabb = frappe.db.get_value(
|
||||
"Serial and Batch Bundle", serial_and_batch_bundle, ["posting_date", "posting_time"], as_dict=True
|
||||
)
|
||||
batch_obj = BatchNoValuation(
|
||||
sle=frappe._dict(
|
||||
{
|
||||
@@ -1985,8 +1982,9 @@ def get_valuation_rate(
|
||||
"warehouse": warehouse,
|
||||
"actual_qty": -1,
|
||||
"serial_and_batch_bundle": serial_and_batch_bundle,
|
||||
"posting_date": sabb.posting_date,
|
||||
"posting_time": sabb.posting_time,
|
||||
"posting_datetime": frappe.get_value(
|
||||
"Serial and Batch Bundle", serial_and_batch_bundle, "posting_datetime"
|
||||
),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import datetime
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import CombineDatetime, IfNull, Sum
|
||||
from frappe.utils import cstr, flt, get_link_to_form, get_time, getdate, nowdate, nowtime
|
||||
from frappe.utils.data import DateTimeLikeObject
|
||||
|
||||
import erpnext
|
||||
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
|
||||
@@ -94,13 +96,13 @@ def get_stock_value_on(
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_stock_balance(
|
||||
item_code,
|
||||
warehouse,
|
||||
posting_date=None,
|
||||
posting_time=None,
|
||||
with_valuation_rate=False,
|
||||
with_serial_no=False,
|
||||
inventory_dimensions_dict=None,
|
||||
item_code: str,
|
||||
warehouse: str | None,
|
||||
posting_date: DateTimeLikeObject | None = None,
|
||||
posting_time: DateTimeLikeObject | datetime.timedelta | None = None,
|
||||
with_valuation_rate: bool = False,
|
||||
with_serial_no: bool = False,
|
||||
inventory_dimensions_dict: dict | None = None,
|
||||
):
|
||||
"""Returns stock balance quantity at given warehouse on given posting date or current date.
|
||||
|
||||
@@ -146,8 +148,7 @@ def get_stock_balance(
|
||||
{
|
||||
"item_code": item_code,
|
||||
"warehouse": warehouse,
|
||||
"posting_date": posting_date,
|
||||
"posting_time": posting_time,
|
||||
"posting_datetime": get_combine_datetime(posting_date, posting_time),
|
||||
"ignore_warehouse": 1,
|
||||
}
|
||||
)
|
||||
@@ -247,13 +248,16 @@ def _create_bin(item_code, warehouse):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_incoming_rate(args, raise_error_if_no_rate=True, fallbacks: bool = True):
|
||||
def get_incoming_rate(args: dict | str, raise_error_if_no_rate: bool = True, fallbacks: bool = True):
|
||||
"""Get Incoming Rate based on valuation method"""
|
||||
from erpnext.stock.stock_ledger import get_previous_sle, get_valuation_rate
|
||||
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
if not args.get("posting_datetime") and args.get("posting_date"):
|
||||
args["posting_datetime"] = get_combine_datetime(args.get("posting_date"), args.get("posting_time"))
|
||||
|
||||
in_rate = None
|
||||
|
||||
item_details = frappe.get_cached_value(
|
||||
|
||||
Reference in New Issue
Block a user