mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-03 04:09:11 +00:00
Merge branch 'develop' into use-frappe.in_test
This commit is contained in:
3
.github/workflows/backport.yml
vendored
3
.github/workflows/backport.yml
vendored
@@ -5,6 +5,9 @@ on:
|
|||||||
- closed
|
- closed
|
||||||
- labeled
|
- labeled
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
main:
|
main:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
4
.github/workflows/docker-release.yml
vendored
4
.github/workflows/docker-release.yml
vendored
@@ -2,6 +2,10 @@ name: Trigger Docker build on release
|
|||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [released]
|
types: [released]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
curl:
|
curl:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
3
.github/workflows/docs-checker.yml
vendored
3
.github/workflows/docs-checker.yml
vendored
@@ -3,6 +3,9 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
types: [ opened, synchronize, reopened, edited ]
|
types: [ opened, synchronize, reopened, edited ]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
4
.github/workflows/initiate_release.yml
vendored
4
.github/workflows/initiate_release.yml
vendored
@@ -2,6 +2,10 @@
|
|||||||
# To add/remove versions just modify the matrix.
|
# To add/remove versions just modify the matrix.
|
||||||
|
|
||||||
name: Create weekly release pull requests
|
name: Create weekly release pull requests
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
# 9:30 UTC => 3 PM IST Tuesday
|
# 9:30 UTC => 3 PM IST Tuesday
|
||||||
|
|||||||
4
.github/workflows/labeller.yml
vendored
4
.github/workflows/labeller.yml
vendored
@@ -3,6 +3,10 @@ on:
|
|||||||
pull_request_target:
|
pull_request_target:
|
||||||
types: [opened, reopened]
|
types: [opened, reopened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
triage:
|
triage:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
3
.github/workflows/linters.yml
vendored
3
.github/workflows/linters.yml
vendored
@@ -3,6 +3,9 @@ name: Linters
|
|||||||
on:
|
on:
|
||||||
pull_request: { }
|
pull_request: { }
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
|
|||||||
3
.github/workflows/patch.yml
vendored
3
.github/workflows/patch.yml
vendored
@@ -10,6 +10,9 @@ on:
|
|||||||
- '**.csv'
|
- '**.csv'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: patch-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }}
|
group: patch-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|||||||
3
.github/workflows/patch_faux.yml
vendored
3
.github/workflows/patch_faux.yml
vendored
@@ -11,6 +11,9 @@ on:
|
|||||||
- "**.html"
|
- "**.html"
|
||||||
- "**.csv"
|
- "**.csv"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -3,6 +3,10 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- version-13
|
- version-13
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
name: Release
|
name: Release
|
||||||
|
|||||||
3
.github/workflows/run-indinvidual-tests.yml
vendored
3
.github/workflows/run-indinvidual-tests.yml
vendored
@@ -7,6 +7,9 @@ concurrency:
|
|||||||
group: server-individual-tests-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }}
|
group: server-individual-tests-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }}
|
||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
discover:
|
discover:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ on:
|
|||||||
- "**.md"
|
- "**.md"
|
||||||
- "**.html"
|
- "**.html"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
3
.github/workflows/server-tests-mariadb.yml
vendored
3
.github/workflows/server-tests-mariadb.yml
vendored
@@ -25,6 +25,9 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: server-mariadb-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }}
|
group: server-mariadb-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|||||||
3
.github/workflows/server-tests-postgres.yml
vendored
3
.github/workflows/server-tests-postgres.yml
vendored
@@ -12,6 +12,9 @@ concurrency:
|
|||||||
group: server-postgres-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }}
|
group: server-postgres-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
if: ${{ contains(github.event.pull_request.labels.*.name, 'postgres') }}
|
if: ${{ contains(github.event.pull_request.labels.*.name, 'postgres') }}
|
||||||
|
|||||||
@@ -139,6 +139,11 @@ frappe.treeview_settings["Account"] = {
|
|||||||
description: __(
|
description: __(
|
||||||
"Further accounts can be made under Groups, but entries can be made against non-Groups"
|
"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",
|
fieldtype: "Select",
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
"role_to_override_stop_action",
|
"role_to_override_stop_action",
|
||||||
"currency_exchange_section",
|
"currency_exchange_section",
|
||||||
"allow_stale",
|
"allow_stale",
|
||||||
|
"allow_pegged_currencies_exchange_rates",
|
||||||
"column_break_yuug",
|
"column_break_yuug",
|
||||||
"stale_days",
|
"stale_days",
|
||||||
"section_break_jpd0",
|
"section_break_jpd0",
|
||||||
@@ -614,6 +615,13 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_feyo",
|
"fieldname": "column_break_feyo",
|
||||||
"fieldtype": "Column Break"
|
"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,
|
"grid_page_length": 50,
|
||||||
@@ -622,7 +630,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-06-06 11:03:28.095723",
|
"modified": "2025-06-16 16:40:54.871486",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Accounts Settings",
|
"name": "Accounts Settings",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class AccountsSettings(Document):
|
|||||||
acc_frozen_upto: DF.Date | None
|
acc_frozen_upto: DF.Date | None
|
||||||
add_taxes_from_item_tax_template: DF.Check
|
add_taxes_from_item_tax_template: DF.Check
|
||||||
allow_multi_currency_invoices_against_single_party_account: DF.Check
|
allow_multi_currency_invoices_against_single_party_account: DF.Check
|
||||||
|
allow_pegged_currencies_exchange_rates: DF.Check
|
||||||
allow_stale: DF.Check
|
allow_stale: DF.Check
|
||||||
auto_reconcile_payments: DF.Check
|
auto_reconcile_payments: DF.Check
|
||||||
auto_reconciliation_job_trigger: DF.Int
|
auto_reconciliation_job_trigger: DF.Int
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import flt
|
from frappe.utils import flt
|
||||||
from rapidfuzz import fuzz, process
|
from rapidfuzz import fuzz, process
|
||||||
|
from rapidfuzz.utils import default_process
|
||||||
|
|
||||||
|
|
||||||
class AutoMatchParty:
|
class AutoMatchParty:
|
||||||
@@ -132,6 +133,7 @@ class AutoMatchbyPartyNameDescription:
|
|||||||
query=self.get(field),
|
query=self.get(field),
|
||||||
choices={row.get("name"): row.get("party_name") for row in names},
|
choices={row.get("name"): row.get("party_name") for row in names},
|
||||||
scorer=fuzz.token_set_ratio,
|
scorer=fuzz.token_set_ratio,
|
||||||
|
processor=default_process,
|
||||||
)
|
)
|
||||||
party_name, skip = self.process_fuzzy_result(result)
|
party_name, skip = self.process_fuzzy_result(result)
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,6 @@
|
|||||||
"default": "ACC-BTN-.YYYY.-",
|
"default": "ACC-BTN-.YYYY.-",
|
||||||
"fieldname": "naming_series",
|
"fieldname": "naming_series",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"hidden": 1,
|
|
||||||
"label": "Series",
|
"label": "Series",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "ACC-BTN-.YYYY.-",
|
"options": "ACC-BTN-.YYYY.-",
|
||||||
@@ -236,9 +235,10 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-11-18 18:32:47.203694",
|
"modified": "2025-06-18 17:24:57.044666",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Bank Transaction",
|
"name": "Bank Transaction",
|
||||||
@@ -287,9 +287,10 @@
|
|||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "date",
|
"sort_field": "date",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"title_field": "bank_account",
|
"title_field": "bank_account",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,10 @@
|
|||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
|
"naming_series",
|
||||||
"budget_against",
|
"budget_against",
|
||||||
"company",
|
"company",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
"naming_series",
|
|
||||||
"project",
|
"project",
|
||||||
"fiscal_year",
|
"fiscal_year",
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
@@ -199,12 +199,12 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "naming_series",
|
"fieldname": "naming_series",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Select",
|
||||||
"hidden": 1,
|
|
||||||
"label": "Series",
|
"label": "Series",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
|
"options": "BUDGET-.YYYY.-",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1,
|
"reqd": 1,
|
||||||
"set_only_once": 1
|
"set_only_once": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -238,7 +238,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-05-22 13:46:28.510566",
|
"modified": "2025-06-16 15:57:13.114981",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Budget",
|
"name": "Budget",
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class Budget(Document):
|
|||||||
cost_center: DF.Link | None
|
cost_center: DF.Link | None
|
||||||
fiscal_year: DF.Link
|
fiscal_year: DF.Link
|
||||||
monthly_distribution: DF.Link | None
|
monthly_distribution: DF.Link | None
|
||||||
naming_series: DF.Data | None
|
naming_series: DF.Literal["BUDGET-.YYYY.-"]
|
||||||
project: DF.Link | None
|
project: DF.Link | None
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
@@ -139,9 +139,6 @@ class Budget(Document):
|
|||||||
):
|
):
|
||||||
self.applicable_on_booking_actual_expenses = 1
|
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):
|
def validate_expense_against_budget(args, expense_amount=0):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|||||||
@@ -39,7 +39,16 @@ class ItemTaxTemplate(Document):
|
|||||||
check_list = []
|
check_list = []
|
||||||
for d in self.get("taxes"):
|
for d in self.get("taxes"):
|
||||||
if d.tax_type:
|
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 [
|
if account_type not in [
|
||||||
"Tax",
|
"Tax",
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|
||||||
|
// },
|
||||||
|
// });
|
||||||
@@ -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": []
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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": []
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -259,6 +259,7 @@ class POSInvoiceMergeLog(Document):
|
|||||||
if not found:
|
if not found:
|
||||||
tax.charge_type = "Actual"
|
tax.charge_type = "Actual"
|
||||||
tax.idx = idx
|
tax.idx = idx
|
||||||
|
tax.row_id = None
|
||||||
idx += 1
|
idx += 1
|
||||||
tax.included_in_print_rate = 0
|
tax.included_in_print_rate = 0
|
||||||
tax.tax_amount = tax.tax_amount_after_discount_amount
|
tax.tax_amount = tax.tax_amount_after_discount_amount
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ from erpnext.controllers.accounts_controller import validate_account_head
|
|||||||
from erpnext.controllers.buying_controller import BuyingController
|
from erpnext.controllers.buying_controller import BuyingController
|
||||||
from erpnext.stock import get_warehouse_account_map
|
from erpnext.stock import get_warehouse_account_map
|
||||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||||
get_item_account_wise_additional_cost,
|
|
||||||
update_billed_amount_based_on_po,
|
update_billed_amount_based_on_po,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -940,7 +939,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
if self.update_stock and self.auto_accounting_for_stock:
|
if self.update_stock and self.auto_accounting_for_stock:
|
||||||
warehouse_account = get_warehouse_account_map(self.company)
|
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 = {}
|
voucher_wise_stock_value = {}
|
||||||
if self.update_stock:
|
if self.update_stock:
|
||||||
|
|||||||
@@ -1660,7 +1660,7 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
|||||||
|
|
||||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||||
pi.set_posting_time = 1
|
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.items[0].expense_account = "Cost of Goods Sold - _TC"
|
||||||
pi.save()
|
pi.save()
|
||||||
pi.submit()
|
pi.submit()
|
||||||
@@ -1669,30 +1669,38 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
|||||||
|
|
||||||
# Check GLE for Purchase Invoice
|
# Check GLE for Purchase Invoice
|
||||||
expected_gle = [
|
expected_gle = [
|
||||||
["Cost of Goods Sold - _TC", 250, 0, 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)],
|
["Creditors - _TC", 0, 250, add_days(pr.posting_date, 1)],
|
||||||
]
|
]
|
||||||
|
|
||||||
check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
|
check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
|
||||||
|
|
||||||
expected_gle_for_purchase_receipt = [
|
expected_gle_for_purchase_receipt = [
|
||||||
["Provision Account - _TC", 250, 0, pr.posting_date],
|
["_Test Account Cost for Goods Sold - _TC", 250, 0, pr.posting_date],
|
||||||
["_Test Account Cost for Goods Sold - _TC", 0, 250, pr.posting_date],
|
["Provision Account - _TC", 0, 250, pr.posting_date],
|
||||||
["Provision Account - _TC", 0, 250, pi.posting_date],
|
["_Test Account Cost for Goods Sold - _TC", 0, 250, pi.posting_date],
|
||||||
["_Test Account Cost for Goods Sold - _TC", 250, 0, 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
|
# Cancel purchase invoice to check reverse provisional entry cancellation
|
||||||
pi.cancel()
|
pi.cancel()
|
||||||
|
|
||||||
expected_gle_for_purchase_receipt_post_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],
|
["_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()
|
toggle_provisional_accounting_setting()
|
||||||
|
|
||||||
@@ -1713,7 +1721,7 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
|||||||
# Overbill PR: rate = 2000, qty = 10
|
# Overbill PR: rate = 2000, qty = 10
|
||||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||||
pi.set_posting_time = 1
|
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].qty = 10
|
||||||
pi.items[0].rate = 2000
|
pi.items[0].rate = 2000
|
||||||
pi.items[0].expense_account = "Cost of Goods Sold - _TC"
|
pi.items[0].expense_account = "Cost of Goods Sold - _TC"
|
||||||
@@ -1721,30 +1729,38 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
|||||||
pi.submit()
|
pi.submit()
|
||||||
|
|
||||||
expected_gle = [
|
expected_gle = [
|
||||||
["Cost of Goods Sold - _TC", 20000, 0, 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)],
|
["Creditors - _TC", 0, 20000, add_days(pr.posting_date, 1)],
|
||||||
]
|
]
|
||||||
|
|
||||||
check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
|
check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
|
||||||
|
|
||||||
expected_gle_for_purchase_receipt = [
|
expected_gle_for_purchase_receipt = [
|
||||||
["Provision Account - _TC", 5000, 0, pr.posting_date],
|
["_Test Account Cost for Goods Sold - _TC", 5000, 0, pr.posting_date],
|
||||||
["_Test Account Cost for Goods Sold - _TC", 0, 5000, pr.posting_date],
|
["Provision Account - _TC", 0, 5000, pr.posting_date],
|
||||||
["Provision Account - _TC", 0, 5000, pi.posting_date],
|
["_Test Account Cost for Goods Sold - _TC", 0, 5000, pi.posting_date],
|
||||||
["_Test Account Cost for Goods Sold - _TC", 5000, 0, 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
|
# Cancel purchase invoice to check reverse provisional entry cancellation
|
||||||
pi.cancel()
|
pi.cancel()
|
||||||
|
|
||||||
expected_gle_for_purchase_receipt_post_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],
|
["_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()
|
toggle_provisional_accounting_setting()
|
||||||
|
|
||||||
@@ -1777,13 +1793,76 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
|||||||
check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
|
check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
|
||||||
|
|
||||||
expected_gle_for_purchase_receipt = [
|
expected_gle_for_purchase_receipt = [
|
||||||
["Provision Account - _TC", 5000, 0, pr.posting_date],
|
["_Test Account Cost for Goods Sold - _TC", 5000, 0, pr.posting_date],
|
||||||
["_Test Account Cost for Goods Sold - _TC", 0, 5000, pr.posting_date],
|
["Provision Account - _TC", 0, 5000, pr.posting_date],
|
||||||
["Provision Account - _TC", 0, 1000, pi.posting_date],
|
["_Test Account Cost for Goods Sold - _TC", 0, 1000, pi.posting_date],
|
||||||
["_Test Account Cost for Goods Sold - _TC", 1000, 0, 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()
|
toggle_provisional_accounting_setting()
|
||||||
|
|
||||||
|
|||||||
@@ -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 first search in party (Customer / Supplier) record, if not found,
|
||||||
will search in group (Customer Group / Supplier Group),
|
will search in group (Customer Group / Supplier Group),
|
||||||
finally will return default."""
|
finally will return default."""
|
||||||
|
if not party_type:
|
||||||
|
frappe.throw(_("Party Type is mandatory"))
|
||||||
if not company:
|
if not company:
|
||||||
frappe.throw(_("Please select a Company"))
|
frappe.throw(_("Please select a Company"))
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ class ReceivablePayableReport:
|
|||||||
self.filters.report_date = getdate(self.filters.report_date or nowdate())
|
self.filters.report_date = getdate(self.filters.report_date or nowdate())
|
||||||
self.age_as_on = (
|
self.age_as_on = (
|
||||||
getdate(nowdate())
|
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
|
else self.filters.report_date
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -200,7 +200,8 @@ def get_gl_entries(filters, accounting_dimensions):
|
|||||||
voucher_type, voucher_subtype, voucher_no, {dimension_fields}
|
voucher_type, voucher_subtype, voucher_no, {dimension_fields}
|
||||||
cost_center, project, {transaction_currency_fields}
|
cost_center, project, {transaction_currency_fields}
|
||||||
against_voucher_type, against_voucher, account_currency,
|
against_voucher_type, against_voucher, account_currency,
|
||||||
against, is_opening, creation {select_fields}
|
against, is_opening, creation {select_fields},
|
||||||
|
transaction_currency
|
||||||
from `tabGL Entry`
|
from `tabGL Entry`
|
||||||
where company=%(company)s {get_conditions(filters)}
|
where company=%(company)s {get_conditions(filters)}
|
||||||
{order_by_statement}
|
{order_by_statement}
|
||||||
|
|||||||
@@ -3,12 +3,15 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import qb
|
from frappe import qb
|
||||||
from frappe.tests import IntegrationTestCase
|
from frappe.tests import IntegrationTestCase, change_settings
|
||||||
from frappe.utils import flt, today
|
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.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||||
from erpnext.accounts.report.general_ledger.general_ledger import execute
|
from erpnext.accounts.report.general_ledger.general_ledger import execute
|
||||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
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):
|
class TestGeneralLedger(IntegrationTestCase):
|
||||||
@@ -168,6 +171,90 @@ class TestGeneralLedger(IntegrationTestCase):
|
|||||||
self.assertEqual(data[3]["debit"], 100)
|
self.assertEqual(data[3]["debit"], 100)
|
||||||
self.assertEqual(data[3]["credit"], 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):
|
def test_ignore_exchange_rate_journals_filter(self):
|
||||||
# create a new account with USD currency
|
# create a new account with USD currency
|
||||||
account_name = "Test Debtors USD"
|
account_name = "Test Debtors USD"
|
||||||
|
|||||||
@@ -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))
|
account_currencies = list(set(entry["account_currency"] for entry in gl_entries))
|
||||||
|
|
||||||
for entry in gl_entries:
|
for entry in gl_entries:
|
||||||
|
transaction_currency = entry.get("transaction_currency")
|
||||||
debit = flt(entry["debit"])
|
debit = flt(entry["debit"])
|
||||||
credit = flt(entry["credit"])
|
credit = flt(entry["credit"])
|
||||||
debit_in_account_currency = flt(entry["debit_in_account_currency"])
|
debit_in_account_currency = flt(entry["debit_in_account_currency"])
|
||||||
credit_in_account_currency = flt(entry["credit_in_account_currency"])
|
credit_in_account_currency = flt(entry["credit_in_account_currency"])
|
||||||
account_currency = entry["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["debit"] = debit_in_account_currency
|
||||||
entry["credit"] = credit_in_account_currency
|
entry["credit"] = credit_in_account_currency
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -72,6 +72,12 @@ frappe.ui.form.on("Asset", {
|
|||||||
filters: { item_code: doc.item_code },
|
filters: { item_code: doc.item_code },
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (frm.doc.docstatus == 1) {
|
||||||
|
frm.custom_make_buttons = {
|
||||||
|
"Asset Capitalization": "Asset Capitalization",
|
||||||
|
};
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
refresh: function (frm) {
|
refresh: function (frm) {
|
||||||
|
|||||||
@@ -208,14 +208,11 @@ class Asset(AccountsController):
|
|||||||
add_asset_activity(self.name, _("Asset cancelled"))
|
add_asset_activity(self.name, _("Asset cancelled"))
|
||||||
|
|
||||||
def after_insert(self):
|
def after_insert(self):
|
||||||
if (
|
if not frappe.db.exists(
|
||||||
not frappe.db.exists(
|
{
|
||||||
{
|
"doctype": "Asset Activity",
|
||||||
"doctype": "Asset Activity",
|
"asset": self.name,
|
||||||
"asset": self.name,
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
and not self.flags.asset_created_via_asset_capitalization
|
|
||||||
):
|
):
|
||||||
add_asset_activity(self.name, _("Asset created"))
|
add_asset_activity(self.name, _("Asset created"))
|
||||||
|
|
||||||
@@ -1006,7 +1003,6 @@ def create_asset_capitalization(company, asset, asset_name, item_code):
|
|||||||
{
|
{
|
||||||
"target_asset": asset,
|
"target_asset": asset,
|
||||||
"company": company,
|
"company": company,
|
||||||
"capitalization_method": "Choose a WIP composite asset",
|
|
||||||
"target_asset_name": asset_name,
|
"target_asset_name": asset_name,
|
||||||
"target_item_code": item_code,
|
"target_item_code": item_code,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1748,6 +1748,7 @@ def create_asset(**args):
|
|||||||
"asset_owner": args.asset_owner or "Company",
|
"asset_owner": args.asset_owner or "Company",
|
||||||
"is_existing_asset": args.is_existing_asset or 1,
|
"is_existing_asset": args.is_existing_asset or 1,
|
||||||
"is_composite_asset": args.is_composite_asset or 0,
|
"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,
|
"asset_quantity": args.get("asset_quantity") or 1,
|
||||||
"depr_entry_posting_status": args.depr_entry_posting_status or "",
|
"depr_entry_posting_status": args.depr_entry_posting_status or "",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,10 +134,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
|||||||
}
|
}
|
||||||
|
|
||||||
target_asset() {
|
target_asset() {
|
||||||
if (
|
if (this.frm.doc.target_asset) {
|
||||||
this.frm.doc.target_asset &&
|
|
||||||
this.frm.doc.capitalization_method === "Choose a WIP composite asset"
|
|
||||||
) {
|
|
||||||
this.set_consumed_stock_items_tagged_to_wip_composite_asset(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();
|
this.get_target_asset_details();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,19 +9,16 @@
|
|||||||
"field_order": [
|
"field_order": [
|
||||||
"title",
|
"title",
|
||||||
"naming_series",
|
"naming_series",
|
||||||
"capitalization_method",
|
|
||||||
"target_item_code",
|
|
||||||
"target_item_name",
|
|
||||||
"target_asset",
|
"target_asset",
|
||||||
"target_asset_name",
|
"target_asset_name",
|
||||||
|
"target_item_code",
|
||||||
|
"finance_book",
|
||||||
"target_qty",
|
"target_qty",
|
||||||
"target_asset_location",
|
|
||||||
"column_break_9",
|
"column_break_9",
|
||||||
"company",
|
"company",
|
||||||
"posting_date",
|
"posting_date",
|
||||||
"posting_time",
|
"posting_time",
|
||||||
"set_posting_time",
|
"set_posting_time",
|
||||||
"finance_book",
|
|
||||||
"target_batch_no",
|
"target_batch_no",
|
||||||
"target_serial_no",
|
"target_serial_no",
|
||||||
"amended_from",
|
"amended_from",
|
||||||
@@ -54,20 +51,12 @@
|
|||||||
"label": "Title"
|
"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",
|
"fieldname": "target_item_code",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Target Item Code",
|
"label": "Target Item Code",
|
||||||
"mandatory_depends_on": "eval:doc.capitalization_method=='Create a new composite asset'",
|
"options": "Item",
|
||||||
"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",
|
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -80,18 +69,14 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:(doc.target_asset && !doc.__islocal) || doc.capitalization_method=='Choose a WIP composite asset'",
|
|
||||||
"fieldname": "target_asset",
|
"fieldname": "target_asset",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Target Asset",
|
"label": "Target Asset",
|
||||||
"mandatory_depends_on": "eval:doc.capitalization_method=='Choose a WIP composite asset'",
|
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Asset",
|
"options": "Asset"
|
||||||
"read_only_depends_on": "eval:doc.capitalization_method=='Create a new composite 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",
|
"fetch_from": "target_asset.asset_name",
|
||||||
"fieldname": "target_asset_name",
|
"fieldname": "target_asset_name",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
@@ -176,7 +161,9 @@
|
|||||||
"default": "1",
|
"default": "1",
|
||||||
"fieldname": "target_qty",
|
"fieldname": "target_qty",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Target Qty"
|
"hidden": 1,
|
||||||
|
"label": "Target Qty",
|
||||||
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
@@ -298,26 +285,12 @@
|
|||||||
"label": "Target Fixed Asset Account",
|
"label": "Target Fixed Asset Account",
|
||||||
"options": "Account",
|
"options": "Account",
|
||||||
"read_only": 1
|
"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,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-01-08 13:14:33.008458",
|
"modified": "2025-05-20 15:15:12.110035",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Assets",
|
"module": "Assets",
|
||||||
"name": "Asset Capitalization",
|
"name": "Asset Capitalization",
|
||||||
@@ -355,10 +328,11 @@
|
|||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"title_field": "title",
|
"title_field": "title",
|
||||||
"track_changes": 1,
|
"track_changes": 1,
|
||||||
"track_seen": 1
|
"track_seen": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import erpnext
|
|||||||
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
|
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
|
||||||
from erpnext.assets.doctype.asset.depreciation import (
|
from erpnext.assets.doctype.asset.depreciation import (
|
||||||
depreciate_asset,
|
depreciate_asset,
|
||||||
|
get_disposal_account_and_cost_center,
|
||||||
get_gl_entries_on_asset_disposal,
|
get_gl_entries_on_asset_disposal,
|
||||||
get_value_after_depreciation_on_disposal_date,
|
get_value_after_depreciation_on_disposal_date,
|
||||||
reset_depreciation_schedule,
|
reset_depreciation_schedule,
|
||||||
@@ -70,7 +71,6 @@ class AssetCapitalization(StockController):
|
|||||||
amended_from: DF.Link | None
|
amended_from: DF.Link | None
|
||||||
asset_items: DF.Table[AssetCapitalizationAssetItem]
|
asset_items: DF.Table[AssetCapitalizationAssetItem]
|
||||||
asset_items_total: DF.Currency
|
asset_items_total: DF.Currency
|
||||||
capitalization_method: DF.Literal["", "Create new composite asset", "Use existing composite asset"]
|
|
||||||
company: DF.Link
|
company: DF.Link
|
||||||
cost_center: DF.Link | None
|
cost_center: DF.Link | None
|
||||||
finance_book: DF.Link | None
|
finance_book: DF.Link | None
|
||||||
@@ -83,7 +83,6 @@ class AssetCapitalization(StockController):
|
|||||||
stock_items: DF.Table[AssetCapitalizationStockItem]
|
stock_items: DF.Table[AssetCapitalizationStockItem]
|
||||||
stock_items_total: DF.Currency
|
stock_items_total: DF.Currency
|
||||||
target_asset: DF.Link | None
|
target_asset: DF.Link | None
|
||||||
target_asset_location: DF.Link | None
|
|
||||||
target_asset_name: DF.Data | None
|
target_asset_name: DF.Data | None
|
||||||
target_batch_no: DF.Link | None
|
target_batch_no: DF.Link | None
|
||||||
target_fixed_asset_account: DF.Link | None
|
target_fixed_asset_account: DF.Link | None
|
||||||
@@ -92,7 +91,6 @@ class AssetCapitalization(StockController):
|
|||||||
target_incoming_rate: DF.Currency
|
target_incoming_rate: DF.Currency
|
||||||
target_is_fixed_asset: DF.Check
|
target_is_fixed_asset: DF.Check
|
||||||
target_item_code: DF.Link | None
|
target_item_code: DF.Link | None
|
||||||
target_item_name: DF.Data | None
|
|
||||||
target_qty: DF.Float
|
target_qty: DF.Float
|
||||||
target_serial_no: DF.SmallText | None
|
target_serial_no: DF.SmallText | None
|
||||||
title: DF.Data | None
|
title: DF.Data | None
|
||||||
@@ -118,7 +116,7 @@ class AssetCapitalization(StockController):
|
|||||||
|
|
||||||
def before_submit(self):
|
def before_submit(self):
|
||||||
self.validate_source_mandatory()
|
self.validate_source_mandatory()
|
||||||
self.create_target_asset()
|
# self.create_target_asset()
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.make_bundle_using_old_serial_batch_fields()
|
self.make_bundle_using_old_serial_batch_fields()
|
||||||
@@ -143,7 +141,7 @@ class AssetCapitalization(StockController):
|
|||||||
self.update_target_asset()
|
self.update_target_asset()
|
||||||
|
|
||||||
def set_title(self):
|
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):
|
def set_missing_values(self, for_validate=False):
|
||||||
target_item_details = get_target_item_details(self.target_item_code, self.company)
|
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")
|
d.cost_center = frappe.get_cached_value("Company", self.company, "cost_center")
|
||||||
|
|
||||||
def validate_source_mandatory(self):
|
def validate_source_mandatory(self):
|
||||||
if self.capitalization_method == "Create a new composite asset" and not (
|
if not (self.get("stock_items") or self.get("asset_items") or self.get("service_items")):
|
||||||
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")):
|
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_(
|
_(
|
||||||
"Consumed Stock Items, Consumed Asset Items or Consumed Service Items is mandatory for Capitalization"
|
"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_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_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
|
return gl_entries
|
||||||
|
|
||||||
@@ -493,34 +486,34 @@ class AssetCapitalization(StockController):
|
|||||||
for item in self.asset_items:
|
for item in self.asset_items:
|
||||||
asset = frappe.get_doc("Asset", item.asset)
|
asset = frappe.get_doc("Asset", item.asset)
|
||||||
|
|
||||||
if asset.calculate_depreciation:
|
if not asset.is_composite_component:
|
||||||
notes = _(
|
if asset.calculate_depreciation:
|
||||||
"This schedule was created when Asset {0} was consumed through Asset Capitalization {1}."
|
notes = _(
|
||||||
).format(
|
"This schedule was created when Asset {0} was consumed through Asset Capitalization {1}."
|
||||||
get_link_to_form(asset.doctype, asset.name),
|
).format(
|
||||||
get_link_to_form(self.doctype, self.get("name")),
|
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()
|
depreciate_asset(asset, self.posting_date, notes)
|
||||||
|
asset.reload()
|
||||||
|
|
||||||
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
|
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
|
||||||
asset,
|
asset,
|
||||||
item.asset_value,
|
item.asset_value,
|
||||||
item.get("finance_book") or self.get("finance_book"),
|
item.get("finance_book") or self.get("finance_book"),
|
||||||
self.get("doctype"),
|
self.get("doctype"),
|
||||||
self.get("name"),
|
self.get("name"),
|
||||||
self.get("posting_date"),
|
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)
|
asset.db_set("disposal_date", self.posting_date)
|
||||||
|
|
||||||
self.set_consumed_asset_status(asset)
|
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(
|
def get_gl_entries_for_consumed_service_items(
|
||||||
self, gl_entries, target_account, target_against, precision
|
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:
|
if self.target_is_fixed_asset:
|
||||||
# Capitalization
|
total_value = flt(self.total_value - composite_component_value, precision)
|
||||||
gl_entries.append(
|
if total_value:
|
||||||
self.get_gl_dict(
|
# Capitalization
|
||||||
{
|
gl_entries.append(
|
||||||
"account": target_account,
|
self.get_gl_dict(
|
||||||
"against": ", ".join(target_against),
|
{
|
||||||
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
|
"account": target_account,
|
||||||
"debit": flt(self.total_value, precision),
|
"against": ", ".join(target_against),
|
||||||
"cost_center": self.get("cost_center"),
|
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
|
||||||
},
|
"debit": total_value,
|
||||||
item=self,
|
"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):
|
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"))
|
total_target_asset_value = flt(self.total_value, self.precision("total_value"))
|
||||||
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
||||||
|
|
||||||
|
|||||||
@@ -59,10 +59,16 @@ class TestAssetCapitalization(IntegrationTestCase):
|
|||||||
company=company,
|
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
|
# Create and submit Asset Captitalization
|
||||||
asset_capitalization = create_asset_capitalization(
|
asset_capitalization = create_asset_capitalization(
|
||||||
capitalization_method="Create a new composite asset",
|
target_asset=wip_composite_asset.name,
|
||||||
target_item_code="Macbook Pro",
|
|
||||||
target_asset_location="Test Location",
|
target_asset_location="Test Location",
|
||||||
stock_qty=stock_qty,
|
stock_qty=stock_qty,
|
||||||
stock_rate=stock_rate,
|
stock_rate=stock_rate,
|
||||||
@@ -148,10 +154,16 @@ class TestAssetCapitalization(IntegrationTestCase):
|
|||||||
company=company,
|
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
|
# Create and submit Asset Captitalization
|
||||||
asset_capitalization = create_asset_capitalization(
|
asset_capitalization = create_asset_capitalization(
|
||||||
capitalization_method="Create a new composite asset",
|
target_asset=wip_composite_asset.name,
|
||||||
target_item_code="Macbook Pro",
|
|
||||||
target_asset_location="Test Location",
|
target_asset_location="Test Location",
|
||||||
stock_qty=stock_qty,
|
stock_qty=stock_qty,
|
||||||
stock_rate=stock_rate,
|
stock_rate=stock_rate,
|
||||||
@@ -240,7 +252,6 @@ class TestAssetCapitalization(IntegrationTestCase):
|
|||||||
|
|
||||||
# Create and submit Asset Captitalization
|
# Create and submit Asset Captitalization
|
||||||
asset_capitalization = create_asset_capitalization(
|
asset_capitalization = create_asset_capitalization(
|
||||||
capitalization_method="Choose a WIP composite asset",
|
|
||||||
target_asset=wip_composite_asset.name,
|
target_asset=wip_composite_asset.name,
|
||||||
target_asset_location="Test Location",
|
target_asset_location="Test Location",
|
||||||
stock_qty=stock_qty,
|
stock_qty=stock_qty,
|
||||||
@@ -251,7 +262,6 @@ class TestAssetCapitalization(IntegrationTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Test Asset Capitalization values
|
# 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.target_qty, 1)
|
||||||
|
|
||||||
self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate)
|
self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate)
|
||||||
@@ -310,7 +320,6 @@ class TestAssetCapitalization(IntegrationTestCase):
|
|||||||
|
|
||||||
# Create and submit Asset Captitalization
|
# Create and submit Asset Captitalization
|
||||||
asset_capitalization = create_asset_capitalization(
|
asset_capitalization = create_asset_capitalization(
|
||||||
capitalization_method="Choose a WIP composite asset",
|
|
||||||
target_asset=wip_composite_asset.name,
|
target_asset=wip_composite_asset.name,
|
||||||
target_asset_location="Test Location",
|
target_asset_location="Test Location",
|
||||||
service_qty=service_qty,
|
service_qty=service_qty,
|
||||||
@@ -340,6 +349,50 @@ class TestAssetCapitalization(IntegrationTestCase):
|
|||||||
self.assertFalse(get_actual_gle_dict(asset_capitalization.name))
|
self.assertFalse(get_actual_gle_dict(asset_capitalization.name))
|
||||||
self.assertFalse(get_actual_sle_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():
|
def create_asset_capitalization_data():
|
||||||
create_item("Capitalization Target Stock Item", is_stock_item=1, is_fixed_asset=0, is_purchase_item=0)
|
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 = frappe.new_doc("Asset Capitalization")
|
||||||
asset_capitalization.update(
|
asset_capitalization.update(
|
||||||
{
|
{
|
||||||
"capitalization_method": args.capitalization_method or None,
|
|
||||||
"company": company,
|
"company": company,
|
||||||
"posting_date": args.posting_date or now.strftime("%Y-%m-%d"),
|
"posting_date": args.posting_date or now.strftime("%Y-%m-%d"),
|
||||||
"posting_time": args.posting_time or now.strftime("%H:%M:%S.%f"),
|
"posting_time": args.posting_time or now.strftime("%H:%M:%S.%f"),
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ class AssetValueAdjustment(Document):
|
|||||||
asset = self.update_asset_value_after_depreciation()
|
asset = self.update_asset_value_after_depreciation()
|
||||||
note = self.get_adjustment_note()
|
note = self.get_adjustment_note()
|
||||||
reschedule_depreciation(asset, note)
|
reschedule_depreciation(asset, note)
|
||||||
|
asset.set_status()
|
||||||
|
|
||||||
def update_asset_value_after_depreciation(self):
|
def update_asset_value_after_depreciation(self):
|
||||||
difference_amount = self.difference_amount if self.docstatus == 1 else -1 * self.difference_amount
|
difference_amount = self.difference_amount if self.docstatus == 1 else -1 * self.difference_amount
|
||||||
|
|||||||
@@ -59,6 +59,19 @@ frappe.ui.form.on("Purchase Order", {
|
|||||||
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
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) {
|
refresh: function (frm) {
|
||||||
if (frm.doc.is_old_subcontracting_flow) {
|
if (frm.doc.is_old_subcontracting_flow) {
|
||||||
frm.trigger("get_materials_from_supplier");
|
frm.trigger("get_materials_from_supplier");
|
||||||
@@ -75,6 +88,7 @@ frappe.ui.form.on("Purchase Order", {
|
|||||||
if (frm.doc.docstatus == 0) {
|
if (frm.doc.docstatus == 0) {
|
||||||
erpnext.set_unit_price_items_note(frm);
|
erpnext.set_unit_price_items_note(frm);
|
||||||
}
|
}
|
||||||
|
prevent_past_schedule_dates(frm);
|
||||||
},
|
},
|
||||||
|
|
||||||
supplier: function (frm) {
|
supplier: function (frm) {
|
||||||
@@ -776,10 +790,6 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
|||||||
items_on_form_rendered() {
|
items_on_form_rendered() {
|
||||||
set_schedule_date(this.frm);
|
set_schedule_date(this.frm);
|
||||||
}
|
}
|
||||||
|
|
||||||
schedule_date() {
|
|
||||||
set_schedule_date(this.frm);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// for backward compatibility: combine new and previous states
|
// 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);
|
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),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,11 +30,15 @@
|
|||||||
"stock_qty",
|
"stock_qty",
|
||||||
"sec_break_price_list",
|
"sec_break_price_list",
|
||||||
"price_list_rate",
|
"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_percentage",
|
||||||
"discount_amount",
|
"discount_amount",
|
||||||
"distributed_discount_amount",
|
"distributed_discount_amount",
|
||||||
"col_break_price_list",
|
|
||||||
"base_price_list_rate",
|
|
||||||
"sec_break1",
|
"sec_break1",
|
||||||
"rate",
|
"rate",
|
||||||
"amount",
|
"amount",
|
||||||
@@ -531,10 +535,6 @@
|
|||||||
"fieldname": "sec_break_price_list",
|
"fieldname": "sec_break_price_list",
|
||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "col_break_price_list",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"fieldname": "ad_sec_break",
|
"fieldname": "ad_sec_break",
|
||||||
@@ -572,21 +572,57 @@
|
|||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Distributed Discount Amount",
|
"label": "Distributed Discount Amount",
|
||||||
"options": "currency"
|
"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,
|
"idx": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-06-02 06:22:17.864822",
|
"modified": "2025-06-17 12:05:52.441645",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Supplier Quotation Item",
|
"name": "Supplier Quotation Item",
|
||||||
"naming_rule": "Random",
|
"naming_rule": "Random",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ class SupplierQuotationItem(Document):
|
|||||||
lead_time_days: DF.Int
|
lead_time_days: DF.Int
|
||||||
manufacturer: DF.Link | None
|
manufacturer: DF.Link | None
|
||||||
manufacturer_part_no: DF.Data | 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: DF.Link | None
|
||||||
material_request_item: DF.Data | None
|
material_request_item: DF.Data | None
|
||||||
net_amount: DF.Currency
|
net_amount: DF.Currency
|
||||||
@@ -52,6 +54,7 @@ class SupplierQuotationItem(Document):
|
|||||||
project: DF.Link | None
|
project: DF.Link | None
|
||||||
qty: DF.Float
|
qty: DF.Float
|
||||||
rate: DF.Currency
|
rate: DF.Currency
|
||||||
|
rate_with_margin: DF.Currency
|
||||||
request_for_quotation: DF.Link | None
|
request_for_quotation: DF.Link | None
|
||||||
request_for_quotation_item: DF.Data | None
|
request_for_quotation_item: DF.Data | None
|
||||||
sales_order: DF.Link | None
|
sales_order: DF.Link | None
|
||||||
|
|||||||
@@ -241,18 +241,6 @@ class BuyingController(SubcontractingController):
|
|||||||
|
|
||||||
return [d.item_code for d in self.items if d.is_fixed_asset]
|
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):
|
def validate_from_warehouse(self):
|
||||||
for item in self.get("items"):
|
for item in self.get("items"):
|
||||||
if item.get("from_warehouse") and (item.get("from_warehouse") == item.get("warehouse")):
|
if item.get("from_warehouse") and (item.get("from_warehouse") == item.get("warehouse")):
|
||||||
|
|||||||
@@ -164,6 +164,17 @@ status_map = {
|
|||||||
["Draft", None],
|
["Draft", None],
|
||||||
["Completed", "eval:self.docstatus == 1"],
|
["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"],
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from collections import defaultdict
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, bold
|
from frappe import _, bold
|
||||||
|
from frappe.query_builder.functions import Sum
|
||||||
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate
|
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
@@ -884,6 +885,91 @@ class StockController(AccountsController):
|
|||||||
|
|
||||||
return sl_dict
|
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:
|
def update_inventory_dimensions(self, row, sl_dict) -> None:
|
||||||
# To handle delivery note and sales invoice
|
# To handle delivery note and sales invoice
|
||||||
if row.get("item_row"):
|
if row.get("item_row"):
|
||||||
@@ -934,7 +1020,7 @@ class StockController(AccountsController):
|
|||||||
fieldname = f"{dimension.source_fieldname}"
|
fieldname = f"{dimension.source_fieldname}"
|
||||||
|
|
||||||
sl_dict[dimension.target_fieldname] = row.get(fieldname)
|
sl_dict[dimension.target_fieldname] = row.get(fieldname)
|
||||||
return
|
continue
|
||||||
|
|
||||||
sl_dict[dimension.target_fieldname] = row.get(dimension.source_fieldname)
|
sl_dict[dimension.target_fieldname] = row.get(dimension.source_fieldname)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import frappe
|
|||||||
from frappe.core.doctype.user_permission.test_user_permission import create_user
|
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.core.doctype.user_permission.user_permission import add_user_permissions
|
||||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||||
from frappe.tests import IntegrationTestCase
|
|
||||||
|
|
||||||
from erpnext.controllers import queries
|
from erpnext.controllers import queries
|
||||||
from erpnext.tests.utils import ERPNextTestSuite
|
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 = {
|
params = {
|
||||||
"doctype": "Employee",
|
"doctype": "Employee",
|
||||||
"txt": "",
|
"txt": "",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"actions": [],
|
"actions": [],
|
||||||
"allow_import": 1,
|
"allow_import": 1,
|
||||||
"allow_rename": 1,
|
"allow_rename": 1,
|
||||||
|
"autoname": "CON-.YYYY.-.#####",
|
||||||
"creation": "2018-04-12 06:32:04.582486",
|
"creation": "2018-04-12 06:32:04.582486",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
@@ -256,10 +257,11 @@
|
|||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-05-23 13:54:03.346537",
|
"modified": "2025-06-19 17:48:45.049007",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "CRM",
|
"module": "CRM",
|
||||||
"name": "Contract",
|
"name": "Contract",
|
||||||
|
"naming_rule": "Expression (old style)",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
@@ -324,10 +326,12 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"row_format": "Dynamic",
|
"row_format": "Dynamic",
|
||||||
|
"search_fields": "party_type, party_name, contract_template",
|
||||||
"show_name_in_global_search": 1,
|
"show_name_in_global_search": 1,
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
|
"title_field": "party_name",
|
||||||
"track_changes": 1,
|
"track_changes": 1,
|
||||||
"track_seen": 1
|
"track_seen": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,19 +46,6 @@ class Contract(Document):
|
|||||||
status: DF.Literal["Unsigned", "Active", "Inactive"]
|
status: DF.Literal["Unsigned", "Active", "Inactive"]
|
||||||
# end: auto-generated types
|
# 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):
|
def validate(self):
|
||||||
self.set_missing_values()
|
self.set_missing_values()
|
||||||
self.validate_dates()
|
self.validate_dates()
|
||||||
|
|||||||
1464
erpnext/locale/ar.po
1464
erpnext/locale/ar.po
File diff suppressed because it is too large
Load Diff
1464
erpnext/locale/bs.po
1464
erpnext/locale/bs.po
File diff suppressed because it is too large
Load Diff
1464
erpnext/locale/de.po
1464
erpnext/locale/de.po
File diff suppressed because it is too large
Load Diff
1464
erpnext/locale/eo.po
1464
erpnext/locale/eo.po
File diff suppressed because it is too large
Load Diff
1464
erpnext/locale/es.po
1464
erpnext/locale/es.po
File diff suppressed because it is too large
Load Diff
1464
erpnext/locale/fa.po
1464
erpnext/locale/fa.po
File diff suppressed because it is too large
Load Diff
1464
erpnext/locale/fr.po
1464
erpnext/locale/fr.po
File diff suppressed because it is too large
Load Diff
1460
erpnext/locale/hr.po
1460
erpnext/locale/hr.po
File diff suppressed because it is too large
Load Diff
1460
erpnext/locale/hu.po
1460
erpnext/locale/hu.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1460
erpnext/locale/pl.po
1460
erpnext/locale/pl.po
File diff suppressed because it is too large
Load Diff
1460
erpnext/locale/pt.po
1460
erpnext/locale/pt.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1460
erpnext/locale/ru.po
1460
erpnext/locale/ru.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1484
erpnext/locale/sv.po
1484
erpnext/locale/sv.po
File diff suppressed because it is too large
Load Diff
1460
erpnext/locale/th.po
1460
erpnext/locale/th.po
File diff suppressed because it is too large
Load Diff
1520
erpnext/locale/tr.po
1520
erpnext/locale/tr.po
File diff suppressed because it is too large
Load Diff
1464
erpnext/locale/zh.po
1464
erpnext/locale/zh.po
File diff suppressed because it is too large
Load Diff
@@ -134,6 +134,7 @@
|
|||||||
"fieldname": "quantity",
|
"fieldname": "quantity",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Quantity",
|
"label": "Quantity",
|
||||||
|
"non_negative": 1,
|
||||||
"oldfieldname": "quantity",
|
"oldfieldname": "quantity",
|
||||||
"oldfieldtype": "Currency",
|
"oldfieldtype": "Currency",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
@@ -663,7 +664,7 @@
|
|||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-06-03 16:24:47.518411",
|
"modified": "2025-06-16 16:13:22.497695",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "BOM",
|
"name": "BOM",
|
||||||
@@ -696,10 +697,11 @@
|
|||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"search_fields": "item, item_name",
|
"search_fields": "item, item_name",
|
||||||
"show_name_in_global_search": 1,
|
"show_name_in_global_search": 1,
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,8 +45,7 @@ frappe.ui.form.on("Job Card", {
|
|||||||
|
|
||||||
setup_stock_entry(frm) {
|
setup_stock_entry(frm) {
|
||||||
if (
|
if (
|
||||||
frm.doc.manufactured_qty &&
|
frm.doc.track_semi_finished_goods &&
|
||||||
frm.doc.finished_good &&
|
|
||||||
frm.doc.docstatus === 1 &&
|
frm.doc.docstatus === 1 &&
|
||||||
!frm.doc.is_subcontracted &&
|
!frm.doc.is_subcontracted &&
|
||||||
flt(frm.doc.for_quantity) + flt(frm.doc.process_loss_qty) > flt(frm.doc.manufactured_qty)
|
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",
|
fieldtype: "Float",
|
||||||
label: __("Process Loss Quantity"),
|
label: __("Process Loss Quantity"),
|
||||||
fieldname: "process_loss_qty",
|
fieldname: "process_loss_qty",
|
||||||
reqd: 1,
|
|
||||||
onchange() {
|
onchange() {
|
||||||
let doc = frm.job_completion_dialog;
|
let doc = frm.job_completion_dialog;
|
||||||
|
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ class JobCard(Document):
|
|||||||
row.sub_operation = row.operation
|
row.sub_operation = row.operation
|
||||||
self.append("sub_operations", row)
|
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_time_in_mins = 0.0
|
||||||
self.total_completed_qty = 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"))
|
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:
|
for row in self.sub_operations:
|
||||||
self.total_completed_qty += row.completed_qty
|
self.total_completed_qty += row.completed_qty
|
||||||
|
|
||||||
@@ -670,7 +678,7 @@ class JobCard(Document):
|
|||||||
return
|
return
|
||||||
|
|
||||||
for d in doc.required_items:
|
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(
|
frappe.throw(
|
||||||
_("Row {0} : Operation is required against the raw material item {1}").format(
|
_("Row {0} : Operation is required against the raw material item {1}").format(
|
||||||
d.idx, d.item_code
|
d.idx, d.item_code
|
||||||
@@ -1221,6 +1229,8 @@ class JobCard(Document):
|
|||||||
if not self.employee and kwargs.employees:
|
if not self.employee and kwargs.employees:
|
||||||
self.set_employees(kwargs.employees)
|
self.set_employees(kwargs.employees)
|
||||||
|
|
||||||
|
self.validate_time_logs(save=True)
|
||||||
|
|
||||||
def update_workstation_status(self):
|
def update_workstation_status(self):
|
||||||
status_map = {
|
status_map = {
|
||||||
"Open": "Off",
|
"Open": "Off",
|
||||||
|
|||||||
@@ -1245,6 +1245,7 @@ def get_exploded_items(item_details, company, bom_no, include_non_stock_items, p
|
|||||||
item.purchase_uom,
|
item.purchase_uom,
|
||||||
item_uom.conversion_factor,
|
item_uom.conversion_factor,
|
||||||
item.safety_stock,
|
item.safety_stock,
|
||||||
|
bom.item.as_("main_bom_item"),
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
(bei.docstatus < 2)
|
(bei.docstatus < 2)
|
||||||
@@ -1993,6 +1994,7 @@ def get_raw_materials_of_sub_assembly_items(
|
|||||||
item.purchase_uom,
|
item.purchase_uom,
|
||||||
item_uom.conversion_factor,
|
item_uom.conversion_factor,
|
||||||
item.safety_stock,
|
item.safety_stock,
|
||||||
|
bom.item.as_("main_bom_item"),
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
(bei.docstatus == 1)
|
(bei.docstatus == 1)
|
||||||
|
|||||||
@@ -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
|
// formatter for work order operation
|
||||||
frm.set_indicator_formatter("operation", function (doc) {
|
frm.set_indicator_formatter("operation", function (doc) {
|
||||||
return frm.doc.qty == doc.completed_qty ? "green" : "orange";
|
return frm.doc.qty == doc.completed_qty ? "green" : "orange";
|
||||||
@@ -506,7 +517,6 @@ frappe.ui.form.on("Work Order", {
|
|||||||
callback: function (r) {
|
callback: function (r) {
|
||||||
if (r.message) {
|
if (r.message) {
|
||||||
frm.set_value("sales_order", "");
|
frm.set_value("sales_order", "");
|
||||||
frm.trigger("set_sales_order");
|
|
||||||
erpnext.in_production_item_onchange = true;
|
erpnext.in_production_item_onchange = true;
|
||||||
|
|
||||||
$.each(
|
$.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);
|
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) {
|
additional_operating_cost: function (frm) {
|
||||||
erpnext.work_order.calculate_cost(frm.doc);
|
erpnext.work_order.calculate_cost(frm.doc);
|
||||||
erpnext.work_order.calculate_total_cost(frm);
|
erpnext.work_order.calculate_total_cost(frm);
|
||||||
|
|||||||
@@ -2004,17 +2004,19 @@ def stop_unstop(work_order, status):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@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(
|
return frappe.get_list(
|
||||||
"Sales Order",
|
"Sales Order",
|
||||||
|
fields=["name"],
|
||||||
filters=[
|
filters=[
|
||||||
["Sales Order", "docstatus", "=", 1],
|
["Sales Order", "docstatus", "=", 1],
|
||||||
],
|
],
|
||||||
or_filters=[
|
or_filters=[
|
||||||
["Sales Order Item", "item_code", "=", production_item],
|
["Sales Order Item", "item_code", "=", filters.get("production_item")],
|
||||||
["Packed Item", "item_code", "=", production_item],
|
["Packed Item", "item_code", "=", filters.get("production_item")],
|
||||||
],
|
],
|
||||||
pluck="name",
|
as_list=True,
|
||||||
distinct=True,
|
distinct=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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.v15_0.remove_agriculture_roles
|
||||||
erpnext.patches.v14_0.update_full_name_in_contract
|
erpnext.patches.v14_0.update_full_name_in_contract
|
||||||
erpnext.patches.v15_0.drop_sle_indexes
|
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)
|
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
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import frappe
|
||||||
from frappe.model.utils.rename_field import rename_field
|
from frappe.model.utils.rename_field import rename_field
|
||||||
|
|
||||||
|
|
||||||
def execute():
|
def execute():
|
||||||
rename_field("POS Closing Entry", "pos_transactions", "pos_invoices")
|
rename_field("POS Closing Entry", "pos_transactions", "pos_invoices", validate=False)
|
||||||
rename_field("POS Closing Entry", "sales_invoice_transactions", "sales_invoices")
|
if frappe.db.exists("DocType", "Sales Invoice Reference"):
|
||||||
|
rename_field("POS Closing Entry", "sales_invoice_transactions", "sales_invoices", validate=False)
|
||||||
|
|||||||
7
erpnext/patches/v15_0/update_pegged_currencies.py
Normal file
7
erpnext/patches/v15_0/update_pegged_currencies.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
from erpnext.setup.install import update_pegged_currencies
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
update_pegged_currencies()
|
||||||
28
erpnext/patches/v15_0/update_pick_list_fields.py
Normal file
28
erpnext/patches/v15_0/update_pick_list_fields.py
Normal 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()
|
||||||
@@ -1017,7 +1017,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
}
|
}
|
||||||
|
|
||||||
var party = me.frm.doc[frappe.model.scrub(party_type)];
|
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({
|
return frappe.call({
|
||||||
method: "erpnext.accounts.party.get_party_account",
|
method: "erpnext.accounts.party.get_party_account",
|
||||||
args: {
|
args: {
|
||||||
|
|||||||
@@ -61,9 +61,13 @@ erpnext.setup.slides_settings = [
|
|||||||
|
|
||||||
onload: function (slide) {
|
onload: function (slide) {
|
||||||
this.bind_events(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 () {
|
validate: function () {
|
||||||
if (!this.validate_fy_dates()) {
|
if (!this.validate_fy_dates()) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1031,7 +1031,7 @@ erpnext.utils.map_current_doc = function (opts) {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
opts.allow_child_item_selection ||
|
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
|
// args contains filtered child docnames
|
||||||
opts.args = args;
|
opts.args = args;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from frappe.tests import IntegrationTestCase, change_settings
|
|||||||
from frappe.utils import add_days, add_months, flt, getdate, nowdate
|
from frappe.utils import add_days, add_months, flt, getdate, nowdate
|
||||||
|
|
||||||
from erpnext.controllers.accounts_controller import InvalidQtyError
|
from erpnext.controllers.accounts_controller import InvalidQtyError
|
||||||
|
from erpnext.setup.utils import get_exchange_rate
|
||||||
|
|
||||||
EXTRA_TEST_RECORD_DEPENDENCIES = ["Product Bundle"]
|
EXTRA_TEST_RECORD_DEPENDENCIES = ["Product Bundle"]
|
||||||
|
|
||||||
@@ -863,6 +864,24 @@ class TestQuotation(IntegrationTestCase):
|
|||||||
quotation.reload()
|
quotation.reload()
|
||||||
self.assertEqual(quotation.status, "Ordered")
|
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):
|
def enable_calculate_bundle_price(enable=1):
|
||||||
selling_settings = frappe.get_doc("Selling Settings")
|
selling_settings = frappe.get_doc("Selling Settings")
|
||||||
|
|||||||
@@ -7,9 +7,12 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"item_code",
|
"item_code",
|
||||||
|
"item_name",
|
||||||
"customer_item_code",
|
"customer_item_code",
|
||||||
"col_break1",
|
"col_break1",
|
||||||
"item_name",
|
"is_free_item",
|
||||||
|
"is_alternative",
|
||||||
|
"has_alternative_item",
|
||||||
"section_break_5",
|
"section_break_5",
|
||||||
"description",
|
"description",
|
||||||
"item_group",
|
"item_group",
|
||||||
@@ -53,9 +56,6 @@
|
|||||||
"base_net_amount",
|
"base_net_amount",
|
||||||
"pricing_rules",
|
"pricing_rules",
|
||||||
"stock_uom_rate",
|
"stock_uom_rate",
|
||||||
"is_free_item",
|
|
||||||
"is_alternative",
|
|
||||||
"has_alternative_item",
|
|
||||||
"section_break_43",
|
"section_break_43",
|
||||||
"valuation_rate",
|
"valuation_rate",
|
||||||
"column_break_45",
|
"column_break_45",
|
||||||
@@ -698,7 +698,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-12-12 13:49:17.765883",
|
"modified": "2025-06-12 17:31:47.775890",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Quotation Item",
|
"name": "Quotation Item",
|
||||||
|
|||||||
@@ -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.qty = flt(sre.reserved_qty) / flt(dn_item.get("conversion_factor", 1))
|
||||||
dn_item.warehouse = sre.warehouse
|
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)
|
dn_item.serial_and_batch_bundle = get_ssb_bundle_for_voucher(sre)
|
||||||
|
|
||||||
target_doc.append("items", dn_item)
|
target_doc.append("items", dn_item)
|
||||||
@@ -1774,8 +1780,8 @@ def create_pick_list(source_name, target_doc=None):
|
|||||||
"doctype": "Pick List Item",
|
"doctype": "Pick List Item",
|
||||||
"field_map": {
|
"field_map": {
|
||||||
"parent": "sales_order",
|
"parent": "sales_order",
|
||||||
"name": "sales_order_item",
|
"parent_detail_docname": "sales_order_item",
|
||||||
"parent_detail_docname": "product_bundle_item",
|
"name": "product_bundle_item",
|
||||||
},
|
},
|
||||||
"field_no_map": ["picked_qty"],
|
"field_no_map": ["picked_qty"],
|
||||||
"postprocess": update_packed_item_qty,
|
"postprocess": update_packed_item_qty,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ def after_install():
|
|||||||
add_app_name()
|
add_app_name()
|
||||||
update_roles()
|
update_roles()
|
||||||
make_default_operations()
|
make_default_operations()
|
||||||
|
update_pegged_currencies()
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
|
|
||||||
|
|
||||||
@@ -223,6 +224,27 @@ def create_default_role_profiles():
|
|||||||
role_profile.insert(ignore_permissions=True)
|
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 = {
|
DEFAULT_ROLE_PROFILES = {
|
||||||
"Inventory": [
|
"Inventory": [
|
||||||
"Stock User",
|
"Stock User",
|
||||||
|
|||||||
@@ -367,7 +367,7 @@ def add_uom_data():
|
|||||||
if not frappe.db.exists("UOM", d.get("uom_name")):
|
if not frappe.db.exists("UOM", d.get("uom_name")):
|
||||||
doc = frappe.new_doc("UOM")
|
doc = frappe.new_doc("UOM")
|
||||||
doc.update(d)
|
doc.update(d)
|
||||||
doc.save()
|
doc.insert(ignore_permissions=True)
|
||||||
|
|
||||||
# bootstrap uom conversion factors
|
# bootstrap uom conversion factors
|
||||||
uom_conversions = json.loads(
|
uom_conversions = json.loads(
|
||||||
@@ -505,6 +505,7 @@ def update_stock_settings():
|
|||||||
stock_settings.auto_insert_price_list_rate_if_missing = 1
|
stock_settings.auto_insert_price_list_rate_if_missing = 1
|
||||||
stock_settings.update_price_list_based_on = "Rate"
|
stock_settings.update_price_list_based_on = "Rate"
|
||||||
stock_settings.set_qty_in_transactions_based_on_serial_no_input = 1
|
stock_settings.set_qty_in_transactions_based_on_serial_no_input = 1
|
||||||
|
stock_settings.flags.ignore_permissions = True
|
||||||
stock_settings.save()
|
stock_settings.save()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,6 @@ from frappe.utils.nestedset import get_root_of
|
|||||||
|
|
||||||
from erpnext import get_default_company
|
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():
|
def before_tests():
|
||||||
frappe.clear_cache()
|
frappe.clear_cache()
|
||||||
@@ -47,11 +43,51 @@ def before_tests():
|
|||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
|
|
||||||
|
|
||||||
def get_pegged_rate(from_currency: str, to_currency: str, transaction_date) -> float | None:
|
def get_pegged_currencies():
|
||||||
if rate := PEGGED_CURRENCIES.get(from_currency, {}).get(to_currency):
|
pegged_currencies = frappe.get_all(
|
||||||
return rate
|
"Pegged Currency Details",
|
||||||
elif rate := PEGGED_CURRENCIES.get(to_currency, {}).get(from_currency):
|
filters={"parent": "Pegged Currencies"},
|
||||||
return 1 / rate
|
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 doesn’t match pegged currency logic, return None """
|
||||||
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"):
|
if frappe.get_cached_value("Currency Exchange Settings", "Currency Exchange Settings", "disabled"):
|
||||||
return 0.00
|
return 0.00
|
||||||
|
|
||||||
if rate := get_pegged_rate(from_currency, to_currency, transaction_date):
|
pegged_currencies = {}
|
||||||
return rate
|
|
||||||
|
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:
|
try:
|
||||||
cache = frappe.cache()
|
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")
|
settings = frappe.get_cached_doc("Currency Exchange Settings")
|
||||||
req_params = {
|
req_params = {
|
||||||
"transaction_date": transaction_date,
|
"transaction_date": transaction_date,
|
||||||
"from_currency": from_currency if from_currency != "AED" else "USD",
|
"from_currency": from_currency
|
||||||
"to_currency": to_currency if to_currency != "AED" else "USD",
|
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 = {}
|
params = {}
|
||||||
for row in settings.req_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)]
|
value = value[format_ces_api(str(res_key.key), req_params)]
|
||||||
cache.setex(name=key, time=21600, value=flt(value))
|
cache.setex(name=key, time=21600, value=flt(value))
|
||||||
|
|
||||||
# Support AED conversion through pegged USD
|
# Support multiple pegged currencies
|
||||||
value = flt(value)
|
value = flt(value)
|
||||||
if to_currency == "AED":
|
|
||||||
value *= 3.6725
|
if currency_settings.allow_pegged_currencies_exchange_rates and to_currency in pegged_currencies:
|
||||||
if from_currency == "AED":
|
value *= flt(pegged_currencies[to_currency]["ratio"])
|
||||||
value /= 3.6725
|
if currency_settings.allow_pegged_currencies_exchange_rates and from_currency in pegged_currencies:
|
||||||
|
value /= flt(pegged_currencies[from_currency]["ratio"])
|
||||||
|
|
||||||
return flt(value)
|
return flt(value)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -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.is_return && doc.status != "Closed") {
|
||||||
if (doc.docstatus == 1 && frappe.model.can_create("Shipment")) {
|
if (doc.docstatus == 1 && frappe.model.can_create("Shipment")) {
|
||||||
this.frm.add_custom_button(
|
this.frm.add_custom_button(
|
||||||
|
|||||||
@@ -37,7 +37,6 @@
|
|||||||
"ignore_pricing_rule",
|
"ignore_pricing_rule",
|
||||||
"items_section",
|
"items_section",
|
||||||
"scan_barcode",
|
"scan_barcode",
|
||||||
"pick_list",
|
|
||||||
"col_break_warehouse",
|
"col_break_warehouse",
|
||||||
"set_warehouse",
|
"set_warehouse",
|
||||||
"set_target_warehouse",
|
"set_target_warehouse",
|
||||||
@@ -1196,15 +1195,6 @@
|
|||||||
"options": "Sales Team",
|
"options": "Sales Team",
|
||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "pick_list",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"hidden": 1,
|
|
||||||
"label": "Pick List",
|
|
||||||
"options": "Pick List",
|
|
||||||
"read_only": 1,
|
|
||||||
"search_index": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fetch_from": "customer.is_internal_customer",
|
"fetch_from": "customer.is_internal_customer",
|
||||||
|
|||||||
@@ -176,6 +176,19 @@ class DeliveryNote(SellingController):
|
|||||||
"overflow_type": "delivery",
|
"overflow_type": "delivery",
|
||||||
"no_allowance": 1,
|
"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):
|
if cint(self.is_return):
|
||||||
self.status_updater.extend(
|
self.status_updater.extend(
|
||||||
@@ -328,18 +341,15 @@ class DeliveryNote(SellingController):
|
|||||||
def set_serial_and_batch_bundle_from_pick_list(self):
|
def set_serial_and_batch_bundle_from_pick_list(self):
|
||||||
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
||||||
|
|
||||||
if not self.pick_list:
|
|
||||||
return
|
|
||||||
|
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
if item.use_serial_batch_fields:
|
if item.use_serial_batch_fields or not item.against_pick_list:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if item.pick_list_item and not item.serial_and_batch_bundle:
|
if item.pick_list_item and not item.serial_and_batch_bundle:
|
||||||
filters = {
|
filters = {
|
||||||
"item_code": item.item_code,
|
"item_code": item.item_code,
|
||||||
"voucher_type": "Pick List",
|
"voucher_type": "Pick List",
|
||||||
"voucher_no": self.pick_list,
|
"voucher_no": item.against_pick_list,
|
||||||
"voucher_detail_no": item.pick_list_item,
|
"voucher_detail_no": item.pick_list_item,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,8 +454,6 @@ class DeliveryNote(SellingController):
|
|||||||
self.update_prevdoc_status()
|
self.update_prevdoc_status()
|
||||||
self.update_billing_status()
|
self.update_billing_status()
|
||||||
|
|
||||||
self.update_stock_reservation_entries()
|
|
||||||
|
|
||||||
if not self.is_return:
|
if not self.is_return:
|
||||||
self.check_credit_limit()
|
self.check_credit_limit()
|
||||||
elif self.issue_credit_note:
|
elif self.issue_credit_note:
|
||||||
@@ -458,6 +466,8 @@ class DeliveryNote(SellingController):
|
|||||||
self.make_bundle_for_sales_purchase_return(table_name)
|
self.make_bundle_for_sales_purchase_return(table_name)
|
||||||
self.make_bundle_using_old_serial_batch_fields(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,
|
# Updating stock ledger should always be called after updating prevdoc status,
|
||||||
# because updating reserved qty in bin depends upon updated delivered qty in SO
|
# because updating reserved qty in bin depends upon updated delivered qty in SO
|
||||||
self.update_stock_ledger()
|
self.update_stock_ledger()
|
||||||
@@ -588,7 +598,9 @@ class DeliveryNote(SellingController):
|
|||||||
def update_pick_list_status(self):
|
def update_pick_list_status(self):
|
||||||
from erpnext.stock.doctype.pick_list.pick_list import update_pick_list_status
|
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):
|
def check_next_docstatus(self):
|
||||||
submit_rv = frappe.db.sql(
|
submit_rv = frappe.db.sql(
|
||||||
@@ -795,12 +807,14 @@ def get_returned_qty_map(delivery_note):
|
|||||||
"""returns a map: {so_detail: returned_qty}"""
|
"""returns a map: {so_detail: returned_qty}"""
|
||||||
returned_qty_map = frappe._dict(
|
returned_qty_map = frappe._dict(
|
||||||
frappe.db.sql(
|
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
|
from `tabDelivery Note Item` dn_item, `tabDelivery Note` dn
|
||||||
where dn.name = dn_item.parent
|
where dn.name = dn_item.parent
|
||||||
and dn.docstatus = 1
|
and dn.docstatus = 1
|
||||||
and dn.is_return = 1
|
and dn.is_return = 1
|
||||||
and dn.return_against = %s
|
and dn.return_against = %s
|
||||||
|
and dn_item.qty <= 0
|
||||||
|
group by dn_item.item_code
|
||||||
""",
|
""",
|
||||||
delivery_note,
|
delivery_note,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import json
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
import frappe
|
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 frappe.utils import add_days, cstr, flt, getdate, nowdate, nowtime, today
|
||||||
|
|
||||||
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
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.per_billed, 100)
|
||||||
self.assertEqual(dn2.status, "Completed")
|
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):
|
def test_dn_billing_status_case3(self):
|
||||||
# SO -> DN1 -> SI and SO -> SI and SO -> DN2
|
# SO -> DN1 -> SI and SO -> SI and SO -> DN2
|
||||||
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
|
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
|
||||||
|
|||||||
@@ -77,6 +77,7 @@
|
|||||||
"against_sales_invoice",
|
"against_sales_invoice",
|
||||||
"si_detail",
|
"si_detail",
|
||||||
"dn_detail",
|
"dn_detail",
|
||||||
|
"against_pick_list",
|
||||||
"pick_list_item",
|
"pick_list_item",
|
||||||
"section_break_40",
|
"section_break_40",
|
||||||
"pick_serial_and_batch",
|
"pick_serial_and_batch",
|
||||||
@@ -935,6 +936,16 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_fguf",
|
"fieldname": "column_break_fguf",
|
||||||
"fieldtype": "Column Break"
|
"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,
|
"grid_page_length": 50,
|
||||||
@@ -942,7 +953,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-03-07 12:33:40.868499",
|
"modified": "2025-05-31 18:51:32.651562",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Delivery Note Item",
|
"name": "Delivery Note Item",
|
||||||
@@ -953,4 +964,4 @@
|
|||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": []
|
"states": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class DeliveryNoteItem(Document):
|
|||||||
|
|
||||||
actual_batch_qty: DF.Float
|
actual_batch_qty: DF.Float
|
||||||
actual_qty: DF.Float
|
actual_qty: DF.Float
|
||||||
|
against_pick_list: DF.Link | None
|
||||||
against_sales_invoice: DF.Link | None
|
against_sales_invoice: DF.Link | None
|
||||||
against_sales_order: DF.Link | None
|
against_sales_order: DF.Link | None
|
||||||
allow_zero_valuation_rate: DF.Check
|
allow_zero_valuation_rate: DF.Check
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"is_fixed_asset",
|
"is_fixed_asset",
|
||||||
"applicable_charges",
|
"applicable_charges",
|
||||||
"purchase_receipt_item",
|
"purchase_receipt_item",
|
||||||
|
"stock_entry_item",
|
||||||
"accounting_dimensions_section",
|
"accounting_dimensions_section",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
"dimension_col_break"
|
"dimension_col_break"
|
||||||
@@ -49,7 +50,7 @@
|
|||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Receipt Document Type",
|
"label": "Receipt Document Type",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Purchase Invoice\nPurchase Receipt",
|
"options": "Purchase Invoice\nPurchase Receipt\nStock Entry\nSubcontracting Receipt",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
@@ -131,18 +132,27 @@
|
|||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Is Fixed Asset",
|
"label": "Is Fixed Asset",
|
||||||
"read_only": 1
|
"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,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:09:59.220459",
|
"modified": "2025-06-11 08:53:38.096761",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Landed Cost Item",
|
"name": "Landed Cost Item",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": []
|
"states": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ class LandedCostItem(Document):
|
|||||||
qty: DF.Float
|
qty: DF.Float
|
||||||
rate: DF.Currency
|
rate: DF.Currency
|
||||||
receipt_document: DF.DynamicLink | None
|
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
|
# end: auto-generated types
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user