mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-27 02:28:30 +00:00
Merge pull request #39404 from frappe/version-14-hotfix
chore: release v14
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.model.docstatus import DocStatus
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.controllers.status_updater import StatusUpdater
|
||||
@@ -68,7 +69,7 @@ class BankTransaction(StatusUpdater):
|
||||
"payment_entry": voucher["payment_name"],
|
||||
"allocated_amount": 0.0, # Temporary
|
||||
}
|
||||
child = self.append("payment_entries", pe)
|
||||
self.append("payment_entries", pe)
|
||||
added = True
|
||||
|
||||
# runs on_update_after_submit
|
||||
@@ -393,3 +394,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()
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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', "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", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries", "Bank Transaction"];
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
|
||||
@@ -7,7 +7,7 @@ cur_frm.cscript.tax_table = "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);
|
||||
|
||||
@@ -31,7 +31,7 @@ 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"];
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries", "Bank Transaction"];
|
||||
|
||||
if(!this.frm.doc.__islocal) {
|
||||
// show credit_to in print format
|
||||
|
||||
@@ -131,6 +131,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):
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
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"];
|
||||
'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries", "Bank Transaction"];
|
||||
|
||||
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
|
||||
// show debit_to in print format
|
||||
|
||||
@@ -138,6 +138,7 @@
|
||||
"loyalty_amount",
|
||||
"column_break_77",
|
||||
"loyalty_program",
|
||||
"dont_create_loyalty_points",
|
||||
"loyalty_redemption_account",
|
||||
"loyalty_redemption_cost_center",
|
||||
"contact_and_address_tab",
|
||||
@@ -1039,8 +1040,7 @@
|
||||
"label": "Loyalty Program",
|
||||
"no_copy": 1,
|
||||
"options": "Loyalty Program",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
@@ -2153,6 +2153,14 @@
|
||||
"fieldname": "update_billed_amount_in_delivery_note",
|
||||
"fieldtype": "Check",
|
||||
"label": "Update Billed Amount in Delivery Note"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "loyalty_program",
|
||||
"fieldname": "dont_create_loyalty_points",
|
||||
"fieldtype": "Check",
|
||||
"label": "Don't Create Loyalty Points",
|
||||
"no_copy": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@@ -2165,7 +2173,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2023-11-23 16:56:29.679499",
|
||||
"modified": "2024-01-02 17:25:46.027523",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -300,7 +300,12 @@ class SalesInvoice(SellingController):
|
||||
update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference)
|
||||
|
||||
# create the loyalty point ledger entry if the customer is enrolled in any loyalty program
|
||||
if not self.is_return and not self.is_consolidated and self.loyalty_program:
|
||||
if (
|
||||
not self.is_return
|
||||
and not self.is_consolidated
|
||||
and self.loyalty_program
|
||||
and not self.dont_create_loyalty_points
|
||||
):
|
||||
self.make_loyalty_point_entry()
|
||||
elif (
|
||||
self.is_return and self.return_against and not self.is_consolidated and self.loyalty_program
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -129,7 +129,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
|
||||
}
|
||||
|
||||
value = default_formatter(value, row, column, data);
|
||||
if (!data.parent_account) {
|
||||
if (data && !data.parent_account) {
|
||||
value = $(`<span>${value}</span>`);
|
||||
|
||||
var $value = $(value).css("font-weight", "bold");
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -89,6 +89,8 @@ def _execute(filters=None, additional_table_columns=None):
|
||||
"payable_account": inv.credit_to,
|
||||
"mode_of_payment": inv.mode_of_payment,
|
||||
"project": ", ".join(project) if inv.doctype == "Purchase Invoice" else inv.project,
|
||||
"bill_no": inv.bill_no,
|
||||
"bill_date": inv.bill_date,
|
||||
"remarks": inv.remarks,
|
||||
"purchase_order": ", ".join(purchase_order),
|
||||
"purchase_receipt": ", ".join(purchase_receipt),
|
||||
@@ -433,7 +435,7 @@ def get_payments(filters):
|
||||
account_fieldname="paid_to",
|
||||
party="supplier",
|
||||
party_name="supplier_name",
|
||||
party_account=get_party_account("Supplier", filters.supplier, filters.company),
|
||||
party_account=[get_party_account("Supplier", filters.supplier, filters.company)],
|
||||
)
|
||||
payment_entries = get_payment_entries(filters, args)
|
||||
journal_entries = get_journal_entries(filters, args)
|
||||
|
||||
@@ -477,7 +477,7 @@ def get_payments(filters):
|
||||
account_fieldname="paid_from",
|
||||
party="customer",
|
||||
party_name="customer_name",
|
||||
party_account=get_party_account("Customer", filters.customer, filters.company),
|
||||
party_account=[get_party_account("Customer", filters.customer, filters.company)],
|
||||
)
|
||||
payment_entries = get_payment_entries(filters, args)
|
||||
journal_entries = get_journal_entries(filters, args)
|
||||
|
||||
@@ -251,8 +251,9 @@ def get_journal_entries(filters, args):
|
||||
)
|
||||
.where(
|
||||
(je.voucher_type == "Journal Entry")
|
||||
& (je.docstatus == 1)
|
||||
& (journal_account.party == filters.get(args.party))
|
||||
& (journal_account.account == args.party_account)
|
||||
& (journal_account.account.isin(args.party_account))
|
||||
)
|
||||
.orderby(je.posting_date, je.name, order=Order.desc)
|
||||
)
|
||||
@@ -281,7 +282,9 @@ def get_payment_entries(filters, args):
|
||||
pe.cost_center,
|
||||
)
|
||||
.where(
|
||||
(pe.party == filters.get(args.party)) & (pe[args.account_fieldname] == args.party_account)
|
||||
(pe.docstatus == 1)
|
||||
& (pe.party == filters.get(args.party))
|
||||
& (pe[args.account_fieldname].isin(args.party_account))
|
||||
)
|
||||
.orderby(pe.posting_date, pe.name, order=Order.desc)
|
||||
)
|
||||
@@ -365,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
|
||||
|
||||
|
||||
|
||||
@@ -22,6 +22,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")
|
||||
@@ -125,6 +129,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 = [
|
||||
{
|
||||
|
||||
@@ -1222,8 +1222,13 @@ def get_autoname_with_number(number_value, doc_title, company):
|
||||
|
||||
def parse_naming_series_variable(doc, variable):
|
||||
if variable == "FY":
|
||||
date = doc.get("posting_date") or doc.get("transaction_date") or getdate()
|
||||
return get_fiscal_year(date=date, company=doc.get("company"))[0]
|
||||
if doc:
|
||||
date = doc.get("posting_date") or doc.get("transaction_date") or getdate()
|
||||
company = doc.get("company")
|
||||
else:
|
||||
date = getdate()
|
||||
company = None
|
||||
return get_fiscal_year(date=date, company=company)[0]
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
{
|
||||
@@ -583,7 +582,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",
|
||||
|
||||
@@ -61,6 +61,7 @@ class Asset(AccountsController):
|
||||
def on_cancel(self):
|
||||
self.validate_cancellation()
|
||||
self.cancel_movement_entries()
|
||||
self.cancel_capitalization()
|
||||
self.delete_depreciation_entries()
|
||||
self.set_status()
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
|
||||
@@ -831,6 +832,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 d in self.get("schedules"):
|
||||
@@ -1351,6 +1362,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)
|
||||
|
||||
|
||||
@@ -20,10 +20,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() {
|
||||
@@ -119,13 +119,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();
|
||||
}
|
||||
|
||||
@@ -72,11 +72,23 @@ class AssetCapitalization(StockController):
|
||||
self.update_target_asset()
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
|
||||
self.ignore_linked_doctypes = (
|
||||
"GL Entry",
|
||||
"Stock Ledger Entry",
|
||||
"Repost Item Valuation",
|
||||
"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
|
||||
|
||||
@@ -782,7 +794,6 @@ def get_consumed_asset_details(args):
|
||||
out.cost_center = get_default_cost_center(
|
||||
args, item_defaults, item_group_defaults, brand_defaults
|
||||
)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
@@ -830,10 +841,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
|
||||
|
||||
@@ -188,7 +188,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",
|
||||
@@ -214,39 +214,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",
|
||||
|
||||
@@ -1347,11 +1347,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)
|
||||
|
||||
@@ -6,9 +6,12 @@ import json
|
||||
from collections import 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 import Criterion, CustomFunction
|
||||
from frappe.query_builder.functions import Locate
|
||||
from frappe.utils import nowdate, unique
|
||||
from pypika import Order
|
||||
|
||||
import erpnext
|
||||
from erpnext.stock.get_item_details import _get_item_tax_template
|
||||
@@ -329,37 +332,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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -239,11 +239,12 @@ def new_bank_transaction(transaction):
|
||||
withdrawal = 0.0
|
||||
|
||||
tags = []
|
||||
try:
|
||||
tags += transaction["category"]
|
||||
tags += [f'Plaid Cat. {transaction["category_id"]}']
|
||||
except KeyError:
|
||||
pass
|
||||
if transaction["category"]:
|
||||
try:
|
||||
tags += transaction["category"]
|
||||
tags += [f'Plaid Cat. {transaction["category_id"]}']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if not frappe.db.exists(
|
||||
"Bank Transaction", dict(transaction_id=transaction["transaction_id"])
|
||||
|
||||
@@ -16,7 +16,7 @@ app_include_js = "erpnext.bundle.js"
|
||||
app_include_css = "erpnext.bundle.css"
|
||||
web_include_js = "erpnext-web.bundle.js"
|
||||
web_include_css = "erpnext-web.bundle.css"
|
||||
email_css = "email_erpnext.bundle.css"
|
||||
email_css = "erpnext_email.bundle.scss"
|
||||
|
||||
doctype_js = {
|
||||
"Address": "public/js/address.js",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -63,7 +63,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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -630,26 +630,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();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1077,6 +1077,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",
|
||||
|
||||
@@ -13,7 +13,6 @@ from erpnext.stock.doctype.item.test_item import create_item, make_item
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice
|
||||
from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError, get_serial_nos
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
from erpnext.stock.stock_ledger import SerialNoExistsInFutureTransaction
|
||||
|
||||
|
||||
class TestPurchaseReceipt(FrappeTestCase):
|
||||
@@ -197,84 +196,6 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
self.assertFalse(frappe.db.get_value("Batch", {"item": item.name, "reference_name": pr.name}))
|
||||
self.assertFalse(frappe.db.get_all("Serial No", {"batch_no": batch_no}))
|
||||
|
||||
def test_duplicate_serial_nos(self):
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
item = frappe.db.exists("Item", {"item_name": "Test Serialized Item 123"})
|
||||
if not item:
|
||||
item = create_item("Test Serialized Item 123")
|
||||
item.has_serial_no = 1
|
||||
item.serial_no_series = "TSI123-.####"
|
||||
item.save()
|
||||
else:
|
||||
item = frappe.get_doc("Item", {"item_name": "Test Serialized Item 123"})
|
||||
|
||||
# First make purchase receipt
|
||||
pr = make_purchase_receipt(item_code=item.name, qty=2, rate=500)
|
||||
pr.load_from_db()
|
||||
|
||||
serial_nos = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "item_code": item.name},
|
||||
"serial_no",
|
||||
)
|
||||
|
||||
serial_nos = get_serial_nos(serial_nos)
|
||||
|
||||
self.assertEquals(get_serial_nos(pr.items[0].serial_no), serial_nos)
|
||||
|
||||
# Then tried to receive same serial nos in difference company
|
||||
pr_different_company = make_purchase_receipt(
|
||||
item_code=item.name,
|
||||
qty=2,
|
||||
rate=500,
|
||||
serial_no="\n".join(serial_nos),
|
||||
company="_Test Company 1",
|
||||
do_not_submit=True,
|
||||
warehouse="Stores - _TC1",
|
||||
)
|
||||
|
||||
self.assertRaises(SerialNoDuplicateError, pr_different_company.submit)
|
||||
|
||||
# Then made delivery note to remove the serial nos from stock
|
||||
dn = create_delivery_note(item_code=item.name, qty=2, rate=1500, serial_no="\n".join(serial_nos))
|
||||
dn.load_from_db()
|
||||
self.assertEquals(get_serial_nos(dn.items[0].serial_no), serial_nos)
|
||||
|
||||
posting_date = add_days(today(), -3)
|
||||
|
||||
# Try to receive same serial nos again in the same company with backdated.
|
||||
pr1 = make_purchase_receipt(
|
||||
item_code=item.name,
|
||||
qty=2,
|
||||
rate=500,
|
||||
posting_date=posting_date,
|
||||
serial_no="\n".join(serial_nos),
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
self.assertRaises(SerialNoExistsInFutureTransaction, pr1.submit)
|
||||
|
||||
# Try to receive same serial nos with different company with backdated.
|
||||
pr2 = make_purchase_receipt(
|
||||
item_code=item.name,
|
||||
qty=2,
|
||||
rate=500,
|
||||
posting_date=posting_date,
|
||||
serial_no="\n".join(serial_nos),
|
||||
company="_Test Company 1",
|
||||
do_not_submit=True,
|
||||
warehouse="Stores - _TC1",
|
||||
)
|
||||
|
||||
self.assertRaises(SerialNoExistsInFutureTransaction, pr2.submit)
|
||||
|
||||
# Receive the same serial nos after the delivery note posting date and time
|
||||
make_purchase_receipt(item_code=item.name, qty=2, rate=500, serial_no="\n".join(serial_nos))
|
||||
|
||||
# Raise the error for backdated deliver note entry cancel
|
||||
self.assertRaises(SerialNoExistsInFutureTransaction, dn.cancel)
|
||||
|
||||
def test_purchase_receipt_gl_entry(self):
|
||||
pr = make_purchase_receipt(
|
||||
company="_Test Company with perpetual inventory",
|
||||
|
||||
@@ -68,6 +68,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:
|
||||
|
||||
@@ -216,6 +216,33 @@ class TestQualityInspection(FrappeTestCase):
|
||||
qa.save()
|
||||
self.assertEqual(qa.status, "Accepted")
|
||||
|
||||
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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -557,7 +557,8 @@
|
||||
"label": "Job Card Item",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -572,7 +573,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",
|
||||
|
||||
@@ -634,30 +634,48 @@ class StockReconciliation(StockController):
|
||||
if voucher_detail_no != row.name:
|
||||
continue
|
||||
|
||||
current_qty = get_batch_qty_for_stock_reco(
|
||||
row.item_code, row.warehouse, row.batch_no, self.posting_date, self.posting_time, self.name
|
||||
)
|
||||
if row.serial_no:
|
||||
item_dict = get_stock_balance_for(
|
||||
row.item_code,
|
||||
row.warehouse,
|
||||
self.posting_date,
|
||||
self.posting_time,
|
||||
voucher_no=self.name,
|
||||
)
|
||||
|
||||
current_qty = item_dict.get("qty")
|
||||
row.current_serial_no = item_dict.get("serial_nos")
|
||||
row.current_valuation_rate = item_dict.get("rate")
|
||||
else:
|
||||
current_qty = get_batch_qty_for_stock_reco(
|
||||
row.item_code, row.warehouse, row.batch_no, self.posting_date, self.posting_time, self.name
|
||||
)
|
||||
|
||||
precesion = row.precision("current_qty")
|
||||
if flt(current_qty, precesion) != flt(row.current_qty, precesion):
|
||||
val_rate = get_valuation_rate(
|
||||
row.item_code,
|
||||
row.warehouse,
|
||||
self.doctype,
|
||||
self.name,
|
||||
company=self.company,
|
||||
batch_no=row.batch_no,
|
||||
)
|
||||
if not row.serial_no:
|
||||
val_rate = get_valuation_rate(
|
||||
row.item_code,
|
||||
row.warehouse,
|
||||
self.doctype,
|
||||
self.name,
|
||||
company=self.company,
|
||||
batch_no=row.batch_no,
|
||||
)
|
||||
|
||||
row.current_valuation_rate = val_rate
|
||||
|
||||
row.current_valuation_rate = val_rate
|
||||
row.current_qty = current_qty
|
||||
row.db_set(
|
||||
{
|
||||
"current_qty": row.current_qty,
|
||||
"current_valuation_rate": row.current_valuation_rate,
|
||||
"current_amount": flt(row.current_qty * row.current_valuation_rate),
|
||||
}
|
||||
)
|
||||
values_to_update = {
|
||||
"current_qty": row.current_qty,
|
||||
"current_valuation_rate": row.current_valuation_rate,
|
||||
"current_amount": flt(row.current_qty * row.current_valuation_rate),
|
||||
}
|
||||
|
||||
if row.current_serial_no:
|
||||
values_to_update["current_serial_no"] = row.current_serial_no
|
||||
|
||||
row.db_set(values_to_update)
|
||||
|
||||
if (
|
||||
add_new_sle
|
||||
@@ -880,6 +898,7 @@ def get_stock_balance_for(
|
||||
batch_no: Optional[str] = None,
|
||||
with_valuation_rate: bool = True,
|
||||
inventory_dimensions_dict=None,
|
||||
voucher_no=None,
|
||||
):
|
||||
frappe.has_permission("Stock Reconciliation", "write", throw=True)
|
||||
|
||||
@@ -910,6 +929,7 @@ def get_stock_balance_for(
|
||||
with_serial_no=has_serial_no,
|
||||
inventory_dimensions_dict=inventory_dimensions_dict,
|
||||
batch_no=batch_no,
|
||||
voucher_no=voucher_no,
|
||||
)
|
||||
|
||||
if has_serial_no:
|
||||
|
||||
@@ -1055,6 +1055,74 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
||||
self.assertEqual(sr.items[0].current_qty, se2.items[0].qty)
|
||||
self.assertEqual(len(sr.items[0].current_serial_no.split("\n")), sr.items[0].current_qty)
|
||||
|
||||
def test_backdated_purchase_receipt_with_stock_reco(self):
|
||||
item_code = self.make_item(
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_serial_no": 1,
|
||||
"serial_no_series": "TEST-SERIAL-.###",
|
||||
}
|
||||
).name
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
# Step - 1: Create a Backdated Purchase Receipt
|
||||
|
||||
pr1 = make_purchase_receipt(
|
||||
item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -3)
|
||||
)
|
||||
pr1.reload()
|
||||
|
||||
serial_nos = sorted(get_serial_nos(pr1.items[0].serial_no))[:5]
|
||||
|
||||
# Step - 2: Create a Stock Reconciliation
|
||||
sr1 = create_stock_reconciliation(
|
||||
item_code=item_code,
|
||||
warehouse=warehouse,
|
||||
qty=5,
|
||||
serial_no="\n".join(serial_nos),
|
||||
)
|
||||
|
||||
data = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
fields=["serial_no", "actual_qty", "stock_value_difference"],
|
||||
filters={"voucher_no": sr1.name, "is_cancelled": 0},
|
||||
order_by="creation",
|
||||
)
|
||||
|
||||
for d in data:
|
||||
if d.actual_qty < 0:
|
||||
self.assertEqual(d.actual_qty, -10.0)
|
||||
self.assertAlmostEqual(d.stock_value_difference, -1000.0)
|
||||
else:
|
||||
self.assertEqual(d.actual_qty, 5.0)
|
||||
self.assertAlmostEqual(d.stock_value_difference, 500.0)
|
||||
|
||||
# Step - 3: Create a Purchase Receipt before the first Purchase Receipt
|
||||
make_purchase_receipt(
|
||||
item_code=item_code, warehouse=warehouse, qty=10, rate=200, posting_date=add_days(nowdate(), -5)
|
||||
)
|
||||
|
||||
data = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
fields=["serial_no", "actual_qty", "stock_value_difference"],
|
||||
filters={"voucher_no": sr1.name, "is_cancelled": 0},
|
||||
order_by="creation",
|
||||
)
|
||||
|
||||
for d in data:
|
||||
if d.actual_qty < 0:
|
||||
self.assertEqual(d.actual_qty, -20.0)
|
||||
self.assertAlmostEqual(d.stock_value_difference, -3000.0)
|
||||
else:
|
||||
self.assertEqual(d.actual_qty, 5.0)
|
||||
self.assertAlmostEqual(d.stock_value_difference, 500.0)
|
||||
|
||||
active_serial_no = frappe.get_all(
|
||||
"Serial No", filters={"status": "Active", "item_code": item_code}
|
||||
)
|
||||
self.assertEqual(len(active_serial_no), 5)
|
||||
|
||||
|
||||
def create_batch_item_with_batch(item_name, batch_id):
|
||||
batch_item_doc = create_item(item_name, is_stock_item=1)
|
||||
|
||||
@@ -10,8 +10,9 @@
|
||||
"has_item_scanned",
|
||||
"item_code",
|
||||
"item_name",
|
||||
"warehouse",
|
||||
"item_group",
|
||||
"column_break_6",
|
||||
"warehouse",
|
||||
"qty",
|
||||
"valuation_rate",
|
||||
"amount",
|
||||
@@ -49,6 +50,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.item_name",
|
||||
"fieldname": "item_name",
|
||||
"fieldtype": "Data",
|
||||
"in_global_search": 1,
|
||||
@@ -186,11 +188,18 @@
|
||||
"fieldtype": "Data",
|
||||
"label": "Has Item Scanned",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.item_group",
|
||||
"fieldname": "item_group",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item Group",
|
||||
"options": "Item Group"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-07-25 11:58:44.992419",
|
||||
"modified": "2024-01-14 10:04:23.599951",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Reconciliation Item",
|
||||
@@ -201,4 +210,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import copy
|
||||
import json
|
||||
from typing import Optional, Set, Tuple
|
||||
|
||||
@@ -27,10 +26,6 @@ class NegativeStockError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class SerialNoExistsInFutureTransaction(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
|
||||
"""Create SL entries from SL entry dicts
|
||||
|
||||
@@ -54,9 +49,6 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
|
||||
future_sle_exists(args, sl_entries)
|
||||
|
||||
for sle in sl_entries:
|
||||
if sle.serial_no and not via_landed_cost_voucher:
|
||||
validate_serial_no(sle)
|
||||
|
||||
if cancel:
|
||||
sle["actual_qty"] = -flt(sle.get("actual_qty"))
|
||||
|
||||
@@ -133,35 +125,6 @@ def get_args_for_future_sle(row):
|
||||
)
|
||||
|
||||
|
||||
def validate_serial_no(sle):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
for sn in get_serial_nos(sle.serial_no):
|
||||
args = copy.deepcopy(sle)
|
||||
args.serial_no = sn
|
||||
args.warehouse = ""
|
||||
|
||||
vouchers = []
|
||||
for row in get_stock_ledger_entries(args, ">"):
|
||||
voucher_type = frappe.bold(row.voucher_type)
|
||||
voucher_no = frappe.bold(get_link_to_form(row.voucher_type, row.voucher_no))
|
||||
vouchers.append(f"{voucher_type} {voucher_no}")
|
||||
|
||||
if vouchers:
|
||||
serial_no = frappe.bold(sn)
|
||||
msg = (
|
||||
f"""The serial no {serial_no} has been used in the future transactions so you need to cancel them first.
|
||||
The list of the transactions are as below."""
|
||||
+ "<br><br><ul><li>"
|
||||
)
|
||||
|
||||
msg += "</li><li>".join(vouchers)
|
||||
msg += "</li></ul>"
|
||||
|
||||
title = "Cannot Submit" if not sle.get("is_cancelled") else "Cannot Cancel"
|
||||
frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction)
|
||||
|
||||
|
||||
def validate_cancellation(args):
|
||||
if args[0].get("is_cancelled"):
|
||||
repost_entry = frappe.db.get_value(
|
||||
@@ -573,7 +536,12 @@ class update_entries_after(object):
|
||||
if not self.args.get("sle_id"):
|
||||
self.get_dynamic_incoming_outgoing_rate(sle)
|
||||
|
||||
if sle.voucher_type == "Stock Reconciliation" and sle.batch_no and sle.voucher_detail_no:
|
||||
if (
|
||||
sle.voucher_type == "Stock Reconciliation"
|
||||
and not self.args.get("sle_id")
|
||||
and sle.voucher_detail_no
|
||||
and (sle.batch_no or sle.serial_no)
|
||||
):
|
||||
self.reset_actual_qty_for_stock_reco(sle)
|
||||
|
||||
if (
|
||||
@@ -651,11 +619,52 @@ class update_entries_after(object):
|
||||
doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty >= 0)
|
||||
|
||||
if sle.actual_qty < 0:
|
||||
sle.actual_qty = (
|
||||
flt(frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "current_qty"))
|
||||
* -1
|
||||
stock_reco_details = frappe.db.get_value(
|
||||
"Stock Reconciliation Item",
|
||||
sle.voucher_detail_no,
|
||||
["current_qty", "current_serial_no as sn_no"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
sle.actual_qty = flt(stock_reco_details.current_qty) * -1
|
||||
|
||||
if stock_reco_details.sn_no:
|
||||
sle.serial_no = stock_reco_details.sn_no
|
||||
sle.qty_after_transaction = 0.0
|
||||
|
||||
if sle.serial_no:
|
||||
self.update_serial_no_status(sle)
|
||||
|
||||
def update_serial_no_status(self, sle):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
serial_nos = get_serial_nos(sle.serial_no)
|
||||
warehouse = None
|
||||
status = "Delivered"
|
||||
if sle.actual_qty > 0:
|
||||
warehouse = sle.warehouse
|
||||
status = "Active"
|
||||
|
||||
sn_table = frappe.qb.DocType("Serial No")
|
||||
|
||||
query = (
|
||||
frappe.qb.update(sn_table)
|
||||
.set(sn_table.warehouse, warehouse)
|
||||
.set(sn_table.status, status)
|
||||
.where(sn_table.name.isin(serial_nos))
|
||||
)
|
||||
|
||||
if sle.actual_qty > 0:
|
||||
query = query.set(sn_table.purchase_document_type, sle.voucher_type)
|
||||
query = query.set(sn_table.purchase_document_no, sle.voucher_no)
|
||||
query = query.set(sn_table.delivery_document_type, None)
|
||||
query = query.set(sn_table.delivery_document_no, None)
|
||||
else:
|
||||
query = query.set(sn_table.delivery_document_type, sle.voucher_type)
|
||||
query = query.set(sn_table.delivery_document_no, sle.voucher_no)
|
||||
|
||||
query.run()
|
||||
|
||||
def validate_negative_stock(self, sle):
|
||||
"""
|
||||
validate negative stock for entries current datetime onwards
|
||||
@@ -1282,6 +1291,9 @@ def get_stock_ledger_entries(
|
||||
if operator in (">", "<=") and previous_sle.get("name"):
|
||||
conditions += " and name!=%(name)s"
|
||||
|
||||
if operator in (">", "<=") and previous_sle.get("voucher_no"):
|
||||
conditions += " and voucher_no!=%(voucher_no)s"
|
||||
|
||||
if extra_cond:
|
||||
conditions += f"{extra_cond}"
|
||||
|
||||
|
||||
@@ -96,6 +96,7 @@ def get_stock_balance(
|
||||
with_serial_no=False,
|
||||
inventory_dimensions_dict=None,
|
||||
batch_no=None,
|
||||
voucher_no=None,
|
||||
):
|
||||
"""Returns stock balance quantity at given warehouse on given posting date or current date.
|
||||
|
||||
@@ -115,6 +116,9 @@ def get_stock_balance(
|
||||
"posting_time": posting_time,
|
||||
}
|
||||
|
||||
if voucher_no:
|
||||
args["voucher_no"] = voucher_no
|
||||
|
||||
extra_cond = ""
|
||||
if inventory_dimensions_dict:
|
||||
for field, value in inventory_dimensions_dict.items():
|
||||
|
||||
Reference in New Issue
Block a user