Merge branch 'develop' into use-frappe.in_test

This commit is contained in:
Sagar Vora
2025-06-20 08:09:35 +00:00
committed by GitHub
130 changed files with 16591 additions and 14032 deletions

View File

@@ -5,6 +5,9 @@ on:
- closed
- labeled
permissions:
contents: read
jobs:
main:
runs-on: ubuntu-latest

View File

@@ -2,6 +2,10 @@ name: Trigger Docker build on release
on:
release:
types: [released]
permissions:
contents: read
jobs:
curl:
runs-on: ubuntu-latest

View File

@@ -3,6 +3,9 @@ on:
pull_request:
types: [ opened, synchronize, reopened, edited ]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest

View File

@@ -2,6 +2,10 @@
# To add/remove versions just modify the matrix.
name: Create weekly release pull requests
permissions:
contents: read
on:
schedule:
# 9:30 UTC => 3 PM IST Tuesday

View File

@@ -3,6 +3,10 @@ on:
pull_request_target:
types: [opened, reopened]
permissions:
issues: write
pull-requests: write
jobs:
triage:
runs-on: ubuntu-latest

View File

@@ -3,6 +3,9 @@ name: Linters
on:
pull_request: { }
permissions:
contents: read
jobs:
linters:

View File

@@ -10,6 +10,9 @@ on:
- '**.csv'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: patch-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }}
cancel-in-progress: true

View File

@@ -11,6 +11,9 @@ on:
- "**.html"
- "**.csv"
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest

View File

@@ -3,6 +3,10 @@ on:
push:
branches:
- version-13
permissions:
contents: read
jobs:
release:
name: Release

View File

@@ -7,6 +7,9 @@ concurrency:
group: server-individual-tests-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }}
cancel-in-progress: false
permissions:
contents: read
jobs:
discover:
runs-on: ubuntu-latest

View File

@@ -10,6 +10,9 @@ on:
- "**.md"
- "**.html"
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest

View File

@@ -25,6 +25,9 @@ on:
required: false
type: string
permissions:
contents: read
concurrency:
group: server-mariadb-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }}
cancel-in-progress: true

View File

@@ -12,6 +12,9 @@ concurrency:
group: server-postgres-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }}
cancel-in-progress: true
permissions:
contents: read
jobs:
test:
if: ${{ contains(github.event.pull_request.labels.*.name, 'postgres') }}

View File

@@ -139,6 +139,11 @@ frappe.treeview_settings["Account"] = {
description: __(
"Further accounts can be made under Groups, but entries can be made against non-Groups"
),
onchange: function () {
if (!this.value) {
this.layout.set_value("root_type", "");
}
},
},
{
fieldtype: "Select",

View File

@@ -46,6 +46,7 @@
"role_to_override_stop_action",
"currency_exchange_section",
"allow_stale",
"allow_pegged_currencies_exchange_rates",
"column_break_yuug",
"stale_days",
"section_break_jpd0",
@@ -614,6 +615,13 @@
{
"fieldname": "column_break_feyo",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "Enable this field to fetch the exchange rates for Pegged Currencies.\n\n",
"fieldname": "allow_pegged_currencies_exchange_rates",
"fieldtype": "Check",
"label": "Allow Pegged Currencies Exchange Rates"
}
],
"grid_page_length": 50,
@@ -622,7 +630,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-06-06 11:03:28.095723",
"modified": "2025-06-16 16:40:54.871486",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -26,6 +26,7 @@ class AccountsSettings(Document):
acc_frozen_upto: DF.Date | None
add_taxes_from_item_tax_template: DF.Check
allow_multi_currency_invoices_against_single_party_account: DF.Check
allow_pegged_currencies_exchange_rates: DF.Check
allow_stale: DF.Check
auto_reconcile_payments: DF.Check
auto_reconciliation_job_trigger: DF.Int

View File

@@ -1,6 +1,7 @@
import frappe
from frappe.utils import flt
from rapidfuzz import fuzz, process
from rapidfuzz.utils import default_process
class AutoMatchParty:
@@ -132,6 +133,7 @@ class AutoMatchbyPartyNameDescription:
query=self.get(field),
choices={row.get("name"): row.get("party_name") for row in names},
scorer=fuzz.token_set_ratio,
processor=default_process,
)
party_name, skip = self.process_fuzzy_result(result)

View File

@@ -45,7 +45,6 @@
"default": "ACC-BTN-.YYYY.-",
"fieldname": "naming_series",
"fieldtype": "Select",
"hidden": 1,
"label": "Series",
"no_copy": 1,
"options": "ACC-BTN-.YYYY.-",
@@ -236,9 +235,10 @@
"fieldtype": "Column Break"
}
],
"grid_page_length": 50,
"is_submittable": 1,
"links": [],
"modified": "2023-11-18 18:32:47.203694",
"modified": "2025-06-18 17:24:57.044666",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Transaction",
@@ -287,9 +287,10 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "date",
"sort_order": "DESC",
"states": [],
"title_field": "bank_account",
"track_changes": 1
}
}

View File

@@ -7,10 +7,10 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"naming_series",
"budget_against",
"company",
"cost_center",
"naming_series",
"project",
"fiscal_year",
"column_break_3",
@@ -199,12 +199,12 @@
},
{
"fieldname": "naming_series",
"fieldtype": "Data",
"hidden": 1,
"fieldtype": "Select",
"label": "Series",
"no_copy": 1,
"options": "BUDGET-.YYYY.-",
"print_hide": 1,
"read_only": 1,
"reqd": 1,
"set_only_once": 1
},
{
@@ -238,7 +238,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-05-22 13:46:28.510566",
"modified": "2025-06-16 15:57:13.114981",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Budget",

View File

@@ -51,7 +51,7 @@ class Budget(Document):
cost_center: DF.Link | None
fiscal_year: DF.Link
monthly_distribution: DF.Link | None
naming_series: DF.Data | None
naming_series: DF.Literal["BUDGET-.YYYY.-"]
project: DF.Link | None
# end: auto-generated types
@@ -139,9 +139,6 @@ class Budget(Document):
):
self.applicable_on_booking_actual_expenses = 1
def before_naming(self):
self.naming_series = f"{{{frappe.scrub(self.budget_against)}}}./.{self.fiscal_year}/.###"
def validate_expense_against_budget(args, expense_amount=0):
args = frappe._dict(args)

View File

