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

View File

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

View File

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

View File

@@ -542,8 +542,11 @@ class JournalEntry(AccountsController):
def validate_party(self): def validate_party(self):
for d in self.get("accounts"): for d in self.get("accounts"):
account_type = frappe.get_cached_value("Account", d.account, "account_type") 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 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( frappe.throw(
_( _(
"Row {0}: Party Type and Party is required for Receivable / Payable account {1}" "Row {0}: Party Type and Party is required for Receivable / Payable account {1}"

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,7 @@
import frappe import frappe
from frappe import _, msgprint, qb from frappe import _, msgprint, qb
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.meta import get_field_precision
from frappe.query_builder import Criterion from frappe.query_builder import Criterion
from frappe.query_builder.custom import ConstantColumn from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today 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")) inv.outstanding_amount = flt(entry.get("outstanding_amount"))
def get_difference_amount(self, payment_entry, invoice, allocated_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 difference_amount = 0
if frappe.get_cached_value( if frappe.get_cached_value(
"Account", self.receivable_payable_account, "account_currency" "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( if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get(
"exchange_rate", 1 "exchange_rate", 1
): ):
allocated_amount_in_ref_rate = payment_entry.get("exchange_rate", 1) * allocated_amount allocated_amount_in_ref_rate = flt(
allocated_amount_in_inv_rate = invoice.get("exchange_rate", 1) * allocated_amount 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 difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate
return difference_amount return difference_amount

View File

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

View File

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

View File

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

View File

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

View File

@@ -974,6 +974,7 @@ class ReceivablePayableReport:
if self.account_type == "Receivable": if self.account_type == "Receivable":
self.add_customer_filters() self.add_customer_filters()
self.exclude_employee_transaction()
elif self.account_type == "Payable": elif self.account_type == "Payable":
self.add_supplier_filters() 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): def add_supplier_filters(self):
supplier = qb.DocType("Supplier") supplier = qb.DocType("Supplier")
if self.filters.get("supplier_group"): 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, get_report_summary as get_pl_summary,
) )
from erpnext.accounts.report.utils import convert, convert_to_presentation_currency from erpnext.accounts.report.utils import convert, convert_to_presentation_currency
from erpnext.accounts.utils import get_zero_cutoff
def execute(filters=None): 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) 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 # ignore zero values
has_value = True has_value = True
total += flt(row[company]) total += flt(row[company])

View File

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

View File

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

View File

@@ -18,7 +18,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_dimension_with_children, get_dimension_with_children,
) )
from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency 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( 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) 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 # ignore zero values
has_value = True has_value = True
total += flt(row[period.key]) total += flt(row[period.key])

View File

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

View File

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

View File

@@ -12,6 +12,7 @@ from erpnext.accounts.report.financial_statements import (
filter_out_zero_value_rows, filter_out_zero_value_rows,
) )
from erpnext.accounts.report.trial_balance.trial_balance import validate_filters 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") 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: for key in value_fields:
row[key] = flt(d.get(key, 0.0), 3) 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 # ignore zero values
has_value = True has_value = True

View File

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

View File

