mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-16 11:39:18 +00:00
Merge pull request #39405 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -76,6 +76,7 @@ class TestBankReconciliationTool(AccountsTestMixin, FrappeTestCase):
|
|||||||
"deposit": 100,
|
"deposit": 100,
|
||||||
"bank_account": self.bank_account,
|
"bank_account": self.bank_account,
|
||||||
"reference_number": "123",
|
"reference_number": "123",
|
||||||
|
"currency": "INR",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.save()
|
.save()
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from frappe.model.docstatus import DocStatus
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import flt
|
from frappe.utils import flt
|
||||||
|
|
||||||
@@ -48,6 +49,24 @@ class BankTransaction(Document):
|
|||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
self.validate_duplicate_references()
|
self.validate_duplicate_references()
|
||||||
|
self.validate_currency()
|
||||||
|
|
||||||
|
def validate_currency(self):
|
||||||
|
"""
|
||||||
|
Bank Transaction should be on the same currency as the Bank Account.
|
||||||
|
"""
|
||||||
|
if self.currency and self.bank_account:
|
||||||
|
account = frappe.get_cached_value("Bank Account", self.bank_account, "account")
|
||||||
|
account_currency = frappe.get_cached_value("Account", account, "account_currency")
|
||||||
|
|
||||||
|
if self.currency != account_currency:
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"Transaction currency: {0} cannot be different from Bank Account({1}) currency: {2}"
|
||||||
|
).format(
|
||||||
|
frappe.bold(self.currency), frappe.bold(self.bank_account), frappe.bold(account_currency)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def set_status(self):
|
def set_status(self):
|
||||||
if self.docstatus == 2:
|
if self.docstatus == 2:
|
||||||
@@ -415,3 +434,21 @@ def unclear_reference_payment(doctype, docname, bt_name):
|
|||||||
bt = frappe.get_doc("Bank Transaction", bt_name)
|
bt = frappe.get_doc("Bank Transaction", bt_name)
|
||||||
set_voucher_clearance(doctype, docname, None, bt)
|
set_voucher_clearance(doctype, docname, None, bt)
|
||||||
return docname
|
return docname
|
||||||
|
|
||||||
|
|
||||||
|
def remove_from_bank_transaction(doctype, docname):
|
||||||
|
"""Remove a (cancelled) voucher from all Bank Transactions."""
|
||||||
|
for bt_name in get_reconciled_bank_transactions(doctype, docname):
|
||||||
|
bt = frappe.get_doc("Bank Transaction", bt_name)
|
||||||
|
if bt.docstatus == DocStatus.cancelled():
|
||||||
|
continue
|
||||||
|
|
||||||
|
modified = False
|
||||||
|
|
||||||
|
for pe in bt.payment_entries:
|
||||||
|
if pe.payment_document == doctype and pe.payment_entry == docname:
|
||||||
|
bt.remove(pe)
|
||||||
|
modified = True
|
||||||
|
|
||||||
|
if modified:
|
||||||
|
bt.save()
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import unittest
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import utils
|
from frappe import utils
|
||||||
|
from frappe.model.docstatus import DocStatus
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests.utils import FrappeTestCase
|
||||||
|
|
||||||
from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
|
from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
|
||||||
@@ -81,6 +81,29 @@ class TestBankTransaction(FrappeTestCase):
|
|||||||
clearance_date = frappe.db.get_value("Payment Entry", payment.name, "clearance_date")
|
clearance_date = frappe.db.get_value("Payment Entry", payment.name, "clearance_date")
|
||||||
self.assertFalse(clearance_date)
|
self.assertFalse(clearance_date)
|
||||||
|
|
||||||
|
def test_cancel_voucher(self):
|
||||||
|
bank_transaction = frappe.get_doc(
|
||||||
|
"Bank Transaction",
|
||||||
|
dict(description="1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G"),
|
||||||
|
)
|
||||||
|
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1700))
|
||||||
|
vouchers = json.dumps(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"payment_doctype": "Payment Entry",
|
||||||
|
"payment_name": payment.name,
|
||||||
|
"amount": bank_transaction.unallocated_amount,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
reconcile_vouchers(bank_transaction.name, vouchers)
|
||||||
|
payment.reload()
|
||||||
|
payment.cancel()
|
||||||
|
bank_transaction.reload()
|
||||||
|
self.assertEqual(bank_transaction.docstatus, DocStatus.submitted())
|
||||||
|
self.assertEqual(bank_transaction.unallocated_amount, 1700)
|
||||||
|
self.assertEqual(bank_transaction.payment_entries, [])
|
||||||
|
|
||||||
# Check if ERPNext can correctly filter a linked payments based on the debit/credit amount
|
# Check if ERPNext can correctly filter a linked payments based on the debit/credit amount
|
||||||
def test_debit_credit_output(self):
|
def test_debit_credit_output(self):
|
||||||
bank_transaction = frappe.get_doc(
|
bank_transaction = frappe.get_doc(
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ def test_record_generator():
|
|||||||
]
|
]
|
||||||
|
|
||||||
start = 2012
|
start = 2012
|
||||||
end = now_datetime().year + 5
|
end = now_datetime().year + 25
|
||||||
for year in range(start, end):
|
for year in range(start, end):
|
||||||
test_records.append(
|
test_records.append(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry");
|
|||||||
frappe.ui.form.on("Journal Entry", {
|
frappe.ui.form.on("Journal Entry", {
|
||||||
setup: function(frm) {
|
setup: function(frm) {
|
||||||
frm.add_fetch("bank_account", "account", "account");
|
frm.add_fetch("bank_account", "account", "account");
|
||||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement', 'Asset Depreciation Schedule', "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries"];
|
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement', 'Asset Depreciation Schedule', "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries", "Bank Transaction"];
|
||||||
},
|
},
|
||||||
|
|
||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ erpnext.accounts.taxes.setup_tax_filters("Advance Taxes and Charges");
|
|||||||
|
|
||||||
frappe.ui.form.on('Payment Entry', {
|
frappe.ui.form.on('Payment Entry', {
|
||||||
onload: function(frm) {
|
onload: function(frm) {
|
||||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger', 'Unreconcile Payment', 'Unreconcile Payment Entries'];
|
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger', 'Unreconcile Payment', 'Unreconcile Payment Entries', "Bank Transaction"];
|
||||||
|
|
||||||
if(frm.doc.__islocal) {
|
if(frm.doc.__islocal) {
|
||||||
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
|
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
|
||||||
|
|||||||
@@ -35,7 +35,17 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
|||||||
super.onload();
|
super.onload();
|
||||||
|
|
||||||
// Ignore linked advances
|
// Ignore linked advances
|
||||||
this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries", "Serial and Batch Bundle"];
|
this.frm.ignore_doctypes_on_cancel_all = [
|
||||||
|
"Journal Entry",
|
||||||
|
"Payment Entry",
|
||||||
|
"Purchase Invoice",
|
||||||
|
"Repost Payment Ledger",
|
||||||
|
"Repost Accounting Ledger",
|
||||||
|
"Unreconcile Payment",
|
||||||
|
"Unreconcile Payment Entries",
|
||||||
|
"Serial and Batch Bundle",
|
||||||
|
"Bank Transaction",
|
||||||
|
];
|
||||||
|
|
||||||
if(!this.frm.doc.__islocal) {
|
if(!this.frm.doc.__islocal) {
|
||||||
// show credit_to in print format
|
// show credit_to in print format
|
||||||
|
|||||||
@@ -296,6 +296,18 @@ class PurchaseInvoice(BuyingController):
|
|||||||
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||||
self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
|
self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
|
||||||
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
|
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
|
||||||
|
self.set_percentage_received()
|
||||||
|
|
||||||
|
def set_percentage_received(self):
|
||||||
|
total_billed_qty = 0.0
|
||||||
|
total_received_qty = 0.0
|
||||||
|
for row in self.items:
|
||||||
|
if row.purchase_receipt and row.pr_detail and row.received_qty:
|
||||||
|
total_billed_qty += row.qty
|
||||||
|
total_received_qty += row.received_qty
|
||||||
|
|
||||||
|
if total_billed_qty and total_received_qty:
|
||||||
|
self.per_received = total_received_qty / total_billed_qty * 100
|
||||||
|
|
||||||
def validate_release_date(self):
|
def validate_release_date(self):
|
||||||
if self.release_date and getdate(nowdate()) >= getdate(self.release_date):
|
if self.release_date and getdate(nowdate()) >= getdate(self.release_date):
|
||||||
|
|||||||
@@ -126,7 +126,7 @@
|
|||||||
"fieldname": "rate",
|
"fieldname": "rate",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Rate",
|
"label": "Tax Rate",
|
||||||
"oldfieldname": "rate",
|
"oldfieldname": "rate",
|
||||||
"oldfieldtype": "Currency"
|
"oldfieldtype": "Currency"
|
||||||
},
|
},
|
||||||
@@ -230,7 +230,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-08-05 20:04:36.618240",
|
"modified": "2024-01-14 10:04:36.618240",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Purchase Taxes and Charges",
|
"name": "Purchase Taxes and Charges",
|
||||||
|
|||||||
@@ -36,9 +36,19 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
|||||||
var me = this;
|
var me = this;
|
||||||
super.onload();
|
super.onload();
|
||||||
|
|
||||||
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log',
|
this.frm.ignore_doctypes_on_cancel_all = [
|
||||||
'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries",
|
"POS Invoice",
|
||||||
'Serial and Batch Bundle'
|
"Timesheet",
|
||||||
|
"POS Invoice Merge Log",
|
||||||
|
"POS Closing Entry",
|
||||||
|
"Journal Entry",
|
||||||
|
"Payment Entry",
|
||||||
|
"Repost Payment Ledger",
|
||||||
|
"Repost Accounting Ledger",
|
||||||
|
"Unreconcile Payment",
|
||||||
|
"Unreconcile Payment Entries",
|
||||||
|
"Serial and Batch Bundle",
|
||||||
|
"Bank Transaction",
|
||||||
];
|
];
|
||||||
|
|
||||||
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
|
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
|
||||||
|
|||||||
@@ -108,7 +108,7 @@
|
|||||||
"fieldname": "rate",
|
"fieldname": "rate",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Rate",
|
"label": "Tax Rate",
|
||||||
"oldfieldname": "rate",
|
"oldfieldname": "rate",
|
||||||
"oldfieldtype": "Currency"
|
"oldfieldtype": "Currency"
|
||||||
},
|
},
|
||||||
@@ -218,7 +218,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-10-17 13:08:17.776528",
|
"modified": "2024-01-14 10:08:17.776528",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Taxes and Charges",
|
"name": "Sales Taxes and Charges",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from frappe.utils.data import (
|
|||||||
date_diff,
|
date_diff,
|
||||||
flt,
|
flt,
|
||||||
get_last_day,
|
get_last_day,
|
||||||
|
get_link_to_form,
|
||||||
getdate,
|
getdate,
|
||||||
nowdate,
|
nowdate,
|
||||||
)
|
)
|
||||||
@@ -317,6 +318,37 @@ class Subscription(Document):
|
|||||||
if self.is_new():
|
if self.is_new():
|
||||||
self.set_subscription_status()
|
self.set_subscription_status()
|
||||||
|
|
||||||
|
self.validate_party_billing_currency()
|
||||||
|
|
||||||
|
def validate_party_billing_currency(self):
|
||||||
|
"""
|
||||||
|
Subscription should be of the same currency as the Party's default billing currency or company default.
|
||||||
|
"""
|
||||||
|
if self.party:
|
||||||
|
party_billing_currency = frappe.get_cached_value(
|
||||||
|
self.party_type, self.party, "default_currency"
|
||||||
|
) or frappe.get_cached_value("Company", self.company, "default_currency")
|
||||||
|
|
||||||
|
plans = [x.plan for x in self.plans]
|
||||||
|
subscription_plan_currencies = frappe.db.get_all(
|
||||||
|
"Subscription Plan", filters={"name": ("in", plans)}, fields=["name", "currency"]
|
||||||
|
)
|
||||||
|
unsupported_plans = []
|
||||||
|
for x in subscription_plan_currencies:
|
||||||
|
if x.currency != party_billing_currency:
|
||||||
|
unsupported_plans.append("{0}".format(get_link_to_form("Subscription Plan", x.name)))
|
||||||
|
|
||||||
|
if unsupported_plans:
|
||||||
|
unsupported_plans = [
|
||||||
|
_(
|
||||||
|
"Below Subscription Plans are of different currency to the party default billing currency/Company currency: {0}"
|
||||||
|
).format(frappe.bold(party_billing_currency))
|
||||||
|
] + unsupported_plans
|
||||||
|
|
||||||
|
frappe.throw(
|
||||||
|
unsupported_plans, frappe.ValidationError, "Unsupported Subscription Plans", as_list=True
|
||||||
|
)
|
||||||
|
|
||||||
def validate_trial_period(self) -> None:
|
def validate_trial_period(self) -> None:
|
||||||
"""
|
"""
|
||||||
Runs sanity checks on trial period dates for the `Subscription`
|
Runs sanity checks on trial period dates for the `Subscription`
|
||||||
@@ -563,6 +595,8 @@ class Subscription(Document):
|
|||||||
) and self.can_generate_new_invoice(posting_date):
|
) and self.can_generate_new_invoice(posting_date):
|
||||||
self.generate_invoice(posting_date=posting_date)
|
self.generate_invoice(posting_date=posting_date)
|
||||||
self.update_subscription_period(add_days(self.current_invoice_end, 1))
|
self.update_subscription_period(add_days(self.current_invoice_end, 1))
|
||||||
|
elif posting_date and getdate(posting_date) > getdate(self.current_invoice_end):
|
||||||
|
self.update_subscription_period()
|
||||||
|
|
||||||
if self.cancel_at_period_end and (
|
if self.cancel_at_period_end and (
|
||||||
getdate(posting_date) >= getdate(self.current_invoice_end)
|
getdate(posting_date) >= getdate(self.current_invoice_end)
|
||||||
|
|||||||
@@ -460,11 +460,13 @@ class TestSubscription(FrappeTestCase):
|
|||||||
self.assertEqual(len(subscription.invoices), 1)
|
self.assertEqual(len(subscription.invoices), 1)
|
||||||
|
|
||||||
def test_multi_currency_subscription(self):
|
def test_multi_currency_subscription(self):
|
||||||
|
party = "_Test Subscription Customer"
|
||||||
|
frappe.db.set_value("Customer", party, "default_currency", "USD")
|
||||||
subscription = create_subscription(
|
subscription = create_subscription(
|
||||||
start_date="2018-01-01",
|
start_date="2018-01-01",
|
||||||
generate_invoice_at="Beginning of the current subscription period",
|
generate_invoice_at="Beginning of the current subscription period",
|
||||||
plans=[{"plan": "_Test Plan Multicurrency", "qty": 1}],
|
plans=[{"plan": "_Test Plan Multicurrency", "qty": 1, "currency": "USD"}],
|
||||||
party="_Test Subscription Customer",
|
party=party,
|
||||||
)
|
)
|
||||||
|
|
||||||
subscription.process()
|
subscription.process()
|
||||||
@@ -528,13 +530,21 @@ class TestSubscription(FrappeTestCase):
|
|||||||
|
|
||||||
|
|
||||||
def make_plans():
|
def make_plans():
|
||||||
create_plan(plan_name="_Test Plan Name", cost=900)
|
create_plan(plan_name="_Test Plan Name", cost=900, currency="INR")
|
||||||
create_plan(plan_name="_Test Plan Name 2", cost=1999)
|
create_plan(plan_name="_Test Plan Name 2", cost=1999, currency="INR")
|
||||||
create_plan(
|
create_plan(
|
||||||
plan_name="_Test Plan Name 3", cost=1999, billing_interval="Day", billing_interval_count=14
|
plan_name="_Test Plan Name 3",
|
||||||
|
cost=1999,
|
||||||
|
billing_interval="Day",
|
||||||
|
billing_interval_count=14,
|
||||||
|
currency="INR",
|
||||||
)
|
)
|
||||||
create_plan(
|
create_plan(
|
||||||
plan_name="_Test Plan Name 4", cost=20000, billing_interval="Month", billing_interval_count=3
|
plan_name="_Test Plan Name 4",
|
||||||
|
cost=20000,
|
||||||
|
billing_interval="Month",
|
||||||
|
billing_interval_count=3,
|
||||||
|
currency="INR",
|
||||||
)
|
)
|
||||||
create_plan(
|
create_plan(
|
||||||
plan_name="_Test Plan Multicurrency", cost=50, billing_interval="Month", currency="USD"
|
plan_name="_Test Plan Multicurrency", cost=50, billing_interval="Month", currency="USD"
|
||||||
|
|||||||
@@ -41,7 +41,8 @@
|
|||||||
"fieldname": "currency",
|
"fieldname": "currency",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Currency",
|
"label": "Currency",
|
||||||
"options": "Currency"
|
"options": "Currency",
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_3",
|
"fieldname": "column_break_3",
|
||||||
@@ -148,10 +149,11 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-12-10 15:24:15.794477",
|
"modified": "2024-01-14 17:59:34.687977",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Subscription Plan",
|
"name": "Subscription Plan",
|
||||||
|
"naming_rule": "By fieldname",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
@@ -193,5 +195,6 @@
|
|||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ class SubscriptionPlan(Document):
|
|||||||
billing_interval_count: DF.Int
|
billing_interval_count: DF.Int
|
||||||
cost: DF.Currency
|
cost: DF.Currency
|
||||||
cost_center: DF.Link | None
|
cost_center: DF.Link | None
|
||||||
currency: DF.Link | None
|
currency: DF.Link
|
||||||
item: DF.Link
|
item: DF.Link
|
||||||
payment_gateway: DF.Link | None
|
payment_gateway: DF.Link | None
|
||||||
plan_name: DF.Data
|
plan_name: DF.Data
|
||||||
|
|||||||
@@ -84,10 +84,6 @@ function get_filters() {
|
|||||||
options: budget_against_options,
|
options: budget_against_options,
|
||||||
default: "Cost Center",
|
default: "Cost Center",
|
||||||
reqd: 1,
|
reqd: 1,
|
||||||
get_data: function() {
|
|
||||||
console.log(this.options);
|
|
||||||
return ["Emacs", "Rocks"];
|
|
||||||
},
|
|
||||||
on_change: function() {
|
on_change: function() {
|
||||||
frappe.query_report.set_filter_value("budget_against_filter", []);
|
frappe.query_report.set_filter_value("budget_against_filter", []);
|
||||||
frappe.query_report.refresh();
|
frappe.query_report.refresh();
|
||||||
|
|||||||
@@ -376,6 +376,10 @@ class PartyLedgerSummaryReport(object):
|
|||||||
if not income_or_expense_accounts:
|
if not income_or_expense_accounts:
|
||||||
# prevent empty 'in' condition
|
# prevent empty 'in' condition
|
||||||
income_or_expense_accounts.append("")
|
income_or_expense_accounts.append("")
|
||||||
|
else:
|
||||||
|
# escape '%' in account name
|
||||||
|
# ignoring frappe.db.escape as it replaces single quotes with double quotes
|
||||||
|
income_or_expense_accounts = [x.replace("%", "%%") for x in income_or_expense_accounts]
|
||||||
|
|
||||||
accounts_query = (
|
accounts_query = (
|
||||||
qb.from_(gl)
|
qb.from_(gl)
|
||||||
|
|||||||
@@ -368,7 +368,7 @@ def filter_invoices_based_on_dimensions(filters, query, parent_doc):
|
|||||||
dimension.document_type, filters.get(dimension.fieldname)
|
dimension.document_type, filters.get(dimension.fieldname)
|
||||||
)
|
)
|
||||||
fieldname = dimension.fieldname
|
fieldname = dimension.fieldname
|
||||||
query = query.where(parent_doc[fieldname] == filters.fieldname)
|
query = query.where(parent_doc[fieldname].isin(filters[fieldname]))
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ class TestUtils(unittest.TestCase):
|
|||||||
super(TestUtils, cls).setUpClass()
|
super(TestUtils, cls).setUpClass()
|
||||||
make_test_objects("Address", ADDRESS_RECORDS)
|
make_test_objects("Address", ADDRESS_RECORDS)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
frappe.db.rollback()
|
||||||
|
|
||||||
def test_get_party_shipping_address(self):
|
def test_get_party_shipping_address(self):
|
||||||
address = get_party_shipping_address("Customer", "_Test Customer 1")
|
address = get_party_shipping_address("Customer", "_Test Customer 1")
|
||||||
self.assertEqual(address, "_Test Billing Address 2 Title-Billing")
|
self.assertEqual(address, "_Test Billing Address 2 Title-Billing")
|
||||||
@@ -126,6 +130,38 @@ class TestUtils(unittest.TestCase):
|
|||||||
self.assertEqual(len(payment_entry.references), 1)
|
self.assertEqual(len(payment_entry.references), 1)
|
||||||
self.assertEqual(payment_entry.difference_amount, 0)
|
self.assertEqual(payment_entry.difference_amount, 0)
|
||||||
|
|
||||||
|
def test_naming_series_variable_parsing(self):
|
||||||
|
"""
|
||||||
|
Tests parsing utility used by Naming Series Variable hook for FY
|
||||||
|
"""
|
||||||
|
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||||
|
from frappe.utils import nowdate
|
||||||
|
|
||||||
|
from erpnext.accounts.utils import get_fiscal_year
|
||||||
|
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||||
|
|
||||||
|
# Configure Supplier Naming in Buying Settings
|
||||||
|
frappe.db.set_default("supp_master_name", "Auto Name")
|
||||||
|
|
||||||
|
# Configure Autoname in Supplier DocType
|
||||||
|
make_property_setter(
|
||||||
|
"Supplier", None, "naming_rule", "Expression", "Data", for_doctype="Doctype"
|
||||||
|
)
|
||||||
|
make_property_setter(
|
||||||
|
"Supplier", None, "autoname", "SUP-.FY.-.#####", "Data", for_doctype="Doctype"
|
||||||
|
)
|
||||||
|
|
||||||
|
fiscal_year = get_fiscal_year(nowdate())[0]
|
||||||
|
|
||||||
|
# Create Supplier
|
||||||
|
supplier = create_supplier()
|
||||||
|
|
||||||
|
# Check Naming Series in generated Supplier ID
|
||||||
|
doc_name = supplier.name.split("-")
|
||||||
|
self.assertEqual(len(doc_name), 3)
|
||||||
|
self.assertSequenceEqual(doc_name[0:2], ("SUP", fiscal_year))
|
||||||
|
frappe.db.set_default("supp_master_name", "Supplier Name")
|
||||||
|
|
||||||
|
|
||||||
ADDRESS_RECORDS = [
|
ADDRESS_RECORDS = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1263,7 +1263,7 @@ def get_autoname_with_number(number_value, doc_title, company):
|
|||||||
def parse_naming_series_variable(doc, variable):
|
def parse_naming_series_variable(doc, variable):
|
||||||
if variable == "FY":
|
if variable == "FY":
|
||||||
if doc:
|
if doc:
|
||||||
date = doc.get("posting_date") or doc.get("transaction_date")
|
date = doc.get("posting_date") or doc.get("transaction_date") or getdate()
|
||||||
company = doc.get("company")
|
company = doc.get("company")
|
||||||
else:
|
else:
|
||||||
date = getdate()
|
date = getdate()
|
||||||
|
|||||||
@@ -202,8 +202,7 @@
|
|||||||
"fieldname": "purchase_date",
|
"fieldname": "purchase_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"label": "Purchase Date",
|
"label": "Purchase Date",
|
||||||
"mandatory_depends_on": "eval:!doc.is_existing_asset",
|
"mandatory_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset",
|
||||||
"read_only": 1,
|
|
||||||
"read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset"
|
"read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -590,7 +589,7 @@
|
|||||||
"link_fieldname": "target_asset"
|
"link_fieldname": "target_asset"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2024-01-05 17:36:53.131512",
|
"modified": "2024-01-15 17:35:49.226603",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Assets",
|
"module": "Assets",
|
||||||
"name": "Asset",
|
"name": "Asset",
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ class Asset(AccountsController):
|
|||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.validate_cancellation()
|
self.validate_cancellation()
|
||||||
self.cancel_movement_entries()
|
self.cancel_movement_entries()
|
||||||
|
self.cancel_capitalization()
|
||||||
self.delete_depreciation_entries()
|
self.delete_depreciation_entries()
|
||||||
cancel_asset_depr_schedules(self)
|
cancel_asset_depr_schedules(self)
|
||||||
self.set_status()
|
self.set_status()
|
||||||
@@ -517,6 +518,16 @@ class Asset(AccountsController):
|
|||||||
movement = frappe.get_doc("Asset Movement", movement.get("name"))
|
movement = frappe.get_doc("Asset Movement", movement.get("name"))
|
||||||
movement.cancel()
|
movement.cancel()
|
||||||
|
|
||||||
|
def cancel_capitalization(self):
|
||||||
|
asset_capitalization = frappe.db.get_value(
|
||||||
|
"Asset Capitalization",
|
||||||
|
{"target_asset": self.name, "docstatus": 1, "entry_type": "Capitalization"},
|
||||||
|
)
|
||||||
|
|
||||||
|
if asset_capitalization:
|
||||||
|
asset_capitalization = frappe.get_doc("Asset Capitalization", asset_capitalization)
|
||||||
|
asset_capitalization.cancel()
|
||||||
|
|
||||||
def delete_depreciation_entries(self):
|
def delete_depreciation_entries(self):
|
||||||
if self.calculate_depreciation:
|
if self.calculate_depreciation:
|
||||||
for row in self.get("finance_books"):
|
for row in self.get("finance_books"):
|
||||||
@@ -1027,6 +1038,8 @@ def is_cwip_accounting_enabled(asset_category):
|
|||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_asset_value_after_depreciation(asset_name, finance_book=None):
|
def get_asset_value_after_depreciation(asset_name, finance_book=None):
|
||||||
asset = frappe.get_doc("Asset", asset_name)
|
asset = frappe.get_doc("Asset", asset_name)
|
||||||
|
if not asset.calculate_depreciation:
|
||||||
|
return flt(asset.value_after_depreciation)
|
||||||
|
|
||||||
return asset.get_value_after_depreciation(finance_book)
|
return asset.get_value_after_depreciation(finance_book)
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from frappe.utils import (
|
|||||||
)
|
)
|
||||||
from frappe.utils.user import get_users_with_role
|
from frappe.utils.user import get_users_with_role
|
||||||
|
|
||||||
|
import erpnext
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||||
get_checks_for_pl_and_bs_accounts,
|
get_checks_for_pl_and_bs_accounts,
|
||||||
)
|
)
|
||||||
@@ -522,6 +523,13 @@ def depreciate_asset(asset_doc, date, notes):
|
|||||||
|
|
||||||
make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date)
|
make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date)
|
||||||
|
|
||||||
|
cancel_depreciation_entries(asset_doc, date)
|
||||||
|
|
||||||
|
|
||||||
|
@erpnext.allow_regional
|
||||||
|
def cancel_depreciation_entries(asset_doc, date):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def reset_depreciation_schedule(asset_doc, date, notes):
|
def reset_depreciation_schedule(asset_doc, date, notes):
|
||||||
if not asset_doc.calculate_depreciation:
|
if not asset_doc.calculate_depreciation:
|
||||||
|
|||||||
@@ -891,7 +891,7 @@ class TestDepreciationMethods(AssetSetup):
|
|||||||
["2030-12-31", 28630.14, 28630.14],
|
["2030-12-31", 28630.14, 28630.14],
|
||||||
["2031-12-31", 35684.93, 64315.07],
|
["2031-12-31", 35684.93, 64315.07],
|
||||||
["2032-12-31", 17842.46, 82157.53],
|
["2032-12-31", 17842.46, 82157.53],
|
||||||
["2033-06-06", 5342.47, 87500.0],
|
["2033-06-06", 5342.46, 87499.99],
|
||||||
]
|
]
|
||||||
|
|
||||||
schedules = [
|
schedules = [
|
||||||
@@ -1003,7 +1003,7 @@ class TestDepreciationBasics(AssetSetup):
|
|||||||
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active")
|
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active")
|
||||||
|
|
||||||
depreciation_amount = get_depreciation_amount(
|
depreciation_amount = get_depreciation_amount(
|
||||||
asset_depr_schedule_doc, asset, 100000, asset.finance_books[0]
|
asset_depr_schedule_doc, asset, 100000, 100000, asset.finance_books[0]
|
||||||
)
|
)
|
||||||
self.assertEqual(depreciation_amount, 30000)
|
self.assertEqual(depreciation_amount, 30000)
|
||||||
|
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
|||||||
this.show_stock_ledger();
|
this.show_stock_ledger();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.frm.doc.stock_items && !this.frm.doc.stock_items.length && this.frm.doc.target_asset && this.frm.doc.capitalization_method === "Choose a WIP composite asset") {
|
// if (this.frm.doc.stock_items && !this.frm.doc.stock_items.length && 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();
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
setup_queries() {
|
setup_queries() {
|
||||||
@@ -143,13 +143,20 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
|||||||
},
|
},
|
||||||
callback: function (r) {
|
callback: function (r) {
|
||||||
if (!r.exc && r.message) {
|
if (!r.exc && r.message) {
|
||||||
me.frm.clear_table("stock_items");
|
if(r.message[0] && r.message[0].length) {
|
||||||
|
me.frm.clear_table("stock_items");
|
||||||
for (let item of r.message) {
|
for (let item of r.message[0]) {
|
||||||
me.frm.add_child("stock_items", item);
|
me.frm.add_child("stock_items", item);
|
||||||
|
}
|
||||||
|
refresh_field("stock_items");
|
||||||
|
}
|
||||||
|
if (r.message[1] && r.message[1].length) {
|
||||||
|
me.frm.clear_table("asset_items");
|
||||||
|
for (let item of r.message[1]) {
|
||||||
|
me.frm.add_child("asset_items", item);
|
||||||
|
}
|
||||||
|
me.frm.refresh_field("asset_items");
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh_field("stock_items");
|
|
||||||
|
|
||||||
me.calculate_totals();
|
me.calculate_totals();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,11 +136,19 @@ class AssetCapitalization(StockController):
|
|||||||
"Stock Ledger Entry",
|
"Stock Ledger Entry",
|
||||||
"Repost Item Valuation",
|
"Repost Item Valuation",
|
||||||
"Serial and Batch Bundle",
|
"Serial and Batch Bundle",
|
||||||
|
"Asset",
|
||||||
)
|
)
|
||||||
|
self.cancel_target_asset()
|
||||||
self.update_stock_ledger()
|
self.update_stock_ledger()
|
||||||
self.make_gl_entries()
|
self.make_gl_entries()
|
||||||
self.restore_consumed_asset_items()
|
self.restore_consumed_asset_items()
|
||||||
|
|
||||||
|
def cancel_target_asset(self):
|
||||||
|
if self.entry_type == "Capitalization" and self.target_asset:
|
||||||
|
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
||||||
|
if asset_doc.docstatus == 1:
|
||||||
|
asset_doc.cancel()
|
||||||
|
|
||||||
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_name or self.target_item_code
|
||||||
|
|
||||||
@@ -881,7 +889,6 @@ def get_consumed_asset_details(args):
|
|||||||
out.cost_center = get_default_cost_center(
|
out.cost_center = get_default_cost_center(
|
||||||
args, item_defaults, item_group_defaults, brand_defaults
|
args, item_defaults, item_group_defaults, brand_defaults
|
||||||
)
|
)
|
||||||
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@@ -929,10 +936,27 @@ def get_items_tagged_to_wip_composite_asset(asset):
|
|||||||
"qty",
|
"qty",
|
||||||
"valuation_rate",
|
"valuation_rate",
|
||||||
"amount",
|
"amount",
|
||||||
|
"is_fixed_asset",
|
||||||
|
"parent",
|
||||||
]
|
]
|
||||||
|
|
||||||
pr_items = frappe.get_all(
|
pr_items = frappe.get_all(
|
||||||
"Purchase Receipt Item", filters={"wip_composite_asset": asset}, fields=fields
|
"Purchase Receipt Item", filters={"wip_composite_asset": asset, "docstatus": 1}, fields=fields
|
||||||
)
|
)
|
||||||
|
|
||||||
return pr_items
|
stock_items = []
|
||||||
|
asset_items = []
|
||||||
|
for d in pr_items:
|
||||||
|
if not d.is_fixed_asset:
|
||||||
|
stock_items.append(frappe._dict(d))
|
||||||
|
else:
|
||||||
|
asset_details = frappe.db.get_value(
|
||||||
|
"Asset",
|
||||||
|
{"item_code": d.item_code, "purchase_receipt": d.parent},
|
||||||
|
["name as asset", "asset_name"],
|
||||||
|
as_dict=1,
|
||||||
|
)
|
||||||
|
d.update(asset_details)
|
||||||
|
asset_items.append(frappe._dict(d))
|
||||||
|
|
||||||
|
return stock_items, asset_items
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from frappe.model.document import Document
|
|||||||
from frappe.utils import (
|
from frappe.utils import (
|
||||||
add_days,
|
add_days,
|
||||||
add_months,
|
add_months,
|
||||||
|
add_years,
|
||||||
cint,
|
cint,
|
||||||
date_diff,
|
date_diff,
|
||||||
flt,
|
flt,
|
||||||
@@ -18,6 +19,7 @@ from frappe.utils import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
|
from erpnext.accounts.utils import get_fiscal_year
|
||||||
|
|
||||||
|
|
||||||
class AssetDepreciationSchedule(Document):
|
class AssetDepreciationSchedule(Document):
|
||||||
@@ -283,12 +285,20 @@ class AssetDepreciationSchedule(Document):
|
|||||||
depreciation_amount = 0
|
depreciation_amount = 0
|
||||||
|
|
||||||
number_of_pending_depreciations = final_number_of_depreciations - start
|
number_of_pending_depreciations = final_number_of_depreciations - start
|
||||||
|
yearly_opening_wdv = value_after_depreciation
|
||||||
|
current_fiscal_year_end_date = None
|
||||||
for n in range(start, final_number_of_depreciations):
|
for n in range(start, final_number_of_depreciations):
|
||||||
# If depreciation is already completed (for double declining balance)
|
# If depreciation is already completed (for double declining balance)
|
||||||
if skip_row:
|
if skip_row:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
schedule_date = 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):
|
||||||
|
current_fiscal_year_end_date = add_years(current_fiscal_year_end_date, 1)
|
||||||
|
yearly_opening_wdv = value_after_depreciation
|
||||||
|
|
||||||
if n > 0 and len(self.get("depreciation_schedule")) > n - 1:
|
if n > 0 and len(self.get("depreciation_schedule")) > n - 1:
|
||||||
prev_depreciation_amount = self.get("depreciation_schedule")[n - 1].depreciation_amount
|
prev_depreciation_amount = self.get("depreciation_schedule")[n - 1].depreciation_amount
|
||||||
else:
|
else:
|
||||||
@@ -298,6 +308,7 @@ class AssetDepreciationSchedule(Document):
|
|||||||
self,
|
self,
|
||||||
asset_doc,
|
asset_doc,
|
||||||
value_after_depreciation,
|
value_after_depreciation,
|
||||||
|
yearly_opening_wdv,
|
||||||
row,
|
row,
|
||||||
n,
|
n,
|
||||||
prev_depreciation_amount,
|
prev_depreciation_amount,
|
||||||
@@ -341,10 +352,7 @@ class AssetDepreciationSchedule(Document):
|
|||||||
n == 0
|
n == 0
|
||||||
and (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata)
|
and (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata)
|
||||||
and not self.opening_accumulated_depreciation
|
and not self.opening_accumulated_depreciation
|
||||||
and get_updated_rate_of_depreciation_for_wdv_and_dd(
|
and not self.flags.wdv_it_act_applied
|
||||||
asset_doc, value_after_depreciation, row, False
|
|
||||||
)
|
|
||||||
== row.rate_of_depreciation
|
|
||||||
):
|
):
|
||||||
from_date = add_days(
|
from_date = add_days(
|
||||||
asset_doc.available_for_use_date, -1
|
asset_doc.available_for_use_date, -1
|
||||||
@@ -404,8 +412,9 @@ class AssetDepreciationSchedule(Document):
|
|||||||
|
|
||||||
if not depreciation_amount:
|
if not depreciation_amount:
|
||||||
continue
|
continue
|
||||||
value_after_depreciation -= flt(
|
value_after_depreciation = flt(
|
||||||
depreciation_amount, asset_doc.precision("gross_purchase_amount")
|
value_after_depreciation - flt(depreciation_amount),
|
||||||
|
asset_doc.precision("gross_purchase_amount"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Adjust depreciation amount in the last period based on the expected value after useful life
|
# Adjust depreciation amount in the last period based on the expected value after useful life
|
||||||
@@ -585,6 +594,7 @@ def get_depreciation_amount(
|
|||||||
asset_depr_schedule,
|
asset_depr_schedule,
|
||||||
asset,
|
asset,
|
||||||
depreciable_value,
|
depreciable_value,
|
||||||
|
yearly_opening_wdv,
|
||||||
fb_row,
|
fb_row,
|
||||||
schedule_idx=0,
|
schedule_idx=0,
|
||||||
prev_depreciation_amount=0,
|
prev_depreciation_amount=0,
|
||||||
@@ -596,26 +606,18 @@ def get_depreciation_amount(
|
|||||||
asset_depr_schedule, asset, fb_row, schedule_idx, number_of_pending_depreciations
|
asset_depr_schedule, asset, fb_row, schedule_idx, number_of_pending_depreciations
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
rate_of_depreciation = get_updated_rate_of_depreciation_for_wdv_and_dd(
|
|
||||||
asset, depreciable_value, fb_row
|
|
||||||
)
|
|
||||||
return get_wdv_or_dd_depr_amount(
|
return get_wdv_or_dd_depr_amount(
|
||||||
|
asset,
|
||||||
|
fb_row,
|
||||||
depreciable_value,
|
depreciable_value,
|
||||||
rate_of_depreciation,
|
yearly_opening_wdv,
|
||||||
fb_row.frequency_of_depreciation,
|
|
||||||
schedule_idx,
|
schedule_idx,
|
||||||
prev_depreciation_amount,
|
prev_depreciation_amount,
|
||||||
has_wdv_or_dd_non_yearly_pro_rata,
|
has_wdv_or_dd_non_yearly_pro_rata,
|
||||||
|
asset_depr_schedule,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@erpnext.allow_regional
|
|
||||||
def get_updated_rate_of_depreciation_for_wdv_and_dd(
|
|
||||||
asset, depreciable_value, fb_row, show_msg=True
|
|
||||||
):
|
|
||||||
return fb_row.rate_of_depreciation
|
|
||||||
|
|
||||||
|
|
||||||
def get_straight_line_or_manual_depr_amount(
|
def get_straight_line_or_manual_depr_amount(
|
||||||
asset_depr_schedule, asset, row, schedule_idx, number_of_pending_depreciations
|
asset_depr_schedule, asset, row, schedule_idx, number_of_pending_depreciations
|
||||||
):
|
):
|
||||||
@@ -751,30 +753,57 @@ def get_asset_shift_factors_map():
|
|||||||
return dict(frappe.db.get_all("Asset Shift Factor", ["shift_name", "shift_factor"], as_list=True))
|
return dict(frappe.db.get_all("Asset Shift Factor", ["shift_name", "shift_factor"], as_list=True))
|
||||||
|
|
||||||
|
|
||||||
|
@erpnext.allow_regional
|
||||||
def get_wdv_or_dd_depr_amount(
|
def get_wdv_or_dd_depr_amount(
|
||||||
|
asset,
|
||||||
|
fb_row,
|
||||||
depreciable_value,
|
depreciable_value,
|
||||||
rate_of_depreciation,
|
yearly_opening_wdv,
|
||||||
frequency_of_depreciation,
|
|
||||||
schedule_idx,
|
schedule_idx,
|
||||||
prev_depreciation_amount,
|
prev_depreciation_amount,
|
||||||
has_wdv_or_dd_non_yearly_pro_rata,
|
has_wdv_or_dd_non_yearly_pro_rata,
|
||||||
|
asset_depr_schedule,
|
||||||
):
|
):
|
||||||
if cint(frequency_of_depreciation) == 12:
|
return get_default_wdv_or_dd_depr_amount(
|
||||||
return flt(depreciable_value) * (flt(rate_of_depreciation) / 100)
|
asset,
|
||||||
|
fb_row,
|
||||||
|
depreciable_value,
|
||||||
|
schedule_idx,
|
||||||
|
prev_depreciation_amount,
|
||||||
|
has_wdv_or_dd_non_yearly_pro_rata,
|
||||||
|
asset_depr_schedule,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_wdv_or_dd_depr_amount(
|
||||||
|
asset,
|
||||||
|
fb_row,
|
||||||
|
depreciable_value,
|
||||||
|
schedule_idx,
|
||||||
|
prev_depreciation_amount,
|
||||||
|
has_wdv_or_dd_non_yearly_pro_rata,
|
||||||
|
asset_depr_schedule,
|
||||||
|
):
|
||||||
|
if cint(fb_row.frequency_of_depreciation) == 12:
|
||||||
|
return flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100)
|
||||||
else:
|
else:
|
||||||
if has_wdv_or_dd_non_yearly_pro_rata:
|
if has_wdv_or_dd_non_yearly_pro_rata:
|
||||||
if schedule_idx == 0:
|
if schedule_idx == 0:
|
||||||
return flt(depreciable_value) * (flt(rate_of_depreciation) / 100)
|
return flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100)
|
||||||
elif schedule_idx % (12 / cint(frequency_of_depreciation)) == 1:
|
elif schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 1:
|
||||||
return (
|
return (
|
||||||
flt(depreciable_value) * flt(frequency_of_depreciation) * (flt(rate_of_depreciation) / 1200)
|
flt(depreciable_value)
|
||||||
|
* flt(fb_row.frequency_of_depreciation)
|
||||||
|
* (flt(fb_row.rate_of_depreciation) / 1200)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return prev_depreciation_amount
|
return prev_depreciation_amount
|
||||||
else:
|
else:
|
||||||
if schedule_idx % (12 / cint(frequency_of_depreciation)) == 0:
|
if schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 0:
|
||||||
return (
|
return (
|
||||||
flt(depreciable_value) * flt(frequency_of_depreciation) * (flt(rate_of_depreciation) / 1200)
|
flt(depreciable_value)
|
||||||
|
* flt(fb_row.frequency_of_depreciation)
|
||||||
|
* (flt(fb_row.rate_of_depreciation) / 1200)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return prev_depreciation_amount
|
return prev_depreciation_amount
|
||||||
|
|||||||
@@ -94,7 +94,6 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"depends_on": "eval:doc.depreciation_method == \"Straight Line\" || doc.depreciation_method == \"Manual\"",
|
|
||||||
"fieldname": "daily_prorata_based",
|
"fieldname": "daily_prorata_based",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Depreciate based on daily pro-rata"
|
"label": "Depreciate based on daily pro-rata"
|
||||||
@@ -110,7 +109,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-11-29 00:57:07.579777",
|
"modified": "2023-12-29 08:49:39.876439",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Assets",
|
"module": "Assets",
|
||||||
"name": "Asset Finance Book",
|
"name": "Asset Finance Book",
|
||||||
|
|||||||
@@ -214,7 +214,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-01-05 15:26:02.320942",
|
"modified": "2024-01-12 16:42:01.894346",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Buying Settings",
|
"name": "Buying Settings",
|
||||||
@@ -240,39 +240,24 @@
|
|||||||
"write": 1
|
"write": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"email": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"role": "Accounts User",
|
"role": "Accounts User"
|
||||||
"share": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"email": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"role": "Accounts Manager",
|
"role": "Accounts Manager"
|
||||||
"share": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"email": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"role": "Stock Manager",
|
"role": "Stock Manager"
|
||||||
"share": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"email": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"role": "Stock User",
|
"role": "Stock User"
|
||||||
"share": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"email": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"role": "Purchase User",
|
"role": "Purchase User"
|
||||||
"share": 1
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
|
|||||||
@@ -1414,11 +1414,16 @@ class AccountsController(TransactionBase):
|
|||||||
reconcile_against_document(lst)
|
reconcile_against_document(lst)
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
|
from erpnext.accounts.doctype.bank_transaction.bank_transaction import (
|
||||||
|
remove_from_bank_transaction,
|
||||||
|
)
|
||||||
from erpnext.accounts.utils import (
|
from erpnext.accounts.utils import (
|
||||||
cancel_exchange_gain_loss_journal,
|
cancel_exchange_gain_loss_journal,
|
||||||
unlink_ref_doc_from_payment_entries,
|
unlink_ref_doc_from_payment_entries,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
remove_from_bank_transaction(self.doctype, self.name)
|
||||||
|
|
||||||
if self.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]:
|
if self.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]:
|
||||||
# Cancel Exchange Gain/Loss Journal before unlinking
|
# Cancel Exchange Gain/Loss Journal before unlinking
|
||||||
cancel_exchange_gain_loss_journal(self)
|
cancel_exchange_gain_loss_journal(self)
|
||||||
@@ -1947,7 +1952,7 @@ class AccountsController(TransactionBase):
|
|||||||
self.remove(item)
|
self.remove(item)
|
||||||
|
|
||||||
def set_payment_schedule(self):
|
def set_payment_schedule(self):
|
||||||
if self.doctype == "Sales Invoice" and self.is_pos:
|
if (self.doctype == "Sales Invoice" and self.is_pos) or self.get("is_opening") == "Yes":
|
||||||
self.payment_terms_template = ""
|
self.payment_terms_template = ""
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -2130,7 +2135,7 @@ class AccountsController(TransactionBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def validate_payment_schedule_amount(self):
|
def validate_payment_schedule_amount(self):
|
||||||
if self.doctype == "Sales Invoice" and self.is_pos:
|
if (self.doctype == "Sales Invoice" and self.is_pos) or self.get("is_opening") == "Yes":
|
||||||
return
|
return
|
||||||
|
|
||||||
party_account_currency = self.get("party_account_currency")
|
party_account_currency = self.get("party_account_currency")
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import json
|
|||||||
from collections import OrderedDict, defaultdict
|
from collections import OrderedDict, defaultdict
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import scrub
|
from frappe import qb, scrub
|
||||||
from frappe.desk.reportview import get_filters_cond, get_match_cond
|
from frappe.desk.reportview import get_filters_cond, get_match_cond
|
||||||
from frappe.query_builder.functions import Concat, Sum
|
from frappe.query_builder import Criterion, CustomFunction
|
||||||
|
from frappe.query_builder.functions import Concat, Locate, Sum
|
||||||
from frappe.utils import nowdate, today, unique
|
from frappe.utils import nowdate, today, unique
|
||||||
|
from pypika import Order
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.stock.get_item_details import _get_item_tax_template
|
from erpnext.stock.get_item_details import _get_item_tax_template
|
||||||
@@ -339,37 +341,46 @@ def bom(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@frappe.validate_and_sanitize_search_inputs
|
@frappe.validate_and_sanitize_search_inputs
|
||||||
def get_project_name(doctype, txt, searchfield, start, page_len, filters):
|
def get_project_name(doctype, txt, searchfield, start, page_len, filters):
|
||||||
doctype = "Project"
|
proj = qb.DocType("Project")
|
||||||
cond = ""
|
qb_filter_and_conditions = []
|
||||||
|
qb_filter_or_conditions = []
|
||||||
|
ifelse = CustomFunction("IF", ["condition", "then", "else"])
|
||||||
|
|
||||||
if filters and filters.get("customer"):
|
if filters and filters.get("customer"):
|
||||||
cond = """(`tabProject`.customer = %s or
|
qb_filter_and_conditions.append(proj.customer == filters.get("customer"))
|
||||||
ifnull(`tabProject`.customer,"")="") and""" % (
|
|
||||||
frappe.db.escape(filters.get("customer"))
|
qb_filter_and_conditions.append(proj.status.notin(["Completed", "Cancelled"]))
|
||||||
)
|
|
||||||
|
q = qb.from_(proj)
|
||||||
|
|
||||||
fields = get_fields(doctype, ["name", "project_name"])
|
fields = get_fields(doctype, ["name", "project_name"])
|
||||||
searchfields = frappe.get_meta(doctype).get_search_fields()
|
for x in fields:
|
||||||
searchfields = " or ".join(["`tabProject`." + field + " like %(txt)s" for field in searchfields])
|
q = q.select(proj[x])
|
||||||
|
|
||||||
return frappe.db.sql(
|
# don't consider 'customer' and 'status' fields for pattern search, as they must be exactly matched
|
||||||
"""select {fields} from `tabProject`
|
searchfields = [
|
||||||
where
|
x for x in frappe.get_meta(doctype).get_search_fields() if x not in ["customer", "status"]
|
||||||
`tabProject`.status not in ('Completed', 'Cancelled')
|
]
|
||||||
and {cond} {scond} {match_cond}
|
|
||||||
order by
|
# pattern search
|
||||||
(case when locate(%(_txt)s, `tabProject`.name) > 0 then locate(%(_txt)s, `tabProject`.name) else 99999 end),
|
if txt:
|
||||||
`tabProject`.idx desc,
|
for x in searchfields:
|
||||||
`tabProject`.name asc
|
qb_filter_or_conditions.append(proj[x].like(f"%{txt}%"))
|
||||||
limit {page_len} offset {start}""".format(
|
|
||||||
fields=", ".join(["`tabProject`.{0}".format(f) for f in fields]),
|
q = q.where(Criterion.all(qb_filter_and_conditions)).where(Criterion.any(qb_filter_or_conditions))
|
||||||
cond=cond,
|
|
||||||
scond=searchfields,
|
# ordering
|
||||||
match_cond=get_match_cond(doctype),
|
if txt:
|
||||||
start=start,
|
# project_name containing search string 'txt' will be given higher precedence
|
||||||
page_len=page_len,
|
q = q.orderby(ifelse(Locate(txt, proj.project_name) > 0, Locate(txt, proj.project_name), 99999))
|
||||||
),
|
q = q.orderby(proj.idx, order=Order.desc).orderby(proj.name)
|
||||||
{"txt": "%{0}%".format(txt), "_txt": txt.replace("%", "")},
|
|
||||||
)
|
if page_len:
|
||||||
|
q = q.limit(page_len)
|
||||||
|
|
||||||
|
if start:
|
||||||
|
q = q.offset(start)
|
||||||
|
return q.run()
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@@ -416,23 +427,14 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
meta = frappe.get_meta(doctype, cached=True)
|
meta = frappe.get_meta(doctype, cached=True)
|
||||||
searchfields = meta.get_search_fields()
|
searchfields = meta.get_search_fields()
|
||||||
|
|
||||||
query = get_batches_from_stock_ledger_entries(searchfields, txt, filters)
|
batches = get_batches_from_stock_ledger_entries(searchfields, txt, filters, start, page_len)
|
||||||
bundle_query = get_batches_from_serial_and_batch_bundle(searchfields, txt, filters)
|
batches.extend(
|
||||||
|
get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start, page_len)
|
||||||
data = (
|
|
||||||
frappe.qb.from_((query) + (bundle_query))
|
|
||||||
.select("batch_no", "qty", "manufacturing_date", "expiry_date")
|
|
||||||
.offset(start)
|
|
||||||
.limit(page_len)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for field in searchfields:
|
filtered_batches = get_filterd_batches(batches)
|
||||||
data = data.select(field)
|
|
||||||
|
|
||||||
data = data.run()
|
return filtered_batches
|
||||||
data = get_filterd_batches(data)
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def get_filterd_batches(data):
|
def get_filterd_batches(data):
|
||||||
@@ -452,7 +454,7 @@ def get_filterd_batches(data):
|
|||||||
return filterd_batch
|
return filterd_batch
|
||||||
|
|
||||||
|
|
||||||
def get_batches_from_stock_ledger_entries(searchfields, txt, filters):
|
def get_batches_from_stock_ledger_entries(searchfields, txt, filters, start=0, page_len=100):
|
||||||
stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
|
stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
|
||||||
batch_table = frappe.qb.DocType("Batch")
|
batch_table = frappe.qb.DocType("Batch")
|
||||||
|
|
||||||
@@ -474,6 +476,8 @@ def get_batches_from_stock_ledger_entries(searchfields, txt, filters):
|
|||||||
& (stock_ledger_entry.batch_no.isnotnull())
|
& (stock_ledger_entry.batch_no.isnotnull())
|
||||||
)
|
)
|
||||||
.groupby(stock_ledger_entry.batch_no, stock_ledger_entry.warehouse)
|
.groupby(stock_ledger_entry.batch_no, stock_ledger_entry.warehouse)
|
||||||
|
.offset(start)
|
||||||
|
.limit(page_len)
|
||||||
)
|
)
|
||||||
|
|
||||||
query = query.select(
|
query = query.select(
|
||||||
@@ -488,16 +492,16 @@ def get_batches_from_stock_ledger_entries(searchfields, txt, filters):
|
|||||||
query = query.select(batch_table[field])
|
query = query.select(batch_table[field])
|
||||||
|
|
||||||
if txt:
|
if txt:
|
||||||
txt_condition = batch_table.name.like(txt)
|
txt_condition = batch_table.name.like("%{0}%".format(txt))
|
||||||
for field in searchfields + ["name"]:
|
for field in searchfields + ["name"]:
|
||||||
txt_condition |= batch_table[field].like(txt)
|
txt_condition |= batch_table[field].like("%{0}%".format(txt))
|
||||||
|
|
||||||
query = query.where(txt_condition)
|
query = query.where(txt_condition)
|
||||||
|
|
||||||
return query
|
return query.run(as_list=1) or []
|
||||||
|
|
||||||
|
|
||||||
def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters):
|
def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start=0, page_len=100):
|
||||||
bundle = frappe.qb.DocType("Serial and Batch Entry")
|
bundle = frappe.qb.DocType("Serial and Batch Entry")
|
||||||
stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
|
stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
|
||||||
batch_table = frappe.qb.DocType("Batch")
|
batch_table = frappe.qb.DocType("Batch")
|
||||||
@@ -522,6 +526,8 @@ def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters):
|
|||||||
& (stock_ledger_entry.serial_and_batch_bundle.isnotnull())
|
& (stock_ledger_entry.serial_and_batch_bundle.isnotnull())
|
||||||
)
|
)
|
||||||
.groupby(bundle.batch_no, bundle.warehouse)
|
.groupby(bundle.batch_no, bundle.warehouse)
|
||||||
|
.offset(start)
|
||||||
|
.limit(page_len)
|
||||||
)
|
)
|
||||||
|
|
||||||
bundle_query = bundle_query.select(
|
bundle_query = bundle_query.select(
|
||||||
@@ -536,13 +542,13 @@ def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters):
|
|||||||
bundle_query = bundle_query.select(batch_table[field])
|
bundle_query = bundle_query.select(batch_table[field])
|
||||||
|
|
||||||
if txt:
|
if txt:
|
||||||
txt_condition = batch_table.name.like(txt)
|
txt_condition = batch_table.name.like("%{0}%".format(txt))
|
||||||
for field in searchfields + ["name"]:
|
for field in searchfields + ["name"]:
|
||||||
txt_condition |= batch_table[field].like(txt)
|
txt_condition |= batch_table[field].like("%{0}%".format(txt))
|
||||||
|
|
||||||
bundle_query = bundle_query.where(txt_condition)
|
bundle_query = bundle_query.where(txt_condition)
|
||||||
|
|
||||||
return bundle_query
|
return bundle_query.run(as_list=1)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ class TestQueries(unittest.TestCase):
|
|||||||
self.assertGreaterEqual(len(query(txt="_Test Item Home Desktop Manufactured")), 1)
|
self.assertGreaterEqual(len(query(txt="_Test Item Home Desktop Manufactured")), 1)
|
||||||
|
|
||||||
def test_project_query(self):
|
def test_project_query(self):
|
||||||
query = add_default_params(queries.get_project_name, "BOM")
|
query = add_default_params(queries.get_project_name, "Project")
|
||||||
|
|
||||||
self.assertGreaterEqual(len(query(txt="_Test Project")), 1)
|
self.assertGreaterEqual(len(query(txt="_Test Project")), 1)
|
||||||
|
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ frappe.ui.form.on('Production Plan', {
|
|||||||
method: "set_status",
|
method: "set_status",
|
||||||
freeze: true,
|
freeze: true,
|
||||||
doc: frm.doc,
|
doc: frm.doc,
|
||||||
args: {close : close},
|
args: {close : close, update_bin: true},
|
||||||
callback: function() {
|
callback: function() {
|
||||||
frm.reload_doc();
|
frm.reload_doc();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -579,7 +579,7 @@ class ProductionPlan(Document):
|
|||||||
frappe.delete_doc("Work Order", d.name)
|
frappe.delete_doc("Work Order", d.name)
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def set_status(self, close=None):
|
def set_status(self, close=None, update_bin=False):
|
||||||
self.status = {0: "Draft", 1: "Submitted", 2: "Cancelled"}.get(self.docstatus)
|
self.status = {0: "Draft", 1: "Submitted", 2: "Cancelled"}.get(self.docstatus)
|
||||||
|
|
||||||
if close:
|
if close:
|
||||||
@@ -599,7 +599,7 @@ class ProductionPlan(Document):
|
|||||||
if close is not None:
|
if close is not None:
|
||||||
self.db_set("status", self.status)
|
self.db_set("status", self.status)
|
||||||
|
|
||||||
if self.docstatus == 1 and self.status != "Completed":
|
if update_bin and self.docstatus == 1 and self.status != "Completed":
|
||||||
self.update_bin_qty()
|
self.update_bin_qty()
|
||||||
|
|
||||||
def update_ordered_status(self):
|
def update_ordered_status(self):
|
||||||
|
|||||||
@@ -1486,14 +1486,14 @@ class TestProductionPlan(FrappeTestCase):
|
|||||||
before_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
|
before_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
|
||||||
|
|
||||||
pln.reload()
|
pln.reload()
|
||||||
pln.set_status(close=True)
|
pln.set_status(close=True, update_bin=True)
|
||||||
|
|
||||||
bin_name = get_or_make_bin(rm_item, rm_warehouse)
|
bin_name = get_or_make_bin(rm_item, rm_warehouse)
|
||||||
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
|
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
|
||||||
self.assertAlmostEqual(after_qty, before_qty - 10)
|
self.assertAlmostEqual(after_qty, before_qty - 10)
|
||||||
|
|
||||||
pln.reload()
|
pln.reload()
|
||||||
pln.set_status(close=False)
|
pln.set_status(close=False, update_bin=True)
|
||||||
|
|
||||||
bin_name = get_or_make_bin(rm_item, rm_warehouse)
|
bin_name = get_or_make_bin(rm_item, rm_warehouse)
|
||||||
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
|
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ $.extend(erpnext, {
|
|||||||
},
|
},
|
||||||
|
|
||||||
toggle_naming_series: function() {
|
toggle_naming_series: function() {
|
||||||
if(cur_frm.fields_dict.naming_series) {
|
if(cur_frm && cur_frm.fields_dict.naming_series) {
|
||||||
cur_frm.toggle_display("naming_series", cur_frm.doc.__islocal?true:false);
|
cur_frm.toggle_display("naming_series", cur_frm.doc.__islocal?true:false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ erpnext.accounts.dimensions = {
|
|||||||
},
|
},
|
||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
me.accounting_dimensions = r.message[0];
|
me.accounting_dimensions = r.message[0];
|
||||||
|
// Ignoring "Project" as it is already handled specifically in Sales Order and Delivery Note
|
||||||
|
me.accounting_dimensions = me.accounting_dimensions.filter(x=>{return x.document_type != "Project"});
|
||||||
me.default_dimensions = r.message[1];
|
me.default_dimensions = r.message[1];
|
||||||
me.setup_filters(frm, doctype);
|
me.setup_filters(frm, doctype);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,6 +94,9 @@ frappe.ui.form.on("Sales Order", {
|
|||||||
frm.set_value("reserve_stock", 0);
|
frm.set_value("reserve_stock", 0);
|
||||||
frm.set_df_property("reserve_stock", "read_only", 1);
|
frm.set_df_property("reserve_stock", "read_only", 1);
|
||||||
frm.set_df_property("reserve_stock", "hidden", 1);
|
frm.set_df_property("reserve_stock", "hidden", 1);
|
||||||
|
frm.fields_dict.items.grid.update_docfield_property('reserve_stock', 'hidden', 1);
|
||||||
|
frm.fields_dict.items.grid.update_docfield_property('reserve_stock', 'default', 0);
|
||||||
|
frm.fields_dict.items.grid.update_docfield_property('reserve_stock', 'read_only', 1);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,6 +200,7 @@ class SalesOrder(SellingController):
|
|||||||
self.validate_for_items()
|
self.validate_for_items()
|
||||||
self.validate_warehouse()
|
self.validate_warehouse()
|
||||||
self.validate_drop_ship()
|
self.validate_drop_ship()
|
||||||
|
self.validate_reserved_stock()
|
||||||
self.validate_serial_no_based_delivery()
|
self.validate_serial_no_based_delivery()
|
||||||
validate_against_blanket_order(self)
|
validate_against_blanket_order(self)
|
||||||
validate_inter_company_party(
|
validate_inter_company_party(
|
||||||
@@ -660,6 +661,17 @@ class SalesOrder(SellingController):
|
|||||||
).format(item.item_code)
|
).format(item.item_code)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def validate_reserved_stock(self):
|
||||||
|
"""Clean reserved stock flag for non-stock Item"""
|
||||||
|
|
||||||
|
enable_stock_reservation = frappe.db.get_single_value(
|
||||||
|
"Stock Settings", "enable_stock_reservation"
|
||||||
|
)
|
||||||
|
|
||||||
|
for item in self.items:
|
||||||
|
if item.reserve_stock and (not enable_stock_reservation or not cint(item.is_stock_item)):
|
||||||
|
item.reserve_stock = 0
|
||||||
|
|
||||||
def has_unreserved_stock(self) -> bool:
|
def has_unreserved_stock(self) -> bool:
|
||||||
"""Returns True if there is any unreserved item in the Sales Order."""
|
"""Returns True if there is any unreserved item in the Sales Order."""
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"item_code",
|
"item_code",
|
||||||
"customer_item_code",
|
"customer_item_code",
|
||||||
"ensure_delivery_based_on_produced_serial_no",
|
"ensure_delivery_based_on_produced_serial_no",
|
||||||
|
"is_stock_item",
|
||||||
"reserve_stock",
|
"reserve_stock",
|
||||||
"col_break1",
|
"col_break1",
|
||||||
"delivery_date",
|
"delivery_date",
|
||||||
@@ -867,6 +868,7 @@
|
|||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"default": "1",
|
"default": "1",
|
||||||
|
"depends_on": "eval:doc.is_stock_item",
|
||||||
"fieldname": "reserve_stock",
|
"fieldname": "reserve_stock",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Reserve Stock",
|
"label": "Reserve Stock",
|
||||||
@@ -891,6 +893,16 @@
|
|||||||
"label": "Production Plan Qty",
|
"label": "Production Plan Qty",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fetch_from": "item_code.is_stock_item",
|
||||||
|
"fieldname": "is_stock_item",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Is Stock Item",
|
||||||
|
"print_hide": 1,
|
||||||
|
"report_hide": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ class HolidayList(Document):
|
|||||||
for holiday_date, holiday_name in country_holidays(
|
for holiday_date, holiday_name in country_holidays(
|
||||||
self.country,
|
self.country,
|
||||||
subdiv=self.subdivision,
|
subdiv=self.subdivision,
|
||||||
years=[from_date.year, to_date.year],
|
years=list(range(from_date.year, to_date.year + 1)),
|
||||||
language=frappe.local.lang,
|
language=frappe.local.lang,
|
||||||
).items():
|
).items():
|
||||||
if holiday_date in existing_holidays:
|
if holiday_date in existing_holidays:
|
||||||
|
|||||||
@@ -48,17 +48,58 @@ class TestHolidayList(unittest.TestCase):
|
|||||||
|
|
||||||
def test_local_holidays(self):
|
def test_local_holidays(self):
|
||||||
holiday_list = frappe.new_doc("Holiday List")
|
holiday_list = frappe.new_doc("Holiday List")
|
||||||
holiday_list.from_date = "2023-04-01"
|
holiday_list.from_date = "2022-01-01"
|
||||||
holiday_list.to_date = "2023-04-30"
|
holiday_list.to_date = "2024-12-31"
|
||||||
holiday_list.country = "DE"
|
holiday_list.country = "DE"
|
||||||
holiday_list.subdivision = "SN"
|
holiday_list.subdivision = "SN"
|
||||||
holiday_list.get_local_holidays()
|
holiday_list.get_local_holidays()
|
||||||
|
|
||||||
holidays = [holiday.holiday_date for holiday in holiday_list.holidays]
|
holidays = holiday_list.get_holidays()
|
||||||
self.assertNotIn(date(2023, 1, 1), holidays)
|
self.assertIn(date(2022, 1, 1), holidays)
|
||||||
|
self.assertIn(date(2022, 4, 15), holidays)
|
||||||
|
self.assertIn(date(2022, 4, 18), holidays)
|
||||||
|
self.assertIn(date(2022, 5, 1), holidays)
|
||||||
|
self.assertIn(date(2022, 5, 26), holidays)
|
||||||
|
self.assertIn(date(2022, 6, 6), holidays)
|
||||||
|
self.assertIn(date(2022, 10, 3), holidays)
|
||||||
|
self.assertIn(date(2022, 10, 31), holidays)
|
||||||
|
self.assertIn(date(2022, 11, 16), holidays)
|
||||||
|
self.assertIn(date(2022, 12, 25), holidays)
|
||||||
|
self.assertIn(date(2022, 12, 26), holidays)
|
||||||
|
self.assertIn(date(2023, 1, 1), holidays)
|
||||||
self.assertIn(date(2023, 4, 7), holidays)
|
self.assertIn(date(2023, 4, 7), holidays)
|
||||||
self.assertIn(date(2023, 4, 10), holidays)
|
self.assertIn(date(2023, 4, 10), holidays)
|
||||||
self.assertNotIn(date(2023, 5, 1), holidays)
|
self.assertIn(date(2023, 5, 1), holidays)
|
||||||
|
self.assertIn(date(2023, 5, 18), holidays)
|
||||||
|
self.assertIn(date(2023, 5, 29), holidays)
|
||||||
|
self.assertIn(date(2023, 10, 3), holidays)
|
||||||
|
self.assertIn(date(2023, 10, 31), holidays)
|
||||||
|
self.assertIn(date(2023, 11, 22), holidays)
|
||||||
|
self.assertIn(date(2023, 12, 25), holidays)
|
||||||
|
self.assertIn(date(2023, 12, 26), holidays)
|
||||||
|
self.assertIn(date(2024, 1, 1), holidays)
|
||||||
|
self.assertIn(date(2024, 3, 29), holidays)
|
||||||
|
self.assertIn(date(2024, 4, 1), holidays)
|
||||||
|
self.assertIn(date(2024, 5, 1), holidays)
|
||||||
|
self.assertIn(date(2024, 5, 9), holidays)
|
||||||
|
self.assertIn(date(2024, 5, 20), holidays)
|
||||||
|
self.assertIn(date(2024, 10, 3), holidays)
|
||||||
|
self.assertIn(date(2024, 10, 31), holidays)
|
||||||
|
self.assertIn(date(2024, 11, 20), holidays)
|
||||||
|
self.assertIn(date(2024, 12, 25), holidays)
|
||||||
|
self.assertIn(date(2024, 12, 26), holidays)
|
||||||
|
|
||||||
|
# check some random dates that should not be local holidays
|
||||||
|
self.assertNotIn(date(2022, 1, 2), holidays)
|
||||||
|
self.assertNotIn(date(2023, 4, 16), holidays)
|
||||||
|
self.assertNotIn(date(2024, 4, 19), holidays)
|
||||||
|
self.assertNotIn(date(2022, 5, 2), holidays)
|
||||||
|
self.assertNotIn(date(2023, 5, 27), holidays)
|
||||||
|
self.assertNotIn(date(2024, 6, 7), holidays)
|
||||||
|
self.assertNotIn(date(2022, 10, 4), holidays)
|
||||||
|
self.assertNotIn(date(2023, 10, 30), holidays)
|
||||||
|
self.assertNotIn(date(2024, 11, 17), holidays)
|
||||||
|
self.assertNotIn(date(2022, 12, 24), holidays)
|
||||||
|
|
||||||
def test_localized_country_names(self):
|
def test_localized_country_names(self):
|
||||||
lang = frappe.local.lang
|
lang = frappe.local.lang
|
||||||
|
|||||||
@@ -186,7 +186,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"in_create": 1,
|
"in_create": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-11-01 16:51:17.079107",
|
"modified": "2024-01-16 15:11:46.140323",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Bin",
|
"name": "Bin",
|
||||||
@@ -213,6 +213,21 @@
|
|||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "Stock User"
|
"role": "Stock User"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Stock Manager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Purchase Manager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Sales Manager"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
|
|||||||
@@ -103,15 +103,6 @@
|
|||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "amended_from",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"label": "Amended From",
|
|
||||||
"no_copy": 1,
|
|
||||||
"options": "Closing Stock Balance",
|
|
||||||
"print_hide": 1,
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "include_uom",
|
"fieldname": "include_uom",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
@@ -122,7 +113,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-05-17 11:46:04.448220",
|
"modified": "2023-05-18 11:46:04.448220",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Closing Stock Balance",
|
"name": "Closing Stock Balance",
|
||||||
|
|||||||
@@ -600,26 +600,12 @@ $.extend(erpnext.item, {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
frappe.call({
|
let values = [];
|
||||||
method: "frappe.client.get",
|
for(var i = d.from_range; i <= d.to_range; i = flt(i + d.increment, 6)) {
|
||||||
args: {
|
values.push(i);
|
||||||
doctype: "Item Attribute",
|
}
|
||||||
name: d.attribute
|
attr_val_fields[d.attribute] = values;
|
||||||
}
|
resolve();
|
||||||
}).then((r) => {
|
|
||||||
if(r.message) {
|
|
||||||
const from = r.message.from_range;
|
|
||||||
const to = r.message.to_range;
|
|
||||||
const increment = r.message.increment;
|
|
||||||
|
|
||||||
let values = [];
|
|
||||||
for(var i = from; i <= to; i = flt(i + increment, 6)) {
|
|
||||||
values.push(i);
|
|
||||||
}
|
|
||||||
attr_val_fields[d.attribute] = values;
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1228,6 +1228,7 @@ def make_purchase_invoice(source_name, target_doc=None, args=None):
|
|||||||
"field_map": {
|
"field_map": {
|
||||||
"name": "pr_detail",
|
"name": "pr_detail",
|
||||||
"parent": "purchase_receipt",
|
"parent": "purchase_receipt",
|
||||||
|
"qty": "received_qty",
|
||||||
"purchase_order_item": "po_detail",
|
"purchase_order_item": "po_detail",
|
||||||
"purchase_order": "purchase_order",
|
"purchase_order": "purchase_order",
|
||||||
"is_fixed_asset": "is_fixed_asset",
|
"is_fixed_asset": "is_fixed_asset",
|
||||||
|
|||||||
@@ -111,6 +111,9 @@ class QualityInspection(Document):
|
|||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.update_qc_reference()
|
self.update_qc_reference()
|
||||||
|
|
||||||
|
def on_trash(self):
|
||||||
|
self.update_qc_reference()
|
||||||
|
|
||||||
def validate_readings_status_mandatory(self):
|
def validate_readings_status_mandatory(self):
|
||||||
for reading in self.readings:
|
for reading in self.readings:
|
||||||
if not reading.status:
|
if not reading.status:
|
||||||
|
|||||||
@@ -250,6 +250,33 @@ class TestQualityInspection(FrappeTestCase):
|
|||||||
qa.delete()
|
qa.delete()
|
||||||
dn.delete()
|
dn.delete()
|
||||||
|
|
||||||
|
def test_delete_quality_inspection_linked_with_stock_entry(self):
|
||||||
|
item_code = create_item("_Test Cicuular Dependecy Item with QA").name
|
||||||
|
|
||||||
|
se = make_stock_entry(
|
||||||
|
item_code=item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100, do_not_submit=True
|
||||||
|
)
|
||||||
|
|
||||||
|
se.inspection_required = 1
|
||||||
|
se.save()
|
||||||
|
|
||||||
|
qa = create_quality_inspection(
|
||||||
|
item_code=item_code, reference_type="Stock Entry", reference_name=se.name, do_not_submit=True
|
||||||
|
)
|
||||||
|
|
||||||
|
se.reload()
|
||||||
|
se.items[0].quality_inspection = qa.name
|
||||||
|
se.save()
|
||||||
|
|
||||||
|
qa.delete()
|
||||||
|
|
||||||
|
se.reload()
|
||||||
|
|
||||||
|
qc = se.items[0].quality_inspection
|
||||||
|
self.assertFalse(qc)
|
||||||
|
|
||||||
|
se.delete()
|
||||||
|
|
||||||
|
|
||||||
def create_quality_inspection(**args):
|
def create_quality_inspection(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|||||||
@@ -104,7 +104,8 @@
|
|||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Stock Entry Type",
|
"label": "Stock Entry Type",
|
||||||
"options": "Stock Entry Type",
|
"options": "Stock Entry Type",
|
||||||
"reqd": 1
|
"reqd": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.purpose == 'Material Transfer'",
|
"depends_on": "eval:doc.purpose == 'Material Transfer'",
|
||||||
@@ -546,7 +547,8 @@
|
|||||||
"label": "Job Card",
|
"label": "Job Card",
|
||||||
"options": "Job Card",
|
"options": "Job Card",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "amended_from",
|
"fieldname": "amended_from",
|
||||||
@@ -679,7 +681,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-06-19 18:23:40.748114",
|
"modified": "2024-01-12 11:56:58.644882",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Entry",
|
"name": "Stock Entry",
|
||||||
|
|||||||
@@ -561,7 +561,8 @@
|
|||||||
"label": "Job Card Item",
|
"label": "Job Card Item",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
@@ -589,7 +590,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-05-09 12:41:18.210864",
|
"modified": "2024-01-12 11:56:04.626103",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Entry Detail",
|
"name": "Stock Entry Detail",
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ class StockEntryDetail(Document):
|
|||||||
allow_alternative_item: DF.Check
|
allow_alternative_item: DF.Check
|
||||||
allow_zero_valuation_rate: DF.Check
|
allow_zero_valuation_rate: DF.Check
|
||||||
amount: DF.Currency
|
amount: DF.Currency
|
||||||
attach_something_here: DF.Attach | None
|
|
||||||
barcode: DF.Data | None
|
barcode: DF.Data | None
|
||||||
basic_amount: DF.Currency
|
basic_amount: DF.Currency
|
||||||
basic_rate: DF.Currency
|
basic_rate: DF.Currency
|
||||||
|
|||||||
@@ -10,8 +10,9 @@
|
|||||||
"has_item_scanned",
|
"has_item_scanned",
|
||||||
"item_code",
|
"item_code",
|
||||||
"item_name",
|
"item_name",
|
||||||
"warehouse",
|
"item_group",
|
||||||
"column_break_6",
|
"column_break_6",
|
||||||
|
"warehouse",
|
||||||
"qty",
|
"qty",
|
||||||
"valuation_rate",
|
"valuation_rate",
|
||||||
"amount",
|
"amount",
|
||||||
@@ -52,6 +53,7 @@
|
|||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"fetch_from": "item_code.item_name",
|
||||||
"fieldname": "item_name",
|
"fieldname": "item_name",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"in_global_search": 1,
|
"in_global_search": 1,
|
||||||
@@ -213,11 +215,18 @@
|
|||||||
"fieldname": "add_serial_batch_bundle",
|
"fieldname": "add_serial_batch_bundle",
|
||||||
"fieldtype": "Button",
|
"fieldtype": "Button",
|
||||||
"label": "Add Serial / Batch No"
|
"label": "Add Serial / Batch No"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "item_code.item_group",
|
||||||
|
"fieldname": "item_group",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Item Group",
|
||||||
|
"options": "Item Group"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-11-02 15:47:07.929550",
|
"modified": "2024-01-14 10:04:23.599951",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Reconciliation Item",
|
"name": "Stock Reconciliation Item",
|
||||||
|
|||||||
Reference in New Issue
Block a user