Merge pull request #39405 from frappe/version-15-hotfix

chore: release v15
This commit is contained in:
Deepesh Garg 2024-01-17 22:34:57 +05:30 committed by GitHub
commit e00533ff4e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
52 changed files with 558 additions and 203 deletions

View File

@ -76,6 +76,7 @@ class TestBankReconciliationTool(AccountsTestMixin, FrappeTestCase):
"deposit": 100,
"bank_account": self.bank_account,
"reference_number": "123",
"currency": "INR",
}
)
.save()

View File

@ -3,6 +3,7 @@
import frappe
from frappe import _
from frappe.model.docstatus import DocStatus
from frappe.model.document import Document
from frappe.utils import flt
@ -48,6 +49,24 @@ class BankTransaction(Document):
def validate(self):
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):
if self.docstatus == 2:
@ -415,3 +434,21 @@ def unclear_reference_payment(doctype, docname, bt_name):
bt = frappe.get_doc("Bank Transaction", bt_name)
set_voucher_clearance(doctype, docname, None, bt)
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()

View File

@ -2,10 +2,10 @@
# See license.txt
import json
import unittest
import frappe
from frappe import utils
from frappe.model.docstatus import DocStatus
from frappe.tests.utils import FrappeTestCase
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")
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
def test_debit_credit_output(self):
bank_transaction = frappe.get_doc(

View File

@ -39,7 +39,7 @@ def test_record_generator():
]
start = 2012
end = now_datetime().year + 5
end = now_datetime().year + 25
for year in range(start, end):
test_records.append(
{

View File

@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry");
frappe.ui.form.on("Journal Entry", {
setup: function(frm) {
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) {

View File

@ -9,7 +9,7 @@ erpnext.accounts.taxes.setup_tax_filters("Advance Taxes and Charges");
frappe.ui.form.on('Payment Entry', {
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.paid_from) frm.set_value("paid_from_account_currency", null);

View File

@ -35,7 +35,17 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
super.onload();
// 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) {
// show credit_to in print format

View File

@ -296,6 +296,18 @@ class PurchaseInvoice(BuyingController):
self.reset_default_field_value("set_warehouse", "items", "warehouse")
self.reset_default_field_value("rejected_warehouse", "items", "rejected_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):
if self.release_date and getdate(nowdate()) >= getdate(self.release_date):

View File

@ -126,7 +126,7 @@
"fieldname": "rate",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Rate",
"label": "Tax Rate",
"oldfieldname": "rate",
"oldfieldtype": "Currency"
},
@ -230,7 +230,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-08-05 20:04:36.618240",
"modified": "2024-01-14 10:04:36.618240",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Taxes and Charges",
@ -239,4 +239,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}
}

View File

@ -36,9 +36,19 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
var me = this;
super.onload();
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', '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'
this.frm.ignore_doctypes_on_cancel_all = [
"POS Invoice",
"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) {

View File

@ -108,7 +108,7 @@
"fieldname": "rate",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Rate",
"label": "Tax Rate",
"oldfieldname": "rate",
"oldfieldtype": "Currency"
},
@ -218,7 +218,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-10-17 13:08:17.776528",
"modified": "2024-01-14 10:08:17.776528",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Taxes and Charges",
@ -227,4 +227,4 @@
"sort_field": "modified",
"sort_order": "ASC",
"states": []
}
}

View File

@ -16,6 +16,7 @@ from frappe.utils.data import (
date_diff,
flt,
get_last_day,
get_link_to_form,
getdate,
nowdate,
)
@ -317,6 +318,37 @@ class Subscription(Document):
if self.is_new():
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:
"""
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):
self.generate_invoice(posting_date=posting_date)
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 (
getdate(posting_date) >= getdate(self.current_invoice_end)

View File

@ -460,11 +460,13 @@ class TestSubscription(FrappeTestCase):
self.assertEqual(len(subscription.invoices), 1)
def test_multi_currency_subscription(self):
party = "_Test Subscription Customer"
frappe.db.set_value("Customer", party, "default_currency", "USD")
subscription = create_subscription(
start_date="2018-01-01",
generate_invoice_at="Beginning of the current subscription period",
plans=[{"plan": "_Test Plan Multicurrency", "qty": 1}],
party="_Test Subscription Customer",
plans=[{"plan": "_Test Plan Multicurrency", "qty": 1, "currency": "USD"}],
party=party,
)
subscription.process()
@ -528,13 +530,21 @@ class TestSubscription(FrappeTestCase):
def make_plans():
create_plan(plan_name="_Test Plan Name", cost=900)
create_plan(plan_name="_Test Plan Name 2", cost=1999)
create_plan(plan_name="_Test Plan Name", cost=900, currency="INR")
create_plan(plan_name="_Test Plan Name 2", cost=1999, currency="INR")
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(
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(
plan_name="_Test Plan Multicurrency", cost=50, billing_interval="Month", currency="USD"

View File

@ -41,7 +41,8 @@
"fieldname": "currency",
"fieldtype": "Link",
"label": "Currency",
"options": "Currency"
"options": "Currency",
"reqd": 1
},
{
"fieldname": "column_break_3",
@ -148,10 +149,11 @@
}
],
"links": [],
"modified": "2021-12-10 15:24:15.794477",
"modified": "2024-01-14 17:59:34.687977",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Subscription Plan",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
@ -193,5 +195,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -24,7 +24,7 @@ class SubscriptionPlan(Document):
billing_interval_count: DF.Int
cost: DF.Currency
cost_center: DF.Link | None
currency: DF.Link | None
currency: DF.Link
item: DF.Link
payment_gateway: DF.Link | None
plan_name: DF.Data

View File

@ -84,10 +84,6 @@ function get_filters() {
options: budget_against_options,
default: "Cost Center",
reqd: 1,
get_data: function() {
console.log(this.options);
return ["Emacs", "Rocks"];
},
on_change: function() {
frappe.query_report.set_filter_value("budget_against_filter", []);
frappe.query_report.refresh();

View File

@ -376,6 +376,10 @@ class PartyLedgerSummaryReport(object):
if not income_or_expense_accounts:
# prevent empty 'in' condition
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 = (
qb.from_(gl)

View File

@ -368,7 +368,7 @@ def filter_invoices_based_on_dimensions(filters, query, parent_doc):
dimension.document_type, filters.get(dimension.fieldname)
)
fieldname = dimension.fieldname
query = query.where(parent_doc[fieldname] == filters.fieldname)
query = query.where(parent_doc[fieldname].isin(filters[fieldname]))
return query

View File

@ -23,6 +23,10 @@ class TestUtils(unittest.TestCase):
super(TestUtils, cls).setUpClass()
make_test_objects("Address", ADDRESS_RECORDS)
@classmethod
def tearDownClass(cls):
frappe.db.rollback()
def test_get_party_shipping_address(self):
address = get_party_shipping_address("Customer", "_Test Customer 1")
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(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 = [
{

View File

@ -1263,7 +1263,7 @@ def get_autoname_with_number(number_value, doc_title, company):
def parse_naming_series_variable(doc, variable):
if variable == "FY":
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")
else:
date = getdate()

View File

@ -202,8 +202,7 @@
"fieldname": "purchase_date",
"fieldtype": "Date",
"label": "Purchase Date",
"mandatory_depends_on": "eval:!doc.is_existing_asset",
"read_only": 1,
"mandatory_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"
}
],
"modified": "2024-01-05 17:36:53.131512",
"modified": "2024-01-15 17:35:49.226603",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",

View File

@ -162,6 +162,7 @@ class Asset(AccountsController):
def on_cancel(self):
self.validate_cancellation()
self.cancel_movement_entries()
self.cancel_capitalization()
self.delete_depreciation_entries()
cancel_asset_depr_schedules(self)
self.set_status()
@ -517,6 +518,16 @@ class Asset(AccountsController):
movement = frappe.get_doc("Asset Movement", movement.get("name"))
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):
if self.calculate_depreciation:
for row in self.get("finance_books"):
@ -1027,6 +1038,8 @@ def is_cwip_accounting_enabled(asset_category):
@frappe.whitelist()
def get_asset_value_after_depreciation(asset_name, finance_book=None):
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)

View File

@ -19,6 +19,7 @@ from frappe.utils import (
)
from frappe.utils.user import get_users_with_role
import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
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)
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):
if not asset_doc.calculate_depreciation:

View File

@ -891,7 +891,7 @@ class TestDepreciationMethods(AssetSetup):
["2030-12-31", 28630.14, 28630.14],
["2031-12-31", 35684.93, 64315.07],
["2032-12-31", 17842.46, 82157.53],
["2033-06-06", 5342.47, 87500.0],
["2033-06-06", 5342.46, 87499.99],
]
schedules = [
@ -1003,7 +1003,7 @@ class TestDepreciationBasics(AssetSetup):
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active")
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)

View File

@ -21,10 +21,10 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
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") {
this.set_consumed_stock_items_tagged_to_wip_composite_asset(this.frm.doc.target_asset);
this.get_target_asset_details();
}
// 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.get_target_asset_details();
// }
}
setup_queries() {
@ -143,13 +143,20 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
},
callback: function (r) {
if (!r.exc && r.message) {
me.frm.clear_table("stock_items");
for (let item of r.message) {
me.frm.add_child("stock_items", item);
if(r.message[0] && r.message[0].length) {
me.frm.clear_table("stock_items");
for (let item of r.message[0]) {
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();
}

View File

@ -136,11 +136,19 @@ class AssetCapitalization(StockController):
"Stock Ledger Entry",
"Repost Item Valuation",
"Serial and Batch Bundle",
"Asset",
)
self.cancel_target_asset()
self.update_stock_ledger()
self.make_gl_entries()
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):
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(
args, item_defaults, item_group_defaults, brand_defaults
)
return out
@ -929,10 +936,27 @@ def get_items_tagged_to_wip_composite_asset(asset):
"qty",
"valuation_rate",
"amount",
"is_fixed_asset",
"parent",
]
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

View File

@ -7,6 +7,7 @@ from frappe.model.document import Document
from frappe.utils import (
add_days,
add_months,
add_years,
cint,
date_diff,
flt,
@ -18,6 +19,7 @@ from frappe.utils import (
)
import erpnext
from erpnext.accounts.utils import get_fiscal_year
class AssetDepreciationSchedule(Document):
@ -283,12 +285,20 @@ class AssetDepreciationSchedule(Document):
depreciation_amount = 0
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):
# If depreciation is already completed (for double declining balance)
if skip_row:
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:
prev_depreciation_amount = self.get("depreciation_schedule")[n - 1].depreciation_amount
else:
@ -298,6 +308,7 @@ class AssetDepreciationSchedule(Document):
self,
asset_doc,
value_after_depreciation,
yearly_opening_wdv,
row,
n,
prev_depreciation_amount,
@ -341,10 +352,7 @@ class AssetDepreciationSchedule(Document):
n == 0
and (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata)
and not self.opening_accumulated_depreciation
and get_updated_rate_of_depreciation_for_wdv_and_dd(
asset_doc, value_after_depreciation, row, False
)
== row.rate_of_depreciation
and not self.flags.wdv_it_act_applied
):
from_date = add_days(
asset_doc.available_for_use_date, -1
@ -404,8 +412,9 @@ class AssetDepreciationSchedule(Document):
if not depreciation_amount:
continue
value_after_depreciation -= flt(
depreciation_amount, asset_doc.precision("gross_purchase_amount")
value_after_depreciation = flt(
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
@ -585,6 +594,7 @@ def get_depreciation_amount(
asset_depr_schedule,
asset,
depreciable_value,
yearly_opening_wdv,
fb_row,
schedule_idx=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
)
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(
asset,
fb_row,
depreciable_value,
rate_of_depreciation,
fb_row.frequency_of_depreciation,
yearly_opening_wdv,
schedule_idx,
prev_depreciation_amount,
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(
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))
@erpnext.allow_regional
def get_wdv_or_dd_depr_amount(
asset,
fb_row,
depreciable_value,
rate_of_depreciation,
frequency_of_depreciation,
yearly_opening_wdv,
schedule_idx,
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
asset_depr_schedule,
):
if cint(frequency_of_depreciation) == 12:
return flt(depreciable_value) * (flt(rate_of_depreciation) / 100)
return 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,
)
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:
if has_wdv_or_dd_non_yearly_pro_rata:
if schedule_idx == 0:
return flt(depreciable_value) * (flt(rate_of_depreciation) / 100)
elif schedule_idx % (12 / cint(frequency_of_depreciation)) == 1:
return flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100)
elif schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 1:
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:
return prev_depreciation_amount
else:
if schedule_idx % (12 / cint(frequency_of_depreciation)) == 0:
if schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 0:
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:
return prev_depreciation_amount

View File

@ -94,7 +94,6 @@
},
{
"default": "0",
"depends_on": "eval:doc.depreciation_method == \"Straight Line\" || doc.depreciation_method == \"Manual\"",
"fieldname": "daily_prorata_based",
"fieldtype": "Check",
"label": "Depreciate based on daily pro-rata"
@ -110,7 +109,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-11-29 00:57:07.579777",
"modified": "2023-12-29 08:49:39.876439",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Finance Book",

View File

@ -214,7 +214,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2024-01-05 15:26:02.320942",
"modified": "2024-01-12 16:42:01.894346",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",
@ -240,39 +240,24 @@
"write": 1
},
{
"email": 1,
"print": 1,
"read": 1,
"role": "Accounts User",
"share": 1
"role": "Accounts User"
},
{
"email": 1,
"print": 1,
"read": 1,
"role": "Accounts Manager",
"share": 1
"role": "Accounts Manager"
},
{
"email": 1,
"print": 1,
"read": 1,
"role": "Stock Manager",
"share": 1
"role": "Stock Manager"
},
{
"email": 1,
"print": 1,
"read": 1,
"role": "Stock User",
"share": 1
"role": "Stock User"
},
{
"email": 1,
"print": 1,
"read": 1,
"role": "Purchase User",
"share": 1
"role": "Purchase User"
}
],
"sort_field": "modified",

View File

@ -1414,11 +1414,16 @@ class AccountsController(TransactionBase):
reconcile_against_document(lst)
def on_cancel(self):
from erpnext.accounts.doctype.bank_transaction.bank_transaction import (
remove_from_bank_transaction,
)
from erpnext.accounts.utils import (
cancel_exchange_gain_loss_journal,
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"]:
# Cancel Exchange Gain/Loss Journal before unlinking
cancel_exchange_gain_loss_journal(self)
@ -1947,7 +1952,7 @@ class AccountsController(TransactionBase):
self.remove(item)
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 = ""
return
@ -2130,7 +2135,7 @@ class AccountsController(TransactionBase):
)
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
party_account_currency = self.get("party_account_currency")

View File

@ -6,10 +6,12 @@ import json
from collections import OrderedDict, defaultdict
import frappe
from frappe import scrub
from frappe import qb, scrub
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 pypika import Order
import erpnext
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.validate_and_sanitize_search_inputs
def get_project_name(doctype, txt, searchfield, start, page_len, filters):
doctype = "Project"
cond = ""
proj = qb.DocType("Project")
qb_filter_and_conditions = []
qb_filter_or_conditions = []
ifelse = CustomFunction("IF", ["condition", "then", "else"])
if filters and filters.get("customer"):
cond = """(`tabProject`.customer = %s or
ifnull(`tabProject`.customer,"")="") and""" % (
frappe.db.escape(filters.get("customer"))
)
qb_filter_and_conditions.append(proj.customer == filters.get("customer"))
qb_filter_and_conditions.append(proj.status.notin(["Completed", "Cancelled"]))
q = qb.from_(proj)
fields = get_fields(doctype, ["name", "project_name"])
searchfields = frappe.get_meta(doctype).get_search_fields()
searchfields = " or ".join(["`tabProject`." + field + " like %(txt)s" for field in searchfields])
for x in fields:
q = q.select(proj[x])
return frappe.db.sql(
"""select {fields} from `tabProject`
where
`tabProject`.status not in ('Completed', 'Cancelled')
and {cond} {scond} {match_cond}
order by
(case when locate(%(_txt)s, `tabProject`.name) > 0 then locate(%(_txt)s, `tabProject`.name) else 99999 end),
`tabProject`.idx desc,
`tabProject`.name asc
limit {page_len} offset {start}""".format(
fields=", ".join(["`tabProject`.{0}".format(f) for f in fields]),
cond=cond,
scond=searchfields,
match_cond=get_match_cond(doctype),
start=start,
page_len=page_len,
),
{"txt": "%{0}%".format(txt), "_txt": txt.replace("%", "")},
)
# don't consider 'customer' and 'status' fields for pattern search, as they must be exactly matched
searchfields = [
x for x in frappe.get_meta(doctype).get_search_fields() if x not in ["customer", "status"]
]
# pattern search
if txt:
for x in searchfields:
qb_filter_or_conditions.append(proj[x].like(f"%{txt}%"))
q = q.where(Criterion.all(qb_filter_and_conditions)).where(Criterion.any(qb_filter_or_conditions))
# ordering
if txt:
# project_name containing search string 'txt' will be given higher precedence
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)
if page_len:
q = q.limit(page_len)
if start:
q = q.offset(start)
return q.run()
@frappe.whitelist()
@ -416,23 +427,14 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
meta = frappe.get_meta(doctype, cached=True)
searchfields = meta.get_search_fields()
query = get_batches_from_stock_ledger_entries(searchfields, txt, filters)
bundle_query = get_batches_from_serial_and_batch_bundle(searchfields, txt, filters)
data = (
frappe.qb.from_((query) + (bundle_query))
.select("batch_no", "qty", "manufacturing_date", "expiry_date")
.offset(start)
.limit(page_len)
batches = get_batches_from_stock_ledger_entries(searchfields, txt, filters, start, page_len)
batches.extend(
get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start, page_len)
)
for field in searchfields:
data = data.select(field)
filtered_batches = get_filterd_batches(batches)
data = data.run()
data = get_filterd_batches(data)
return data
return filtered_batches
def get_filterd_batches(data):
@ -452,7 +454,7 @@ def get_filterd_batches(data):
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")
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())
)
.groupby(stock_ledger_entry.batch_no, stock_ledger_entry.warehouse)
.offset(start)
.limit(page_len)
)
query = query.select(
@ -488,16 +492,16 @@ def get_batches_from_stock_ledger_entries(searchfields, txt, filters):
query = query.select(batch_table[field])
if txt:
txt_condition = batch_table.name.like(txt)
txt_condition = batch_table.name.like("%{0}%".format(txt))
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)
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")
stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
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())
)
.groupby(bundle.batch_no, bundle.warehouse)
.offset(start)
.limit(page_len)
)
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])
if txt:
txt_condition = batch_table.name.like(txt)
txt_condition = batch_table.name.like("%{0}%".format(txt))
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)
return bundle_query
return bundle_query.run(as_list=1)
@frappe.whitelist()

View File

@ -68,7 +68,7 @@ class TestQueries(unittest.TestCase):
self.assertGreaterEqual(len(query(txt="_Test Item Home Desktop Manufactured")), 1)
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)

View File

@ -173,7 +173,7 @@ frappe.ui.form.on('Production Plan', {
method: "set_status",
freeze: true,
doc: frm.doc,
args: {close : close},
args: {close : close, update_bin: true},
callback: function() {
frm.reload_doc();
}

View File

@ -579,7 +579,7 @@ class ProductionPlan(Document):
frappe.delete_doc("Work Order", d.name)
@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)
if close:
@ -599,7 +599,7 @@ class ProductionPlan(Document):
if close is not None:
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()
def update_ordered_status(self):

View File

@ -1486,14 +1486,14 @@ class TestProductionPlan(FrappeTestCase):
before_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
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)
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
self.assertAlmostEqual(after_qty, before_qty - 10)
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)
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))

View File

@ -21,7 +21,7 @@ $.extend(erpnext, {
},
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);
}
},

View File

@ -16,6 +16,8 @@ erpnext.accounts.dimensions = {
},
callback: function(r) {
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.setup_filters(frm, doctype);
}

View File

@ -94,6 +94,9 @@ frappe.ui.form.on("Sales Order", {
frm.set_value("reserve_stock", 0);
frm.set_df_property("reserve_stock", "read_only", 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);
}
})
}

View File

@ -200,6 +200,7 @@ class SalesOrder(SellingController):
self.validate_for_items()
self.validate_warehouse()
self.validate_drop_ship()
self.validate_reserved_stock()
self.validate_serial_no_based_delivery()
validate_against_blanket_order(self)
validate_inter_company_party(
@ -660,6 +661,17 @@ class SalesOrder(SellingController):
).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:
"""Returns True if there is any unreserved item in the Sales Order."""

View File

@ -10,6 +10,7 @@
"item_code",
"customer_item_code",
"ensure_delivery_based_on_produced_serial_no",
"is_stock_item",
"reserve_stock",
"col_break1",
"delivery_date",
@ -867,6 +868,7 @@
{
"allow_on_submit": 1,
"default": "1",
"depends_on": "eval:doc.is_stock_item",
"fieldname": "reserve_stock",
"fieldtype": "Check",
"label": "Reserve Stock",
@ -891,6 +893,16 @@
"label": "Production Plan Qty",
"no_copy": 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,

View File

@ -87,7 +87,7 @@ class HolidayList(Document):
for holiday_date, holiday_name in country_holidays(
self.country,
subdiv=self.subdivision,
years=[from_date.year, to_date.year],
years=list(range(from_date.year, to_date.year + 1)),
language=frappe.local.lang,
).items():
if holiday_date in existing_holidays:

View File

@ -48,17 +48,58 @@ class TestHolidayList(unittest.TestCase):
def test_local_holidays(self):
holiday_list = frappe.new_doc("Holiday List")
holiday_list.from_date = "2023-04-01"
holiday_list.to_date = "2023-04-30"
holiday_list.from_date = "2022-01-01"
holiday_list.to_date = "2024-12-31"
holiday_list.country = "DE"
holiday_list.subdivision = "SN"
holiday_list.get_local_holidays()
holidays = [holiday.holiday_date for holiday in holiday_list.holidays]
self.assertNotIn(date(2023, 1, 1), holidays)
holidays = holiday_list.get_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, 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):
lang = frappe.local.lang

View File

@ -186,7 +186,7 @@
"idx": 1,
"in_create": 1,
"links": [],
"modified": "2023-11-01 16:51:17.079107",
"modified": "2024-01-16 15:11:46.140323",
"modified_by": "Administrator",
"module": "Stock",
"name": "Bin",
@ -213,6 +213,21 @@
"read": 1,
"report": 1,
"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,

View File

@ -103,15 +103,6 @@
"print_hide": 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",
"fieldtype": "Link",
@ -122,7 +113,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-05-17 11:46:04.448220",
"modified": "2023-05-18 11:46:04.448220",
"modified_by": "Administrator",
"module": "Stock",
"name": "Closing Stock Balance",

View File

@ -600,26 +600,12 @@ $.extend(erpnext.item, {
}
});
} else {
frappe.call({
method: "frappe.client.get",
args: {
doctype: "Item Attribute",
name: d.attribute
}
}).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();
}
});
let values = [];
for(var i = d.from_range; i <= d.to_range; i = flt(i + d.increment, 6)) {
values.push(i);
}
attr_val_fields[d.attribute] = values;
resolve();
}
});

View File

@ -1228,6 +1228,7 @@ def make_purchase_invoice(source_name, target_doc=None, args=None):
"field_map": {
"name": "pr_detail",
"parent": "purchase_receipt",
"qty": "received_qty",
"purchase_order_item": "po_detail",
"purchase_order": "purchase_order",
"is_fixed_asset": "is_fixed_asset",

View File

@ -111,6 +111,9 @@ class QualityInspection(Document):
def on_cancel(self):
self.update_qc_reference()
def on_trash(self):
self.update_qc_reference()
def validate_readings_status_mandatory(self):
for reading in self.readings:
if not reading.status:

View File

@ -250,6 +250,33 @@ class TestQualityInspection(FrappeTestCase):
qa.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):
args = frappe._dict(args)

View File

@ -104,7 +104,8 @@
"in_standard_filter": 1,
"label": "Stock Entry Type",
"options": "Stock Entry Type",
"reqd": 1
"reqd": 1,
"search_index": 1
},
{
"depends_on": "eval:doc.purpose == 'Material Transfer'",
@ -546,7 +547,8 @@
"label": "Job Card",
"options": "Job Card",
"print_hide": 1,
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"fieldname": "amended_from",
@ -679,7 +681,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-06-19 18:23:40.748114",
"modified": "2024-01-12 11:56:58.644882",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry",

View File

@ -561,7 +561,8 @@
"label": "Job Card Item",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"default": "0",
@ -589,7 +590,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-05-09 12:41:18.210864",
"modified": "2024-01-12 11:56:04.626103",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry Detail",

View File

@ -20,7 +20,6 @@ class StockEntryDetail(Document):
allow_alternative_item: DF.Check
allow_zero_valuation_rate: DF.Check
amount: DF.Currency
attach_something_here: DF.Attach | None
barcode: DF.Data | None
basic_amount: DF.Currency
basic_rate: DF.Currency

View File

@ -10,8 +10,9 @@
"has_item_scanned",
"item_code",
"item_name",
"warehouse",
"item_group",
"column_break_6",
"warehouse",
"qty",
"valuation_rate",
"amount",
@ -52,6 +53,7 @@
"reqd": 1
},
{
"fetch_from": "item_code.item_name",
"fieldname": "item_name",
"fieldtype": "Data",
"in_global_search": 1,
@ -213,11 +215,18 @@
"fieldname": "add_serial_batch_bundle",
"fieldtype": "Button",
"label": "Add Serial / Batch No"
},
{
"fetch_from": "item_code.item_group",
"fieldname": "item_group",
"fieldtype": "Link",
"label": "Item Group",
"options": "Item Group"
}
],
"istable": 1,
"links": [],
"modified": "2023-11-02 15:47:07.929550",
"modified": "2024-01-14 10:04:23.599951",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reconciliation Item",