@@ -9,6 +9,7 @@ from erpnext.accounts.party import get_party_shipping_address
from erpnext.accounts.utils import ( from erpnext.accounts.utils import (
get_future_stock_vouchers, get_future_stock_vouchers,
get_voucherwise_gl_entries, get_voucherwise_gl_entries,
get_zero_cutoff,
sort_stock_vouchers_by_posting_date, sort_stock_vouchers_by_posting_date,
) )
from erpnext.stock.doctype.item.test_item import make_item 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)) self.assertSequenceEqual(doc_name[0:2], ("SUP", fiscal_year))
frappe.db.set_default("supp_master_name", "Supplier Name") 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 = [ ADDRESS_RECORDS = [
{ {

View File

@@ -27,6 +27,7 @@ from frappe.utils import (
now, now,
nowdate, nowdate,
) )
from frappe.utils.caching import site_cache
from pypika import Order from pypika import Order
from pypika.functions import Coalesce from pypika.functions import Coalesce
from pypika.terms import ExistsCriterion from pypika.terms import ExistsCriterion
@@ -1130,6 +1131,29 @@ def get_currency_precision():
return 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): def get_held_invoices(party_type, party):
""" """
Returns a list of names Purchase Invoices for the given party that are on hold 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 filter in match_filters:
for link_option, allowed_values in filter.items(): for link_option, allowed_values in filter.items():
fieldnames = link_fields_map.get(link_option, []) fieldnames = link_fields_map.get(link_option, [])
cond = None
if link_option == doctype:
cond = _dt["name"].isin(allowed_values)
for fieldname in fieldnames: for fieldname in fieldnames:
field = _dt[fieldname] field = _dt[fieldname]
@@ -2459,6 +2487,7 @@ def build_qb_match_conditions(doctype, user=None) -> list:
if not apply_strict_user_permissions: if not apply_strict_user_permissions:
cond = (Coalesce(field, "") == "") | cond cond = (Coalesce(field, "") == "") | cond
if cond:
criterion.append(cond) criterion.append(cond)
return criterion return criterion

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,10 @@ def execute():
"POS Invoice Merge Log", {"docstatus": 1}, ["name", "pos_closing_entry"] "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: for log in pos_invoice_merge_logs:
if log.pos_closing_entry and frappe.db.exists("POS Closing Entry", log.pos_closing_entry): 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") 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.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 party_name = me.frm.doc.party_name
} }
else{ 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()]; party_name = me.frm.doc[party_type.toLowerCase()];
} }
if (party_name) { 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.qty = qty_to_be_picked
target.stock_qty = qty_to_be_picked * flt(source.conversion_factor) 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: def update_packed_item_qty(source, target, source_parent) -> None:
qty = flt(source.qty) qty = flt(source.qty)
for item in source_parent.items: for item in source_parent.items:

View File

@@ -5,8 +5,10 @@ import unittest
import frappe import frappe
import frappe.utils import frappe.utils
from frappe.query_builder import Criterion
import erpnext import erpnext
from erpnext.accounts.utils import build_qb_match_conditions
from erpnext.setup.doctype.employee.employee import InactiveEmployeeStatusError from erpnext.setup.doctype.employee.employee import InactiveEmployeeStatusError
test_records = frappe.get_test_records("Employee") test_records = frappe.get_test_records("Employee")
@@ -34,6 +36,32 @@ class TestEmployee(unittest.TestCase):
employee_doc.save() employee_doc.save()
self.assertTrue("Employee" not in frappe.get_roles(user)) 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): def tearDown(self):
frappe.db.rollback() frappe.db.rollback()

View File

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

View File

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

View File

