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

chore: release v15
This commit is contained in:
ruthra kumar
2025-09-23 19:29:11 +05:30
committed by GitHub
43 changed files with 350 additions and 57 deletions

View File

@@ -16,6 +16,7 @@
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"section_break_8",
"rate",
"section_break_9",
@@ -92,6 +93,13 @@
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"fieldname": "section_break_8",
"fieldtype": "Section Break"

View File

@@ -137,8 +137,10 @@ def get_payment_entries_for_bank_clearance(
entries = []
condition = ""
pe_condition = ""
if not include_reconciled_entries:
condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')"
pe_condition = "and (pe.clearance_date IS NULL or pe.clearance_date='0000-00-00')"
journal_entries = frappe.db.sql(
f"""
@@ -163,19 +165,20 @@ def get_payment_entries_for_bank_clearance(
payment_entries = frappe.db.sql(
f"""
select
"Payment Entry" as payment_document, name as payment_entry,
reference_no as cheque_number, reference_date as cheque_date,
if(paid_from=%(account)s, paid_amount + total_taxes_and_charges, 0) as credit,
if(paid_from=%(account)s, 0, received_amount + total_taxes_and_charges) as debit,
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
from `tabPayment Entry`
"Payment Entry" as payment_document, pe.name as payment_entry,
pe.reference_no as cheque_number, pe.reference_date as cheque_date,
if(pe.paid_from=%(account)s, pe.paid_amount + if(pe.payment_type = 'Pay' and c.default_currency = pe.paid_from_account_currency, pe.base_total_taxes_and_charges, pe.total_taxes_and_charges) , 0) as credit,
if(pe.paid_from=%(account)s, 0, pe.received_amount + pe.total_taxes_and_charges) as debit,
pe.posting_date, ifnull(pe.party,if(pe.paid_from=%(account)s,pe.paid_to,pe.paid_from)) as against_account, pe.clearance_date,
if(pe.paid_to=%(account)s, pe.paid_to_account_currency, pe.paid_from_account_currency) as account_currency
from `tabPayment Entry` as pe
join `tabCompany` c on c.name = pe.company
where
(paid_from=%(account)s or paid_to=%(account)s) and docstatus=1
and posting_date >= %(from)s and posting_date <= %(to)s
{condition}
(pe.paid_from=%(account)s or pe.paid_to=%(account)s) and pe.docstatus=1
and pe.posting_date >= %(from)s and pe.posting_date <= %(to)s
{pe_condition}
order by
posting_date ASC, name DESC
pe.posting_date ASC, pe.name DESC
""",
{
"account": account,

View File

@@ -131,18 +131,20 @@ class GLEntry(Document):
if not self.is_cancelled and not (self.party_type and self.party):
account_type = frappe.get_cached_value("Account", self.account, "account_type")
if account_type == "Receivable":
frappe.throw(
_("{0} {1}: Customer is required against Receivable account {2}").format(
self.voucher_type, self.voucher_no, self.account
# skipping validation for payroll entry creation in case party is not required
if not frappe.flags.party_not_required_for_receivable_payable:
if account_type == "Receivable":
frappe.throw(
_("{0} {1}: Customer is required against Receivable account {2}").format(
self.voucher_type, self.voucher_no, self.account
)
)
)
elif account_type == "Payable":
frappe.throw(
_("{0} {1}: Supplier is required against Payable account {2}").format(
self.voucher_type, self.voucher_no, self.account
elif account_type == "Payable":
frappe.throw(
_("{0} {1}: Supplier is required against Payable account {2}").format(
self.voucher_type, self.voucher_no, self.account
)
)
)
# Zero value transaction is not allowed
if not (

View File

@@ -542,8 +542,11 @@ class JournalEntry(AccountsController):
def validate_party(self):
for d in self.get("accounts"):
account_type = frappe.get_cached_value("Account", d.account, "account_type")
# skipping validation for payroll entry creation
skip_validation = frappe.flags.party_not_required_for_receivable_payable
if account_type in ["Receivable", "Payable"]:
if not (d.party_type and d.party):
if not (d.party_type and d.party) and not skip_validation:
frappe.throw(
_(
"Row {0}: Party Type and Party is required for Receivable / Payable account {1}"

View File

@@ -24,6 +24,7 @@
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"help_section",
"loyalty_program_help"
],
@@ -143,6 +144,12 @@
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
}
],
"modified": "2019-05-26 09:11:46.120251",

View File

@@ -13,6 +13,7 @@
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"section_break_4",
"invoices"
],
@@ -62,6 +63,12 @@
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"collapsible": 1,
"fieldname": "accounting_dimensions_section",

View File

@@ -28,6 +28,7 @@
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"sec_break1",
"invoice_name",
"invoices",
@@ -193,6 +194,12 @@
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"depends_on": "eval:doc.party",
"description": "Only 'Payment Entries' made against this advance account are supported.",

View File

@@ -5,6 +5,7 @@
import frappe
from frappe import _, msgprint, qb
from frappe.model.document import Document
from frappe.model.meta import get_field_precision
from frappe.query_builder import Criterion
from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today
@@ -392,6 +393,12 @@ class PaymentReconciliation(Document):
inv.outstanding_amount = flt(entry.get("outstanding_amount"))
def get_difference_amount(self, payment_entry, invoice, allocated_amount):
allocated_amount_precision = get_field_precision(
frappe.get_meta("Payment Reconciliation Allocation").get_field("allocated_amount")
)
difference_amount_precision = get_field_precision(
frappe.get_meta("Payment Reconciliation Allocation").get_field("difference_amount")
)
difference_amount = 0
if frappe.get_cached_value(
"Account", self.receivable_payable_account, "account_currency"
@@ -399,8 +406,14 @@ class PaymentReconciliation(Document):
if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get(
"exchange_rate", 1
):
allocated_amount_in_ref_rate = payment_entry.get("exchange_rate", 1) * allocated_amount
allocated_amount_in_inv_rate = invoice.get("exchange_rate", 1) * allocated_amount
allocated_amount_in_ref_rate = flt(
payment_entry.get("exchange_rate", 1) * flt(allocated_amount, allocated_amount_precision),
difference_amount_precision,
)
allocated_amount_in_inv_rate = flt(
invoice.get("exchange_rate", 1) * flt(allocated_amount, allocated_amount_precision),
difference_amount_precision,
)
difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate
return difference_amount

View File

@@ -22,6 +22,7 @@
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"section_break_9",
"account_currency",
"tax_amount",
@@ -211,6 +212,13 @@
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"default": "0",
"depends_on": "eval:['Purchase Taxes and Charges Template', 'Payment Entry'].includes(parent.doctype)",

View File

@@ -16,6 +16,7 @@
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"section_break_8",
"rate",
"section_break_9",
@@ -188,6 +189,13 @@
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"default": "0",
"depends_on": "eval:['Sales Taxes and Charges Template', 'Payment Entry'].includes(parent.doctype)",

View File

@@ -16,6 +16,7 @@
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"shipping_amount_section",
"calculate_based_on",
"column_break_8",
@@ -136,6 +137,12 @@
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
}
],
"icon": "fa fa-truck",

View File

@@ -164,6 +164,12 @@
{% } %}
</tr>
</thead>
<div class="show-filters">
{% if subtitle %}
{{ subtitle }}
<hr>
{% endif %}
</div>
<tbody>
{% for(var i=0, l=data.length; i<l; i++) { %}
<tr>

View File

@@ -974,6 +974,7 @@ class ReceivablePayableReport:
if self.account_type == "Receivable":
self.add_customer_filters()
self.exclude_employee_transaction()
elif self.account_type == "Payable":
self.add_supplier_filters()
@@ -1053,6 +1054,9 @@ class ReceivablePayableReport:
)
)
def exclude_employee_transaction(self):
self.qb_selection_filter.append(self.ple.party_type != "Employee")
def add_supplier_filters(self):
supplier = qb.DocType("Supplier")
if self.filters.get("supplier_group"):

View File

@@ -38,6 +38,7 @@ from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement
get_report_summary as get_pl_summary,
)
from erpnext.accounts.report.utils import convert, convert_to_presentation_currency
from erpnext.accounts.utils import get_zero_cutoff
def execute(filters=None):
@@ -563,7 +564,7 @@ def prepare_data(accounts, start_date, end_date, balance_must_be, companies, com
row[company] = flt(d.get(company, 0.0), 3)
if abs(row[company]) >= 0.005:
if abs(row[company]) >= get_zero_cutoff(filters.presentation_currency):
# ignore zero values
has_value = True
total += flt(row[company])

View File

@@ -12,6 +12,7 @@ from erpnext.accounts.report.financial_statements import (
filter_out_zero_value_rows,
)
from erpnext.accounts.report.trial_balance.trial_balance import validate_filters
from erpnext.accounts.utils import get_zero_cutoff
def execute(filters=None):
@@ -154,7 +155,7 @@ def prepare_data(accounts, filters, company_currency, dimension_list):
for dimension in dimension_list:
row[frappe.scrub(dimension)] = flt(d.get(frappe.scrub(dimension), 0.0), 3)
if abs(row[frappe.scrub(dimension)]) >= 0.005:
if abs(row[frappe.scrub(dimension)]) >= get_zero_cutoff(company_currency):
# ignore zero values
has_value = True
total += flt(d.get(frappe.scrub(dimension), 0.0), 3)

View File

@@ -34,6 +34,12 @@
</h5>
{% } %}
<hr>
<div class="show-filters">
{% if subtitle %}
{{ subtitle }}
<hr>
{% endif %}
</div>
<table class="table table-bordered">
<thead>
<tr>

View File

@@ -18,7 +18,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_dimension_with_children,
)
from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency
from erpnext.accounts.utils import get_fiscal_year
from erpnext.accounts.utils import get_fiscal_year, get_zero_cutoff
def get_period_list(
@@ -304,7 +304,7 @@ def prepare_data(accounts, balance_must_be, period_list, company_currency, accum
row[period.key] = flt(d.get(period.key, 0.0), 3)
if abs(row[period.key]) >= 0.005:
if abs(row[period.key]) >= get_zero_cutoff(company_currency):
# ignore zero values
has_value = True
total += flt(row[period.key])

View File

@@ -21,6 +21,12 @@
{%= frappe.datetime.str_to_user(filters.to_date) %}
</h5>
<hr>
<div class="show-filters">
{% if subtitle %}
{{ subtitle }}
<hr>
{% endif %}
</div>
<table class="table table-bordered">
<thead>
<tr>

View File

@@ -645,7 +645,7 @@ def get_columns(filters):
"options": "GL Entry",
"hidden": 1,
},
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120},
{
"label": _("Account"),
"fieldname": "account",

View File

@@ -12,6 +12,7 @@ from erpnext.accounts.report.financial_statements import (
filter_out_zero_value_rows,
)
from erpnext.accounts.report.trial_balance.trial_balance import validate_filters
from erpnext.accounts.utils import get_zero_cutoff
value_fields = ("income", "expense", "gross_profit_loss")
@@ -149,7 +150,7 @@ def prepare_data(accounts, filters, total_row, parent_children_map, based_on):
for key in value_fields:
row[key] = flt(d.get(key, 0.0), 3)
if abs(row[key]) >= 0.005:
if abs(row[key]) >= get_zero_cutoff(company_currency):
# ignore zero values
has_value = True

View File

@@ -18,6 +18,7 @@ from erpnext.accounts.report.financial_statements import (
set_gl_entries_by_account,
)
from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency
from erpnext.accounts.utils import get_zero_cutoff
value_fields = (
"opening_debit",
@@ -413,7 +414,7 @@ def prepare_data(accounts, filters, parent_children_map, company_currency):
for key in value_fields:
row[key] = flt(d.get(key, 0.0), 3)
if abs(row[key]) >= 0.005:
if abs(row[key]) >= get_zero_cutoff(company_currency):
# ignore zero values
has_value = True

View File

@@ -9,6 +9,7 @@ from erpnext.accounts.party import get_party_shipping_address
from erpnext.accounts.utils import (
get_future_stock_vouchers,
get_voucherwise_gl_entries,
get_zero_cutoff,
sort_stock_vouchers_by_posting_date,
)
from erpnext.stock.doctype.item.test_item import make_item
@@ -156,6 +157,11 @@ class TestUtils(unittest.TestCase):
self.assertSequenceEqual(doc_name[0:2], ("SUP", fiscal_year))
frappe.db.set_default("supp_master_name", "Supplier Name")
def test_get_zero_cutoff(self):
self.assertEqual(get_zero_cutoff(None), 0.005)
self.assertEqual(get_zero_cutoff("EUR"), 0.005)
self.assertEqual(get_zero_cutoff("BHD"), 0.0005)
ADDRESS_RECORDS = [
{

View File

@@ -27,6 +27,7 @@ from frappe.utils import (
now,
nowdate,
)
from frappe.utils.caching import site_cache
from pypika import Order
from pypika.functions import Coalesce
from pypika.terms import ExistsCriterion
@@ -1130,6 +1131,29 @@ def get_currency_precision():
return precision
def get_fraction_units(currency: str) -> int:
"""Returns the number of fraction units for a currency."""
fraction_units = frappe.db.get_value("Currency", currency, "fraction_units")
if fraction_units is None:
fraction_units = 100
return fraction_units
@site_cache()
def get_zero_cutoff(currency: str) -> float:
"""Returns the zero cutoff for a currency.
For example, if the Fraction Units for a currency are set to 100, then the zero cutoff is 0.005.
We don't want to display values less than the zero cutoff.
This value was chosen for compatibility with the previous hard-coded value of 0.005.
"""
fraction_units = get_fraction_units(currency)
return 0.5 / (fraction_units or 1)
def get_held_invoices(party_type, party):
"""
Returns a list of names Purchase Invoices for the given party that are on hold
@@ -2451,6 +2475,10 @@ def build_qb_match_conditions(doctype, user=None) -> list:
for filter in match_filters:
for link_option, allowed_values in filter.items():
fieldnames = link_fields_map.get(link_option, [])
cond = None
if link_option == doctype:
cond = _dt["name"].isin(allowed_values)
for fieldname in fieldnames:
field = _dt[fieldname]
@@ -2459,6 +2487,7 @@ def build_qb_match_conditions(doctype, user=None) -> list:
if not apply_strict_user_permissions:
cond = (Coalesce(field, "") == "") | cond
if cond:
criterion.append(cond)
return criterion

View File

@@ -44,6 +44,7 @@
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"target_fixed_asset_account"
],
"fields": [
@@ -288,6 +289,12 @@
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"

View File

@@ -18,6 +18,7 @@
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"fixed_asset_account"
],
"fields": [
@@ -98,6 +99,13 @@
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"fieldname": "finance_book",
"fieldtype": "Link",

View File

@@ -118,7 +118,8 @@
"fieldname": "rate",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Rate"
"label": "Rate",
"options": "currency"
},
{
"columns": 1,
@@ -161,7 +162,8 @@
"fieldname": "amount",
"fieldtype": "Currency",
"label": "Amount",
"read_only": 1
"read_only": 1,
"options": "currency"
},
{
"fieldname": "column_break_yuca",
@@ -183,13 +185,15 @@
"fieldname": "base_amount",
"fieldtype": "Currency",
"hidden": 1,
"label": "Base Amount"
"label": "Base Amount",
"options": "Company:company:default_currency"
},
{
"fieldname": "base_rate",
"fieldtype": "Currency",
"hidden": 1,
"label": "Base Rate"
"label": "Base Rate",
"options": "Company:company:default_currency"
},
{
"default": "0",

View File

@@ -6,7 +6,10 @@ def execute():
"POS Invoice Merge Log", {"docstatus": 1}, ["name", "pos_closing_entry"]
)
frappe.db.auto_commit_on_many_writes = 1
for log in pos_invoice_merge_logs:
if log.pos_closing_entry and frappe.db.exists("POS Closing Entry", log.pos_closing_entry):
company = frappe.db.get_value("POS Closing Entry", log.pos_closing_entry, "company")
frappe.db.set_value("POS Invoice Merge Log", log.name, "company", company)
frappe.db.auto_commit_on_many_writes = 0

View File

@@ -931,7 +931,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
party_name = me.frm.doc.party_name
}
else{
party_type = frappe.meta.has_field(me.frm.doc.doctype, "customer") ? "Customer" : "Supplier";
party_type = frappe.meta.has_field(me.frm.doc.doctype, "supplier") ? "Supplier" : "Customer";
party_name = me.frm.doc[party_type.toLowerCase()];
}
if (party_name) {

View File

@@ -1762,6 +1762,11 @@ def create_pick_list(source_name, target_doc=None):
target.qty = qty_to_be_picked
target.stock_qty = qty_to_be_picked * flt(source.conversion_factor)
# update available qty
bin_details = get_bin_details(source.item_code, source.warehouse, source_parent.company)
target.actual_qty = bin_details.get("actual_qty")
target.company_total_stock = bin_details.get("company_total_stock")
def update_packed_item_qty(source, target, source_parent) -> None:
qty = flt(source.qty)
for item in source_parent.items:

View File

@@ -5,8 +5,10 @@ import unittest
import frappe
import frappe.utils
from frappe.query_builder import Criterion
import erpnext
from erpnext.accounts.utils import build_qb_match_conditions
from erpnext.setup.doctype.employee.employee import InactiveEmployeeStatusError
test_records = frappe.get_test_records("Employee")
@@ -34,6 +36,32 @@ class TestEmployee(unittest.TestCase):
employee_doc.save()
self.assertTrue("Employee" not in frappe.get_roles(user))
def test_employee_user_permission(self):
employee1 = make_employee("employee_1_test@company.com", create_user_permission=1)
employee2 = make_employee("employee_2_test@company.com", create_user_permission=1)
make_employee("employee_3_test@company.com", create_user_permission=1)
employee1_doc = frappe.get_doc("Employee", employee1)
employee2_doc = frappe.get_doc("Employee", employee2)
employee2_doc.reload()
employee2_doc.reports_to = employee1_doc.name
employee2_doc.save()
frappe.set_user(employee1_doc.user_id)
Employee = frappe.qb.DocType("Employee")
qb_employee_list = (
frappe.qb.from_(Employee)
.select(Employee.name)
.where(Criterion.all(build_qb_match_conditions("Employee")))
.orderby(Employee.Name)
).run(pluck=Employee.name)
employee_list = frappe.db.get_list("Employee", pluck="name", order_by="name")
self.assertEqual(qb_employee_list, employee_list)
frappe.set_user("Administrator")
def tearDown(self):
frappe.db.rollback()

View File

@@ -218,6 +218,7 @@ def get_batch_qty(
batch_no=None,
warehouse=None,
item_code=None,
creation=None,
posting_date=None,
posting_time=None,
ignore_voucher_nos=None,
@@ -244,6 +245,7 @@ def get_batch_qty(
{
"item_code": item_code,
"warehouse": warehouse,
"creation": creation,
"posting_date": posting_date,
"posting_time": posting_time,
"batch_no": batch_no,

View File

@@ -724,7 +724,10 @@ class Item(Document):
item_defaults = frappe.db.get_values(
"Item Default",
{"parent": self.item_group},
{
"parent": self.item_group,
"parenttype": "Item Group",
},
[
"company",
"default_warehouse",

View File

@@ -354,10 +354,12 @@ frappe.ui.form.on("Pick List Item", {
item_code: (frm, cdt, cdn) => {
let row = frappe.get_doc(cdt, cdn);
if (row.item_code) {
get_item_details(row.item_code).then((data) => {
get_item_details(row.item_code, row.uom, row.warehouse, frm.doc.company).then((data) => {
frappe.model.set_value(cdt, cdn, "uom", data.stock_uom);
frappe.model.set_value(cdt, cdn, "stock_uom", data.stock_uom);
frappe.model.set_value(cdt, cdn, "conversion_factor", 1);
frappe.model.set_value(cdt, cdn, "actual_qty", data.actual_qty);
frappe.model.set_value(cdt, cdn, "company_total_stock", data.company_total_stock);
});
}
},
@@ -371,6 +373,15 @@ frappe.ui.form.on("Pick List Item", {
}
},
warehouse: (frm, cdt, cdn) => {
const row = frappe.get_doc(cdt, cdn);
if (!row.item_code || !row.warehouse) return;
get_item_details(row.item_code, row.uom, row.warehouse, frm.doc.company).then((data) => {
frappe.model.set_value(cdt, cdn, "actual_qty", data.actual_qty);
frappe.model.set_value(cdt, cdn, "company_total_stock", data.company_total_stock);
});
},
qty: (frm, cdt, cdn) => {
let row = frappe.get_doc(cdt, cdn);
frappe.model.set_value(cdt, cdn, "stock_qty", row.qty * row.conversion_factor);
@@ -412,11 +423,13 @@ frappe.ui.form.on("Pick List Item", {
},
});
function get_item_details(item_code, uom = null) {
function get_item_details(item_code, uom = null, warehouse = null, company = null) {
if (item_code) {
return frappe.xcall("erpnext.stock.doctype.pick_list.pick_list.get_item_details", {
item_code,
uom,
warehouse,
company,
});
}
}

View File

@@ -21,7 +21,7 @@ from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle impor
get_auto_batch_nos,
get_picked_serial_nos,
)
from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.stock.get_item_details import get_company_total_stock, get_conversion_factor
from erpnext.stock.serial_batch_bundle import (
SerialBatchCreation,
get_batches_from_bundle,
@@ -74,6 +74,9 @@ class PickList(TransactionBase):
if self.has_reserved_stock():
self.set_onload("has_reserved_stock", True)
for item in self.get("locations"):
item.update(get_item_details(item.item_code, item.uom, item.warehouse, self.company))
def validate(self):
self.validate_expired_batches()
self.validate_for_qty()
@@ -1442,15 +1445,29 @@ def get_pending_work_orders(doctype, txt, searchfield, start, page_length, filte
@frappe.whitelist()
def get_item_details(item_code, uom=None):
def get_item_details(item_code, uom=None, warehouse=None, company=None):
details = frappe.db.get_value("Item", item_code, ["stock_uom", "name"], as_dict=1)
details.uom = uom or details.stock_uom
if uom:
details.update(get_conversion_factor(item_code, uom))
if warehouse:
details.actual_qty = flt(get_actual_qty(item_code, warehouse))
if company:
details.company_total_stock = get_company_total_stock(item_code, company)
return details
def get_actual_qty(item_code, warehouse):
return frappe.db.get_value(
"Bin",
{"item_code": item_code, "warehouse": warehouse},
"actual_qty",
)
def update_delivery_note_item(source, target, delivery_note):
cost_center = frappe.db.get_value("Project", delivery_note.project, "cost_center")
if not cost_center:

View File

@@ -22,6 +22,10 @@
"conversion_factor",
"stock_uom",
"delivered_qty",
"available_quantity_section",
"actual_qty",
"column_break_kyek",
"company_total_stock",
"serial_no_and_batch_section",
"pick_serial_and_batch",
"serial_and_batch_bundle",
@@ -124,7 +128,7 @@
"fieldname": "stock_qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Stock Qty",
"label": "Qty (in Stock UOM)",
"read_only": 1
},
{
@@ -248,11 +252,38 @@
"print_hide": 1,
"read_only": 1,
"report_hide": 1
},
{
"fieldname": "available_quantity_section",
"fieldtype": "Section Break",
"label": "Available Qty"
},
{
"allow_on_submit": 1,
"fieldname": "actual_qty",
"fieldtype": "Float",
"label": "Qty (Warehouse)",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "column_break_kyek",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "company_total_stock",
"fieldtype": "Float",
"label": "Qty (Company)",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
}
],
"istable": 1,
"links": [],
"modified": "2025-05-31 19:57:43.531298",
"modified": "2025-09-23 00:02:57.817040",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List Item",

View File

@@ -15,7 +15,9 @@ class PickListItem(Document):
if TYPE_CHECKING:
from frappe.types import DF
actual_qty: DF.Float
batch_no: DF.Link | None
company_total_stock: DF.Float
conversion_factor: DF.Float
delivered_qty: DF.Float
description: DF.Text | None

View File

@@ -2360,6 +2360,16 @@ def get_available_batches(kwargs):
kwargs.posting_date, kwargs.posting_time
)
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
== get_combine_datetime(kwargs.posting_date, kwargs.posting_time)
) & (stock_ledger_entry.creation < kwargs.creation)
query = query.where(timestamp_condition)
for field in ["warehouse", "item_code"]:
@@ -2601,6 +2611,16 @@ def get_stock_ledgers_for_serial_nos(kwargs):
kwargs.posting_date, kwargs.posting_time
)
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
== get_combine_datetime(kwargs.posting_date, kwargs.posting_time)
) & (stock_ledger_entry.creation < kwargs.creation)
query = query.where(timestamp_condition)
for field in ["warehouse", "item_code", "serial_no"]:
@@ -2659,6 +2679,16 @@ def get_stock_ledgers_batches(kwargs):
kwargs.posting_date, kwargs.posting_time
)
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
== get_combine_datetime(kwargs.posting_date, kwargs.posting_time)
) & (stock_ledger_entry.creation < kwargs.creation)
query = query.where(timestamp_condition)
if kwargs.get("ignore_voucher_nos"):

View File

@@ -5,7 +5,7 @@
import frappe
from frappe import _, bold, json, msgprint
from frappe.query_builder.functions import CombineDatetime, Sum
from frappe.utils import add_to_date, cint, cstr, flt, get_datetime
from frappe.utils import add_to_date, cint, cstr, flt, get_datetime, now
import erpnext
from erpnext.accounts.utils import get_company_default
@@ -1034,7 +1034,7 @@ class StockReconciliation(StockController):
val_rate = 0.0
current_qty = 0.0
if row.current_serial_and_batch_bundle:
current_qty = self.get_current_qty_for_serial_or_batch(row)
current_qty = self.get_current_qty_for_serial_or_batch(row, sle_creation)
elif row.serial_no:
item_dict = get_stock_balance_for(
row.item_code,
@@ -1143,17 +1143,17 @@ class StockReconciliation(StockController):
return allow_negative_stock
def get_current_qty_for_serial_or_batch(self, row):
def get_current_qty_for_serial_or_batch(self, row, sle_creation):
doc = frappe.get_doc("Serial and Batch Bundle", row.current_serial_and_batch_bundle)
current_qty = 0.0
if doc.has_serial_no:
current_qty = self.get_current_qty_for_serial_nos(doc)
current_qty = self.get_current_qty_for_serial_nos(doc, sle_creation)
elif doc.has_batch_no:
current_qty = self.get_current_qty_for_batch_nos(doc)
current_qty = self.get_current_qty_for_batch_nos(doc, sle_creation)
return abs(current_qty)
def get_current_qty_for_serial_nos(self, doc):
def get_current_qty_for_serial_nos(self, doc, sle_creation):
serial_nos_details = get_available_serial_nos(
frappe._dict(
{
@@ -1161,6 +1161,7 @@ class StockReconciliation(StockController):
"warehouse": doc.warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"creation": sle_creation,
"voucher_no": self.name,
"ignore_warehouse": 1,
}
@@ -1190,7 +1191,7 @@ class StockReconciliation(StockController):
return current_qty
def get_current_qty_for_batch_nos(self, doc):
def get_current_qty_for_batch_nos(self, doc, sle_creation):
current_qty = 0.0
precision = doc.entries[0].precision("qty")
for d in doc.entries:
@@ -1198,6 +1199,7 @@ class StockReconciliation(StockController):
get_batch_qty(
d.batch_no,
doc.warehouse,
creation=sle_creation,
posting_date=doc.posting_date,
posting_time=doc.posting_time,
ignore_voucher_nos=[doc.voucher_no],
@@ -1494,6 +1496,7 @@ def get_stock_balance_for(
"company": company,
"posting_date": posting_date,
"posting_time": posting_time,
"creation": row.get("creation") if row and row.get("creation") else now(),
}
)
)

View File

@@ -312,7 +312,7 @@ def is_first_response(issue):
def calculate_first_response_time(issue, first_responded_on):
issue_creation_date = issue.service_level_agreement_creation or issue.creation
issue_creation_date = get_datetime(issue.service_level_agreement_creation or issue.creation)
issue_creation_time = get_time_in_seconds(issue_creation_date)
first_responded_on_in_seconds = get_time_in_seconds(first_responded_on)
support_hours = frappe.get_cached_doc(

View File

@@ -25,7 +25,7 @@ from frappe.utils.caching import redis_cache
from frappe.utils.nestedset import get_ancestors_of
from frappe.utils.safe_exec import get_safe_globals
from erpnext.support.doctype.issue.issue import get_holidays
from erpnext.support.doctype.issue.issue import calculate_first_response_time, get_holidays
class ServiceLevelAgreement(Document):
@@ -552,6 +552,8 @@ def handle_status_change(doc, apply_sla_for_resolution):
def set_first_response():
if doc.meta.has_field("first_responded_on") and not doc.get("first_responded_on"):
doc.first_responded_on = now_time
if doc.meta.has_field("first_response_time"):
doc.first_response_time = calculate_first_response_time(doc, doc.first_responded_on)
if get_datetime(doc.get("first_responded_on")) > get_datetime(doc.get("response_by")):
record_assigned_users_on_failure(doc)

View File

@@ -5,5 +5,6 @@ from erpnext.utilities.activation import get_level
class TestActivation(FrappeTestCase):
def test_activation(self):
levels = get_level()
site_info = {"activation": {"activation_level": 0, "sales_data": []}}
levels = get_level(site_info)
self.assertTrue(levels)

View File

@@ -37,7 +37,7 @@ def get_site_info(site_info):
if company:
domain = frappe.get_cached_value("Company", cstr(company), "domain")
return {"company": company, "domain": domain, "activation": get_level()}
return {"company": company, "domain": domain, "activation": get_level(site_info)}
@contextmanager

View File

@@ -9,9 +9,9 @@ from frappe.core.doctype.installed_applications.installed_applications import ge
import erpnext
def get_level():
activation_level = 0
sales_data = []
def get_level(site_info):
activation_level = site_info.get("activation", {}).get("activation_level", 0)
sales_data = site_info.get("activation", {}).get("sales_data", [])
min_count = 0
doctypes = {
"Asset": 5,