@@ -39,7 +39,16 @@ class ItemTaxTemplate(Document):
check_list = []
for d in self.get("taxes"):
if d.tax_type:
account_type = frappe.get_cached_value("Account", d.tax_type, "account_type")
account_type, account_company = frappe.get_cached_value(
"Account", d.tax_type, ["account_type", "company"]
)
if account_company != self.company:
frappe.throw(
_("Item Tax Row {0}: Account must belong to Company - {1}").format(
d.idx, frappe.bold(self.company)
)
)
if account_type not in [
"Tax",

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Pegged Currencies", {
// refresh(frm) {
// },
// });

View File

@@ -0,0 +1,47 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-05-30 11:47:03.670913",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"pegged_currencies_item_section",
"pegged_currency_item"
],
"fields": [
{
"fieldname": "pegged_currencies_item_section",
"fieldtype": "Section Break"
},
{
"fieldname": "pegged_currency_item",
"fieldtype": "Table",
"options": "Pegged Currency Details"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-06-02 11:46:31.936714",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Pegged Currencies",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,22 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class PeggedCurrencies(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
from erpnext.accounts.doctype.pegged_currencies.pegged_currencies import PeggedCurrencies
pegged_currency_item: DF.Table[PeggedCurrencies]
# end: auto-generated types
pass

View File

@@ -0,0 +1,29 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase, UnitTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class UnitTestPeggedCurrencies(UnitTestCase):
"""
Unit tests for PeggedCurrencies.
Use this class for testing individual functions and methods.
"""
pass
class IntegrationTestPeggedCurrencies(IntegrationTestCase):
"""
Integration tests for PeggedCurrencies.
Use this class for testing interactions between multiple components.
"""
pass

View File

@@ -0,0 +1,49 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-05-30 11:59:28.219277",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"source_currency",
"pegged_against",
"pegged_exchange_rate"
],
"fields": [
{
"fieldname": "source_currency",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Currency",
"options": "Currency"
},
{
"fieldname": "pegged_exchange_rate",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Exchange Rate"
},
{
"fieldname": "pegged_against",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Pegged Against",
"options": "Currency"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-06-17 14:11:16.521193",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Pegged Currency Details",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,25 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class PeggedCurrencyDetails(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
pegged_against: DF.Link | None
pegged_exchange_rate: DF.Data | None
source_currency: DF.Link | None
# end: auto-generated types
pass

View File

@@ -259,6 +259,7 @@ class POSInvoiceMergeLog(Document):
if not found:
tax.charge_type = "Actual"
tax.idx = idx
tax.row_id = None
idx += 1
tax.included_in_print_rate = 0
tax.tax_amount = tax.tax_amount_after_discount_amount

View File

@@ -40,7 +40,6 @@ from erpnext.controllers.accounts_controller import validate_account_head
from erpnext.controllers.buying_controller import BuyingController
from erpnext.stock import get_warehouse_account_map
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
get_item_account_wise_additional_cost,
update_billed_amount_based_on_po,
)
@@ -940,7 +939,7 @@ class PurchaseInvoice(BuyingController):
if self.update_stock and self.auto_accounting_for_stock:
warehouse_account = get_warehouse_account_map(self.company)
landed_cost_entries = get_item_account_wise_additional_cost(self.name)
landed_cost_entries = self.get_item_account_wise_lcv_entries()
voucher_wise_stock_value = {}
if self.update_stock:

View File

@@ -1660,7 +1660,7 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
pi = create_purchase_invoice_from_receipt(pr.name)
pi.set_posting_time = 1
pi.posting_date = add_days(pr.posting_date, -1)
pi.posting_date = add_days(pr.posting_date, 1)
pi.items[0].expense_account = "Cost of Goods Sold - _TC"
pi.save()
pi.submit()
@@ -1669,30 +1669,38 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
# Check GLE for Purchase Invoice
expected_gle = [
["Cost of Goods Sold - _TC", 250, 0, add_days(pr.posting_date, -1)],
["Creditors - _TC", 0, 250, add_days(pr.posting_date, -1)],
["Cost of Goods Sold - _TC", 250, 0, add_days(pr.posting_date, 1)],
["Creditors - _TC", 0, 250, add_days(pr.posting_date, 1)],
]
check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
expected_gle_for_purchase_receipt = [
["Provision Account - _TC", 250, 0, pr.posting_date],
["_Test Account Cost for Goods Sold - _TC", 0, 250, pr.posting_date],
["Provision Account - _TC", 0, 250, pi.posting_date],
["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date],
["_Test Account Cost for Goods Sold - _TC", 250, 0, pr.posting_date],
["Provision Account - _TC", 0, 250, pr.posting_date],
["_Test Account Cost for Goods Sold - _TC", 0, 250, pi.posting_date],
["Provision Account - _TC", 250, 0, pi.posting_date],
]
check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
check_gl_entries(
self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date, voucher_type="Purchase Receipt"
)
# Cancel purchase invoice to check reverse provisional entry cancellation
pi.cancel()
expected_gle_for_purchase_receipt_post_pi_cancel = [
["Provision Account - _TC", 0, 250, pi.posting_date],
["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date],
["Provision Account - _TC", 0, 250, pi.posting_date],
]
check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt_post_pi_cancel, pr.posting_date)
check_gl_entries(
self,
pr.name,
expected_gle_for_purchase_receipt_post_pi_cancel,
pi.posting_date,
voucher_type="Purchase Receipt",
)
toggle_provisional_accounting_setting()
@@ -1713,7 +1721,7 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
# Overbill PR: rate = 2000, qty = 10
pi = create_purchase_invoice_from_receipt(pr.name)
pi.set_posting_time = 1
pi.posting_date = add_days(pr.posting_date, -1)
pi.posting_date = add_days(pr.posting_date, 1)
pi.items[0].qty = 10
pi.items[0].rate = 2000
pi.items[0].expense_account = "Cost of Goods Sold - _TC"
@@ -1721,30 +1729,38 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
pi.submit()
expected_gle = [
["Cost of Goods Sold - _TC", 20000, 0, add_days(pr.posting_date, -1)],
["Creditors - _TC", 0, 20000, add_days(pr.posting_date, -1)],
["Cost of Goods Sold - _TC", 20000, 0, add_days(pr.posting_date, 1)],
["Creditors - _TC", 0, 20000, add_days(pr.posting_date, 1)],
]
check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
expected_gle_for_purchase_receipt = [
["Provision Account - _TC", 5000, 0, pr.posting_date],
["_Test Account Cost for Goods Sold - _TC", 0, 5000, pr.posting_date],
["Provision Account - _TC", 0, 5000, pi.posting_date],
["_Test Account Cost for Goods Sold - _TC", 5000, 0, pi.posting_date],
["_Test Account Cost for Goods Sold - _TC", 5000, 0, pr.posting_date],
["Provision Account - _TC", 0, 5000, pr.posting_date],
["_Test Account Cost for Goods Sold - _TC", 0, 5000, pi.posting_date],
["Provision Account - _TC", 5000, 0, pi.posting_date],
]
check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
check_gl_entries(
self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date, voucher_type="Purchase Receipt"
)
# Cancel purchase invoice to check reverse provisional entry cancellation
pi.cancel()
expected_gle_for_purchase_receipt_post_pi_cancel = [
["Provision Account - _TC", 0, 5000, pi.posting_date],
["_Test Account Cost for Goods Sold - _TC", 5000, 0, pi.posting_date],
["Provision Account - _TC", 0, 5000, pi.posting_date],
]
check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt_post_pi_cancel, pr.posting_date)
check_gl_entries(
self,
pr.name,
expected_gle_for_purchase_receipt_post_pi_cancel,
pi.posting_date,
voucher_type="Purchase Receipt",
)
toggle_provisional_accounting_setting()
@@ -1777,13 +1793,76 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
expected_gle_for_purchase_receipt = [
["Provision Account - _TC", 5000, 0, pr.posting_date],
["_Test Account Cost for Goods Sold - _TC", 0, 5000, pr.posting_date],
["Provision Account - _TC", 0, 1000, pi.posting_date],
["_Test Account Cost for Goods Sold - _TC", 1000, 0, pi.posting_date],
["_Test Account Cost for Goods Sold - _TC", 5000, 0, pr.posting_date],
["Provision Account - _TC", 0, 5000, pr.posting_date],
["_Test Account Cost for Goods Sold - _TC", 0, 1000, pi.posting_date],
["Provision Account - _TC", 1000, 0, pi.posting_date],
]
check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
check_gl_entries(
self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date, voucher_type="Purchase Receipt"
)
toggle_provisional_accounting_setting()
def test_provisional_accounting_entry_multi_currency(self):
setup_provisional_accounting()
pr = make_purchase_receipt(
item_code="_Test Non Stock Item",
posting_date=add_days(nowdate(), -2),
qty=1000,
rate=111.11,
currency="USD",
do_not_save=1,
supplier="_Test Supplier USD",
)
pr.conversion_rate = 0.014783000
pr.save()
pr.submit()
pi = create_purchase_invoice_from_receipt(pr.name)
pi.set_posting_time = 1
pi.posting_date = add_days(pr.posting_date, 1)
pi.items[0].expense_account = "Cost of Goods Sold - _TC"
pi.save()
pi.submit()
self.assertEqual(pr.items[0].provisional_expense_account, "Provision Account - _TC")
# Check GLE for Purchase Invoice
expected_gle = [
["_Test Payable USD - _TC", 0, 1642.54, add_days(pr.posting_date, 1)],
["Cost of Goods Sold - _TC", 1642.54, 0, add_days(pr.posting_date, 1)],
]
check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
expected_gle_for_purchase_receipt = [
["_Test Account Cost for Goods Sold - _TC", 1642.54, 0, pr.posting_date],
["Provision Account - _TC", 0, 1642.54, pr.posting_date],
["_Test Account Cost for Goods Sold - _TC", 0, 1642.54, pi.posting_date],
["Provision Account - _TC", 1642.54, 0, pi.posting_date],
]
check_gl_entries(
self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date, voucher_type="Purchase Receipt"
)
# Cancel purchase invoice to check reverse provisional entry cancellation
pi.cancel()
expected_gle_for_purchase_receipt_post_pi_cancel = [
["_Test Account Cost for Goods Sold - _TC", 1642.54, 0, pi.posting_date],
["Provision Account - _TC", 0, 1642.54, pi.posting_date],
]
check_gl_entries(
self,
pr.name,
expected_gle_for_purchase_receipt_post_pi_cancel,
pi.posting_date,
voucher_type="Purchase Receipt",
)
toggle_provisional_accounting_setting()

View File

@@ -424,6 +424,8 @@ def get_party_account(party_type, party=None, company=None, include_advance=Fals
Will first search in party (Customer / Supplier) record, if not found,
will search in group (Customer Group / Supplier Group),
finally will return default."""
if not party_type:
frappe.throw(_("Party Type is mandatory"))
if not company:
frappe.throw(_("Please select a Company"))

View File

@@ -49,7 +49,8 @@ class ReceivablePayableReport:
self.filters.report_date = getdate(self.filters.report_date or nowdate())
self.age_as_on = (
getdate(nowdate())
if self.filters.calculate_ageing_with == "Today Date"
if "calculate_ageing_with" not in self.filters
or self.filters.calculate_ageing_with == "Today Date"
else self.filters.report_date
)

View File

@@ -200,7 +200,8 @@ def get_gl_entries(filters, accounting_dimensions):
voucher_type, voucher_subtype, voucher_no, {dimension_fields}
cost_center, project, {transaction_currency_fields}
against_voucher_type, against_voucher, account_currency,
against, is_opening, creation {select_fields}
against, is_opening, creation {select_fields},
transaction_currency
from `tabGL Entry`
where company=%(company)s {get_conditions(filters)}
{order_by_statement}

View File

@@ -3,12 +3,15 @@
import frappe
from frappe import qb
from frappe.tests import IntegrationTestCase
from frappe.tests import IntegrationTestCase, change_settings
from frappe.utils import flt, today
from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.general_ledger.general_ledger import execute
from erpnext.controllers.sales_and_purchase_return import make_return_doc
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
class TestGeneralLedger(IntegrationTestCase):
@@ -168,6 +171,90 @@ class TestGeneralLedger(IntegrationTestCase):
self.assertEqual(data[3]["debit"], 100)
self.assertEqual(data[3]["credit"], 100)
@change_settings("Accounts Settings", {"delete_linked_ledger_entries": True})
def test_debit_in_exchange_gain_loss_account(self):
company = "_Test Company"
exchange_gain_loss_account = frappe.db.get_value("Company", "exchange_gain_loss_account")
if not exchange_gain_loss_account:
frappe.db.set_value(
"Company", company, "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"
)
account_name = "_Test Receivable USD - _TC"
customer_name = "_Test Customer USD"
sales_invoice = create_sales_invoice(
company=company,
customer=customer_name,
currency="USD",
debit_to=account_name,
conversion_rate=85,
posting_date=today(),
)
payment_entry = create_payment_entry(
company=company,
party_type="Customer",
party=customer_name,
payment_type="Receive",
paid_from=account_name,
paid_from_account_currency="USD",
paid_to="Cash - _TC",
paid_to_account_currency="INR",
paid_amount=10,
do_not_submit=True,
)
payment_entry.base_paid_amount = 800
payment_entry.received_amount = 800
payment_entry.currency = "USD"
payment_entry.source_exchange_rate = 80
payment_entry.append(
"references",
frappe._dict(
{
"reference_doctype": "Sales Invoice",
"reference_name": sales_invoice.name,
"total_amount": 10,
"outstanding_amount": 10,
"exchange_rate": 85,
"allocated_amount": 10,
"exchange_gain_loss": -50,
}
),
)
payment_entry.save()
payment_entry.submit()
journal_entry = frappe.get_all(
"Journal Entry Account", filters={"reference_name": sales_invoice.name}, fields=["parent"]
)
columns, data = execute(
frappe._dict(
{
"company": company,
"from_date": today(),
"to_date": today(),
"include_dimensions": 1,
"include_default_book_entries": 1,
"account": ["_Test Exchange Gain/Loss - _TC"],
"categorize_by": "Categorize by Voucher (Consolidated)",
}
)
)
entry = data[1]
self.assertEqual(entry["debit"], 50)
self.assertEqual(entry["voucher_type"], "Journal Entry")
self.assertEqual(entry["voucher_no"], journal_entry[0]["parent"])
payment_entry.cancel()
payment_entry.delete()
sales_invoice.reload()
sales_invoice.cancel()
sales_invoice.delete()
def test_ignore_exchange_rate_journals_filter(self):
# create a new account with USD currency
account_name = "Test Debtors USD"

View File

@@ -101,13 +101,18 @@ def convert_to_presentation_currency(gl_entries, currency_info):
account_currencies = list(set(entry["account_currency"] for entry in gl_entries))
for entry in gl_entries:
transaction_currency = entry.get("transaction_currency")
debit = flt(entry["debit"])
credit = flt(entry["credit"])
debit_in_account_currency = flt(entry["debit_in_account_currency"])
credit_in_account_currency = flt(entry["credit_in_account_currency"])
account_currency = entry["account_currency"]
if len(account_currencies) == 1 and account_currency == presentation_currency:
if (
len(account_currencies) == 1
and account_currency == presentation_currency
and (transaction_currency is None or account_currency == transaction_currency)
):
entry["debit"] = debit_in_account_currency
entry["credit"] = credit_in_account_currency
else:

View File

@@ -72,6 +72,12 @@ frappe.ui.form.on("Asset", {
filters: { item_code: doc.item_code },
};
});
if (frm.doc.docstatus == 1) {
frm.custom_make_buttons = {
"Asset Capitalization": "Asset Capitalization",
};
}
},
refresh: function (frm) {

View File

@@ -208,14 +208,11 @@ class Asset(AccountsController):
add_asset_activity(self.name, _("Asset cancelled"))
def after_insert(self):
if (
not frappe.db.exists(
{
"doctype": "Asset Activity",
"asset": self.name,
}
)
and not self.flags.asset_created_via_asset_capitalization
if not frappe.db.exists(
{
"doctype": "Asset Activity",
"asset": self.name,
}
):
add_asset_activity(self.name, _("Asset created"))
@@ -1006,7 +1003,6 @@ def create_asset_capitalization(company, asset, asset_name, item_code):
{
"target_asset": asset,
"company": company,
"capitalization_method": "Choose a WIP composite asset",
"target_asset_name": asset_name,
"target_item_code": item_code,
}

View File

@@ -1748,6 +1748,7 @@ def create_asset(**args):
"asset_owner": args.asset_owner or "Company",
"is_existing_asset": args.is_existing_asset or 1,
"is_composite_asset": args.is_composite_asset or 0,
"is_composite_component": args.is_composite_component or 0,
"asset_quantity": args.get("asset_quantity") or 1,
"depr_entry_posting_status": args.depr_entry_posting_status or "",
}

View File

@@ -134,10 +134,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
}
target_asset() {
if (
this.frm.doc.target_asset &&
this.frm.doc.capitalization_method === "Choose a WIP composite asset"
) {
if (this.frm.doc.target_asset) {
this.set_consumed_stock_items_tagged_to_wip_composite_asset(this.frm.doc.target_asset);
this.get_target_asset_details();
}

View File

@@ -9,19 +9,16 @@
"field_order": [
"title",
"naming_series",
"capitalization_method",
"target_item_code",
"target_item_name",
"target_asset",
"target_asset_name",
"target_item_code",
"finance_book",
"target_qty",
"target_asset_location",
"column_break_9",
"company",
"posting_date",
"posting_time",
"set_posting_time",
"finance_book",
"target_batch_no",
"target_serial_no",
"amended_from",
@@ -54,20 +51,12 @@
"label": "Title"
},
{
"depends_on": "eval:(doc.target_item_code && !doc.__islocal && doc.capitalization_method !== 'Choose a WIP composite asset') || doc.capitalization_method=='Create a new composite asset'",
"depends_on": "eval:(doc.target_item_code && !doc.__islocal)",
"fieldname": "target_item_code",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Target Item Code",
"mandatory_depends_on": "eval:doc.capitalization_method=='Create a new composite asset'",
"options": "Item"
},
{
"depends_on": "eval:doc.target_item_code && doc.target_item_name != doc.target_item_code",
"fetch_from": "target_item_code.item_name",
"fieldname": "target_item_name",
"fieldtype": "Data",
"label": "Target Item Name",
"options": "Item",
"read_only": 1
},
{
@@ -80,18 +69,14 @@
"read_only": 1
},
{
"depends_on": "eval:(doc.target_asset && !doc.__islocal) || doc.capitalization_method=='Choose a WIP composite asset'",
"fieldname": "target_asset",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Target Asset",
"mandatory_depends_on": "eval:doc.capitalization_method=='Choose a WIP composite asset'",
"no_copy": 1,
"options": "Asset",
"read_only_depends_on": "eval:doc.capitalization_method=='Create a new composite asset'"
"options": "Asset"
},
{
"depends_on": "eval:(doc.target_asset_name && !doc.__islocal) || (doc.target_asset && doc.capitalization_method=='Choose a WIP composite asset')",
"fetch_from": "target_asset.asset_name",
"fieldname": "target_asset_name",
"fieldtype": "Data",
@@ -176,7 +161,9 @@
"default": "1",
"fieldname": "target_qty",
"fieldtype": "Float",
"label": "Target Qty"
"hidden": 1,
"label": "Target Qty",
"read_only": 1
},
{
"default": "0",
@@ -298,26 +285,12 @@
"label": "Target Fixed Asset Account",
"options": "Account",
"read_only": 1
},
{
"depends_on": "eval:doc.capitalization_method=='Create a new composite asset'",
"fieldname": "target_asset_location",
"fieldtype": "Link",
"label": "Target Asset Location",
"mandatory_depends_on": "eval:doc.capitalization_method=='Create a new composite asset'",
"options": "Location"
},
{
"fieldname": "capitalization_method",
"fieldtype": "Select",
"label": "Capitalization Method",
"options": "\nCreate a new composite asset\nChoose a WIP composite asset"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-01-08 13:14:33.008458",
"modified": "2025-05-20 15:15:12.110035",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Capitalization",
@@ -355,10 +328,11 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "title",
"track_changes": 1,
"track_seen": 1
}
}

View File

@@ -13,6 +13,7 @@ import erpnext
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
from erpnext.assets.doctype.asset.depreciation import (
depreciate_asset,
get_disposal_account_and_cost_center,
get_gl_entries_on_asset_disposal,
get_value_after_depreciation_on_disposal_date,
reset_depreciation_schedule,
@@ -70,7 +71,6 @@ class AssetCapitalization(StockController):
amended_from: DF.Link | None
asset_items: DF.Table[AssetCapitalizationAssetItem]
asset_items_total: DF.Currency
capitalization_method: DF.Literal["", "Create new composite asset", "Use existing composite asset"]
company: DF.Link
cost_center: DF.Link | None
finance_book: DF.Link | None
@@ -83,7 +83,6 @@ class AssetCapitalization(StockController):
stock_items: DF.Table[AssetCapitalizationStockItem]
stock_items_total: DF.Currency
target_asset: DF.Link | None
target_asset_location: DF.Link | None
target_asset_name: DF.Data | None
target_batch_no: DF.Link | None
target_fixed_asset_account: DF.Link | None
@@ -92,7 +91,6 @@ class AssetCapitalization(StockController):
target_incoming_rate: DF.Currency
target_is_fixed_asset: DF.Check
target_item_code: DF.Link | None
target_item_name: DF.Data | None
target_qty: DF.Float
target_serial_no: DF.SmallText | None
title: DF.Data | None
@@ -118,7 +116,7 @@ class AssetCapitalization(StockController):
def before_submit(self):
self.validate_source_mandatory()
self.create_target_asset()
# self.create_target_asset()
def on_submit(self):
self.make_bundle_using_old_serial_batch_fields()
@@ -143,7 +141,7 @@ class AssetCapitalization(StockController):
self.update_target_asset()
def set_title(self):
self.title = self.target_asset_name or self.target_item_name or self.target_item_code
self.title = self.target_asset_name or self.target_item_code
def set_missing_values(self, for_validate=False):
target_item_details = get_target_item_details(self.target_item_code, self.company)
@@ -301,16 +299,7 @@ class AssetCapitalization(StockController):
d.cost_center = frappe.get_cached_value("Company", self.company, "cost_center")
def validate_source_mandatory(self):
if self.capitalization_method == "Create a new composite asset" and not (
self.get("stock_items") or self.get("asset_items")
):
frappe.throw(
_(
"Consumed Stock Items or Consumed Asset Items are mandatory for creating new composite asset"
)
)
elif not (self.get("stock_items") or self.get("asset_items") or self.get("service_items")):
if not (self.get("stock_items") or self.get("asset_items") or self.get("service_items")):
frappe.throw(
_(
"Consumed Stock Items, Consumed Asset Items or Consumed Service Items is mandatory for Capitalization"
@@ -441,7 +430,11 @@ class AssetCapitalization(StockController):
self.get_gl_entries_for_consumed_asset_items(gl_entries, target_account, target_against, precision)
self.get_gl_entries_for_consumed_service_items(gl_entries, target_account, target_against, precision)
self.get_gl_entries_for_target_item(gl_entries, target_account, target_against, precision)
composite_component_value = self.get_composite_component_value()
self.get_gl_entries_for_target_item(
gl_entries, target_account, target_against, precision, composite_component_value
)
return gl_entries
@@ -493,34 +486,34 @@ class AssetCapitalization(StockController):
for item in self.asset_items:
asset = frappe.get_doc("Asset", item.asset)
if asset.calculate_depreciation:
notes = _(
"This schedule was created when Asset {0} was consumed through Asset Capitalization {1}."
).format(
get_link_to_form(asset.doctype, asset.name),
get_link_to_form(self.doctype, self.get("name")),
)
depreciate_asset(asset, self.posting_date, notes)
asset.reload()
if not asset.is_composite_component:
if asset.calculate_depreciation:
notes = _(
"This schedule was created when Asset {0} was consumed through Asset Capitalization {1}."
).format(
get_link_to_form(asset.doctype, asset.name),
get_link_to_form(self.doctype, self.get("name")),
)
depreciate_asset(asset, self.posting_date, notes)
asset.reload()
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
asset,
item.asset_value,
item.get("finance_book") or self.get("finance_book"),
self.get("doctype"),
self.get("name"),
self.get("posting_date"),
)
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
asset,
item.asset_value,
item.get("finance_book") or self.get("finance_book"),
self.get("doctype"),
self.get("name"),
self.get("posting_date"),
)
for gle in fixed_asset_gl_entries:
gle["against"] = target_account
gl_entries.append(self.get_gl_dict(gle, item=item))
target_against.add(gle["account"])
asset.db_set("disposal_date", self.posting_date)
self.set_consumed_asset_status(asset)
for gle in fixed_asset_gl_entries:
gle["against"] = target_account
gl_entries.append(self.get_gl_dict(gle, item=item))
target_against.add(gle["account"])
def get_gl_entries_for_consumed_service_items(
self, gl_entries, target_account, target_against, precision
):
@@ -543,65 +536,35 @@ class AssetCapitalization(StockController):
)
)
def get_gl_entries_for_target_item(self, gl_entries, target_account, target_against, precision):
def get_composite_component_value(self):
composite_component_value = 0
for item in self.asset_items:
asset = frappe.db.get_value("Asset", item.asset, ["is_composite_component"], as_dict=True)
if asset and asset.is_composite_component:
composite_component_value += flt(item.asset_value, item.precision("asset_value"))
return composite_component_value
def get_gl_entries_for_target_item(
self, gl_entries, target_account, target_against, precision, composite_component_value
):
if self.target_is_fixed_asset:
# Capitalization
gl_entries.append(
self.get_gl_dict(
{
"account": target_account,
"against": ", ".join(target_against),
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
"debit": flt(self.total_value, precision),
"cost_center": self.get("cost_center"),
},
item=self,
total_value = flt(self.total_value - composite_component_value, precision)
if total_value:
# Capitalization
gl_entries.append(
self.get_gl_dict(
{
"account": target_account,
"against": ", ".join(target_against),
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
"debit": total_value,
"cost_center": self.get("cost_center"),
},
item=self,
)
)
)
def create_target_asset(self):
if self.capitalization_method != "Create a new composite asset":
return
total_target_asset_value = flt(self.total_value, self.precision("total_value"))
asset_doc = frappe.new_doc("Asset")
asset_doc.company = self.company
asset_doc.item_code = self.target_item_code
asset_doc.is_composite_asset = 1
asset_doc.location = self.target_asset_location
asset_doc.available_for_use_date = self.posting_date
asset_doc.purchase_date = self.posting_date
asset_doc.gross_purchase_amount = total_target_asset_value
asset_doc.purchase_amount = total_target_asset_value
asset_doc.flags.ignore_validate = True
asset_doc.flags.asset_created_via_asset_capitalization = True
asset_doc.insert()
self.target_asset = asset_doc.name
self.target_fixed_asset_account = get_asset_category_account(
"fixed_asset_account", item=self.target_item_code, company=asset_doc.company
)
asset_doc.set_status("Work In Progress")
add_asset_activity(
asset_doc.name,
_("Asset created after Asset Capitalization {0} was submitted").format(
get_link_to_form("Asset Capitalization", self.name)
),
)
frappe.msgprint(
_("Asset {0} has been created. Please set the depreciation details if any and submit it.").format(
get_link_to_form("Asset", asset_doc.name)
)
)
def update_target_asset(self):
if self.capitalization_method != "Choose a WIP composite asset":
return
total_target_asset_value = flt(self.total_value, self.precision("total_value"))
asset_doc = frappe.get_doc("Asset", self.target_asset)

View File

@@ -59,10 +59,16 @@ class TestAssetCapitalization(IntegrationTestCase):
company=company,
)
wip_composite_asset = create_asset(
asset_name="Asset Capitalization WIP Composite Asset",
is_composite_asset=1,
warehouse="Stores - TCP1",
company=company,
)
# Create and submit Asset Captitalization
asset_capitalization = create_asset_capitalization(
capitalization_method="Create a new composite asset",
target_item_code="Macbook Pro",
target_asset=wip_composite_asset.name,
target_asset_location="Test Location",
stock_qty=stock_qty,
stock_rate=stock_rate,
@@ -148,10 +154,16 @@ class TestAssetCapitalization(IntegrationTestCase):
company=company,
)
wip_composite_asset = create_asset(
asset_name="Asset Capitalization WIP Composite Asset",
is_composite_asset=1,
warehouse="Stores - TCP1",
company=company,
)
# Create and submit Asset Captitalization
asset_capitalization = create_asset_capitalization(
capitalization_method="Create a new composite asset",
target_item_code="Macbook Pro",
target_asset=wip_composite_asset.name,
target_asset_location="Test Location",
stock_qty=stock_qty,
stock_rate=stock_rate,
@@ -240,7 +252,6 @@ class TestAssetCapitalization(IntegrationTestCase):
# Create and submit Asset Captitalization
asset_capitalization = create_asset_capitalization(
capitalization_method="Choose a WIP composite asset",
target_asset=wip_composite_asset.name,
target_asset_location="Test Location",
stock_qty=stock_qty,
@@ -251,7 +262,6 @@ class TestAssetCapitalization(IntegrationTestCase):
)
# Test Asset Capitalization values
self.assertEqual(asset_capitalization.capitalization_method, "Choose a WIP composite asset")
self.assertEqual(asset_capitalization.target_qty, 1)
self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate)
@@ -310,7 +320,6 @@ class TestAssetCapitalization(IntegrationTestCase):
# Create and submit Asset Captitalization
asset_capitalization = create_asset_capitalization(
capitalization_method="Choose a WIP composite asset",
target_asset=wip_composite_asset.name,
target_asset_location="Test Location",
service_qty=service_qty,
@@ -340,6 +349,50 @@ class TestAssetCapitalization(IntegrationTestCase):
self.assertFalse(get_actual_gle_dict(asset_capitalization.name))
self.assertFalse(get_actual_sle_dict(asset_capitalization.name))
def test_capitalize_composite_component(self):
company = "_Test Company with perpetual inventory"
set_depreciation_settings_in_company(company=company)
name = frappe.db.get_value(
"Asset Category Account",
filters={"parent": "Computers", "company_name": company},
fieldname=["name"],
)
frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", "")
wip_composite_asset = create_asset(
asset_name="Asset Capitalization WIP Composite Asset",
is_composite_asset=1,
warehouse="Stores - TCP1",
company=company,
)
consumed_asset_value = 100000
consumed_asset = create_asset(
asset_name="Asset Capitalization Consumable Asset",
asset_value=consumed_asset_value,
submit=1,
warehouse="Stores - _TC",
is_composite_component=1,
company=company,
)
# Create and submit Asset Captitalization
asset_capitalization = create_asset_capitalization(
target_asset=wip_composite_asset.name,
target_asset_location="Test Location",
consumed_asset=consumed_asset.name,
company=company,
submit=1,
)
# Test Asset Capitalization values
self.assertEqual(asset_capitalization.target_qty, 1)
self.assertEqual(asset_capitalization.asset_items[0].asset_value, consumed_asset_value)
actual_gle = get_actual_gle_dict(asset_capitalization.name)
self.assertEqual(actual_gle, {})
def create_asset_capitalization_data():
create_item("Capitalization Target Stock Item", is_stock_item=1, is_fixed_asset=0, is_purchase_item=0)
@@ -362,7 +415,6 @@ def create_asset_capitalization(**args):
asset_capitalization = frappe.new_doc("Asset Capitalization")
asset_capitalization.update(
{
"capitalization_method": args.capitalization_method or None,
"company": company,
"posting_date": args.posting_date or now.strftime("%Y-%m-%d"),
"posting_time": args.posting_time or now.strftime("%H:%M:%S.%f"),

View File

@@ -171,6 +171,7 @@ class AssetValueAdjustment(Document):
asset = self.update_asset_value_after_depreciation()
note = self.get_adjustment_note()
reschedule_depreciation(asset, note)
asset.set_status()
def update_asset_value_after_depreciation(self):
difference_amount = self.difference_amount if self.docstatus == 1 else -1 * self.difference_amount

View File

@@ -59,6 +59,19 @@ frappe.ui.form.on("Purchase Order", {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
schedule_date(frm) {
if (frm.doc.schedule_date) {
frm.doc.items.forEach((d) => {
frappe.model.set_value(d.doctype, d.name, "schedule_date", frm.doc.schedule_date);
});
}
},
transaction_date(frm) {
prevent_past_schedule_dates(frm);
frm.set_value("schedule_date", "");
},
refresh: function (frm) {
if (frm.doc.is_old_subcontracting_flow) {
frm.trigger("get_materials_from_supplier");
@@ -75,6 +88,7 @@ frappe.ui.form.on("Purchase Order", {
if (frm.doc.docstatus == 0) {
erpnext.set_unit_price_items_note(frm);
}
prevent_past_schedule_dates(frm);
},
supplier: function (frm) {
@@ -776,10 +790,6 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
items_on_form_rendered() {
set_schedule_date(this.frm);
}
schedule_date() {
set_schedule_date(this.frm);
}
};
// for backward compatibility: combine new and previous states
@@ -835,3 +845,11 @@ frappe.ui.form.on("Purchase Order", "is_subcontracted", function (frm) {
erpnext.buying.get_default_bom(frm);
}
});
function prevent_past_schedule_dates(frm) {
if (frm.doc.transaction_date) {
frm.fields_dict["schedule_date"].datepicker.update({
minDate: new Date(frm.doc.transaction_date),
});
}
}

View File

@@ -30,11 +30,15 @@
"stock_qty",
"sec_break_price_list",
"price_list_rate",
"base_price_list_rate",
"discount_and_margin_section",
"margin_type",
"margin_rate_or_amount",
"rate_with_margin",
"col_break_6",
"discount_percentage",
"discount_amount",
"distributed_discount_amount",
"col_break_price_list",
"base_price_list_rate",
"sec_break1",
"rate",
"amount",
@@ -531,10 +535,6 @@
"fieldname": "sec_break_price_list",
"fieldtype": "Section Break"
},
{
"fieldname": "col_break_price_list",
"fieldtype": "Column Break"
},
{
"collapsible": 1,
"fieldname": "ad_sec_break",
@@ -572,21 +572,57 @@
"fieldtype": "Currency",
"label": "Distributed Discount Amount",
"options": "currency"
},
{
"depends_on": "price_list_rate",
"fieldname": "margin_type",
"fieldtype": "Select",
"label": "Margin Type",
"options": "\nPercentage\nAmount",
"print_hide": 1
},
{
"depends_on": "eval:doc.margin_type && doc.price_list_rate",
"fieldname": "margin_rate_or_amount",
"fieldtype": "Float",
"label": "Margin Rate or Amount",
"print_hide": 1
},
{
"collapsible": 1,
"collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount || doc.distributed_discount_amount",
"fieldname": "discount_and_margin_section",
"fieldtype": "Section Break",
"label": "Discount and Margin"
},
{
"depends_on": "eval:doc.margin_type && doc.price_list_rate && doc.margin_rate_or_amount",
"fieldname": "rate_with_margin",
"fieldtype": "Currency",
"label": "Rate With Margin",
"options": "currency",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "col_break_6",
"fieldtype": "Column Break"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-06-02 06:22:17.864822",
"modified": "2025-06-17 12:05:52.441645",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Quotation Item",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -38,6 +38,8 @@ class SupplierQuotationItem(Document):
lead_time_days: DF.Int
manufacturer: DF.Link | None
manufacturer_part_no: DF.Data | None
margin_rate_or_amount: DF.Float
margin_type: DF.Literal["", "Percentage", "Amount"]
material_request: DF.Link | None
material_request_item: DF.Data | None
net_amount: DF.Currency
@@ -52,6 +54,7 @@ class SupplierQuotationItem(Document):
project: DF.Link | None
qty: DF.Float
rate: DF.Currency
rate_with_margin: DF.Currency
request_for_quotation: DF.Link | None
request_for_quotation_item: DF.Data | None
sales_order: DF.Link | None

View File

@@ -241,18 +241,6 @@ class BuyingController(SubcontractingController):
return [d.item_code for d in self.items if d.is_fixed_asset]
def set_landed_cost_voucher_amount(self):
for d in self.get("items"):
lc_voucher_data = frappe.db.sql(
"""select sum(applicable_charges), cost_center
from `tabLanded Cost Item`
where docstatus = 1 and purchase_receipt_item = %s and receipt_document = %s""",
(d.name, self.name),
)
d.landed_cost_voucher_amount = lc_voucher_data[0][0] if lc_voucher_data else 0.0
if not d.cost_center and lc_voucher_data and lc_voucher_data[0][1]:
d.db_set("cost_center", lc_voucher_data[0][1])
def validate_from_warehouse(self):
for item in self.get("items"):
if item.get("from_warehouse") and (item.get("from_warehouse") == item.get("warehouse")):

View File

@@ -164,6 +164,17 @@ status_map = {
["Draft", None],
["Completed", "eval:self.docstatus == 1"],
],
"Pick List": [
["Draft", None],
["Open", "eval:self.docstatus == 1"],
["Completed", "stock_entry_exists"],
[
"Partly Delivered",
"eval:self.purpose == 'Delivery' and self.delivery_status == 'Partly Delivered'",
],
["Completed", "eval:self.purpose == 'Delivery' and self.delivery_status == 'Fully Delivered'"],
["Cancelled", "eval:self.docstatus == 2"],
],
}

View File

@@ -6,6 +6,7 @@ from collections import defaultdict
import frappe
from frappe import _, bold
from frappe.query_builder.functions import Sum
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate
import erpnext
@@ -884,6 +885,91 @@ class StockController(AccountsController):
return sl_dict
def set_landed_cost_voucher_amount(self):
for d in self.get("items"):
lcv_item = frappe.qb.DocType("Landed Cost Item")
query = (
frappe.qb.from_(lcv_item)
.select(Sum(lcv_item.applicable_charges), lcv_item.cost_center)
.where((lcv_item.docstatus == 1) & (lcv_item.receipt_document == self.name))
)
if self.doctype == "Stock Entry":
query = query.where(lcv_item.stock_entry_item == d.name)
else:
query = query.where(lcv_item.purchase_receipt_item == d.name)
lc_voucher_data = query.run(as_list=True)
d.landed_cost_voucher_amount = lc_voucher_data[0][0] if lc_voucher_data else 0.0
if not d.cost_center and lc_voucher_data and lc_voucher_data[0][1]:
d.db_set("cost_center", lc_voucher_data[0][1])
def has_landed_cost_amount(self):
for row in self.items:
if row.get("landed_cost_voucher_amount"):
return True
return False
def get_item_account_wise_lcv_entries(self):
if not self.has_landed_cost_amount():
return
landed_cost_vouchers = frappe.get_all(
"Landed Cost Purchase Receipt",
fields=["parent"],
filters={"receipt_document": self.name, "docstatus": 1},
)
if not landed_cost_vouchers:
return
item_account_wise_cost = {}
row_fieldname = "purchase_receipt_item"
if self.doctype == "Stock Entry":
row_fieldname = "stock_entry_item"
for lcv in landed_cost_vouchers:
landed_cost_voucher_doc = frappe.get_doc("Landed Cost Voucher", lcv.parent)
based_on_field = "applicable_charges"
# Use amount field for total item cost for manually cost distributed LCVs
if landed_cost_voucher_doc.distribute_charges_based_on != "Distribute Manually":
based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on)
total_item_cost = 0
if based_on_field:
for item in landed_cost_voucher_doc.items:
total_item_cost += item.get(based_on_field)
for item in landed_cost_voucher_doc.items:
if item.receipt_document == self.name:
for account in landed_cost_voucher_doc.taxes:
exchange_rate = account.exchange_rate or 1
item_account_wise_cost.setdefault((item.item_code, item.get(row_fieldname)), {})
item_account_wise_cost[(item.item_code, item.get(row_fieldname))].setdefault(
account.expense_account, {"amount": 0.0, "base_amount": 0.0}
)
item_row = item_account_wise_cost[(item.item_code, item.get(row_fieldname))][
account.expense_account
]
if total_item_cost > 0:
item_row["amount"] += account.amount * item.get(based_on_field) / total_item_cost
item_row["base_amount"] += (
account.base_amount * item.get(based_on_field) / total_item_cost
)
else:
item_row["amount"] += item.applicable_charges / exchange_rate
item_row["base_amount"] += item.applicable_charges
return item_account_wise_cost
def update_inventory_dimensions(self, row, sl_dict) -> None:
# To handle delivery note and sales invoice
if row.get("item_row"):
@@ -934,7 +1020,7 @@ class StockController(AccountsController):
fieldname = f"{dimension.source_fieldname}"
sl_dict[dimension.target_fieldname] = row.get(fieldname)
return
continue
sl_dict[dimension.target_fieldname] = row.get(dimension.source_fieldname)
else:

View File

@@ -5,7 +5,6 @@ import frappe
from frappe.core.doctype.user_permission.test_user_permission import create_user
from frappe.core.doctype.user_permission.user_permission import add_user_permissions
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.tests import IntegrationTestCase
from erpnext.controllers import queries
from erpnext.tests.utils import ERPNextTestSuite
@@ -121,7 +120,7 @@ class TestQueries(ERPNextTestSuite):
}
)
with IntegrationTestCase.set_user(user.name):
with self.set_user(user.name):
params = {
"doctype": "Employee",
"txt": "",

View File

@@ -2,6 +2,7 @@
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "CON-.YYYY.-.#####",
"creation": "2018-04-12 06:32:04.582486",
"doctype": "DocType",
"editable_grid": 1,
@@ -256,10 +257,11 @@
"grid_page_length": 50,
"is_submittable": 1,
"links": [],
"modified": "2025-05-23 13:54:03.346537",
"modified": "2025-06-19 17:48:45.049007",
"modified_by": "Administrator",
"module": "CRM",
"name": "Contract",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -324,10 +326,12 @@
}
],
"row_format": "Dynamic",
"search_fields": "party_type, party_name, contract_template",
"show_name_in_global_search": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "party_name",
"track_changes": 1,
"track_seen": 1
}

View File

@@ -46,19 +46,6 @@ class Contract(Document):
status: DF.Literal["Unsigned", "Active", "Inactive"]
# end: auto-generated types
def autoname(self):
name = self.party_name
if self.contract_template:
name += f" - {self.contract_template} Agreement"
# If identical, append contract name with the next number in the iteration
if frappe.db.exists("Contract", name):
count = len(frappe.get_all("Contract", filters={"name": ["like", f"%{name}%"]}))
name = f"{name} - {count}"
self.name = _(name)
def validate(self):
self.set_missing_values()
self.validate_dates()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -134,6 +134,7 @@
"fieldname": "quantity",
"fieldtype": "Float",
"label": "Quantity",
"non_negative": 1,
"oldfieldname": "quantity",
"oldfieldtype": "Currency",
"reqd": 1
@@ -663,7 +664,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
"modified": "2024-06-03 16:24:47.518411",
"modified": "2025-06-16 16:13:22.497695",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM",
@@ -696,10 +697,11 @@
"write": 1
}
],
"row_format": "Dynamic",
"search_fields": "item, item_name",
"show_name_in_global_search": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -45,8 +45,7 @@ frappe.ui.form.on("Job Card", {
setup_stock_entry(frm) {
if (
frm.doc.manufactured_qty &&
frm.doc.finished_good &&
frm.doc.track_semi_finished_goods &&
frm.doc.docstatus === 1 &&
!frm.doc.is_subcontracted &&
flt(frm.doc.for_quantity) + flt(frm.doc.process_loss_qty) > flt(frm.doc.manufactured_qty)
@@ -252,7 +251,6 @@ frappe.ui.form.on("Job Card", {
fieldtype: "Float",
label: __("Process Loss Quantity"),
fieldname: "process_loss_qty",
reqd: 1,
onchange() {
let doc = frm.job_completion_dialog;

View File

@@ -241,7 +241,7 @@ class JobCard(Document):
row.sub_operation = row.operation
self.append("sub_operations", row)
def validate_time_logs(self):
def validate_time_logs(self, save=False):
self.total_time_in_mins = 0.0
self.total_completed_qty = 0.0
@@ -272,6 +272,14 @@ class JobCard(Document):
self.total_completed_qty = flt(self.total_completed_qty, self.precision("total_completed_qty"))
if save and self.docstatus == 1:
self.db_set(
{
"total_time_in_mins": self.total_time_in_mins,
"total_completed_qty": self.total_completed_qty,
}
)
for row in self.sub_operations:
self.total_completed_qty += row.completed_qty
@@ -670,7 +678,7 @@ class JobCard(Document):
return
for d in doc.required_items:
if not d.operation and not d.operation_row_id:
if not doc.track_semi_finished_goods and not d.operation and not d.operation_row_id:
frappe.throw(
_("Row {0} : Operation is required against the raw material item {1}").format(
d.idx, d.item_code
@@ -1221,6 +1229,8 @@ class JobCard(Document):
if not self.employee and kwargs.employees:
self.set_employees(kwargs.employees)
self.validate_time_logs(save=True)
def update_workstation_status(self):
status_map = {
"Open": "Off",

View File

@@ -1245,6 +1245,7 @@ def get_exploded_items(item_details, company, bom_no, include_non_stock_items, p
item.purchase_uom,
item_uom.conversion_factor,
item.safety_stock,
bom.item.as_("main_bom_item"),
)
.where(
(bei.docstatus < 2)
@@ -1993,6 +1994,7 @@ def get_raw_materials_of_sub_assembly_items(
item.purchase_uom,
item_uom.conversion_factor,
item.safety_stock,
bom.item.as_("main_bom_item"),
)
.where(
(bei.docstatus == 1)

View File

@@ -101,6 +101,17 @@ frappe.ui.form.on("Work Order", {
};
});
frm.set_query("sales_order", function () {
if (frm.doc.production_item) {
return {
query: "erpnext.manufacturing.doctype.work_order.work_order.query_sales_order",
filters: {
production_item: frm.doc.production_item,
},
};
}
});
// formatter for work order operation
frm.set_indicator_formatter("operation", function (doc) {
return frm.doc.qty == doc.completed_qty ? "green" : "orange";
@@ -506,7 +517,6 @@ frappe.ui.form.on("Work Order", {
callback: function (r) {
if (r.message) {
frm.set_value("sales_order", "");
frm.trigger("set_sales_order");
erpnext.in_production_item_onchange = true;
$.each(
@@ -568,23 +578,6 @@ frappe.ui.form.on("Work Order", {
frm.toggle_reqd("transfer_material_against", frm.doc.operations && frm.doc.operations.length > 0);
},
set_sales_order: function (frm) {
if (frm.doc.production_item) {
frappe.call({
method: "erpnext.manufacturing.doctype.work_order.work_order.query_sales_order",
args: { production_item: frm.doc.production_item },
callback: function (r) {
frm.set_query("sales_order", function () {
erpnext.in_production_item_onchange = true;
return {
filters: [["Sales Order", "name", "in", r.message]],
};
});
},
});
}
},
additional_operating_cost: function (frm) {
erpnext.work_order.calculate_cost(frm.doc);
erpnext.work_order.calculate_total_cost(frm);

View File

@@ -2004,17 +2004,19 @@ def stop_unstop(work_order, status):
@frappe.whitelist()
def query_sales_order(production_item: str) -> list[str]:
@frappe.validate_and_sanitize_search_inputs
def query_sales_order(doctype, txt, searchfield, start, page_len, filters) -> list[str]:
return frappe.get_list(
"Sales Order",
fields=["name"],
filters=[
["Sales Order", "docstatus", "=", 1],
],
or_filters=[
["Sales Order Item", "item_code", "=", production_item],
["Packed Item", "item_code", "=", production_item],
["Sales Order Item", "item_code", "=", filters.get("production_item")],
["Packed Item", "item_code", "=", filters.get("production_item")],
],
pluck="name",
as_list=True,
distinct=True,
)

View File

@@ -419,5 +419,7 @@ erpnext.patches.v15_0.rename_group_by_to_categorize_by_in_custom_reports
erpnext.patches.v15_0.remove_agriculture_roles
erpnext.patches.v14_0.update_full_name_in_contract
erpnext.patches.v15_0.drop_sle_indexes
erpnext.patches.v15_0.update_pick_list_fields
execute:frappe.db.set_single_value("Accounts Settings", "confirm_before_resetting_posting_date", 1)
erpnext.patches.v15_0.rename_pos_closing_entry_fields
erpnext.patches.v15_0.rename_pos_closing_entry_fields #2025-06-13
erpnext.patches.v15_0.update_pegged_currencies

View File

@@ -1,6 +1,8 @@
import frappe
from frappe.model.utils.rename_field import rename_field
def execute():
rename_field("POS Closing Entry", "pos_transactions", "pos_invoices")
rename_field("POS Closing Entry", "sales_invoice_transactions", "sales_invoices")
rename_field("POS Closing Entry", "pos_transactions", "pos_invoices", validate=False)
if frappe.db.exists("DocType", "Sales Invoice Reference"):
rename_field("POS Closing Entry", "sales_invoice_transactions", "sales_invoices", validate=False)

View File

@@ -0,0 +1,7 @@
import frappe
from erpnext.setup.install import update_pegged_currencies
def execute():
update_pegged_currencies()

View File

@@ -0,0 +1,28 @@
import frappe
from frappe.query_builder.functions import IfNull
def execute():
update_delivery_note()
update_pick_list_items()
def update_delivery_note():
DN = frappe.qb.DocType("Delivery Note")
DNI = frappe.qb.DocType("Delivery Note Item")
frappe.qb.update(DNI).join(DN).on(DN.name == DNI.parent).set(DNI.against_pick_list, DN.pick_list).where(
IfNull(DN.pick_list, "") != ""
).run()
def update_pick_list_items():
PL = frappe.qb.DocType("Pick List")
PLI = frappe.qb.DocType("Pick List Item")
pick_lists = frappe.qb.from_(PL).select(PL.name).where(PL.status == "Completed").run(pluck="name")
if not pick_lists:
return
frappe.qb.update(PLI).set(PLI.delivered_qty, PLI.picked_qty).where(PLI.parent.isin(pick_lists)).run()

View File

@@ -1017,7 +1017,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
var party = me.frm.doc[frappe.model.scrub(party_type)];
if(party && me.frm.doc.company) {
if(party && me.frm.doc.company && (!me.frm.doc.__onload?.load_after_mapping || !me.frm.doc.get(party_account_field))) {
return frappe.call({
method: "erpnext.accounts.party.get_party_account",
args: {

View File

@@ -61,9 +61,13 @@ erpnext.setup.slides_settings = [
onload: function (slide) {
this.bind_events(slide);
this.load_chart_of_accounts(slide);
this.set_fy_dates(slide);
},
before_show: function () {
this.load_chart_of_accounts(this);
this.set_fy_dates(this);
},
validate: function () {
if (!this.validate_fy_dates()) {
return false;

View File

@@ -1031,7 +1031,7 @@ erpnext.utils.map_current_doc = function (opts) {
if (
opts.allow_child_item_selection ||
["Purchase Receipt", "Delivery Note"].includes(opts.source_doctype)
["Purchase Receipt", "Delivery Note", "Pick List"].includes(opts.source_doctype)
) {
// args contains filtered child docnames
opts.args = args;

View File

@@ -6,6 +6,7 @@ from frappe.tests import IntegrationTestCase, change_settings
from frappe.utils import add_days, add_months, flt, getdate, nowdate
from erpnext.controllers.accounts_controller import InvalidQtyError
from erpnext.setup.utils import get_exchange_rate
EXTRA_TEST_RECORD_DEPENDENCIES = ["Product Bundle"]
@@ -863,6 +864,24 @@ class TestQuotation(IntegrationTestCase):
quotation.reload()
self.assertEqual(quotation.status, "Ordered")
@change_settings("Accounts Settings", {"allow_pegged_currencies_exchange_rates": True})
def test_make_quotation_qar_to_inr(self):
quotation = make_quotation(
currency="QAR",
transaction_date="2026-06-04",
)
cache = frappe.cache()
key = "currency_exchange_rate_{}:{}:{}".format("2026-06-04", "QAR", "INR")
value = cache.get(key)
expected_rate = flt(value) / 3.64
self.assertEqual(
quotation.conversion_rate,
expected_rate,
f"Expected conversion rate {expected_rate}, got {quotation.conversion_rate}",
)
def enable_calculate_bundle_price(enable=1):
selling_settings = frappe.get_doc("Selling Settings")

View File

@@ -7,9 +7,12 @@
"engine": "InnoDB",
"field_order": [
"item_code",
"item_name",
"customer_item_code",
"col_break1",
"item_name",
"is_free_item",
"is_alternative",
"has_alternative_item",
"section_break_5",
"description",
"item_group",
@@ -53,9 +56,6 @@
"base_net_amount",
"pricing_rules",
"stock_uom_rate",
"is_free_item",
"is_alternative",
"has_alternative_item",
"section_break_43",
"valuation_rate",
"column_break_45",
@@ -698,7 +698,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2024-12-12 13:49:17.765883",
"modified": "2025-06-12 17:31:47.775890",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation Item",

View File

@@ -1100,7 +1100,13 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None):
dn_item.qty = flt(sre.reserved_qty) / flt(dn_item.get("conversion_factor", 1))
dn_item.warehouse = sre.warehouse
if sre.reservation_based_on == "Serial and Batch" and (sre.has_serial_no or sre.has_batch_no):
use_serial_batch_fields = frappe.get_single_value("Stock Settings", "use_serial_batch_fields")
if (
not use_serial_batch_fields
and sre.reservation_based_on == "Serial and Batch"
and (sre.has_serial_no or sre.has_batch_no)
):
dn_item.serial_and_batch_bundle = get_ssb_bundle_for_voucher(sre)
target_doc.append("items", dn_item)
@@ -1774,8 +1780,8 @@ def create_pick_list(source_name, target_doc=None):
"doctype": "Pick List Item",
"field_map": {
"parent": "sales_order",
"name": "sales_order_item",
"parent_detail_docname": "product_bundle_item",
"parent_detail_docname": "sales_order_item",
"name": "product_bundle_item",
},
"field_no_map": ["picked_qty"],
"postprocess": update_packed_item_qty,

View File

@@ -32,6 +32,7 @@ def after_install():
add_app_name()
update_roles()
make_default_operations()
update_pegged_currencies()
frappe.db.commit()
@@ -223,6 +224,27 @@ def create_default_role_profiles():
role_profile.insert(ignore_permissions=True)
def update_pegged_currencies():
doc = frappe.get_doc("Pegged Currencies", "Pegged Currencies")
existing_sources = {item.source_currency for item in doc.pegged_currency_item}
currencies_to_add = [
{"source_currency": "AED", "pegged_against": "USD", "pegged_exchange_rate": 3.6725},
{"source_currency": "BHD", "pegged_against": "USD", "pegged_exchange_rate": 0.376},
{"source_currency": "JOD", "pegged_against": "USD", "pegged_exchange_rate": 0.709},
{"source_currency": "OMR", "pegged_against": "USD", "pegged_exchange_rate": 0.3845},
{"source_currency": "QAR", "pegged_against": "USD", "pegged_exchange_rate": 3.64},
{"source_currency": "SAR", "pegged_against": "USD", "pegged_exchange_rate": 3.75},
]
for currency in currencies_to_add:
if currency["source_currency"] not in existing_sources:
doc.append("pegged_currency_item", currency)
doc.save()
DEFAULT_ROLE_PROFILES = {
"Inventory": [
"Stock User",

View File

@@ -367,7 +367,7 @@ def add_uom_data():
if not frappe.db.exists("UOM", d.get("uom_name")):
doc = frappe.new_doc("UOM")
doc.update(d)
doc.save()
doc.insert(ignore_permissions=True)
# bootstrap uom conversion factors
uom_conversions = json.loads(
@@ -505,6 +505,7 @@ def update_stock_settings():
stock_settings.auto_insert_price_list_rate_if_missing = 1
stock_settings.update_price_list_based_on = "Rate"
stock_settings.set_qty_in_transactions_based_on_serial_no_input = 1
stock_settings.flags.ignore_permissions = True
stock_settings.save()

View File

@@ -9,10 +9,6 @@ from frappe.utils.nestedset import get_root_of
from erpnext import get_default_company
PEGGED_CURRENCIES = {
"USD": {"AED": 3.6725}, # AED is pegged to USD at a rate of 3.6725 since 1997
}
def before_tests():
frappe.clear_cache()
@@ -47,11 +43,51 @@ def before_tests():
frappe.db.commit()
def get_pegged_rate(from_currency: str, to_currency: str, transaction_date) -> float | None:
if rate := PEGGED_CURRENCIES.get(from_currency, {}).get(to_currency):
return rate
elif rate := PEGGED_CURRENCIES.get(to_currency, {}).get(from_currency):
return 1 / rate
def get_pegged_currencies():
pegged_currencies = frappe.get_all(
"Pegged Currency Details",
filters={"parent": "Pegged Currencies"},
fields=["source_currency", "pegged_against", "pegged_exchange_rate"],
)
pegged_map = {
currency.source_currency: {
"pegged_against": currency.pegged_against,
"ratio": flt(currency.pegged_exchange_rate),
}
for currency in pegged_currencies
}
return pegged_map
def get_pegged_rate(pegged_map, from_currency, to_currency, transaction_date=None):
from_entry = pegged_map.get(from_currency)
to_entry = pegged_map.get(to_currency)
if from_currency in pegged_map and to_currency in pegged_map:
# Case 1: Both are present and pegged to same bases
if from_entry["pegged_against"] == to_entry["pegged_against"]:
return (1 / from_entry["ratio"]) * to_entry["ratio"]
# Case 2: Both are present but pegged to different bases
base_from = from_entry["pegged_against"]
base_to = to_entry["pegged_against"]
base_rate = get_exchange_rate(base_from, base_to, transaction_date)
if not base_rate:
return None
return (1 / from_entry["ratio"]) * base_rate * to_entry["ratio"]
# Case 3: from_currency is pegged to to_currency
if from_entry and from_entry["pegged_against"] == to_currency:
return flt(from_entry["ratio"])
# Case 4: to_currency is pegged to from_currency
if to_entry and to_entry["pegged_against"] == from_currency:
return 1 / flt(to_entry["ratio"])
""" If only one entry exists but doesnt match pegged currency logic, return None """
return None
@@ -95,8 +131,12 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No
if frappe.get_cached_value("Currency Exchange Settings", "Currency Exchange Settings", "disabled"):
return 0.00
if rate := get_pegged_rate(from_currency, to_currency, transaction_date):
return rate
pegged_currencies = {}
if currency_settings.allow_pegged_currencies_exchange_rates:
pegged_currencies = get_pegged_currencies()
if rate := get_pegged_rate(pegged_currencies, from_currency, to_currency, transaction_date):
return rate
try:
cache = frappe.cache()
@@ -109,8 +149,12 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No
settings = frappe.get_cached_doc("Currency Exchange Settings")
req_params = {
"transaction_date": transaction_date,
"from_currency": from_currency if from_currency != "AED" else "USD",
"to_currency": to_currency if to_currency != "AED" else "USD",
"from_currency": from_currency
if from_currency not in pegged_currencies
else pegged_currencies[from_currency]["pegged_against"],
"to_currency": to_currency
if to_currency not in pegged_currencies
else pegged_currencies[to_currency]["pegged_against"],
}
params = {}
for row in settings.req_params:
@@ -123,12 +167,13 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No
value = value[format_ces_api(str(res_key.key), req_params)]
cache.setex(name=key, time=21600, value=flt(value))
# Support AED conversion through pegged USD
# Support multiple pegged currencies
value = flt(value)
if to_currency == "AED":
value *= 3.6725
if from_currency == "AED":
value /= 3.6725
if currency_settings.allow_pegged_currencies_exchange_rates and to_currency in pegged_currencies:
value *= flt(pegged_currencies[to_currency]["ratio"])
if currency_settings.allow_pegged_currencies_exchange_rates and from_currency in pegged_currencies:
value /= flt(pegged_currencies[from_currency]["ratio"])
return flt(value)
except Exception:

View File

@@ -188,6 +188,55 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends (
);
}
if (
!doc.is_return &&
doc.status != "Closed" &&
this.frm.has_perm("write") &&
frappe.model.can_read("Pick List") &&
this.frm.doc.docstatus === 0
) {
this.frm.add_custom_button(
__("Pick List"),
function () {
if (!me.frm.doc.customer) {
frappe.throw({
title: __("Mandatory"),
message: __("Please Select a Customer"),
});
}
erpnext.utils.map_current_doc({
method: "erpnext.stock.doctype.pick_list.pick_list.create_dn_for_pick_lists",
source_doctype: "Pick List",
target: me.frm,
setters: [
{
fieldname: "customer",
default: me.frm.doc.customer,
label: __("Customer"),
fieldtype: "Link",
options: "Customer",
reqd: 1,
read_only: 1,
},
{
fieldname: "sales_order",
label: __("Sales Order"),
fieldtype: "Link",
options: "Sales Order",
link_filters: `[["Sales Order","customer","=","${me.frm.doc.customer}"],["Sales Order","docstatus","=","1"],["Sales Order","delivery_status","not in",["Closed","Fully Delivered"]]]`,
},
],
get_query_filters: {
company: me.frm.doc.company,
},
get_query_method: "erpnext.stock.doctype.pick_list.pick_list.get_pick_list_query",
size: "extra-large",
});
},
__("Get Items From")
);
}
if (!doc.is_return && doc.status != "Closed") {
if (doc.docstatus == 1 && frappe.model.can_create("Shipment")) {
this.frm.add_custom_button(

View File

@@ -37,7 +37,6 @@
"ignore_pricing_rule",
"items_section",
"scan_barcode",
"pick_list",
"col_break_warehouse",
"set_warehouse",
"set_target_warehouse",
@@ -1196,15 +1195,6 @@
"options": "Sales Team",
"print_hide": 1
},
{
"fieldname": "pick_list",
"fieldtype": "Link",
"hidden": 1,
"label": "Pick List",
"options": "Pick List",
"read_only": 1,
"search_index": 1
},
{
"default": "0",
"fetch_from": "customer.is_internal_customer",

View File

@@ -176,6 +176,19 @@ class DeliveryNote(SellingController):
"overflow_type": "delivery",
"no_allowance": 1,
},
{
"source_dt": "Delivery Note Item",
"target_dt": "Pick List Item",
"join_field": "pick_list_item",
"target_field": "delivered_qty",
"target_parent_dt": "Pick List",
"target_parent_field": "per_delivered",
"target_ref_field": "picked_qty",
"source_field": "stock_qty",
"percent_join_field": "against_pick_list",
"status_field": "delivery_status",
"keyword": "Delivered",
},
]
if cint(self.is_return):
self.status_updater.extend(
@@ -328,18 +341,15 @@ class DeliveryNote(SellingController):
def set_serial_and_batch_bundle_from_pick_list(self):
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
if not self.pick_list:
return
for item in self.items:
if item.use_serial_batch_fields:
if item.use_serial_batch_fields or not item.against_pick_list:
continue
if item.pick_list_item and not item.serial_and_batch_bundle:
filters = {
"item_code": item.item_code,
"voucher_type": "Pick List",
"voucher_no": self.pick_list,
"voucher_no": item.against_pick_list,
"voucher_detail_no": item.pick_list_item,
}
@@ -444,8 +454,6 @@ class DeliveryNote(SellingController):
self.update_prevdoc_status()
self.update_billing_status()
self.update_stock_reservation_entries()
if not self.is_return:
self.check_credit_limit()
elif self.issue_credit_note:
@@ -458,6 +466,8 @@ class DeliveryNote(SellingController):
self.make_bundle_for_sales_purchase_return(table_name)
self.make_bundle_using_old_serial_batch_fields(table_name)
self.update_stock_reservation_entries()
# Updating stock ledger should always be called after updating prevdoc status,
# because updating reserved qty in bin depends upon updated delivered qty in SO
self.update_stock_ledger()
@@ -588,7 +598,9 @@ class DeliveryNote(SellingController):
def update_pick_list_status(self):
from erpnext.stock.doctype.pick_list.pick_list import update_pick_list_status
update_pick_list_status(self.pick_list)
pick_lists = {row.against_pick_list for row in self.items if row.against_pick_list}
for pick_list in pick_lists:
update_pick_list_status(pick_list)
def check_next_docstatus(self):
submit_rv = frappe.db.sql(
@@ -795,12 +807,14 @@ def get_returned_qty_map(delivery_note):
"""returns a map: {so_detail: returned_qty}"""
returned_qty_map = frappe._dict(
frappe.db.sql(
"""select dn_item.dn_detail, abs(dn_item.qty) as qty
"""select dn_item.dn_detail, sum(abs(dn_item.qty)) as qty
from `tabDelivery Note Item` dn_item, `tabDelivery Note` dn
where dn.name = dn_item.parent
and dn.docstatus = 1
and dn.is_return = 1
and dn.return_against = %s
and dn_item.qty <= 0
group by dn_item.item_code
""",
delivery_note,
)

View File

@@ -6,7 +6,7 @@ import json
from collections import defaultdict
import frappe
from frappe.tests import IntegrationTestCase
from frappe.tests import IntegrationTestCase, change_settings
from frappe.utils import add_days, cstr, flt, getdate, nowdate, nowtime, today
from erpnext.accounts.doctype.account.test_account import get_inventory_account
@@ -1023,6 +1023,30 @@ class TestDeliveryNote(IntegrationTestCase):
self.assertEqual(dn2.per_billed, 100)
self.assertEqual(dn2.status, "Completed")
@change_settings("Accounts Settings", {"delete_linked_ledger_entries": True})
def test_sales_invoice_qty_after_return(self):
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
dn = create_delivery_note(qty=10)
dnr1 = make_sales_return(dn.name)
dnr1.get("items")[0].qty = -3
dnr1.save().submit()
dnr2 = make_sales_return(dn.name)
dnr2.get("items")[0].qty = -2
dnr2.save().submit()
si = make_sales_invoice(dn.name)
si.save().submit()
self.assertEqual(si.get("items")[0].qty, 5)
si.reload().cancel().delete()
dnr1.reload().cancel().delete()
dnr2.reload().cancel().delete()
dn.reload().cancel().delete()
def test_dn_billing_status_case3(self):
# SO -> DN1 -> SI and SO -> SI and SO -> DN2
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note

View File

@@ -77,6 +77,7 @@
"against_sales_invoice",
"si_detail",
"dn_detail",
"against_pick_list",
"pick_list_item",
"section_break_40",
"pick_serial_and_batch",
@@ -935,6 +936,16 @@
{
"fieldname": "column_break_fguf",
"fieldtype": "Column Break"
},
{
"fieldname": "against_pick_list",
"fieldtype": "Link",
"label": "Against Pick List",
"no_copy": 1,
"options": "Pick List",
"print_hide": 1,
"read_only": 1,
"search_index": 1
}
],
"grid_page_length": 50,
@@ -942,7 +953,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-03-07 12:33:40.868499",
"modified": "2025-05-31 18:51:32.651562",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note Item",
@@ -953,4 +964,4 @@
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -16,6 +16,7 @@ class DeliveryNoteItem(Document):
actual_batch_qty: DF.Float
actual_qty: DF.Float
against_pick_list: DF.Link | None
against_sales_invoice: DF.Link | None
against_sales_order: DF.Link | None
allow_zero_valuation_rate: DF.Check

View File

@@ -17,6 +17,7 @@
"is_fixed_asset",
"applicable_charges",
"purchase_receipt_item",
"stock_entry_item",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break"
@@ -49,7 +50,7 @@
"fieldtype": "Select",
"label": "Receipt Document Type",
"no_copy": 1,
"options": "Purchase Invoice\nPurchase Receipt",
"options": "Purchase Invoice\nPurchase Receipt\nStock Entry\nSubcontracting Receipt",
"print_hide": 1,
"read_only": 1
},
@@ -131,18 +132,27 @@
"hidden": 1,
"label": "Is Fixed Asset",
"read_only": 1
},
{
"fieldname": "stock_entry_item",
"fieldtype": "Data",
"label": "Stock Entry Item",
"no_copy": 1,
"read_only": 1
}
],
"grid_page_length": 50,
"idx": 1,
"istable": 1,
"links": [],
"modified": "2024-03-27 13:09:59.220459",
"modified": "2025-06-11 08:53:38.096761",
"modified_by": "Administrator",
"module": "Stock",
"name": "Landed Cost Item",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -27,7 +27,10 @@ class LandedCostItem(Document):
qty: DF.Float
rate: DF.Currency
receipt_document: DF.DynamicLink | None
receipt_document_type: DF.Literal["Purchase Invoice", "Purchase Receipt"]
receipt_document_type: DF.Literal[
"Purchase Invoice", "Purchase Receipt", "Stock Entry", "Subcontracting Receipt"
]
stock_entry_item: DF.Data | None
# end: auto-generated types
pass

Some files were not shown because too many files have changed in this diff Show More