@@ -354,10 +354,12 @@ frappe.ui.form.on("Pick List Item", {
item_code: (frm, cdt, cdn) => { item_code: (frm, cdt, cdn) => {
let row = frappe.get_doc(cdt, cdn); let row = frappe.get_doc(cdt, cdn);
if (row.item_code) { 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, "uom", data.stock_uom);
frappe.model.set_value(cdt, cdn, "stock_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, "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) => { qty: (frm, cdt, cdn) => {
let row = frappe.get_doc(cdt, cdn); let row = frappe.get_doc(cdt, cdn);
frappe.model.set_value(cdt, cdn, "stock_qty", row.qty * row.conversion_factor); 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) { if (item_code) {
return frappe.xcall("erpnext.stock.doctype.pick_list.pick_list.get_item_details", { return frappe.xcall("erpnext.stock.doctype.pick_list.pick_list.get_item_details", {
item_code, item_code,
uom, 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_auto_batch_nos,
get_picked_serial_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 ( from erpnext.stock.serial_batch_bundle import (
SerialBatchCreation, SerialBatchCreation,
get_batches_from_bundle, get_batches_from_bundle,
@@ -74,6 +74,9 @@ class PickList(TransactionBase):
if self.has_reserved_stock(): if self.has_reserved_stock():
self.set_onload("has_reserved_stock", True) 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): def validate(self):
self.validate_expired_batches() self.validate_expired_batches()
self.validate_for_qty() self.validate_for_qty()
@@ -1442,15 +1445,29 @@ def get_pending_work_orders(doctype, txt, searchfield, start, page_length, filte
@frappe.whitelist() @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 = frappe.db.get_value("Item", item_code, ["stock_uom", "name"], as_dict=1)
details.uom = uom or details.stock_uom details.uom = uom or details.stock_uom
if uom: if uom:
details.update(get_conversion_factor(item_code, 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 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): def update_delivery_note_item(source, target, delivery_note):
cost_center = frappe.db.get_value("Project", delivery_note.project, "cost_center") cost_center = frappe.db.get_value("Project", delivery_note.project, "cost_center")
if not cost_center: if not cost_center:

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@
import frappe import frappe
from frappe import _, bold, json, msgprint from frappe import _, bold, json, msgprint
from frappe.query_builder.functions import CombineDatetime, Sum 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 import erpnext
from erpnext.accounts.utils import get_company_default from erpnext.accounts.utils import get_company_default
@@ -1034,7 +1034,7 @@ class StockReconciliation(StockController):
val_rate = 0.0 val_rate = 0.0
current_qty = 0.0 current_qty = 0.0
if row.current_serial_and_batch_bundle: 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: elif row.serial_no:
item_dict = get_stock_balance_for( item_dict = get_stock_balance_for(
row.item_code, row.item_code,
@@ -1143,17 +1143,17 @@ class StockReconciliation(StockController):
return allow_negative_stock 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) doc = frappe.get_doc("Serial and Batch Bundle", row.current_serial_and_batch_bundle)
current_qty = 0.0 current_qty = 0.0
if doc.has_serial_no: 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: 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) 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( serial_nos_details = get_available_serial_nos(
frappe._dict( frappe._dict(
{ {
@@ -1161,6 +1161,7 @@ class StockReconciliation(StockController):
"warehouse": doc.warehouse, "warehouse": doc.warehouse,
"posting_date": self.posting_date, "posting_date": self.posting_date,
"posting_time": self.posting_time, "posting_time": self.posting_time,
"creation": sle_creation,
"voucher_no": self.name, "voucher_no": self.name,
"ignore_warehouse": 1, "ignore_warehouse": 1,
} }
@@ -1190,7 +1191,7 @@ class StockReconciliation(StockController):
return current_qty 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 current_qty = 0.0
precision = doc.entries[0].precision("qty") precision = doc.entries[0].precision("qty")
for d in doc.entries: for d in doc.entries:
@@ -1198,6 +1199,7 @@ class StockReconciliation(StockController):
get_batch_qty( get_batch_qty(
d.batch_no, d.batch_no,
doc.warehouse, doc.warehouse,
creation=sle_creation,
posting_date=doc.posting_date, posting_date=doc.posting_date,
posting_time=doc.posting_time, posting_time=doc.posting_time,
ignore_voucher_nos=[doc.voucher_no], ignore_voucher_nos=[doc.voucher_no],
@@ -1494,6 +1496,7 @@ def get_stock_balance_for(
"company": company, "company": company,
"posting_date": posting_date, "posting_date": posting_date,
"posting_time": posting_time, "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): 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) issue_creation_time = get_time_in_seconds(issue_creation_date)
first_responded_on_in_seconds = get_time_in_seconds(first_responded_on) first_responded_on_in_seconds = get_time_in_seconds(first_responded_on)
support_hours = frappe.get_cached_doc( 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.nestedset import get_ancestors_of
from frappe.utils.safe_exec import get_safe_globals 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): class ServiceLevelAgreement(Document):
@@ -552,6 +552,8 @@ def handle_status_change(doc, apply_sla_for_resolution):
def set_first_response(): def set_first_response():
if doc.meta.has_field("first_responded_on") and not doc.get("first_responded_on"): if doc.meta.has_field("first_responded_on") and not doc.get("first_responded_on"):
doc.first_responded_on = now_time 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")): if get_datetime(doc.get("first_responded_on")) > get_datetime(doc.get("response_by")):
record_assigned_users_on_failure(doc) record_assigned_users_on_failure(doc)

View File

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

View File

@@ -37,7 +37,7 @@ def get_site_info(site_info):
if company: if company:
domain = frappe.get_cached_value("Company", cstr(company), "domain") 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 @contextmanager

View File

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