diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
index e24c7c9409f..e7dab34d04a 100644
--- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
+++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
@@ -47,9 +47,11 @@ def validate_columns(data):
no_of_columns = max([len(d) for d in data])
- if no_of_columns > 8:
+ if no_of_columns != 8:
frappe.throw(
- _("More columns found than expected. Please compare the uploaded file with standard template"),
+ _(
+ "Columns are not according to template. Please compare the uploaded file with standard template"
+ ),
title=(_("Wrong Template")),
)
diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js
index 741039acae6..eeda531c4d6 100644
--- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js
+++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js
@@ -85,18 +85,16 @@ frappe.ui.form.on("Exchange Rate Revaluation", {
},
make_jv: function (frm) {
- let revaluation_journal = null;
- let zero_balance_journal = null;
frappe.call({
method: "make_jv_entries",
doc: frm.doc,
freeze: true,
- freeze_message: "Making Journal Entries...",
+ freeze_message: __("Creating Journal Entries..."),
callback: function (r) {
if (r.message) {
let response = r.message;
if (response["revaluation_jv"] || response["zero_balance_jv"]) {
- frappe.msgprint(__("Journals have been created"));
+ frappe.msgprint(__("Journal entries have been created"));
}
}
},
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index d9a84431bbd..c28dcf525df 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -230,7 +230,7 @@ frappe.ui.form.on("Payment Entry", {
hide_unhide_fields: function (frm) {
var company_currency = frm.doc.company
- ? frappe.get_doc(":Company", frm.doc.company).default_currency
+ ? frappe.get_doc(":Company", frm.doc.company)?.default_currency
: "";
frm.toggle_display(
diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py
index 48bb6f2ae86..a2c6a9d856a 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/payment_request.py
@@ -338,6 +338,17 @@ class PaymentRequest(Document):
payment_entry.received_amount = amount
payment_entry.get("references")[0].allocated_amount = amount
+ # Update 'Paid Amount' on Forex transactions
+ if self.currency != ref_doc.company_currency:
+ if (
+ self.payment_request_type == "Outward"
+ and payment_entry.paid_from_account_currency == ref_doc.company_currency
+ and payment_entry.paid_from_account_currency != payment_entry.paid_to_account_currency
+ ):
+ payment_entry.paid_amount = payment_entry.base_paid_amount = (
+ payment_entry.target_exchange_rate * payment_entry.received_amount
+ )
+
for dimension in get_accounting_dimensions():
payment_entry.update({dimension: self.get(dimension)})
diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py
index 932060895b0..6d15f84d7cf 100644
--- a/erpnext/accounts/doctype/payment_request/test_payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py
@@ -4,10 +4,12 @@
import unittest
import frappe
+from frappe.tests.utils import FrappeTestCase
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.setup.utils import get_exchange_rate
@@ -32,7 +34,7 @@ payment_method = [
]
-class TestPaymentRequest(unittest.TestCase):
+class TestPaymentRequest(FrappeTestCase):
def setUp(self):
if not frappe.db.get_value("Payment Gateway", payment_gateway["gateway"], "name"):
frappe.get_doc(payment_gateway).insert(ignore_permissions=True)
@@ -260,3 +262,19 @@ class TestPaymentRequest(unittest.TestCase):
# Try to make Payment Request more than SO amount, should give validation
pr2.grand_total = 900
self.assertRaises(frappe.ValidationError, pr2.save)
+
+ def test_conversion_on_foreign_currency_accounts(self):
+ po_doc = create_purchase_order(supplier="_Test Supplier USD", currency="USD", do_not_submit=1)
+ po_doc.conversion_rate = 80
+ po_doc.items[0].qty = 1
+ po_doc.items[0].rate = 10
+ po_doc.save().submit()
+
+ pr = make_payment_request(dt=po_doc.doctype, dn=po_doc.name, recipient_id="nabin@erpnext.com")
+ pr = frappe.get_doc(pr).save().submit()
+
+ pe = pr.create_payment_entry()
+ self.assertEqual(pe.base_paid_amount, 800)
+ self.assertEqual(pe.paid_amount, 800)
+ self.assertEqual(pe.base_received_amount, 800)
+ self.assertEqual(pe.received_amount, 10)
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json
index ae6059c0057..22be5299280 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json
@@ -16,6 +16,7 @@
"cost_center",
"territory",
"ignore_exchange_rate_revaluation_journals",
+ "ignore_cr_dr_notes",
"column_break_14",
"to_date",
"finance_book",
@@ -383,10 +384,16 @@
"fieldname": "ignore_exchange_rate_revaluation_journals",
"fieldtype": "Check",
"label": "Ignore Exchange Rate Revaluation Journals"
+ },
+ {
+ "default": "0",
+ "fieldname": "ignore_cr_dr_notes",
+ "fieldtype": "Check",
+ "label": "Ignore System Generated Credit / Debit Notes"
}
],
"links": [],
- "modified": "2023-12-18 12:20:08.965120",
+ "modified": "2024-08-13 10:41:18.381165",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts",
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
index e0ec144e314..509199ccae6 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
@@ -54,6 +54,7 @@ class ProcessStatementOfAccounts(Document):
frequency: DF.Literal["Weekly", "Monthly", "Quarterly"]
from_date: DF.Date | None
group_by: DF.Literal["", "Group by Voucher", "Group by Voucher (Consolidated)"]
+ ignore_cr_dr_notes: DF.Check
ignore_exchange_rate_revaluation_journals: DF.Check
include_ageing: DF.Check
include_break: DF.Check
@@ -133,6 +134,9 @@ def get_statement_dict(doc, get_statement_dict=False):
if doc.ignore_exchange_rate_revaluation_journals:
filters.update({"ignore_err": True})
+ if doc.ignore_cr_dr_notes:
+ filters.update({"ignore_cr_dr_notes": True})
+
if doc.report == "General Ledger":
filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))
col, res = get_soa(filters)
diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
index 1d7b0c2f461..8a2ba36cf62 100644
--- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
+++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
@@ -57,6 +57,7 @@
"base_net_rate",
"base_net_amount",
"valuation_rate",
+ "sales_incoming_rate",
"item_tax_amount",
"landed_cost_voucher_amount",
"rm_supp_cost",
@@ -958,12 +959,22 @@
"print_hide": 1,
"read_only": 1,
"search_index": 1
+ },
+ {
+ "description": "Valuation rate for the item as per Sales Invoice (Only for Internal Transfers)",
+ "fieldname": "sales_incoming_rate",
+ "fieldtype": "Currency",
+ "hidden": 1,
+ "label": "Sales Incoming Rate",
+ "no_copy": 1,
+ "options": "Company:company:default_currency",
+ "print_hide": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2024-06-14 11:57:07.171700",
+ "modified": "2024-07-19 12:12:42.449298",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",
diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py
index baeece4815c..a8f844c6c1c 100644
--- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py
+++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py
@@ -79,6 +79,7 @@ class PurchaseInvoiceItem(Document):
rejected_serial_no: DF.Text | None
rejected_warehouse: DF.Link | None
rm_supp_cost: DF.Currency
+ sales_incoming_rate: DF.Currency
sales_invoice_item: DF.Data | None
serial_and_batch_bundle: DF.Link | None
serial_no: DF.Text | None
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 24e61b0d221..5d6c8a353fb 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -1316,6 +1316,10 @@ class SalesInvoice(SellingController):
for item in self.get("items"):
if flt(item.base_net_amount, item.precision("base_net_amount")):
+ # Do not book income for transfer within same company
+ if self.is_internal_transfer():
+ continue
+
if item.is_fixed_asset:
asset = self.get_asset(item)
@@ -1374,37 +1378,33 @@ class SalesInvoice(SellingController):
self.set_asset_status(asset)
else:
- # Do not book income for transfer within same company
- if not self.is_internal_transfer():
- income_account = (
- item.income_account
- if (not item.enable_deferred_revenue or self.is_return)
- else item.deferred_revenue_account
- )
+ income_account = (
+ item.income_account
+ if (not item.enable_deferred_revenue or self.is_return)
+ else item.deferred_revenue_account
+ )
- amount, base_amount = self.get_amount_and_base_amount(
- item, enable_discount_accounting
- )
+ amount, base_amount = self.get_amount_and_base_amount(item, enable_discount_accounting)
- account_currency = get_account_currency(income_account)
- gl_entries.append(
- self.get_gl_dict(
- {
- "account": income_account,
- "against": self.customer,
- "credit": flt(base_amount, item.precision("base_net_amount")),
- "credit_in_account_currency": (
- flt(base_amount, item.precision("base_net_amount"))
- if account_currency == self.company_currency
- else flt(amount, item.precision("net_amount"))
- ),
- "cost_center": item.cost_center,
- "project": item.project or self.project,
- },
- account_currency,
- item=item,
- )
+ account_currency = get_account_currency(income_account)
+ gl_entries.append(
+ self.get_gl_dict(
+ {
+ "account": income_account,
+ "against": self.customer,
+ "credit": flt(base_amount, item.precision("base_net_amount")),
+ "credit_in_account_currency": (
+ flt(base_amount, item.precision("base_net_amount"))
+ if account_currency == self.company_currency
+ else flt(amount, item.precision("net_amount"))
+ ),
+ "cost_center": item.cost_center,
+ "project": item.project or self.project,
+ },
+ account_currency,
+ item=item,
)
+ )
# expense account gl entries
if cint(self.update_stock) and erpnext.is_perpetual_inventory_enabled(self.company):
@@ -1479,6 +1479,10 @@ class SalesInvoice(SellingController):
if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount:
payment_mode.base_amount -= flt(self.change_amount)
+ against_voucher = self.name
+ if self.is_return and self.return_against and not self.update_outstanding_for_self:
+ against_voucher = self.return_against
+
if payment_mode.base_amount:
# POS, make payment entries
gl_entries.append(
@@ -1492,7 +1496,7 @@ class SalesInvoice(SellingController):
"credit_in_account_currency": payment_mode.base_amount
if self.party_account_currency == self.company_currency
else payment_mode.amount,
- "against_voucher": self.name,
+ "against_voucher": against_voucher,
"against_voucher_type": self.doctype,
"cost_center": self.cost_center,
},
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 9197f2e091a..fef30bdfecd 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -5,6 +5,7 @@ import copy
import json
import frappe
+from frappe import qb
from frappe.model.dynamic_links import get_dynamic_link_map
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, flt, getdate, nowdate, today
@@ -3083,6 +3084,84 @@ class TestSalesInvoice(FrappeTestCase):
party_link.delete()
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0)
+ def test_sales_invoice_against_supplier_usd_with_dimensions(self):
+ from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
+ make_customer,
+ )
+ from erpnext.accounts.doctype.party_link.party_link import create_party_link
+ from erpnext.buying.doctype.supplier.test_supplier import create_supplier
+
+ # create a customer
+ customer = make_customer(customer="_Test Common Supplier USD")
+ cust_doc = frappe.get_doc("Customer", customer)
+ cust_doc.default_currency = "USD"
+ cust_doc.save()
+ # create a supplier
+ supplier = create_supplier(supplier_name="_Test Common Supplier USD").name
+ supp_doc = frappe.get_doc("Supplier", supplier)
+ supp_doc.default_currency = "USD"
+ supp_doc.save()
+
+ # create a party link between customer & supplier
+ party_link = create_party_link("Supplier", supplier, customer)
+
+ # enable common party accounting
+ frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 1)
+
+ # create a dimension and make it mandatory
+ if not frappe.get_all("Accounting Dimension", filters={"document_type": "Department"}):
+ dim = frappe.get_doc(
+ {
+ "doctype": "Accounting Dimension",
+ "document_type": "Department",
+ "dimension_defaults": [{"company": "_Test Company", "mandatory_for_bs": True}],
+ }
+ )
+ dim.save()
+ else:
+ dim = frappe.get_doc(
+ "Accounting Dimension",
+ frappe.get_all("Accounting Dimension", filters={"document_type": "Department"})[0],
+ )
+ dim.disabled = False
+ dim.dimension_defaults = []
+ dim.append("dimension_defaults", {"company": "_Test Company", "mandatory_for_bs": True})
+ dim.save()
+
+ # create a sales invoice
+ si = create_sales_invoice(
+ customer=customer, parent_cost_center="_Test Cost Center - _TC", do_not_submit=True
+ )
+ si.department = "All Departments"
+ si.save().submit()
+
+ # check outstanding of sales invoice
+ si.reload()
+ self.assertEqual(si.status, "Paid")
+ self.assertEqual(flt(si.outstanding_amount), 0.0)
+
+ # check creation of journal entry
+ jv = frappe.get_all(
+ "Journal Entry Account",
+ {
+ "account": si.debit_to,
+ "party_type": "Customer",
+ "party": si.customer,
+ "reference_type": si.doctype,
+ "reference_name": si.name,
+ "department": "All Departments",
+ },
+ pluck="credit_in_account_currency",
+ )
+
+ self.assertTrue(jv)
+ self.assertEqual(jv[0], si.grand_total)
+
+ dim.disabled = True
+ dim.save()
+ party_link.delete()
+ frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0)
+
def test_payment_statuses(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
@@ -3758,6 +3837,40 @@ class TestSalesInvoice(FrappeTestCase):
]
self.assertEqual(expected, actual)
+ def test_pos_returns_without_update_outstanding_for_self(self):
+ from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
+
+ pos_profile = make_pos_profile()
+ pos_profile.payments = []
+ pos_profile.append("payments", {"default": 1, "mode_of_payment": "Cash"})
+ pos_profile.save()
+
+ pos = create_sales_invoice(qty=10, do_not_save=True)
+ pos.is_pos = 1
+ pos.pos_profile = pos_profile.name
+ pos.append(
+ "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 500}
+ )
+ pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 500})
+ pos.save().submit()
+
+ pos_return = make_sales_return(pos.name)
+ pos_return.update_outstanding_for_self = False
+ pos_return.save().submit()
+
+ gle = qb.DocType("GL Entry")
+ res = (
+ qb.from_(gle)
+ .select(gle.against_voucher)
+ .distinct()
+ .where(
+ gle.is_cancelled.eq(0) & gle.voucher_no.eq(pos_return.name) & gle.against_voucher.notnull()
+ )
+ .run(as_list=1)
+ )
+ self.assertEqual(len(res), 1)
+ self.assertEqual(res[0][0], pos_return.return_against)
+
def set_advance_flag(company, flag, default_account):
frappe.db.set_value(
diff --git a/erpnext/accounts/doctype/subscription/subscription.js b/erpnext/accounts/doctype/subscription/subscription.js
index b3e21abcaae..629d118080a 100644
--- a/erpnext/accounts/doctype/subscription/subscription.js
+++ b/erpnext/accounts/doctype/subscription/subscription.js
@@ -38,6 +38,12 @@ frappe.ui.form.on("Subscription", {
__("Actions")
);
+ frm.add_custom_button(
+ __("Force-Fetch Subscription Updates"),
+ () => frm.trigger("force_fetch_subscription_updates"),
+ __("Actions")
+ );
+
frm.add_custom_button(
__("Cancel Subscription"),
() => frm.trigger("cancel_this_subscription"),
@@ -82,4 +88,11 @@ frappe.ui.form.on("Subscription", {
}
});
},
+ force_fetch_subscription_updates: function (frm) {
+ frm.call("force_fetch_subscription_updates").then((r) => {
+ if (!r.exec) {
+ frm.reload_doc();
+ }
+ });
+ },
});
diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py
index a9dbf333335..916757a8d6d 100644
--- a/erpnext/accounts/doctype/subscription/subscription.py
+++ b/erpnext/accounts/doctype/subscription/subscription.py
@@ -717,6 +717,31 @@ class Subscription(Document):
self.update_subscription_period(posting_date or nowdate())
self.save()
+ @frappe.whitelist()
+ def force_fetch_subscription_updates(self):
+ """
+ Process Subscription and create Invoices even if current date doesn't lie between current_invoice_start and currenct_invoice_end
+ It makes use of 'Proces Subscription' to force processing in a specific 'posting_date'
+ """
+
+ # Don't process future subscriptions
+ if nowdate() < self.current_invoice_start:
+ frappe.msgprint(_("Subscription for Future dates cannot be processed."))
+ return
+
+ processing_date = None
+ if self.generate_invoice_at == "Beginning of the current subscription period":
+ processing_date = self.current_invoice_start
+ elif self.generate_invoice_at == "End of the current subscription period":
+ processing_date = self.current_invoice_end
+ elif self.generate_invoice_at == "Days before the current subscription period":
+ processing_date = add_days(self.current_invoice_start, -self.number_of_days)
+
+ process_subscription = frappe.new_doc("Process Subscription")
+ process_subscription.posting_date = processing_date
+ process_subscription.subscription = self.name
+ process_subscription.save().submit()
+
def is_prorate() -> int:
return cint(frappe.db.get_single_value("Subscription Settings", "prorate"))
diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py
index af3916ae469..8d4ec3dd084 100644
--- a/erpnext/accounts/doctype/subscription/test_subscription.py
+++ b/erpnext/accounts/doctype/subscription/test_subscription.py
@@ -521,6 +521,18 @@ class TestSubscription(FrappeTestCase):
subscription.process(posting_date="2023-01-22")
self.assertEqual(len(subscription.invoices), 2)
+ def test_future_subscription(self):
+ """Force-Fetch should not process future subscriptions"""
+ subscription = create_subscription(
+ start_date=add_months(nowdate(), 1),
+ submit_invoice=0,
+ generate_new_invoices_past_due_date=1,
+ party="_Test Subscription Customer John Doe",
+ )
+ subscription.force_fetch_subscription_updates()
+ subscription.reload()
+ self.assertEqual(len(subscription.invoices), 0)
+
def make_plans():
create_plan(plan_name="_Test Plan Name", cost=900, currency="INR")
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index 63e57438fca..2564eb0800f 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -248,7 +248,10 @@ def get_conditions(filters):
as_list=True,
)
if system_generated_cr_dr_journals:
- filters.update({"voucher_no_not_in": [x[0] for x in system_generated_cr_dr_journals]})
+ vouchers_to_ignore = (filters.get("voucher_no_not_in") or []) + [
+ x[0] for x in system_generated_cr_dr_journals
+ ]
+ filters.update({"voucher_no_not_in": vouchers_to_ignore})
if filters.get("voucher_no_not_in"):
conditions.append("voucher_no not in %(voucher_no_not_in)s")
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index 586385adb0d..06f170200a8 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -234,7 +234,7 @@ class TestAsset(AssetSetup):
pro_rata_amount, _, _ = _get_pro_rata_amt(
asset.finance_books[0],
9000,
- get_last_day(add_months(purchase_date, 1)),
+ add_days(get_last_day(add_months(purchase_date, 1)), 1),
date,
original_schedule_date=get_last_day(nowdate()),
)
@@ -320,7 +320,7 @@ class TestAsset(AssetSetup):
pro_rata_amount, _, _ = _get_pro_rata_amt(
asset.finance_books[0],
9000,
- get_last_day(add_months(purchase_date, 1)),
+ add_days(get_last_day(add_months(purchase_date, 1)), 1),
date,
original_schedule_date=get_last_day(nowdate()),
)
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
index e7b5a25cc73..bad89e93259 100644
--- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
@@ -291,7 +291,9 @@ class AssetDepreciationSchedule(Document):
if skip_row:
continue
- schedule_date = add_months(row.depreciation_start_date, n * cint(row.frequency_of_depreciation))
+ schedule_date = get_last_day(
+ add_months(row.depreciation_start_date, n * cint(row.frequency_of_depreciation))
+ )
if not current_fiscal_year_end_date:
current_fiscal_year_end_date = get_fiscal_year(row.depreciation_start_date)[2]
elif getdate(schedule_date) > getdate(current_fiscal_year_end_date):
@@ -330,8 +332,10 @@ class AssetDepreciationSchedule(Document):
getdate(asset_doc.available_for_use_date),
(asset_doc.opening_number_of_booked_depreciations * row.frequency_of_depreciation),
)
+ if is_last_day_of_the_month(getdate(asset_doc.available_for_use_date)):
+ from_date = get_last_day(from_date)
if self.depreciation_schedule:
- from_date = self.depreciation_schedule[-1].schedule_date
+ from_date = add_days(self.depreciation_schedule[-1].schedule_date, 1)
depreciation_amount, days, months = _get_pro_rata_amt(
row,
@@ -353,9 +357,8 @@ class AssetDepreciationSchedule(Document):
and not self.opening_accumulated_depreciation
and not self.flags.wdv_it_act_applied
):
- from_date = add_days(
- asset_doc.available_for_use_date, -1
- ) # needed to calc depr amount for available_for_use_date too
+ from_date = asset_doc.available_for_use_date
+ # needed to calc depr amount for available_for_use_date too
depreciation_amount, days, months = _get_pro_rata_amt(
row,
depreciation_amount,
@@ -406,6 +409,8 @@ class AssetDepreciationSchedule(Document):
(n + self.opening_number_of_booked_depreciations)
* cint(row.frequency_of_depreciation),
)
+ if is_last_day_of_the_month(getdate(asset_doc.available_for_use_date)):
+ asset_doc.to_date = get_last_day(asset_doc.to_date)
depreciation_amount_without_pro_rata = depreciation_amount
@@ -421,7 +426,7 @@ class AssetDepreciationSchedule(Document):
depreciation_amount_without_pro_rata, depreciation_amount
)
- schedule_date = add_days(schedule_date, days)
+ schedule_date = add_days(schedule_date, days - 1)
if not depreciation_amount:
continue
@@ -504,7 +509,10 @@ class AssetDepreciationSchedule(Document):
continue
if not accumulated_depreciation:
- if i > 0 and asset_doc.flags.decrease_in_asset_value_due_to_value_adjustment:
+ if i > 0 and (
+ asset_doc.flags.decrease_in_asset_value_due_to_value_adjustment
+ or asset_doc.flags.increase_in_asset_value_due_to_repair
+ ):
accumulated_depreciation = self.get("depreciation_schedule")[
i - 1
].accumulated_depreciation_amount
@@ -553,9 +561,11 @@ def _check_is_pro_rata(asset_doc, row, wdv_or_dd_non_yearly=False):
# otherwise, if opening_number_of_booked_depreciations = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12
# from_date = 01/01/2022
if row.depreciation_method in ("Straight Line", "Manual"):
- prev_depreciation_start_date = add_months(
- row.depreciation_start_date,
- (row.frequency_of_depreciation * -1) * asset_doc.opening_number_of_booked_depreciations,
+ prev_depreciation_start_date = get_last_day(
+ add_months(
+ row.depreciation_start_date,
+ (row.frequency_of_depreciation * -1) * asset_doc.opening_number_of_booked_depreciations,
+ )
)
from_date = asset_doc.available_for_use_date
days = date_diff(prev_depreciation_start_date, from_date) + 1
@@ -610,7 +620,7 @@ def _get_pro_rata_amt(
has_wdv_or_dd_non_yearly_pro_rata=False,
original_schedule_date=None,
):
- days = date_diff(to_date, from_date)
+ days = date_diff(to_date, from_date) + 1
months = month_diff(to_date, from_date)
if has_wdv_or_dd_non_yearly_pro_rata:
total_days = get_total_days(original_schedule_date or to_date, 12)
@@ -670,7 +680,7 @@ def get_straight_line_or_manual_depr_amount(
# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset value
elif asset.flags.increase_in_asset_value_due_to_repair:
return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / flt(
- row.total_number_of_depreciations
+ number_of_pending_depreciations
)
# if the Depreciation Schedule is being modified after Asset Value Adjustment due to decrease in asset value
elif asset.flags.decrease_in_asset_value_due_to_value_adjustment:
@@ -1034,6 +1044,7 @@ def make_new_active_asset_depr_schedules_and_cancel_current_ones(
date_of_return=None,
value_after_depreciation=None,
ignore_booked_entry=False,
+ difference_amount=None,
):
for row in asset_doc.get("finance_books"):
current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(
@@ -1048,6 +1059,8 @@ def make_new_active_asset_depr_schedules_and_cancel_current_ones(
)
new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
+ if asset_doc.flags.decrease_in_asset_value_due_to_value_adjustment and not value_after_depreciation:
+ value_after_depreciation = row.value_after_depreciation + difference_amount
if asset_doc.flags.increase_in_asset_value_due_to_repair and row.depreciation_method in (
"Written Down Value",
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js
index 9284c86bd92..67ce6e6f7ef 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.js
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.js
@@ -29,6 +29,15 @@ frappe.ui.form.on("Asset Repair", {
};
});
+ frm.set_query("purchase_invoice", function () {
+ return {
+ filters: {
+ company: frm.doc.company,
+ docstatus: 1,
+ },
+ };
+ });
+
frm.set_query("warehouse", "stock_items", function () {
return {
filters: {
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py
index 903e68e32e0..4e73148828d 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.py
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.py
@@ -117,7 +117,9 @@ class AssetRepair(AccountsController):
get_link_to_form(self.doctype, self.name),
)
self.asset_doc.flags.ignore_validate_update_after_submit = True
- make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
+ make_new_active_asset_depr_schedules_and_cancel_current_ones(
+ self.asset_doc, notes, ignore_booked_entry=True
+ )
self.asset_doc.save()
add_asset_activity(
@@ -154,7 +156,9 @@ class AssetRepair(AccountsController):
get_link_to_form(self.doctype, self.name),
)
self.asset_doc.flags.ignore_validate_update_after_submit = True
- make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
+ make_new_active_asset_depr_schedules_and_cancel_current_ones(
+ self.asset_doc, notes, ignore_booked_entry=True
+ )
self.asset_doc.save()
add_asset_activity(
diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
index 0d02347f20a..9b0212b037f 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
+++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
@@ -56,7 +56,8 @@ class AssetValueAdjustment(Document):
)
def on_cancel(self):
- self.update_asset(self.current_asset_value)
+ frappe.get_doc("Journal Entry", self.journal_entry).cancel()
+ self.update_asset()
add_asset_activity(
self.asset,
_("Asset's value adjusted after cancellation of Asset Value Adjustment {0}").format(
@@ -144,7 +145,7 @@ class AssetValueAdjustment(Document):
self.db_set("journal_entry", je.name)
- def update_asset(self, asset_value):
+ def update_asset(self, asset_value=None):
asset = frappe.get_doc("Asset", self.asset)
if not asset.calculate_depreciation:
@@ -170,7 +171,11 @@ class AssetValueAdjustment(Document):
)
make_new_active_asset_depr_schedules_and_cancel_current_ones(
- asset, notes, value_after_depreciation=asset_value, ignore_booked_entry=True
+ asset,
+ notes,
+ value_after_depreciation=asset_value,
+ ignore_booked_entry=True,
+ difference_amount=self.difference_amount,
)
asset.flags.ignore_validate_update_after_submit = True
asset.save()
diff --git a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py
index 634ed413773..963be704524 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py
+++ b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py
@@ -12,6 +12,7 @@ from erpnext.assets.doctype.asset.test_asset import create_asset_data
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_asset_depr_schedule_doc,
)
+from erpnext.assets.doctype.asset_repair.test_asset_repair import create_asset_repair
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
@@ -128,6 +129,136 @@ class TestAssetValueAdjustment(unittest.TestCase):
self.assertEqual(schedules, expected_schedules)
+ def test_depreciation_after_cancelling_asset_repair(self):
+ pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=120000.0, location="Test Location")
+
+ asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name")
+ asset_doc = frappe.get_doc("Asset", asset_name)
+ asset_doc.calculate_depreciation = 1
+ asset_doc.available_for_use_date = "2023-01-15"
+ asset_doc.purchase_date = "2023-01-15"
+
+ asset_doc.append(
+ "finance_books",
+ {
+ "expected_value_after_useful_life": 200,
+ "depreciation_method": "Straight Line",
+ "total_number_of_depreciations": 12,
+ "frequency_of_depreciation": 1,
+ "depreciation_start_date": "2023-01-31",
+ },
+ )
+ asset_doc.submit()
+
+ post_depreciation_entries(getdate("2023-08-21"))
+
+ # create asset repair
+ asset_repair = create_asset_repair(asset=asset_doc, capitalize_repair_cost=1, submit=1)
+
+ first_asset_depr_schedule = get_asset_depr_schedule_doc(asset_doc.name, "Active")
+ self.assertEqual(first_asset_depr_schedule.status, "Active")
+
+ # create asset value adjustment
+ current_value = get_asset_value_after_depreciation(asset_doc.name)
+
+ adj_doc = make_asset_value_adjustment(
+ asset=asset_doc.name,
+ current_asset_value=current_value,
+ new_asset_value=50000.0,
+ date="2023-08-21",
+ )
+ adj_doc.submit()
+
+ first_asset_depr_schedule.load_from_db()
+
+ second_asset_depr_schedule = get_asset_depr_schedule_doc(asset_doc.name, "Active")
+ self.assertEqual(second_asset_depr_schedule.status, "Active")
+ self.assertEqual(first_asset_depr_schedule.status, "Cancelled")
+
+ # Test gl entry creted from asset value adjustemnet
+ expected_gle = (
+ ("_Test Accumulated Depreciations - _TC", 0.0, 5625.29),
+ ("_Test Depreciations - _TC", 5625.29, 0.0),
+ )
+
+ gle = frappe.db.sql(
+ """select account, debit, credit from `tabGL Entry`
+ where voucher_type='Journal Entry' and voucher_no = %s
+ order by account""",
+ adj_doc.journal_entry,
+ )
+
+ self.assertSequenceEqual(gle, expected_gle)
+
+ # test depreciation schedule after asset repair and asset value adjustemnet
+ expected_schedules = [
+ ["2023-01-31", 5474.73, 5474.73],
+ ["2023-02-28", 9983.33, 15458.06],
+ ["2023-03-31", 9983.33, 25441.39],
+ ["2023-04-30", 9983.33, 35424.72],
+ ["2023-05-31", 9983.33, 45408.05],
+ ["2023-06-30", 9983.33, 55391.38],
+ ["2023-07-31", 9983.33, 65374.71],
+ ["2023-08-31", 2766.67, 68141.38],
+ ["2023-09-30", 2766.67, 70908.05],
+ ["2023-10-31", 2766.67, 73674.72],
+ ["2023-11-30", 2766.67, 76441.39],
+ ["2023-12-31", 2766.67, 79208.06],
+ ["2024-01-31", 2766.67, 81974.73],
+ ["2024-02-29", 2766.67, 84741.4],
+ ["2024-03-31", 2766.67, 87508.07],
+ ["2024-04-30", 2766.67, 90274.74],
+ ["2024-05-31", 2766.67, 93041.41],
+ ["2024-06-30", 2766.67, 95808.08],
+ ["2024-07-31", 2766.67, 98574.75],
+ ["2024-08-31", 2766.67, 101341.42],
+ ["2024-09-30", 2766.67, 104108.09],
+ ["2024-10-31", 2766.67, 106874.76],
+ ["2024-11-30", 2766.67, 109641.43],
+ ["2024-12-31", 2766.67, 112408.1],
+ ["2025-01-15", 2766.61, 115174.71],
+ ]
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in second_asset_depr_schedule.get("depreciation_schedule")
+ ]
+
+ self.assertEqual(schedules, expected_schedules)
+
+ # Cancel asset repair
+ asset_repair.cancel()
+ asset_repair.load_from_db()
+ second_asset_depr_schedule.load_from_db()
+
+ third_asset_depr_schedule = get_asset_depr_schedule_doc(asset_doc.name, "Active")
+ self.assertEqual(third_asset_depr_schedule.status, "Active")
+ self.assertEqual(second_asset_depr_schedule.status, "Cancelled")
+
+ # After cancelling asset repair asset life will be decreased and new depreciation schedule should be calculated
+ expected_schedules = [
+ ["2023-01-31", 5474.73, 5474.73],
+ ["2023-02-28", 9983.33, 15458.06],
+ ["2023-03-31", 9983.33, 25441.39],
+ ["2023-04-30", 9983.33, 35424.72],
+ ["2023-05-31", 9983.33, 45408.05],
+ ["2023-06-30", 9983.33, 55391.38],
+ ["2023-07-31", 9983.33, 65374.71],
+ ["2023-08-31", 8133.33, 73508.04],
+ ["2023-09-30", 8133.33, 81641.37],
+ ["2023-10-31", 8133.33, 89774.7],
+ ["2023-11-30", 8133.33, 97908.03],
+ ["2023-12-31", 8133.33, 106041.36],
+ ["2024-01-15", 8133.35, 114174.71],
+ ]
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in third_asset_depr_schedule.get("depreciation_schedule")
+ ]
+
+ self.assertEqual(schedules, expected_schedules)
+
def make_asset_value_adjustment(**args):
args = frappe._dict(args)
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index dc65d76a357..63d0404642a 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -2433,6 +2433,15 @@ class AccountsController(TransactionBase):
advance_entry.cost_center = self.cost_center or erpnext.get_default_cost_center(self.company)
advance_entry.is_advance = "Yes"
+ # update dimesions
+ dimensions_dict = frappe._dict()
+ active_dimensions = get_dimensions()[0]
+ for dim in active_dimensions:
+ dimensions_dict[dim.fieldname] = self.get(dim.fieldname)
+
+ reconcilation_entry.update(dimensions_dict)
+ advance_entry.update(dimensions_dict)
+
if self.doctype == "Sales Invoice":
reconcilation_entry.credit_in_account_currency = self.outstanding_amount
advance_entry.debit_in_account_currency = self.outstanding_amount
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index 958ac266e61..a55eded2a4c 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -314,18 +314,22 @@ class BuyingController(SubcontractingController):
get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0
)
+ net_rate = item.base_net_amount
+ if item.sales_incoming_rate: # for internal transfer
+ net_rate = item.qty * item.sales_incoming_rate
+
qty_in_stock_uom = flt(item.qty * item.conversion_factor)
if self.get("is_old_subcontracting_flow"):
item.rm_supp_cost = self.get_supplied_items_cost(item.name, reset_outgoing_rate)
item.valuation_rate = (
- item.base_net_amount
+ net_rate
+ item.item_tax_amount
+ item.rm_supp_cost
+ flt(item.landed_cost_voucher_amount)
) / qty_in_stock_uom
else:
item.valuation_rate = (
- item.base_net_amount
+ net_rate
+ item.item_tax_amount
+ flt(item.landed_cost_voucher_amount)
+ flt(item.get("rate_difference_with_purchase_invoice"))
@@ -336,72 +340,88 @@ class BuyingController(SubcontractingController):
update_regional_item_valuation_rate(self)
def set_incoming_rate(self):
- if self.doctype not in ("Purchase Receipt", "Purchase Invoice", "Purchase Order"):
+ """
+ Override item rate with incoming rate for internal stock transfer
+ """
+ if self.doctype not in ("Purchase Receipt", "Purchase Invoice"):
+ return
+
+ if not (self.doctype == "Purchase Receipt" or self.get("update_stock")):
+ return
+
+ if cint(self.get("is_return")):
+ # Get outgoing rate based on original item cost based on valuation method
return
if not self.is_internal_transfer():
return
+ allow_at_arms_length_price = frappe.get_cached_value(
+ "Stock Settings", None, "allow_internal_transfer_at_arms_length_price"
+ )
+ if allow_at_arms_length_price:
+ return
+
+ self.set_sales_incoming_rate_for_internal_transfer()
+
+ for d in self.get("items"):
+ d.discount_percentage = 0.0
+ d.discount_amount = 0.0
+ d.margin_rate_or_amount = 0.0
+
+ if d.rate == d.sales_incoming_rate:
+ continue
+
+ d.rate = d.sales_incoming_rate
+ frappe.msgprint(
+ _(
+ "Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer"
+ ).format(d.idx),
+ alert=1,
+ )
+
+ def set_sales_incoming_rate_for_internal_transfer(self):
+ """
+ Set incoming rate from the sales transaction against which the
+ purchase is made (internal transfer)
+ """
ref_doctype_map = {
- "Purchase Order": "Sales Order Item",
"Purchase Receipt": "Delivery Note Item",
"Purchase Invoice": "Sales Invoice Item",
}
ref_doctype = ref_doctype_map.get(self.doctype)
- items = self.get("items")
- for d in items:
- if not cint(self.get("is_return")):
- # Get outgoing rate based on original item cost based on valuation method
+ for d in self.get("items"):
+ if not d.get(frappe.scrub(ref_doctype)):
+ posting_time = self.get("posting_time")
+ if not posting_time:
+ posting_time = nowtime()
- if not d.get(frappe.scrub(ref_doctype)):
- posting_time = self.get("posting_time")
- if not posting_time and self.doctype == "Purchase Order":
- posting_time = nowtime()
+ outgoing_rate = get_incoming_rate(
+ {
+ "item_code": d.item_code,
+ "warehouse": d.get("from_warehouse"),
+ "posting_date": self.get("posting_date") or self.get("transaction_date"),
+ "posting_time": posting_time,
+ "qty": -1 * flt(d.get("stock_qty")),
+ "serial_and_batch_bundle": d.get("serial_and_batch_bundle"),
+ "company": self.company,
+ "voucher_type": self.doctype,
+ "voucher_no": self.name,
+ "allow_zero_valuation": d.get("allow_zero_valuation"),
+ "voucher_detail_no": d.name,
+ },
+ raise_error_if_no_rate=False,
+ )
- outgoing_rate = get_incoming_rate(
- {
- "item_code": d.item_code,
- "warehouse": d.get("from_warehouse"),
- "posting_date": self.get("posting_date") or self.get("transaction_date"),
- "posting_time": posting_time,
- "qty": -1 * flt(d.get("stock_qty")),
- "serial_and_batch_bundle": d.get("serial_and_batch_bundle"),
- "company": self.company,
- "voucher_type": self.doctype,
- "voucher_no": self.name,
- "allow_zero_valuation": d.get("allow_zero_valuation"),
- "voucher_detail_no": d.name,
- },
- raise_error_if_no_rate=False,
- )
-
- rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate"))
- else:
- field = (
- "incoming_rate"
- if self.get("is_internal_supplier") and not self.doctype == "Purchase Order"
- else "rate"
- )
- rate = flt(
- frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field)
- * (d.conversion_factor or 1),
- d.precision("rate"),
- )
-
- if self.is_internal_transfer():
- if self.doctype == "Purchase Receipt" or self.get("update_stock"):
- if rate != d.rate:
- d.rate = rate
- frappe.msgprint(
- _(
- "Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer"
- ).format(d.idx),
- alert=1,
- )
- d.discount_percentage = 0.0
- d.discount_amount = 0.0
- d.margin_rate_or_amount = 0.0
+ d.sales_incoming_rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate"))
+ else:
+ field = "incoming_rate" if self.get("is_internal_supplier") else "rate"
+ d.sales_incoming_rate = flt(
+ frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field)
+ * (d.conversion_factor or 1),
+ d.precision("rate"),
+ )
def validate_for_subcontracting(self):
if self.is_subcontracted and self.get("is_old_subcontracting_flow"):
@@ -566,11 +586,9 @@ class BuyingController(SubcontractingController):
if d.from_warehouse:
sle.dependant_sle_voucher_detail_no = d.name
else:
- val_rate_db_precision = 6 if cint(self.precision("valuation_rate", d)) <= 6 else 9
- incoming_rate = flt(d.valuation_rate, val_rate_db_precision)
sle.update(
{
- "incoming_rate": incoming_rate,
+ "incoming_rate": d.valuation_rate,
"recalculate_rate": 1
if (self.is_subcontracted and (d.bom or d.get("fg_item"))) or d.from_warehouse
else 0,
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index fd856194559..c1f565e7323 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -435,6 +435,9 @@ class SellingController(StockController):
if self.doctype not in ("Delivery Note", "Sales Invoice"):
return
+ allow_at_arms_length_price = frappe.get_cached_value(
+ "Stock Settings", None, "allow_internal_transfer_at_arms_length_price"
+ )
items = self.get("items") + (self.get("packed_items") or [])
for d in items:
if not frappe.get_cached_value("Item", d.item_code, "is_stock_item"):
@@ -481,6 +484,9 @@ class SellingController(StockController):
if d.incoming_rate != incoming_rate:
d.incoming_rate = incoming_rate
else:
+ if allow_at_arms_length_price:
+ continue
+
rate = flt(
flt(d.incoming_rate, d.precision("incoming_rate")) * d.conversion_factor,
d.precision("rate"),
diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py
index 3f6830c2021..b2f8fce3d31 100644
--- a/erpnext/controllers/tests/test_accounts_controller.py
+++ b/erpnext/controllers/tests/test_accounts_controller.py
@@ -5,7 +5,7 @@
import frappe
from frappe import qb
from frappe.query_builder.functions import Sum
-from frappe.tests.utils import FrappeTestCase
+from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, getdate, nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
@@ -13,6 +13,7 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_pay
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.party import get_party_account
+from erpnext.buying.doctype.purchase_order.test_purchase_order import prepare_data_for_internal_transfer
from erpnext.stock.doctype.item.test_item import create_item
@@ -804,6 +805,41 @@ class TestAccountsController(FrappeTestCase):
self.assertEqual(exc_je_for_si, [])
self.assertEqual(exc_je_for_pe, [])
+ @change_settings("Stock Settings", {"allow_internal_transfer_at_arms_length_price": 1})
+ def test_16_internal_transfer_at_arms_length_price(self):
+ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
+
+ prepare_data_for_internal_transfer()
+ company = "_Test Company with perpetual inventory"
+ target_warehouse = create_warehouse("_Test Internal Warehouse New 1", company=company)
+ warehouse = create_warehouse("_Test Internal Warehouse New 2", company=company)
+ arms_length_price = 40
+
+ si = create_sales_invoice(
+ company=company,
+ customer="_Test Internal Customer 2",
+ debit_to="Debtors - TCP1",
+ target_warehouse=target_warehouse,
+ warehouse=warehouse,
+ income_account="Sales - TCP1",
+ expense_account="Cost of Goods Sold - TCP1",
+ cost_center="Main - TCP1",
+ update_stock=True,
+ do_not_save=True,
+ do_not_submit=True,
+ )
+
+ si.items[0].rate = arms_length_price
+ si.save()
+ # rate should not reset to incoming rate
+ self.assertEqual(si.items[0].rate, arms_length_price)
+
+ frappe.db.set_single_value("Stock Settings", "allow_internal_transfer_at_arms_length_price", 0)
+ si.items[0].rate = arms_length_price
+ si.save()
+ # rate should reset to incoming rate
+ self.assertEqual(si.items[0].rate, 100)
+
def test_20_journal_against_sales_invoice(self):
# Invoice in Foreign Currency
si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1)
diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py
index db9d31b53bb..9229ab22227 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.py
+++ b/erpnext/crm/doctype/opportunity/opportunity.py
@@ -93,7 +93,26 @@ class Opportunity(TransactionBase, CRMNote):
def onload(self):
ref_doc = frappe.get_doc(self.opportunity_from, self.party_name)
+
load_address_and_contact(ref_doc)
+ load_address_and_contact(self)
+
+ ref_doc_contact_list = ref_doc.get("__onload").get("contact_list")
+ opportunity_doc_contact_list = [
+ contact
+ for contact in self.get("__onload").get("contact_list")
+ if contact not in ref_doc_contact_list
+ ]
+ ref_doc_contact_list.extend(opportunity_doc_contact_list)
+ ref_doc.set_onload("contact_list", ref_doc_contact_list)
+
+ ref_doc_addr_list = ref_doc.get("__onload").get("addr_list")
+ opportunity_doc_addr_list = [
+ addr for addr in self.get("__onload").get("addr_list") if addr not in ref_doc_addr_list
+ ]
+ ref_doc_addr_list.extend(opportunity_doc_addr_list)
+ ref_doc.set_onload("addr_list", ref_doc_addr_list)
+
self.set("__onload", ref_doc.get("__onload"))
def after_insert(self):
diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py
index 5a36c999747..2e8ae7e340e 100644
--- a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py
+++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py
@@ -108,7 +108,9 @@ class OpportunitySummaryBySalesStage:
self.grouped_data = []
grouping_key = lambda o: (o["sales_stage"], o[based_on]) # noqa
- for (sales_stage, _based_on), rows in groupby(self.query_result, grouping_key):
+ for (sales_stage, _based_on), rows in groupby(
+ sorted(self.query_result, key=grouping_key), key=grouping_key
+ ):
self.grouped_data.append(
{
"sales_stage": sales_stage,
diff --git a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py
index e5d34231a87..9c19e9f0148 100644
--- a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py
+++ b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py
@@ -122,7 +122,9 @@ class SalesPipelineAnalytics:
self.grouped_data = []
grouping_key = lambda o: (o.get(self.pipeline_by) or "Not Assigned", o[self.period_by]) # noqa
- for (pipeline_by, period_by), rows in groupby(self.query_result, grouping_key):
+ for (pipeline_by, period_by), rows in groupby(
+ sorted(self.query_result, key=grouping_key), grouping_key
+ ):
self.grouped_data.append(
{
self.pipeline_by: pipeline_by,
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index e1c2878e9bd..797e8cebc80 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -357,6 +357,7 @@ erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool
erpnext.patches.v15_0.allow_on_submit_dimensions_for_repostable_doctypes
erpnext.patches.v14_0.update_flag_for_return_invoices #2024-03-22
erpnext.patches.v15_0.create_accounting_dimensions_in_payment_request
+erpnext.patches.v14_0.update_pos_return_ledger_entries
# below migration patch should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
diff --git a/erpnext/patches/v14_0/update_pos_return_ledger_entries.py b/erpnext/patches/v14_0/update_pos_return_ledger_entries.py
new file mode 100644
index 00000000000..60e867d1bcb
--- /dev/null
+++ b/erpnext/patches/v14_0/update_pos_return_ledger_entries.py
@@ -0,0 +1,61 @@
+import frappe
+from frappe import qb
+
+
+def execute():
+ sinv = qb.DocType("Sales Invoice")
+ pos_returns_without_self = (
+ qb.from_(sinv)
+ .select(sinv.name)
+ .where(
+ sinv.docstatus.eq(1)
+ & sinv.is_pos.eq(1)
+ & sinv.is_return.eq(1)
+ & sinv.return_against.notnull()
+ & sinv.update_outstanding_for_self.eq(0)
+ )
+ .run()
+ )
+ if pos_returns_without_self:
+ pos_returns_without_self = [x[0] for x in pos_returns_without_self]
+
+ gle = qb.DocType("GL Entry")
+ gl_against_references = (
+ qb.from_(gle)
+ .select(gle.voucher_no, gle.against_voucher)
+ .where(
+ gle.voucher_no.isin(pos_returns_without_self)
+ & gle.against_voucher.notnull()
+ & gle.against_voucher.eq(gle.voucher_no)
+ & gle.is_cancelled.eq(0)
+ )
+ .run()
+ )
+
+ _vouchers = list(set([x[0] for x in gl_against_references]))
+ invoice_return_against = (
+ qb.from_(sinv)
+ .select(sinv.name, sinv.return_against)
+ .where(sinv.name.isin(_vouchers) & sinv.return_against.notnull())
+ .orderby(sinv.name)
+ .run()
+ )
+
+ valid_references = set(invoice_return_against)
+ actual_references = set(gl_against_references)
+
+ invalid_references = actual_references.difference(valid_references)
+
+ if invalid_references:
+ # Repost Accounting Ledger
+ pos_for_reposting = (
+ qb.from_(sinv)
+ .select(sinv.company, sinv.name)
+ .where(sinv.name.isin([x[0] for x in invalid_references]))
+ .run(as_dict=True)
+ )
+ for x in pos_for_reposting:
+ ral = frappe.new_doc("Repost Accounting Ledger")
+ ral.company = x.company
+ ral.append("vouchers", {"voucher_type": "Sales Invoice", "voucher_no": x.name})
+ ral.save().submit()
diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py
index 5b9b5c9a9f6..fb15b507efb 100644
--- a/erpnext/projects/doctype/timesheet/timesheet.py
+++ b/erpnext/projects/doctype/timesheet/timesheet.py
@@ -379,6 +379,9 @@ def make_sales_invoice(source_name, item_code=None, customer=None, currency=None
target.project = timesheet.parent_project
if customer:
target.customer = customer
+ default_price_list = frappe.get_value("Customer", customer, "default_price_list")
+ if default_price_list:
+ target.selling_price_list = default_price_list
if currency:
target.currency = currency
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 8699f253e3d..4696c7f3b4d 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -840,6 +840,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
var get_party_currency = function() {
+ if (me.is_a_mapped_document()) {
+ return;
+ }
+
var party_type = frappe.meta.has_field(me.frm.doc.doctype, "customer") ? "Customer" : "Supplier";
var party_name = me.frm.doc[party_type.toLowerCase()];
if (party_name) {
@@ -1249,16 +1253,24 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
is_a_mapped_document(item) {
const mapped_item_field_map = {
- "Delivery Note Item": ["si_detail", "so_detail", "dn_detail"],
- "Sales Invoice Item": ["dn_detail", "so_detail", "sales_invoice_item"],
- "Purchase Receipt Item": ["purchase_order_item", "purchase_invoice_item", "purchase_receipt_item"],
- "Purchase Invoice Item": ["purchase_order_item", "pr_detail", "po_detail"],
+ "Delivery Note": ["si_detail", "so_detail", "dn_detail"],
+ "Sales Invoice": ["dn_detail", "so_detail", "sales_invoice_item"],
+ "Purchase Receipt": ["purchase_order_item", "purchase_invoice_item", "purchase_receipt_item"],
+ "Purchase Invoice": ["purchase_order_item", "pr_detail", "po_detail"],
+ "Sales Order": ["prevdoc_docname", "quotation_item"],
};
- const mappped_fields = mapped_item_field_map[item.doctype] || [];
+ const mappped_fields = mapped_item_field_map[this.frm.doc.doctype] || [];
- return mappped_fields
- .map((field) => item[field])
- .filter(Boolean).length > 0;
+ if (item) {
+ return mappped_fields
+ .map((field) => item[field])
+ .filter(Boolean).length > 0;
+ } else if (this.frm.doc?.items) {
+ let first_row = this.frm.doc.items[0];
+ let mapped_rows = mappped_fields.filter(d => first_row[d])
+
+ return mapped_rows?.length > 0;
+ }
}
batch_no(doc, cdt, cdn) {
diff --git a/erpnext/selling/page/sales_funnel/sales_funnel.js b/erpnext/selling/page/sales_funnel/sales_funnel.js
index 1e2aa8ea56b..d9a0be9eedd 100644
--- a/erpnext/selling/page/sales_funnel/sales_funnel.js
+++ b/erpnext/selling/page/sales_funnel/sales_funnel.js
@@ -248,7 +248,7 @@ erpnext.SalesFunnel = class SalesFunnel {
context.fill();
// draw text
- context.fillStyle = "black";
+ context.fillStyle = getComputedStyle(document.body).getPropertyValue("--text-color");
context.textBaseline = "middle";
context.font = "1.1em sans-serif";
context.fillText(__(title), width + 20, y_mid);
diff --git a/erpnext/selling/page/sales_funnel/sales_funnel.py b/erpnext/selling/page/sales_funnel/sales_funnel.py
index 24bb0d2fe71..73deb8fe66b 100644
--- a/erpnext/selling/page/sales_funnel/sales_funnel.py
+++ b/erpnext/selling/page/sales_funnel/sales_funnel.py
@@ -93,7 +93,7 @@ def get_opp_by_lead_source(from_date, to_date, company):
summary = {}
sales_stages = set()
group_key = lambda o: (o["source"], o["sales_stage"]) # noqa
- for (source, sales_stage), rows in groupby(cp_opportunities, group_key):
+ for (source, sales_stage), rows in groupby(sorted(cp_opportunities, key=group_key), group_key):
summary.setdefault(source, {})[sales_stage] = sum(r["compound_amount"] for r in rows)
sales_stages.add(sales_stage)
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index c971c8da4f0..8a096aca80c 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -328,6 +328,9 @@ class DeliveryNote(SellingController):
return
for item in self.items:
+ if item.use_serial_batch_fields:
+ continue
+
if item.pick_list_item and not item.serial_and_batch_bundle:
filters = {
"item_code": item.item_code,
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 066dba95b1e..8d7022c3581 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -1097,7 +1097,8 @@ def create_delivery_note(source_name, target_doc=None):
)
)
- for customer, rows in groupby(sales_orders, key=lambda so: so["customer"]):
+ group_key = lambda so: so["customer"] # noqa
+ for customer, rows in groupby(sorted(sales_orders, key=group_key), key=group_key):
sales_dict[customer] = {row.sales_order for row in rows}
if sales_dict:
@@ -1178,6 +1179,7 @@ def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None):
dn_item.qty = flt(location.picked_qty) / (flt(location.conversion_factor) or 1)
dn_item.batch_no = location.batch_no
dn_item.serial_no = location.serial_no
+ dn_item.use_serial_batch_fields = location.use_serial_batch_fields
update_delivery_note_item(source_doc, dn_item, delivery_note)
diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
index fd26b611f45..610bceddf0f 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -67,6 +67,7 @@
"base_net_rate",
"base_net_amount",
"valuation_rate",
+ "sales_incoming_rate",
"item_tax_amount",
"rm_supp_cost",
"landed_cost_voucher_amount",
@@ -1124,12 +1125,22 @@
"fieldtype": "Check",
"label": "Return Qty from Rejected Warehouse",
"read_only": 1
+ },
+ {
+ "description": "Valuation rate for the item as per Sales Invoice (Only for Internal Transfers)",
+ "fieldname": "sales_incoming_rate",
+ "fieldtype": "Currency",
+ "hidden": 1,
+ "label": "Sales Incoming Rate",
+ "no_copy": 1,
+ "options": "Company:company:default_currency",
+ "print_hide": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2024-05-28 09:48:24.448815",
+ "modified": "2024-07-19 12:14:21.521466",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",
diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py
index 393b6a25691..2154007771d 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py
@@ -88,6 +88,7 @@ class PurchaseReceiptItem(Document):
return_qty_from_rejected_warehouse: DF.Check
returned_qty: DF.Float
rm_supp_cost: DF.Currency
+ sales_incoming_rate: DF.Currency
sales_order: DF.Link | None
sales_order_item: DF.Data | None
sample_quantity: DF.Int
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json
index 495af7f173a..3f467d3627a 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.json
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.json
@@ -303,7 +303,7 @@
"depends_on": "from_warehouse",
"fieldname": "source_warehouse_address",
"fieldtype": "Link",
- "label": "Source Warehouse Address",
+ "label": "Source Warehouse Address Link",
"options": "Address"
},
{
@@ -333,7 +333,7 @@
"depends_on": "to_warehouse",
"fieldname": "target_warehouse_address",
"fieldtype": "Link",
- "label": "Target Warehouse Address",
+ "label": "Target Warehouse Address Link",
"options": "Address"
},
{
@@ -686,10 +686,10 @@
"read_only": 1
},
{
- "fieldname": "tab_connections",
- "fieldtype": "Tab Break",
- "label": "Connections",
- "show_dashboard": 1
+ "fieldname": "tab_connections",
+ "fieldtype": "Tab Break",
+ "label": "Connections",
+ "show_dashboard": 1
}
],
"icon": "fa fa-file-text",
@@ -697,7 +697,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2024-06-26 19:12:17.937088",
+ "modified": "2024-08-13 19:02:42.386955",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry",
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json
index 11c4b1c14f7..069e7da41cb 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.json
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.json
@@ -32,6 +32,7 @@
"allow_negative_stock",
"show_barcode_field",
"clean_description_html",
+ "allow_internal_transfer_at_arms_length_price",
"quality_inspection_settings_section",
"action_if_quality_inspection_is_not_submitted",
"column_break_23",
@@ -439,6 +440,13 @@
"fieldtype": "Check",
"label": "Do Not Update Serial / Batch on Creation of Auto Bundle"
},
+ {
+ "default": "0",
+ "description": "If enabled, the item rate won't adjust to the valuation rate during internal transfers, but accounting will still use the valuation rate.",
+ "fieldname": "allow_internal_transfer_at_arms_length_price",
+ "fieldtype": "Check",
+ "label": "Allow Internal Transfers at Arm's Length Price"
+ },
{
"default": "0",
"depends_on": "eval:doc.valuation_method === \"Moving Average\"",
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py
index fae75f49777..c029b7bd1fb 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.py
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.py
@@ -27,6 +27,7 @@ class StockSettings(Document):
action_if_quality_inspection_is_rejected: DF.Literal["Stop", "Warn"]
allow_from_dn: DF.Check
allow_from_pr: DF.Check
+ allow_internal_transfer_at_arms_length_price: DF.Check
allow_negative_stock: DF.Check
allow_partial_reservation: DF.Check
allow_to_edit_stock_uom_qty_for_purchase: DF.Check
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index ac78f1a7f68..693481dfba8 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -6,13 +6,14 @@ import gzip
import json
import frappe
-from frappe import _, scrub
+from frappe import _, bold, scrub
from frappe.model.meta import get_field_precision
from frappe.query_builder.functions import Sum
from frappe.utils import (
cint,
cstr,
flt,
+ format_date,
get_link_to_form,
getdate,
now,
@@ -743,9 +744,29 @@ class update_entries_after:
return self.distinct_item_warehouses[key].dependent_voucher_detail_nos
+ def validate_previous_sle_qty(self, sle):
+ previous_sle = self.data[sle.warehouse].previous_sle
+ if previous_sle and previous_sle.get("qty_after_transaction") < 0 and sle.get("actual_qty") > 0:
+ frappe.msgprint(
+ _(
+ "The stock for the item {0} in the {1} warehouse was negative on the {2}. You should create a positive entry {3} before the date {4} and time {5} to post the correct valuation rate. For more details, please read the documentation."
+ ).format(
+ bold(sle.item_code),
+ bold(sle.warehouse),
+ bold(format_date(previous_sle.posting_date)),
+ sle.voucher_no,
+ bold(format_date(previous_sle.posting_date)),
+ bold(previous_sle.posting_time),
+ ),
+ title=_("Warning on Negative Stock"),
+ indicator="blue",
+ )
+
def process_sle(self, sle):
# previous sle data for this warehouse
self.wh_data = self.data[sle.warehouse]
+
+ self.validate_previous_sle_qty(sle)
self.affected_transactions.add((sle.voucher_type, sle.voucher_no))
if (sle.serial_no and not self.via_landed_cost_voucher) or not cint(self.allow_negative_stock):
diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv
index e80569069e7..12b2854862c 100644
--- a/erpnext/translations/de.csv
+++ b/erpnext/translations/de.csv
@@ -1334,7 +1334,7 @@ Lead to Quotation,Vom Lead zum Angebot,
Learn,Lernen,
Leave Management,Urlaube verwalten,
Leave and Attendance,Urlaub und Anwesenheit,
-Leave application {0} already exists against the student {1},Verlassen der Anwendung {0} ist bereits für den Schüler {1} vorhanden,
+Leave application {0} already exists against the student {1},Abwesenheitsantrag {0} existiert bereits für den Schüler {1},
Leaves has been granted sucessfully,Urlaub wurde genehmigt,
Leaves must be allocated in multiples of 0.5,"Abwesenheiten müssen ein Vielfaches von 0,5 sein",
Ledger,Hauptbuch,
@@ -5529,10 +5529,10 @@ Guardian Details,Erziehungsberechtigten-Details,
Guardians,Erziehungsberechtigte,
Sibling Details,Geschwister-Details,
Siblings,Geschwister,
-Exit,Verlassen,
+Exit,Austritt,
Date of Leaving,Austrittsdatum,
Leaving Certificate Number,Leaving Certificate Nummer,
-Reason For Leaving,Grund für das Verlassen,
+Reason For Leaving,Grund für den Austritt,
Student Admission,Studenten Eintritt,
Admission Start Date,Stichtag zum Zulassungsbeginn,
Admission End Date,Stichtag für Zulassungsende,
@@ -8795,13 +8795,13 @@ Warehouse wise Stock Value,Warenwert nach Lager,
Ex Works,Ab Werk,
Free Carrier,Frei Frachtführer,
Free Alongside Ship,Frei Längsseite Schiff,
-Free on Board,Frei an Bord,
+Free On Board,Frei an Bord,
Carriage Paid To,Frachtfrei,
Carriage and Insurance Paid to,Frachtfrei versichert,
Cost and Freight,Kosten und Fracht,
"Cost, Insurance and Freight","Kosten, Versicherung und Fracht",
-Delivered at Place,Geliefert benannter Ort,
-Delivered at Place Unloaded,Geliefert benannter Ort entladen,
+Delivered At Place,Geliefert benannter Ort,
+Delivered At Place Unloaded,Geliefert benannter Ort entladen,
Delivered Duty Paid,Geliefert verzollt,
Discount Validity,Frist für den Rabatt,
Discount Validity Based On,Frist für den Rabatt berechnet sich nach,