mirror of
https://github.com/frappe/erpnext.git
synced 2026-02-19 09:35:03 +00:00
Merge pull request #50473 from frappe/resolve-payment-entry-settings-conflict
This commit is contained in:
@@ -309,8 +309,8 @@ def get_dimensions(with_cost_center_and_project=False):
|
||||
if with_cost_center_and_project:
|
||||
dimension_filters.extend(
|
||||
[
|
||||
{"fieldname": "cost_center", "document_type": "Cost Center"},
|
||||
{"fieldname": "project", "document_type": "Project"},
|
||||
frappe._dict({"fieldname": "cost_center", "document_type": "Cost Center"}),
|
||||
frappe._dict({"fieldname": "project", "document_type": "Project"}),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -56,6 +56,9 @@
|
||||
"reconciliation_queue_size",
|
||||
"column_break_resa",
|
||||
"exchange_gain_loss_posting_date",
|
||||
"payment_entry_settings",
|
||||
"show_account_balance",
|
||||
"show_party_balance",
|
||||
"invoicing_settings_tab",
|
||||
"accounts_transactions_settings_section",
|
||||
"over_billing_allowance",
|
||||
@@ -95,7 +98,8 @@
|
||||
"legacy_section",
|
||||
"ignore_is_opening_check_for_reporting",
|
||||
"payment_request_settings",
|
||||
"create_pr_in_draft_status"
|
||||
"create_pr_in_draft_status",
|
||||
"column_break_xrnd"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -636,6 +640,23 @@
|
||||
"fieldname": "use_legacy_controller_for_pcv",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Legacy Controller For Period Closing Voucher"
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_entry_settings",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Payment Entry Settings"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_account_balance",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Account Balance"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_party_balance",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Party Balance"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -643,7 +664,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-20 14:06:08.870427",
|
||||
"modified": "2025-11-06 17:48:07.682837",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
@@ -668,8 +689,9 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -65,8 +65,10 @@ class AccountsSettings(Document):
|
||||
role_allowed_to_over_bill: DF.Link | None
|
||||
role_to_override_stop_action: DF.Link | None
|
||||
round_row_wise_tax: DF.Check
|
||||
show_account_balance: DF.Check
|
||||
show_balance_in_coa: DF.Check
|
||||
show_inclusive_tax_in_print: DF.Check
|
||||
show_party_balance: DF.Check
|
||||
show_payment_schedule_in_print: DF.Check
|
||||
show_taxes_as_table_in_print: DF.Check
|
||||
stale_days: DF.Int
|
||||
@@ -105,6 +107,7 @@ class AccountsSettings(Document):
|
||||
frappe.clear_cache()
|
||||
|
||||
self.validate_and_sync_auto_reconcile_config()
|
||||
self.hide_or_show_party_and_account_balance()
|
||||
|
||||
def validate_stale_days(self):
|
||||
if not self.allow_stale and cint(self.stale_days) <= 0:
|
||||
@@ -112,6 +115,18 @@ class AccountsSettings(Document):
|
||||
_("Stale Days should start from 1."), title="Error", indicator="red", raise_exception=1
|
||||
)
|
||||
|
||||
def hide_or_show_party_and_account_balance(self):
|
||||
def set_property(fieldname, value):
|
||||
make_property_setter("Payment Entry", fieldname, "hidden", value, "Check")
|
||||
|
||||
if self.has_value_changed("show_party_balance"):
|
||||
set_property("party_balance", not self.show_party_balance)
|
||||
|
||||
if self.has_value_changed("show_account_balance"):
|
||||
account_fields = ["paid_from_account_balance", "paid_to_account_balance"]
|
||||
for field in account_fields:
|
||||
set_property(field, not self.show_account_balance)
|
||||
|
||||
def enable_payment_schedule_in_print(self):
|
||||
show_in_print = cint(self.show_payment_schedule_in_print)
|
||||
for doctype in ("Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"):
|
||||
|
||||
@@ -449,7 +449,7 @@ class PaymentEntry(AccountsController):
|
||||
self.contact_person = get_default_contact(self.party_type, self.party)
|
||||
|
||||
complete_contact_details(self)
|
||||
if not self.party_balance:
|
||||
if not self.party_balance and frappe.get_single_value("Accounts Settings", "show_party_balance"):
|
||||
self.party_balance = get_balance_on(
|
||||
party_type=self.party_type, party=self.party, date=self.posting_date, company=self.company
|
||||
)
|
||||
@@ -2684,11 +2684,17 @@ def get_party_details(company, party_type, party, date, cost_center=None):
|
||||
|
||||
party_account = get_party_account(party_type, party, company)
|
||||
account_currency = get_account_currency(party_account)
|
||||
account_balance = get_balance_on(party_account, date, cost_center=cost_center)
|
||||
account_balance = (
|
||||
get_balance_on(party_account, date, cost_center=cost_center)
|
||||
if frappe.get_single_value("Accounts Settings", "show_account_balance")
|
||||
else 0
|
||||
)
|
||||
_party_name = "title" if party_type == "Shareholder" else party_type.lower() + "_name"
|
||||
party_name = frappe.db.get_value(party_type, party, _party_name)
|
||||
party_balance = get_balance_on(
|
||||
party_type=party_type, party=party, company=company, cost_center=cost_center
|
||||
party_balance = (
|
||||
get_balance_on(party_type=party_type, party=party, company=company, cost_center=cost_center)
|
||||
if frappe.get_single_value("Accounts Settings", "show_party_balance")
|
||||
else 0
|
||||
)
|
||||
if party_type in ["Customer", "Supplier"]:
|
||||
party_bank_account = get_party_bank_account(party_type, party)
|
||||
@@ -2717,7 +2723,11 @@ def get_account_details(account, date, cost_center=None):
|
||||
if not account_list:
|
||||
frappe.throw(_("Account: {0} is not permitted under Payment Entry").format(account))
|
||||
|
||||
account_balance = get_balance_on(account, date, cost_center=cost_center, ignore_account_permission=True)
|
||||
account_balance = (
|
||||
get_balance_on(account, date, cost_center=cost_center, ignore_account_permission=True)
|
||||
if frappe.get_single_value("Accounts Settings", "show_account_balance")
|
||||
else 0
|
||||
)
|
||||
|
||||
return frappe._dict(
|
||||
{
|
||||
@@ -3529,11 +3539,18 @@ def get_paid_amount(dt, dn, party_type, party, account, due_date):
|
||||
def get_party_and_account_balance(
|
||||
company, date, paid_from=None, paid_to=None, ptype=None, pty=None, cost_center=None
|
||||
):
|
||||
show_account_balance = frappe.get_single_value("Accounts Settings", "show_account_balance")
|
||||
return frappe._dict(
|
||||
{
|
||||
"party_balance": get_balance_on(party_type=ptype, party=pty, cost_center=cost_center),
|
||||
"paid_from_account_balance": get_balance_on(paid_from, date, cost_center=cost_center),
|
||||
"paid_to_account_balance": get_balance_on(paid_to, date=date, cost_center=cost_center),
|
||||
"party_balance": get_balance_on(party_type=ptype, party=pty, cost_center=cost_center)
|
||||
if frappe.get_single_value("Accounts Settings", "show_party_balance")
|
||||
else 0,
|
||||
"paid_from_account_balance": get_balance_on(paid_from, date, cost_center=cost_center)
|
||||
if show_account_balance
|
||||
else 0,
|
||||
"paid_to_account_balance": get_balance_on(paid_to, date=date, cost_center=cost_center)
|
||||
if show_account_balance
|
||||
else 0,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -61,6 +61,22 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
},
|
||||
};
|
||||
});
|
||||
this.frm.set_query("cost_center", "payments", () => {
|
||||
return {
|
||||
filters: {
|
||||
company: this.frm.doc.company,
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
this.frm.set_query("cost_center", "allocation", () => {
|
||||
return {
|
||||
filters: {
|
||||
company: this.frm.doc.company,
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
refresh() {
|
||||
|
||||
@@ -72,7 +72,7 @@ class PaymentReconciliation(Document):
|
||||
self.common_filter_conditions = []
|
||||
self.accounting_dimension_filter_conditions = []
|
||||
self.ple_posting_date_filter = []
|
||||
self.dimensions = get_dimensions()[0]
|
||||
self.dimensions = get_dimensions(with_cost_center_and_project=True)[0]
|
||||
|
||||
def load_from_db(self):
|
||||
# 'modified' attribute is required for `run_doc_method` to work properly.
|
||||
|
||||
@@ -119,6 +119,7 @@ def get_assets_details(assets):
|
||||
|
||||
fields = [
|
||||
"name as asset",
|
||||
"asset_name",
|
||||
"gross_purchase_amount",
|
||||
"opening_accumulated_depreciation",
|
||||
"asset_category",
|
||||
@@ -143,6 +144,12 @@ def get_columns():
|
||||
"options": "Asset",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Asset Name"),
|
||||
"fieldname": "asset_name",
|
||||
"fieldtype": "Data",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Depreciation Date"),
|
||||
"fieldname": "depreciation_date",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"add_total_row": 0,
|
||||
"add_translate_data": 0,
|
||||
"columns": [],
|
||||
"creation": "2013-12-06 13:22:23",
|
||||
@@ -10,7 +10,7 @@
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2025-08-13 12:47:27.645023",
|
||||
"modified": "2025-11-05 15:47:59.597853",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "General Ledger",
|
||||
|
||||
@@ -21,6 +21,7 @@ def get_ordered_to_be_billed_data(args, filters=None):
|
||||
|
||||
doctype = frappe.qb.DocType(doctype)
|
||||
child_doctype = frappe.qb.DocType(child_tab)
|
||||
item = frappe.qb.DocType("Item")
|
||||
|
||||
docname = filters.get(args.get("reference_field"), None)
|
||||
project_field = get_project_field(doctype, child_doctype, party)
|
||||
@@ -29,6 +30,8 @@ def get_ordered_to_be_billed_data(args, filters=None):
|
||||
frappe.qb.from_(doctype)
|
||||
.inner_join(child_doctype)
|
||||
.on(doctype.name == child_doctype.parent)
|
||||
.join(item)
|
||||
.on(item.name == child_doctype.item_code)
|
||||
.select(
|
||||
doctype.name,
|
||||
doctype[args.get("date")].as_("date"),
|
||||
@@ -54,6 +57,7 @@ def get_ordered_to_be_billed_data(args, filters=None):
|
||||
& (doctype.company == filters.get("company"))
|
||||
& (doctype.posting_date <= filters.get("posting_date"))
|
||||
& (child_doctype.amount > 0)
|
||||
& (item.is_stock_item == 1)
|
||||
& (
|
||||
child_doctype.base_amount
|
||||
- Round(child_doctype.billed_amt * IfNull(doctype.conversion_rate, 1), precision)
|
||||
|
||||
@@ -1,32 +1,37 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"apply_user_permissions": 1,
|
||||
"creation": "2013-06-13 18:46:55",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2018-02-21 01:28:31.261299",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Trends",
|
||||
"owner": "Administrator",
|
||||
"ref_doctype": "Purchase Invoice",
|
||||
"report_name": "Purchase Invoice Trends",
|
||||
"report_type": "Script Report",
|
||||
"add_total_row": 0,
|
||||
"add_translate_data": 0,
|
||||
"columns": [],
|
||||
"creation": "2013-06-13 18:46:55",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2025-11-05 11:55:49.950442",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Trends",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Purchase Invoice",
|
||||
"report_name": "Purchase Invoice Trends",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Purchase User"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Auditor"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"timeout": 0
|
||||
}
|
||||
|
||||
@@ -1,26 +1,31 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"apply_user_permissions": 1,
|
||||
"creation": "2013-06-13 18:44:21",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2018-02-21 01:28:03.622485",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Trends",
|
||||
"owner": "Administrator",
|
||||
"ref_doctype": "Sales Invoice",
|
||||
"report_name": "Sales Invoice Trends",
|
||||
"report_type": "Script Report",
|
||||
"add_total_row": 0,
|
||||
"add_translate_data": 0,
|
||||
"columns": [],
|
||||
"creation": "2013-06-13 18:44:21",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2025-11-05 11:55:50.070651",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Trends",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Sales Invoice",
|
||||
"report_name": "Sales Invoice Trends",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Accounts User"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"timeout": 0
|
||||
}
|
||||
|
||||
@@ -1755,24 +1755,22 @@ def check_and_delete_linked_reports(report):
|
||||
frappe.delete_doc("Desktop Icon", icon)
|
||||
|
||||
|
||||
def create_err_and_its_journals(companies: list | None = None) -> None:
|
||||
if companies:
|
||||
for company in companies:
|
||||
err = frappe.new_doc("Exchange Rate Revaluation")
|
||||
err.company = company.name
|
||||
err.posting_date = nowdate()
|
||||
err.rounding_loss_allowance = 0.0
|
||||
def create_err_and_its_journals(company: dict) -> None:
|
||||
err = frappe.new_doc("Exchange Rate Revaluation")
|
||||
err.company = company.name
|
||||
err.posting_date = nowdate()
|
||||
err.rounding_loss_allowance = 0.0
|
||||
|
||||
err.fetch_and_calculate_accounts_data()
|
||||
if err.accounts:
|
||||
err.save().submit()
|
||||
response = err.make_jv_entries()
|
||||
err.fetch_and_calculate_accounts_data()
|
||||
if err.accounts:
|
||||
err.save().submit()
|
||||
response = err.make_jv_entries()
|
||||
|
||||
if company.submit_err_jv:
|
||||
jv = response.get("revaluation_jv", None)
|
||||
jv and frappe.get_doc("Journal Entry", jv).submit()
|
||||
jv = response.get("zero_balance_jv", None)
|
||||
jv and frappe.get_doc("Journal Entry", jv).submit()
|
||||
if company.submit_err_jv:
|
||||
jv = response.get("revaluation_jv", None)
|
||||
jv and frappe.get_doc("Journal Entry", jv).submit()
|
||||
jv = response.get("zero_balance_jv", None)
|
||||
jv and frappe.get_doc("Journal Entry", jv).submit()
|
||||
|
||||
|
||||
def _auto_create_exchange_rate_revaluation_for(frequency: str) -> None:
|
||||
@@ -1785,7 +1783,14 @@ def _auto_create_exchange_rate_revaluation_for(frequency: str) -> None:
|
||||
filters={"auto_exchange_rate_revaluation": 1, "auto_err_frequency": frequency},
|
||||
fields=["name", "submit_err_jv"],
|
||||
)
|
||||
create_err_and_its_journals(companies)
|
||||
|
||||
if companies:
|
||||
for company in companies:
|
||||
frappe.enqueue(
|
||||
"erpnext.accounts.utils.create_err_and_its_journals",
|
||||
company=company,
|
||||
queue="long",
|
||||
)
|
||||
|
||||
|
||||
def auto_create_exchange_rate_revaluation_daily() -> None:
|
||||
|
||||
@@ -458,6 +458,7 @@ class Asset(AccountsController):
|
||||
"asset_name": self.asset_name,
|
||||
"target_location": self.location,
|
||||
"to_employee": self.custodian,
|
||||
"company": self.company,
|
||||
}
|
||||
]
|
||||
asset_movement = frappe.get_doc(
|
||||
|
||||
@@ -76,6 +76,46 @@ class TestRequestforQuotation(FrappeTestCase):
|
||||
self.assertEqual(sq1.get("items")[0].item_code, "_Test Item")
|
||||
self.assertEqual(sq1.get("items")[0].qty, 5)
|
||||
|
||||
def test_make_supplier_quotation_with_taxes(self):
|
||||
"""Test automatic tax addition when supplier quotation is created from RFQ taxes_and_charges are set"""
|
||||
|
||||
# Create a Purchase Taxes and Charges Template for testing
|
||||
tax_template = frappe.new_doc("Purchase Taxes and Charges Template")
|
||||
tax_template.doctype = "Purchase Taxes and Charges Template"
|
||||
tax_template.title = "_Test Purchase Taxes Template for RFQ"
|
||||
tax_template.company = "_Test Company"
|
||||
tax_template.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "_Test Account Service Tax - _TC",
|
||||
"description": "VAT",
|
||||
"rate": 10,
|
||||
},
|
||||
)
|
||||
tax_template.save()
|
||||
|
||||
rfq = make_request_for_quotation()
|
||||
supplier = rfq.get("suppliers")[0].supplier
|
||||
|
||||
tax_rule = frappe.new_doc("Tax Rule")
|
||||
tax_rule.company = "_Test Company"
|
||||
tax_rule.tax_type = "Purchase"
|
||||
tax_rule.supplier = supplier
|
||||
tax_rule.purchase_tax_template = tax_template.name
|
||||
tax_rule.save()
|
||||
|
||||
sq = make_supplier_quotation_from_rfq(rfq.name, for_supplier=supplier)
|
||||
|
||||
# Verify that taxes_and_charges is set from get_party_details
|
||||
self.assertEqual(sq.taxes_and_charges, tax_template.name)
|
||||
|
||||
# Verify that taxes are automatically added
|
||||
self.assertGreaterEqual(len(sq.get("taxes")), 1)
|
||||
|
||||
tax_rule.delete()
|
||||
tax_template.delete()
|
||||
|
||||
def test_make_supplier_quotation_with_special_characters(self):
|
||||
frappe.delete_doc_if_exists("Supplier", "_Test Supplier '1", force=1)
|
||||
supplier = frappe.new_doc("Supplier")
|
||||
|
||||
@@ -1,29 +1,34 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"apply_user_permissions": 1,
|
||||
"creation": "2013-06-13 18:45:01",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2018-02-21 01:28:37.416562",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Trends",
|
||||
"owner": "Administrator",
|
||||
"ref_doctype": "Purchase Order",
|
||||
"report_name": "Purchase Order Trends",
|
||||
"report_type": "Script Report",
|
||||
"add_total_row": 0,
|
||||
"add_translate_data": 0,
|
||||
"columns": [],
|
||||
"creation": "2013-06-13 18:45:01",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2025-11-05 11:55:50.058154",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Trends",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Purchase Order",
|
||||
"report_name": "Purchase Order Trends",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Stock User"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Purchase Manager"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Purchase User"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"timeout": 0
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ from erpnext.accounts.doctype.pricing_rule.utils import (
|
||||
)
|
||||
from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
|
||||
from erpnext.accounts.party import (
|
||||
PURCHASE_TRANSACTION_TYPES,
|
||||
SALES_TRANSACTION_TYPES,
|
||||
get_party_account,
|
||||
get_party_account_currency,
|
||||
get_party_gle_currency,
|
||||
@@ -2918,6 +2920,104 @@ class AccountsController(TransactionBase):
|
||||
x["transaction_currency"] = self.currency
|
||||
x["transaction_exchange_rate"] = self.get("conversion_rate") or 1
|
||||
|
||||
def after_mapping(self, source_doc):
|
||||
self.set_discount_amount_after_mapping(source_doc)
|
||||
|
||||
def set_discount_amount_after_mapping(self, source_doc):
|
||||
"""
|
||||
Ensures that Additional Discount Amount is not copied repeatedly
|
||||
for multiple mappings of a single source transaction.
|
||||
"""
|
||||
|
||||
# source and target doctypes should both be buying / selling
|
||||
for transaction_types in (PURCHASE_TRANSACTION_TYPES, SALES_TRANSACTION_TYPES):
|
||||
if self.doctype in transaction_types and source_doc.doctype in transaction_types:
|
||||
break
|
||||
|
||||
else:
|
||||
return
|
||||
|
||||
# ensure both doctypes have discount_amount field
|
||||
if not self.meta.get_field("discount_amount") or not source_doc.meta.get_field("discount_amount"):
|
||||
return
|
||||
|
||||
# ensure discount_amount is set in source doc
|
||||
if not source_doc.discount_amount:
|
||||
return
|
||||
|
||||
# ensure additional_discount_percentage is not set in the source doc
|
||||
if source_doc.get("additional_discount_percentage"):
|
||||
return
|
||||
|
||||
item_doctype = self.meta.get_field("items").options
|
||||
doctype_table = frappe.qb.DocType(self.doctype)
|
||||
item_table = frappe.qb.DocType(item_doctype)
|
||||
|
||||
is_same_doctype = self.doctype == source_doc.doctype
|
||||
is_return = self.get("is_return") and is_same_doctype
|
||||
|
||||
if is_same_doctype and not is_return:
|
||||
# should never happen
|
||||
# you don't map to the same doctype without it being a return
|
||||
return
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(doctype_table)
|
||||
.where(doctype_table.docstatus == 1)
|
||||
.where(doctype_table.discount_amount != 0)
|
||||
.select(Sum(doctype_table.discount_amount))
|
||||
)
|
||||
|
||||
if is_return:
|
||||
query = query.where(doctype_table.is_return == 1).where(
|
||||
doctype_table.return_against == source_doc.name
|
||||
)
|
||||
|
||||
else:
|
||||
item_meta = frappe.get_meta(item_doctype)
|
||||
reference_fieldname = next(
|
||||
(
|
||||
row.fieldname
|
||||
for row in item_meta.fields
|
||||
if row.fieldtype == "Link"
|
||||
and row.options == source_doc.doctype
|
||||
and not row.get("is_custom_field")
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if not reference_fieldname:
|
||||
return
|
||||
|
||||
query = query.where(
|
||||
doctype_table.name.isin(
|
||||
frappe.qb.from_(item_table)
|
||||
.select(item_table.parent)
|
||||
.where(item_table[reference_fieldname] == source_doc.name)
|
||||
.distinct()
|
||||
)
|
||||
)
|
||||
|
||||
result = query.run()
|
||||
if not result:
|
||||
return
|
||||
|
||||
discount_already_applied = result[0][0]
|
||||
if not discount_already_applied:
|
||||
return
|
||||
|
||||
if is_return:
|
||||
# returns have negative discount
|
||||
discount_already_applied *= -1
|
||||
|
||||
discount_amount = max(source_doc.discount_amount - discount_already_applied, 0)
|
||||
if discount_amount and is_return:
|
||||
discount_amount *= -1
|
||||
|
||||
self.discount_amount = flt(discount_amount, self.precision("discount_amount"))
|
||||
|
||||
self.calculate_taxes_and_totals()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_tax_rate(account_head):
|
||||
|
||||
@@ -13,6 +13,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import g
|
||||
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
|
||||
from erpnext.accounts.party import get_party_details
|
||||
from erpnext.buying.utils import update_last_purchase_rate, validate_for_items
|
||||
from erpnext.controllers.accounts_controller import get_taxes_and_charges
|
||||
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
|
||||
from erpnext.controllers.subcontracting_controller import SubcontractingController
|
||||
from erpnext.stock.get_item_details import get_conversion_factor
|
||||
@@ -180,6 +181,12 @@ class BuyingController(SubcontractingController):
|
||||
|
||||
self.set_missing_item_details(for_validate)
|
||||
|
||||
if self.meta.get_field("taxes"):
|
||||
if self.get("taxes_and_charges") and not self.get("taxes") and not for_validate:
|
||||
taxes = get_taxes_and_charges("Purchase Taxes and Charges Template", self.taxes_and_charges)
|
||||
for tax in taxes:
|
||||
self.append("taxes", tax)
|
||||
|
||||
def set_supplier_from_item_default(self):
|
||||
if self.meta.get_field("supplier") and not self.supplier:
|
||||
for d in self.get("items"):
|
||||
|
||||
@@ -682,6 +682,25 @@ class calculate_taxes_and_totals:
|
||||
self.doc.precision("discount_amount"),
|
||||
)
|
||||
|
||||
discount_amount = self.doc.discount_amount or 0
|
||||
grand_total = self.doc.grand_total
|
||||
|
||||
# validate that discount amount cannot exceed the total before discount
|
||||
if (
|
||||
(grand_total >= 0 and discount_amount > grand_total)
|
||||
or (grand_total < 0 and discount_amount < grand_total) # returns
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Additional Discount Amount ({discount_amount}) cannot exceed "
|
||||
"the total before such discount ({total_before_discount})"
|
||||
).format(
|
||||
discount_amount=self.doc.get_formatted("discount_amount"),
|
||||
total_before_discount=self.doc.get_formatted("grand_total"),
|
||||
),
|
||||
title=_("Invalid Discount Amount"),
|
||||
)
|
||||
|
||||
def apply_discount_amount(self):
|
||||
if self.doc.discount_amount:
|
||||
if not self.doc.apply_discount_on:
|
||||
|
||||
@@ -2258,3 +2258,173 @@ class TestAccountsController(FrappeTestCase):
|
||||
self.assertRaises(frappe.ValidationError, si.save)
|
||||
si.contact_person = customer_contact.name
|
||||
si.save()
|
||||
|
||||
def test_discount_amount_not_mapped_repeatedly_for_sales_transactions(self):
|
||||
"""
|
||||
Test that additional discount amount is not copied repeatedly
|
||||
when creating multiple delivery notes from a single sales order with discount_amount set
|
||||
"""
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
# Create a sales order with discount amount
|
||||
so = make_sales_order(qty=10, rate=100, do_not_submit=True)
|
||||
so.apply_discount_on = "Net Total"
|
||||
so.discount_amount = 100
|
||||
so.save()
|
||||
so.submit()
|
||||
|
||||
# Create first delivery note from sales order (partial qty)
|
||||
dn1 = make_delivery_note(so.name)
|
||||
dn1.items[0].qty = 5
|
||||
dn1.save()
|
||||
dn1.submit()
|
||||
|
||||
# First delivery note should have full discount amount
|
||||
self.assertEqual(dn1.discount_amount, 100)
|
||||
self.assertEqual(dn1.grand_total, 400)
|
||||
|
||||
# Create second delivery note from the same sales order (remaining qty)
|
||||
dn2 = make_delivery_note(so.name)
|
||||
dn2.items[0].qty = 5
|
||||
dn2.save()
|
||||
dn2.submit()
|
||||
|
||||
# Second delivery note should have discount_amount set to 0
|
||||
# because discount was already fully applied in first delivery note
|
||||
self.assertEqual(dn2.discount_amount, 0)
|
||||
self.assertEqual(dn2.grand_total, 500)
|
||||
|
||||
def test_discount_amount_not_mapped_repeatedly_for_purchase_transactions(self):
|
||||
"""
|
||||
Test that additional discount amount is not copied repeatedly
|
||||
when creating multiple purchase receipts from a single purchase order with discount_amount set
|
||||
"""
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
|
||||
# Create a purchase order with discount amount
|
||||
po = create_purchase_order(qty=10, rate=100, do_not_submit=True)
|
||||
po.apply_discount_on = "Net Total"
|
||||
po.discount_amount = 100
|
||||
po.save()
|
||||
po.submit()
|
||||
|
||||
# Create first purchase receipt from purchase order (partial qty)
|
||||
pr1 = make_purchase_receipt(po.name)
|
||||
pr1.items[0].qty = 5
|
||||
pr1.save()
|
||||
pr1.submit()
|
||||
|
||||
# First purchase receipt should have full discount amount
|
||||
self.assertEqual(pr1.discount_amount, 100)
|
||||
self.assertEqual(pr1.grand_total, 400)
|
||||
|
||||
# Create second purchase receipt from the same purchase order (remaining qty)
|
||||
pr2 = make_purchase_receipt(po.name)
|
||||
pr2.items[0].qty = 5
|
||||
pr2.save()
|
||||
pr2.submit()
|
||||
|
||||
# Second purchase receipt should have discount_amount set to 0
|
||||
# because discount was already fully applied in first purchase receipt
|
||||
self.assertEqual(pr2.discount_amount, 0)
|
||||
self.assertEqual(pr2.grand_total, 500)
|
||||
|
||||
def test_discount_amount_partial_application_in_mapped_transactions(self):
|
||||
"""
|
||||
Test that discount amount is partially applied when some discount
|
||||
has already been used in previous mapped transactions
|
||||
"""
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
# Create a sales order with discount amount
|
||||
so = make_sales_order(qty=10, rate=100, do_not_submit=True)
|
||||
so.apply_discount_on = "Net Total"
|
||||
so.discount_amount = 200
|
||||
so.save()
|
||||
so.submit()
|
||||
|
||||
self.assertEqual(so.discount_amount, 200)
|
||||
self.assertEqual(so.grand_total, 800)
|
||||
|
||||
# Create first invoice with partial discount (manually set lower discount)
|
||||
si1 = make_sales_invoice(so.name)
|
||||
si1.items[0].qty = 5
|
||||
si1.discount_amount = 50 # Partial discount application
|
||||
si1.save()
|
||||
si1.submit()
|
||||
|
||||
self.assertEqual(si1.discount_amount, 50)
|
||||
self.assertEqual(si1.grand_total, 450)
|
||||
|
||||
# Create second invoice from the same sales order
|
||||
si2 = make_sales_invoice(so.name)
|
||||
si2.items[0].qty = 5
|
||||
si2.save()
|
||||
si2.submit()
|
||||
|
||||
# Second invoice should have remaining discount (200 - 50 = 150)
|
||||
self.assertEqual(si2.discount_amount, 150)
|
||||
self.assertEqual(si2.grand_total, 350)
|
||||
|
||||
def test_discount_amount_not_mapped_when_percentage_is_set(self):
|
||||
"""
|
||||
Test that discount amount is not adjusted when additional_discount_percentage
|
||||
is set in the source document (as it will be recalculated based on percentage)
|
||||
"""
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
# Create a sales order with discount percentage instead of amount
|
||||
so = make_sales_order(qty=10, rate=100, do_not_submit=True)
|
||||
so.apply_discount_on = "Net Total"
|
||||
so.additional_discount_percentage = 10 # 10% discount
|
||||
so.save()
|
||||
so.submit()
|
||||
|
||||
self.assertEqual(so.discount_amount, 100) # 10% of 1000
|
||||
self.assertEqual(so.grand_total, 900)
|
||||
|
||||
# Create delivery note from sales order
|
||||
dn = make_delivery_note(so.name)
|
||||
dn.items[0].qty = 5
|
||||
dn.save()
|
||||
|
||||
# Delivery note should have discount amount recalculated based on percentage
|
||||
# and not affected by the repeated mapping logic
|
||||
self.assertEqual(dn.additional_discount_percentage, 10)
|
||||
self.assertEqual(dn.discount_amount, 50) # 10% of 500
|
||||
|
||||
def test_discount_amount_for_multiple_returns(self):
|
||||
"""
|
||||
Test that discount amount is correctly adjusted when multiple return invoices
|
||||
are created against the same original invoice to prevent over-returning discount
|
||||
"""
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
||||
|
||||
# Create original sales invoice with discount
|
||||
si = create_sales_invoice(qty=10, rate=100, do_not_submit=True)
|
||||
si.apply_discount_on = "Net Total"
|
||||
si.discount_amount = 100
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
# Create first return - Frappe will copy full discount by default, we need to adjust it
|
||||
return_si_1 = make_sales_return(si.name)
|
||||
return_si_1.items[0].qty = -6 # Return 6 out of 10 items
|
||||
# Manually set discount to match the proportion (60% of discount)
|
||||
return_si_1.discount_amount = -60
|
||||
return_si_1.save()
|
||||
return_si_1.submit()
|
||||
|
||||
self.assertEqual(return_si_1.discount_amount, -60)
|
||||
|
||||
# Create second return for remaining items
|
||||
return_si_2 = make_sales_return(si.name)
|
||||
return_si_2.items[0].qty = -4 # Return remaining 4 out of 10 items
|
||||
return_si_2.save()
|
||||
|
||||
# Second return should only get remaining discount (100 - 60 = 40)
|
||||
self.assertEqual(return_si_2.discount_amount, -40)
|
||||
|
||||
@@ -191,6 +191,9 @@ def get_data(filters, conditions):
|
||||
des[j + inc] = row1[0][j]
|
||||
|
||||
data.append(des)
|
||||
|
||||
total_row = calculate_total_row(data1, conditions["columns"])
|
||||
data.append(total_row)
|
||||
else:
|
||||
data = frappe.db.sql(
|
||||
""" select {} from `tab{}` t1, `tab{} Item` t2 {}
|
||||
@@ -214,9 +217,32 @@ def get_data(filters, conditions):
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
total_row = calculate_total_row(data, conditions["columns"])
|
||||
data.append(total_row)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def calculate_total_row(data, columns):
|
||||
def wrap_in_quotes(label):
|
||||
return f"'{label}'"
|
||||
|
||||
total_values = {}
|
||||
for i, col in enumerate(columns):
|
||||
if "Float" in col or "Currency/currency" in col:
|
||||
total_values[i] = 0
|
||||
|
||||
for row in data:
|
||||
for i in total_values.keys():
|
||||
total_values[i] += row[i] if row[i] is not None else 0
|
||||
|
||||
total_row = [wrap_in_quotes(_("Total"))]
|
||||
for i in range(1, len(columns)):
|
||||
total_row.append(total_values.get(i, None))
|
||||
|
||||
return total_row
|
||||
|
||||
|
||||
def get_mon(dt):
|
||||
return getdate(dt).strftime("%b")
|
||||
|
||||
|
||||
@@ -151,10 +151,9 @@ def get_column(filters):
|
||||
},
|
||||
{
|
||||
"label": _("Document Type"),
|
||||
"fieldtype": "Link",
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "document_type",
|
||||
"width": 150,
|
||||
"options": "DocType",
|
||||
},
|
||||
{
|
||||
"label": _("Document Name"),
|
||||
|
||||
@@ -424,3 +424,5 @@ erpnext.patches.v15_0.update_uae_zero_rated_fetch
|
||||
erpnext.patches.v15_0.update_fieldname_in_accounting_dimension_filter
|
||||
erpnext.patches.v15_0.set_asset_status_if_not_already_set
|
||||
erpnext.patches.v15_0.toggle_legacy_controller_for_period_closing
|
||||
execute:frappe.db.set_single_value("Accounts Settings", "show_party_balance", 1)
|
||||
execute:frappe.db.set_single_value("Accounts Settings", "show_account_balance", 1)
|
||||
|
||||
@@ -17,6 +17,10 @@ class CircularReferenceError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class ParentIsGroupError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class Task(NestedSet):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
@@ -83,6 +87,7 @@ class Task(NestedSet):
|
||||
self.update_depends_on()
|
||||
self.validate_dependencies_for_template_task()
|
||||
self.validate_completed_on()
|
||||
self.validate_parent_is_group()
|
||||
|
||||
def validate_dates(self):
|
||||
self.validate_from_to_dates("exp_start_date", "exp_end_date")
|
||||
@@ -153,20 +158,36 @@ class Task(NestedSet):
|
||||
def validate_parent_template_task(self):
|
||||
if self.parent_task:
|
||||
if not frappe.db.get_value("Task", self.parent_task, "is_template"):
|
||||
parent_task_format = f"""<a href="/app/task/{self.parent_task}">{self.parent_task}</a>"""
|
||||
frappe.throw(_("Parent Task {0} is not a Template Task").format(parent_task_format))
|
||||
frappe.throw(
|
||||
_("Parent Task {0} is not a Template Task").format(
|
||||
get_link_to_form("Task", self.parent_task)
|
||||
)
|
||||
)
|
||||
|
||||
def validate_depends_on_tasks(self):
|
||||
if self.depends_on:
|
||||
for task in self.depends_on:
|
||||
if not frappe.db.get_value("Task", task.task, "is_template"):
|
||||
dependent_task_format = f"""<a href="/app/task/{task.task}">{task.task}</a>"""
|
||||
frappe.throw(_("Dependent Task {0} is not a Template Task").format(dependent_task_format))
|
||||
frappe.throw(
|
||||
_("Dependent Task {0} is not a Template Task").format(
|
||||
get_link_to_form("Task", task.task)
|
||||
)
|
||||
)
|
||||
|
||||
def validate_completed_on(self):
|
||||
if self.completed_on and getdate(self.completed_on) > getdate():
|
||||
frappe.throw(_("Completed On cannot be greater than Today"))
|
||||
|
||||
def validate_parent_is_group(self):
|
||||
if self.parent_task:
|
||||
if not frappe.db.get_value("Task", self.parent_task, "is_group"):
|
||||
frappe.throw(
|
||||
_("Parent Task {0} must be a Group Task").format(
|
||||
get_link_to_form("Task", self.parent_task)
|
||||
),
|
||||
ParentIsGroupError,
|
||||
)
|
||||
|
||||
def update_depends_on(self):
|
||||
depends_on_tasks = ""
|
||||
for d in self.depends_on:
|
||||
|
||||
@@ -6,7 +6,7 @@ import unittest
|
||||
import frappe
|
||||
from frappe.utils import add_days, getdate, nowdate
|
||||
|
||||
from erpnext.projects.doctype.task.task import CircularReferenceError
|
||||
from erpnext.projects.doctype.task.task import CircularReferenceError, ParentIsGroupError
|
||||
|
||||
|
||||
class TestTask(unittest.TestCase):
|
||||
@@ -109,6 +109,20 @@ class TestTask(unittest.TestCase):
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Task", task.name, "status"), "Overdue")
|
||||
|
||||
def test_parent_task_must_be_group(self):
|
||||
parent_task = create_task(
|
||||
subject="_Test Parent Task Non Group",
|
||||
is_group=0,
|
||||
)
|
||||
|
||||
child_task = create_task(
|
||||
subject="_Test Child Task",
|
||||
parent_task=parent_task.name,
|
||||
save=False,
|
||||
)
|
||||
|
||||
self.assertRaises(ParentIsGroupError, child_task.save)
|
||||
|
||||
|
||||
def create_task(
|
||||
subject,
|
||||
|
||||
@@ -21,6 +21,7 @@ frappe.ui.form.on("Timesheet", {
|
||||
filters: {
|
||||
project: child.project,
|
||||
status: ["!=", "Cancelled"],
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -285,7 +285,7 @@ class Timesheet(Document):
|
||||
if data.activity_type or data.is_billable:
|
||||
rate = get_activity_cost(self.employee, data.activity_type)
|
||||
hours = data.billing_hours or 0
|
||||
costing_hours = data.billing_hours or data.hours or 0
|
||||
costing_hours = data.hours or 0
|
||||
if rate:
|
||||
data.billing_rate = (
|
||||
flt(rate.get("billing_rate")) if flt(data.billing_rate) == 0 else data.billing_rate
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.provide("erpnext.buying");
|
||||
// cur_frm.add_fetch('project', 'cost_center', 'cost_center');
|
||||
|
||||
erpnext.buying = {
|
||||
setup_buying_controller: function() {
|
||||
@@ -11,6 +10,7 @@ erpnext.buying = {
|
||||
super.setup();
|
||||
this.toggle_enable_for_stock_uom("allow_to_edit_stock_uom_qty_for_purchase");
|
||||
this.frm.email_field = "contact_email";
|
||||
this.frm.add_fetch("project", "cost_center", "cost_center");
|
||||
}
|
||||
|
||||
onload(doc, cdt, cdn) {
|
||||
@@ -171,15 +171,13 @@ erpnext.buying = {
|
||||
shipping_address: this.frm.doc.shipping_address
|
||||
},
|
||||
callback: (r) => {
|
||||
if (!this.frm.doc.billing_address)
|
||||
this.frm.set_value("billing_address", r.message.primary_address || "");
|
||||
if (!r.message) return;
|
||||
|
||||
if (
|
||||
!frappe.meta.has_field(this.frm.doc.doctype, "shipping_address") ||
|
||||
this.frm.doc.shipping_address
|
||||
)
|
||||
return;
|
||||
this.frm.set_value("shipping_address", r.message.shipping_address || "");
|
||||
this.frm.set_value("billing_address", r.message.primary_address || "");
|
||||
|
||||
if (frappe.meta.has_field(this.frm.doc.doctype, "shipping_address")) {
|
||||
this.frm.set_value("shipping_address", r.message.shipping_address || "");
|
||||
}
|
||||
},
|
||||
});
|
||||
erpnext.utils.set_letter_head(this.frm)
|
||||
|
||||
@@ -558,6 +558,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
|
||||
item.weight_per_unit = 0;
|
||||
item.weight_uom = '';
|
||||
item.uom = null // make UOM blank to update the existing UOM when item changes
|
||||
item.conversion_factor = 0;
|
||||
|
||||
if(['Sales Invoice', 'Purchase Invoice'].includes(this.frm.doc.doctype)) {
|
||||
|
||||
@@ -1179,7 +1179,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
||||
|
||||
make_purchase_order() {
|
||||
let pending_items = this.frm.doc.items.some((item) => {
|
||||
let pending_qty = flt(item.stock_qty) - flt(item.ordered_qty);
|
||||
const pending_qty = flt(item.stock_qty) - this.get_ordered_qty(item, this.frm.doc);
|
||||
return pending_qty > 0;
|
||||
});
|
||||
if (!pending_items) {
|
||||
@@ -1333,8 +1333,10 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
||||
// calculate ordered qty based on packed items in case of product bundle
|
||||
let packed_items = so.packed_items.filter((pi) => pi.parent_detail_docname == item.name);
|
||||
if (packed_items && packed_items.length) {
|
||||
ordered_qty = packed_items.reduce((sum, pi) => sum + flt(pi.ordered_qty), 0);
|
||||
ordered_qty = ordered_qty / packed_items.length;
|
||||
const all_packed_items_ordered = packed_items.every(
|
||||
(pi) => flt(pi.ordered_qty) >= flt(pi.qty)
|
||||
);
|
||||
ordered_qty = all_packed_items_ordered ? item.stock_qty : 0;
|
||||
}
|
||||
}
|
||||
return ordered_qty;
|
||||
|
||||
@@ -1465,7 +1465,8 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
|
||||
"pricing_rules",
|
||||
],
|
||||
"postprocess": update_item_for_packed_item,
|
||||
"condition": lambda doc: doc.parent_item in items_to_map,
|
||||
"condition": lambda doc: doc.parent_item in items_to_map
|
||||
and flt(doc.ordered_qty) < flt(doc.qty),
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
@@ -1603,7 +1604,8 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
|
||||
"pricing_rules",
|
||||
],
|
||||
"postprocess": update_item_for_packed_item,
|
||||
"condition": lambda doc: doc.parent_item in items_to_map,
|
||||
"condition": lambda doc: doc.parent_item in items_to_map
|
||||
and flt(doc.ordered_qty) < flt(doc.qty),
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
|
||||
@@ -1,32 +1,37 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"apply_user_permissions": 1,
|
||||
"creation": "2013-06-07 16:01:16",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2018-02-21 01:28:14.928929",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Quotation Trends",
|
||||
"owner": "Administrator",
|
||||
"ref_doctype": "Quotation",
|
||||
"report_name": "Quotation Trends",
|
||||
"report_type": "Script Report",
|
||||
"add_total_row": 0,
|
||||
"add_translate_data": 0,
|
||||
"columns": [],
|
||||
"creation": "2013-06-07 16:01:16",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2025-11-05 11:55:50.127020",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Quotation Trends",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Quotation",
|
||||
"report_name": "Quotation Trends",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Sales User"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Sales Manager"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Maintenance Manager"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Maintenance User"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"timeout": 0
|
||||
}
|
||||
|
||||
@@ -1,35 +1,40 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"apply_user_permissions": 1,
|
||||
"creation": "2013-06-13 18:43:30",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2018-02-20 08:05:46.191588",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order Trends",
|
||||
"owner": "Administrator",
|
||||
"ref_doctype": "Sales Order",
|
||||
"report_name": "Sales Order Trends",
|
||||
"report_type": "Script Report",
|
||||
"add_total_row": 0,
|
||||
"add_translate_data": 0,
|
||||
"columns": [],
|
||||
"creation": "2013-06-13 18:43:30",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2025-11-05 11:55:50.096303",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order Trends",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Sales Order",
|
||||
"report_name": "Sales Order Trends",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Sales User"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Sales Manager"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Maintenance User"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Stock User"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"timeout": 0
|
||||
}
|
||||
|
||||
@@ -50,6 +50,15 @@ frappe.ui.form.on("Company", {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("default_warehouse_for_sales_return", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.name,
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
company_name: function (frm) {
|
||||
|
||||
@@ -474,6 +474,7 @@ def get_doctypes_to_be_ignored():
|
||||
"Item Default",
|
||||
"Customer",
|
||||
"Supplier",
|
||||
"Department",
|
||||
]
|
||||
|
||||
doctypes_to_be_ignored.extend(frappe.get_hooks("company_data_to_be_ignored") or [])
|
||||
|
||||
@@ -79,7 +79,9 @@ frappe.ui.form.on("Material Request", {
|
||||
});
|
||||
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
||||
frm.doc.buying_price_list = frappe.defaults.get_default("buying_price_list");
|
||||
if (!frm.doc.buying_price_list) {
|
||||
frm.doc.buying_price_list = frappe.defaults.get_default("buying_price_list");
|
||||
}
|
||||
},
|
||||
|
||||
company: function (frm) {
|
||||
|
||||
@@ -75,6 +75,21 @@ class MaterialRequest(BuyingController):
|
||||
work_order: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.status_updater = [
|
||||
{
|
||||
"source_dt": "Material Request Item",
|
||||
"target_dt": "Sales Order Item",
|
||||
"target_field": "ordered_qty",
|
||||
"target_parent_dt": "Sales Order",
|
||||
"target_parent_field": "",
|
||||
"join_field": "sales_order_item",
|
||||
"target_ref_field": "stock_qty",
|
||||
"source_field": "stock_qty",
|
||||
}
|
||||
]
|
||||
|
||||
def check_if_already_pulled(self):
|
||||
pass
|
||||
|
||||
@@ -175,10 +190,10 @@ class MaterialRequest(BuyingController):
|
||||
def on_submit(self):
|
||||
self.update_requested_qty_in_production_plan()
|
||||
self.update_requested_qty()
|
||||
if self.material_request_type == "Purchase" and frappe.db.exists(
|
||||
"Budget", {"applicable_on_material_request": 1, "docstatus": 1}
|
||||
):
|
||||
self.validate_budget()
|
||||
if self.material_request_type == "Purchase":
|
||||
self.update_prevdoc_status()
|
||||
if frappe.db.exists("Budget", {"applicable_on_material_request": 1, "docstatus": 1}):
|
||||
self.validate_budget()
|
||||
|
||||
def before_save(self):
|
||||
self.set_status(update=True)
|
||||
@@ -816,6 +831,16 @@ def raise_work_orders(material_request):
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_pick_list(source_name, target_doc=None):
|
||||
def update_item(obj, target, source_parent):
|
||||
qty = (
|
||||
flt(flt(obj.stock_qty) - flt(obj.ordered_qty)) / target.conversion_factor
|
||||
if flt(obj.stock_qty) > flt(obj.ordered_qty)
|
||||
else 0
|
||||
)
|
||||
target.qty = qty
|
||||
target.stock_qty = qty * obj.conversion_factor
|
||||
target.conversion_factor = obj.conversion_factor
|
||||
|
||||
doc = get_mapped_doc(
|
||||
"Material Request",
|
||||
source_name,
|
||||
@@ -828,6 +853,11 @@ def create_pick_list(source_name, target_doc=None):
|
||||
"Material Request Item": {
|
||||
"doctype": "Pick List Item",
|
||||
"field_map": {"name": "material_request_item", "stock_qty": "stock_qty"},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: (
|
||||
flt(doc.ordered_qty, doc.precision("ordered_qty"))
|
||||
< flt(doc.stock_qty, doc.precision("ordered_qty"))
|
||||
),
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
|
||||
@@ -12,6 +12,7 @@ from frappe.utils import flt, today
|
||||
from erpnext.controllers.accounts_controller import InvalidQtyError
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.stock.doctype.material_request.material_request import (
|
||||
create_pick_list,
|
||||
make_in_transit_stock_entry,
|
||||
make_purchase_order,
|
||||
make_stock_entry,
|
||||
@@ -883,6 +884,48 @@ class TestMaterialRequest(FrappeTestCase):
|
||||
self.assertEqual(mr.per_ordered, 100)
|
||||
self.assertEqual(mr.status, "Ordered")
|
||||
|
||||
def test_material_request_qty_over_sales_order_limit(self):
|
||||
from erpnext.controllers.status_updater import OverAllowanceError
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
so = make_sales_order()
|
||||
mr = make_material_request(qty=100, do_not_submit=True)
|
||||
mr.items[0].sales_order = so.name
|
||||
mr.items[0].sales_order_item = so.items[0].name
|
||||
mr.save()
|
||||
|
||||
self.assertRaises(OverAllowanceError, mr.submit)
|
||||
|
||||
def test_pending_qty_in_pick_list(self):
|
||||
"""Test for pick list mapped doc qty from partially received Material Request Transfer"""
|
||||
import json
|
||||
|
||||
from erpnext.stock.doctype.pick_list.pick_list import create_stock_entry
|
||||
|
||||
mr = make_material_request(material_request_type="Material Transfer")
|
||||
pl = create_pick_list(mr.name)
|
||||
pl.save()
|
||||
pl.locations[0].qty = 5
|
||||
pl.locations[0].stock_qty = 5
|
||||
pl.submit()
|
||||
|
||||
to_warehouse = create_warehouse("Test To Warehouse")
|
||||
|
||||
se_data = create_stock_entry(json.dumps(pl.as_dict()))
|
||||
se = frappe.get_doc(se_data)
|
||||
se.items[0].t_warehouse = to_warehouse
|
||||
se.save()
|
||||
se.submit()
|
||||
|
||||
pl.load_from_db()
|
||||
self.assertEqual(pl.locations[0].picked_qty, se.items[0].qty)
|
||||
|
||||
mr.load_from_db()
|
||||
self.assertEqual(mr.status, "Partially Received")
|
||||
|
||||
pl_for_pending = create_pick_list(mr.name)
|
||||
self.assertEqual(pl_for_pending.locations[0].qty, 5)
|
||||
|
||||
|
||||
def get_in_transit_warehouse(company):
|
||||
if not frappe.db.exists("Warehouse Type", "Transit"):
|
||||
|
||||
@@ -1560,8 +1560,8 @@ def update_stock_entry_items_with_no_reference(pick_list, stock_entry):
|
||||
def update_common_item_properties(item, location):
|
||||
item.item_code = location.item_code
|
||||
item.s_warehouse = location.warehouse
|
||||
item.qty = location.picked_qty * location.conversion_factor
|
||||
item.transfer_qty = location.picked_qty
|
||||
item.qty = location.qty
|
||||
item.uom = location.uom
|
||||
item.conversion_factor = location.conversion_factor
|
||||
item.stock_uom = location.stock_uom
|
||||
|
||||
@@ -283,9 +283,11 @@ class QualityInspection(Document):
|
||||
|
||||
def min_max_criteria_passed(self, reading):
|
||||
"""Determine whether all readings fall in the acceptable range."""
|
||||
has_reading = False
|
||||
for i in range(1, 11):
|
||||
reading_value = reading.get("reading_" + str(i))
|
||||
if reading_value is not None and reading_value.strip():
|
||||
has_reading = True
|
||||
result = (
|
||||
flt(reading.get("min_value"))
|
||||
<= parse_float(reading_value)
|
||||
@@ -293,7 +295,7 @@ class QualityInspection(Document):
|
||||
)
|
||||
if not result:
|
||||
return False
|
||||
return True
|
||||
return has_reading
|
||||
|
||||
def set_status_based_on_acceptance_formula(self, reading):
|
||||
if not reading.acceptance_formula:
|
||||
|
||||
@@ -290,7 +290,7 @@ def create_quality_inspection(**args):
|
||||
|
||||
if not args.readings:
|
||||
create_quality_inspection_parameter("Size")
|
||||
readings = {"specification": "Size", "min_value": 0, "max_value": 10}
|
||||
readings = {"specification": "Size", "min_value": 0, "max_value": 10, "reading_1": "5"}
|
||||
if args.status == "Rejected":
|
||||
readings["reading_1"] = "12" # status is auto set in child on save
|
||||
else:
|
||||
|
||||
@@ -1073,6 +1073,21 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
|
||||
};
|
||||
});
|
||||
|
||||
this.frm.set_query("uom", "items", function (doc, cdt, cdn) {
|
||||
let row = locals[cdt][cdn];
|
||||
|
||||
if (!row.item_code) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_item_uom_query",
|
||||
filters: {
|
||||
item_code: row.item_code,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
this.frm.fields_dict.items.grid.get_field("expense_account").get_query = function () {
|
||||
if (erpnext.is_perpetual_inventory_enabled(me.frm.doc.company)) {
|
||||
return {
|
||||
|
||||
@@ -807,43 +807,45 @@ class StockEntry(StockController):
|
||||
and target to ensure a meaningful transfer is occurring.
|
||||
|
||||
Raises:
|
||||
frappe.ValidationError: If warehouses are same and no inventory dimensions differ
|
||||
frappe.ValidationError: If warehouses are same and no inventory dimensions differ
|
||||
"""
|
||||
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
|
||||
|
||||
inventory_dimensions = get_inventory_dimensions()
|
||||
if self.purpose == "Material Transfer":
|
||||
for item in self.items:
|
||||
if cstr(item.s_warehouse) == cstr(item.t_warehouse):
|
||||
if not inventory_dimensions:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Source and Target Warehouse cannot be the same for Material Transfer"
|
||||
).format(item.idx),
|
||||
title=_("Invalid Source and Target Warehouse"),
|
||||
)
|
||||
else:
|
||||
difference_found = False
|
||||
for dimension in inventory_dimensions:
|
||||
fieldname = (
|
||||
dimension.source_fieldname
|
||||
if dimension.source_fieldname.startswith("to_")
|
||||
else f"to_{dimension.source_fieldname}"
|
||||
)
|
||||
if (
|
||||
item.get(dimension.source_fieldname)
|
||||
and item.get(fieldname)
|
||||
and item.get(dimension.source_fieldname) != item.get(fieldname)
|
||||
):
|
||||
difference_found = True
|
||||
break
|
||||
if not difference_found:
|
||||
if frappe.get_single_value("Stock Settings", "validate_material_transfer_warehouses"):
|
||||
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
|
||||
|
||||
inventory_dimensions = get_inventory_dimensions()
|
||||
if self.purpose == "Material Transfer":
|
||||
for item in self.items:
|
||||
if cstr(item.s_warehouse) == cstr(item.t_warehouse):
|
||||
if not inventory_dimensions:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Source, Target Warehouse and Inventory Dimensions cannot be the exact same for Material Transfer"
|
||||
"Row #{0}: Source and Target Warehouse cannot be the same for Material Transfer"
|
||||
).format(item.idx),
|
||||
title=_("Invalid Source and Target Warehouse"),
|
||||
)
|
||||
else:
|
||||
difference_found = False
|
||||
for dimension in inventory_dimensions:
|
||||
fieldname = (
|
||||
dimension.source_fieldname
|
||||
if dimension.source_fieldname.startswith("to_")
|
||||
else f"to_{dimension.source_fieldname}"
|
||||
)
|
||||
if (
|
||||
item.get(dimension.source_fieldname)
|
||||
and item.get(fieldname)
|
||||
and item.get(dimension.source_fieldname) != item.get(fieldname)
|
||||
):
|
||||
difference_found = True
|
||||
break
|
||||
if not difference_found:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Source, Target Warehouse and Inventory Dimensions cannot be the exact same for Material Transfer"
|
||||
).format(item.idx),
|
||||
title=_("Invalid Source and Target Warehouse"),
|
||||
)
|
||||
|
||||
def get_matched_items(self, item_code):
|
||||
for row in self.items:
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"show_barcode_field",
|
||||
"clean_description_html",
|
||||
"allow_internal_transfer_at_arms_length_price",
|
||||
"validate_material_transfer_warehouses",
|
||||
"serial_and_batch_item_settings_tab",
|
||||
"section_break_7",
|
||||
"allow_existing_serial_no",
|
||||
@@ -530,6 +531,13 @@
|
||||
"label": "Update Price List Based On",
|
||||
"mandatory_depends_on": "eval: doc.auto_insert_price_list_rate_if_missing",
|
||||
"options": "Rate\nPrice List Rate"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If enabled, the source and target warehouse in the Material Transfer Stock Entry must be different else an error will be thrown. If inventory dimensions are present, same source and target warehouse can be allowed but atleast any one of the inventory dimension fields must be different.",
|
||||
"fieldname": "validate_material_transfer_warehouses",
|
||||
"fieldtype": "Check",
|
||||
"label": "Validate Material Transfer Warehouses"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -537,8 +545,8 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-17 18:32:35.829395",
|
||||
"modified_by": "hello@aerele.in",
|
||||
"modified": "2025-11-11 11:35:39.864923",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Settings",
|
||||
"owner": "Administrator",
|
||||
@@ -563,4 +571,4 @@
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ class StockSettings(Document):
|
||||
update_price_list_based_on: DF.Literal["Rate", "Price List Rate"]
|
||||
use_naming_series: DF.Check
|
||||
use_serial_batch_fields: DF.Check
|
||||
validate_material_transfer_warehouses: DF.Check
|
||||
valuation_method: DF.Literal["FIFO", "Moving Average", "LIFO"]
|
||||
# end: auto-generated types
|
||||
|
||||
@@ -186,26 +187,6 @@ class StockSettings(Document):
|
||||
)
|
||||
)
|
||||
|
||||
else:
|
||||
# Don't allow if there are negative stock
|
||||
from frappe.query_builder.functions import Round
|
||||
|
||||
precision = frappe.db.get_single_value("System Settings", "float_precision") or 3
|
||||
bin = frappe.qb.DocType("Bin")
|
||||
bin_with_negative_stock = (
|
||||
frappe.qb.from_(bin)
|
||||
.select(bin.name)
|
||||
.where(Round(bin.actual_qty, precision) < 0)
|
||||
.limit(1)
|
||||
).run()
|
||||
|
||||
if bin_with_negative_stock:
|
||||
frappe.throw(
|
||||
_("As there are negative stock, you can not enable {0}.").format(
|
||||
frappe.bold(_("Stock Reservation"))
|
||||
)
|
||||
)
|
||||
|
||||
# Enable -> Disable
|
||||
else:
|
||||
# Don't allow if there are open Stock Reservation Entries
|
||||
|
||||
@@ -1,32 +1,37 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"apply_user_permissions": 1,
|
||||
"creation": "2013-06-13 18:42:11",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2018-02-21 01:28:47.049042",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Delivery Note Trends",
|
||||
"owner": "Administrator",
|
||||
"ref_doctype": "Delivery Note",
|
||||
"report_name": "Delivery Note Trends",
|
||||
"report_type": "Script Report",
|
||||
"add_total_row": 0,
|
||||
"add_translate_data": 0,
|
||||
"columns": [],
|
||||
"creation": "2013-06-13 18:42:11",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2025-11-05 11:55:50.114173",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Delivery Note Trends",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Delivery Note",
|
||||
"report_name": "Delivery Note Trends",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Stock User"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Stock Manager"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Sales User"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Accounts User"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"timeout": 0
|
||||
}
|
||||
|
||||
@@ -1,32 +1,37 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"apply_user_permissions": 1,
|
||||
"creation": "2013-06-13 18:45:44",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2018-02-21 01:28:22.682161",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Purchase Receipt Trends",
|
||||
"owner": "Administrator",
|
||||
"ref_doctype": "Purchase Receipt",
|
||||
"report_name": "Purchase Receipt Trends",
|
||||
"report_type": "Script Report",
|
||||
"add_total_row": 0,
|
||||
"add_translate_data": 0,
|
||||
"columns": [],
|
||||
"creation": "2013-06-13 18:45:44",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2025-11-05 11:55:49.983683",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Purchase Receipt Trends",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Purchase Receipt",
|
||||
"report_name": "Purchase Receipt Trends",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Stock Manager"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Stock User"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Purchase User"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Accounts User"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"timeout": 0
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ frappe.query_reports["Stock Qty vs Serial No Count"] = {
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "show_disables_items",
|
||||
fieldname: "show_disabled_items",
|
||||
label: __("Show Disabled Items"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
|
||||
@@ -9,7 +9,7 @@ from frappe import _
|
||||
def execute(filters=None):
|
||||
validate_warehouse(filters)
|
||||
columns = get_columns()
|
||||
data = get_data(filters.warehouse, filters.show_disables_items)
|
||||
data = get_data(filters.warehouse, filters.show_disabled_items)
|
||||
return columns, data
|
||||
|
||||
|
||||
@@ -38,9 +38,9 @@ def get_columns():
|
||||
return columns
|
||||
|
||||
|
||||
def get_data(warehouse, show_disables_items):
|
||||
def get_data(warehouse, show_disabled_items):
|
||||
filters = {"has_serial_no": True}
|
||||
if not show_disables_items:
|
||||
if not show_disabled_items:
|
||||
filters["disabled"] = False
|
||||
serial_item_list = frappe.get_all(
|
||||
"Item",
|
||||
|
||||
@@ -2,7 +2,7 @@ from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe import _, bold
|
||||
from frappe.model.naming import make_autoname
|
||||
from frappe.model.naming import NamingSeries, make_autoname, parse_naming_series
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder.functions import CombineDatetime, Sum, Timestamp
|
||||
from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, now, nowtime, today
|
||||
@@ -1334,8 +1334,16 @@ class SerialBatchCreation:
|
||||
if self.get("voucher_no"):
|
||||
voucher_no = self.get("voucher_no")
|
||||
|
||||
obj = NamingSeries(self.serial_no_series)
|
||||
current_value = obj.get_current_value()
|
||||
|
||||
def get_series(partial_series, digits):
|
||||
return f"{current_value:0{digits}d}"
|
||||
|
||||
for _i in range(abs(cint(self.actual_qty))):
|
||||
serial_no = make_autoname(self.serial_no_series, "Serial No")
|
||||
current_value += 1
|
||||
serial_no = parse_naming_series(self.serial_no_series, number_generator=get_series)
|
||||
|
||||
sr_nos.append(serial_no)
|
||||
serial_nos_details.append(
|
||||
(
|
||||
@@ -1376,6 +1384,8 @@ class SerialBatchCreation:
|
||||
|
||||
frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details))
|
||||
|
||||
obj.update_counter(current_value)
|
||||
|
||||
return sr_nos
|
||||
|
||||
|
||||
|
||||
@@ -599,7 +599,7 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
|
||||
for item in self.items:
|
||||
if flt(item.rate) and flt(item.qty):
|
||||
if warehouse_account.get(item.warehouse):
|
||||
if warehouse_account and warehouse_account.get(item.warehouse):
|
||||
stock_value_diff = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
"list_title": "",
|
||||
"login_required": 1,
|
||||
"max_attachment_size": 0,
|
||||
"modified": "2024-01-24 10:28:35.026064",
|
||||
"modified_by": "rohitw1991@gmail.com",
|
||||
"modified": "2025-10-27 21:05:21.125639",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Utilities",
|
||||
"name": "addresses",
|
||||
"owner": "Administrator",
|
||||
@@ -113,7 +113,7 @@
|
||||
"fieldname": "state",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"label": "State",
|
||||
"label": "State/Province",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
@@ -197,4 +197,4 @@
|
||||
"show_in_filter": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user