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

chore: release v15
This commit is contained in:
rohitwaghchaure
2024-03-20 10:52:04 +05:30
committed by GitHub
85 changed files with 1096 additions and 384 deletions

View File

@@ -148,6 +148,9 @@ def start_import(
import_file = ImportFile("Bank Transaction", file=file, import_type="Insert New Records")
data = parse_data_from_template(import_file.raw_data)
# Importer expects 'Data Import' class, which has 'payload_count' attribute
if not data_import.get("payload_count"):
data_import.payload_count = len(data) - 1
if import_file_path:
add_bank_account(data, bank_account)

View File

@@ -56,17 +56,17 @@ class BankTransaction(Document):
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 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)
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:

View File

@@ -3,22 +3,36 @@
frappe.ui.form.on("Currency Exchange Settings", {
service_provider: function (frm) {
if (frm.doc.service_provider == "exchangerate.host") {
let result = ["result"];
let params = {
date: "{transaction_date}",
from: "{from_currency}",
to: "{to_currency}",
};
add_param(frm, "https://api.exchangerate.host/convert", params, result);
} else if (frm.doc.service_provider == "frankfurter.app") {
let result = ["rates", "{to_currency}"];
let params = {
base: "{from_currency}",
symbols: "{to_currency}",
};
add_param(frm, "https://frankfurter.app/{transaction_date}", params, result);
}
frm.call({
method: "erpnext.accounts.doctype.currency_exchange_settings.currency_exchange_settings.get_api_endpoint",
args: {
service_provider: frm.doc.service_provider,
use_http: frm.doc.use_http,
},
callback: function (r) {
if (r && r.message) {
if (frm.doc.service_provider == "exchangerate.host") {
let result = ["result"];
let params = {
date: "{transaction_date}",
from: "{from_currency}",
to: "{to_currency}",
};
add_param(frm, r.message, params, result);
} else if (frm.doc.service_provider == "frankfurter.app") {
let result = ["rates", "{to_currency}"];
let params = {
base: "{from_currency}",
symbols: "{to_currency}",
};
add_param(frm, r.message, params, result);
}
}
},
});
},
use_http: function (frm) {
frm.trigger("service_provider");
},
});

View File

@@ -9,6 +9,7 @@
"disabled",
"service_provider",
"api_endpoint",
"use_http",
"access_key",
"url",
"column_break_3",
@@ -91,12 +92,19 @@
"fieldname": "access_key",
"fieldtype": "Data",
"label": "Access Key"
},
{
"default": "0",
"depends_on": "eval: doc.service_provider != \"Custom\"",
"fieldname": "use_http",
"fieldtype": "Check",
"label": "Use HTTP Protocol"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2023-10-04 15:30:25.333860",
"modified": "2024-03-18 08:32:26.895076",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Currency Exchange Settings",

View File

@@ -31,6 +31,7 @@ class CurrencyExchangeSettings(Document):
result_key: DF.Table[CurrencyExchangeSettingsResult]
service_provider: DF.Literal["frankfurter.app", "exchangerate.host", "Custom"]
url: DF.Data | None
use_http: DF.Check
# end: auto-generated types
def validate(self):
@@ -53,7 +54,7 @@ class CurrencyExchangeSettings(Document):
self.set("result_key", [])
self.set("req_params", [])
self.api_endpoint = "https://api.exchangerate.host/convert"
self.api_endpoint = get_api_endpoint(self.service_provider, self.use_http)
self.append("result_key", {"key": "result"})
self.append("req_params", {"key": "access_key", "value": self.access_key})
self.append("req_params", {"key": "amount", "value": "1"})
@@ -64,7 +65,7 @@ class CurrencyExchangeSettings(Document):
self.set("result_key", [])
self.set("req_params", [])
self.api_endpoint = "https://frankfurter.app/{transaction_date}"
self.api_endpoint = get_api_endpoint(self.service_provider, self.use_http)
self.append("result_key", {"key": "rates"})
self.append("result_key", {"key": "{to_currency}"})
self.append("req_params", {"key": "base", "value": "{from_currency}"})
@@ -103,3 +104,19 @@ class CurrencyExchangeSettings(Document):
frappe.throw(_("Returned exchange rate is neither integer not float."))
self.url = response.url
@frappe.whitelist()
def get_api_endpoint(service_provider: str = None, use_http: bool = False):
if service_provider and service_provider in ["exchangerate.host", "frankfurter.app"]:
if service_provider == "exchangerate.host":
api = "api.exchangerate.host/convert"
elif service_provider == "frankfurter.app":
api = "frankfurter.app/{transaction_date}"
protocol = "https://"
if use_http:
protocol = "http://"
return protocol + api
return None

View File

@@ -628,21 +628,21 @@ def get_account_details(
if account_balance and (
account_balance[0].balance or account_balance[0].balance_in_account_currency
):
account_with_new_balance = ExchangeRateRevaluation.calculate_new_account_balance(
if account_with_new_balance := ExchangeRateRevaluation.calculate_new_account_balance(
company, posting_date, account_balance
)
row = account_with_new_balance[0]
account_details.update(
{
"balance_in_base_currency": row["balance_in_base_currency"],
"balance_in_account_currency": row["balance_in_account_currency"],
"current_exchange_rate": row["current_exchange_rate"],
"new_exchange_rate": row["new_exchange_rate"],
"new_balance_in_base_currency": row["new_balance_in_base_currency"],
"new_balance_in_account_currency": row["new_balance_in_account_currency"],
"zero_balance": row["zero_balance"],
"gain_loss": row["gain_loss"],
}
)
):
row = account_with_new_balance[0]
account_details.update(
{
"balance_in_base_currency": row["balance_in_base_currency"],
"balance_in_account_currency": row["balance_in_account_currency"],
"current_exchange_rate": row["current_exchange_rate"],
"new_exchange_rate": row["new_exchange_rate"],
"new_balance_in_base_currency": row["new_balance_in_base_currency"],
"new_balance_in_account_currency": row["new_balance_in_account_currency"],
"zero_balance": row["zero_balance"],
"gain_loss": row["gain_loss"],
}
)
return account_details

View File

@@ -196,7 +196,7 @@ frappe.ui.form.on("Journal Entry", {
!(frm.doc.accounts || []).length ||
((frm.doc.accounts || []).length === 1 && !frm.doc.accounts[0].account)
) {
if (in_list(["Bank Entry", "Cash Entry"], frm.doc.voucher_type)) {
if (["Bank Entry", "Cash Entry"].includes(frm.doc.voucher_type)) {
return frappe.call({
type: "GET",
method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_default_bank_cash_account",
@@ -308,7 +308,7 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
filters: [[jvd.reference_type, "docstatus", "=", 1]],
};
if (in_list(["Sales Invoice", "Purchase Invoice"], jvd.reference_type)) {
if (["Sales Invoice", "Purchase Invoice"].includes(jvd.reference_type)) {
out.filters.push([jvd.reference_type, "outstanding_amount", "!=", 0]);
// Filter by cost center
if (jvd.cost_center) {
@@ -320,7 +320,7 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
out.filters.push([jvd.reference_type, party_account_field, "=", jvd.account]);
}
if (in_list(["Sales Order", "Purchase Order"], jvd.reference_type)) {
if (["Sales Order", "Purchase Order"].includes(jvd.reference_type)) {
// party_type and party mandatory
frappe.model.validate_missing(jvd, "party_type");
frappe.model.validate_missing(jvd, "party");

View File

@@ -32,7 +32,7 @@ frappe.ui.form.on("Payment Entry", {
frm.set_query("paid_from", function () {
frm.events.validate_company(frm);
var account_types = in_list(["Pay", "Internal Transfer"], frm.doc.payment_type)
var account_types = ["Pay", "Internal Transfer"].includes(frm.doc.payment_type)
? ["Bank", "Cash"]
: [frappe.boot.party_account_types[frm.doc.party_type]];
return {
@@ -87,7 +87,7 @@ frappe.ui.form.on("Payment Entry", {
frm.set_query("paid_to", function () {
frm.events.validate_company(frm);
var account_types = in_list(["Receive", "Internal Transfer"], frm.doc.payment_type)
var account_types = ["Receive", "Internal Transfer"].includes(frm.doc.payment_type)
? ["Bank", "Cash"]
: [frappe.boot.party_account_types[frm.doc.party_type]];
return {
@@ -134,7 +134,7 @@ frappe.ui.form.on("Payment Entry", {
frm.set_query("payment_term", "references", function (frm, cdt, cdn) {
const child = locals[cdt][cdn];
if (
in_list(["Purchase Invoice", "Sales Invoice"], child.reference_doctype) &&
["Purchase Invoice", "Sales Invoice"].includes(child.reference_doctype) &&
child.reference_name
) {
return {
@@ -395,10 +395,6 @@ frappe.ui.form.on("Payment Entry", {
return {
query: "erpnext.controllers.queries.employee_query",
};
} else if (frm.doc.party_type == "Customer") {
return {
query: "erpnext.controllers.queries.customer_query",
};
}
});
@@ -627,7 +623,7 @@ frappe.ui.form.on("Payment Entry", {
if (frm.doc.paid_from_account_currency == company_currency) {
frm.set_value("source_exchange_rate", 1);
} else if (frm.doc.paid_from) {
if (in_list(["Internal Transfer", "Pay"], frm.doc.payment_type)) {
if (["Internal Transfer", "Pay"].includes(frm.doc.payment_type)) {
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
frappe.call({
method: "erpnext.setup.utils.get_exchange_rate",
@@ -1046,7 +1042,7 @@ frappe.ui.form.on("Payment Entry", {
}
var allocated_positive_outstanding = paid_amount + allocated_negative_outstanding;
} else if (in_list(["Customer", "Supplier"], frm.doc.party_type)) {
} else if (["Customer", "Supplier"].includes(frm.doc.party_type)) {
total_negative_outstanding = flt(total_negative_outstanding, precision("outstanding_amount"));
if (paid_amount > total_negative_outstanding) {
if (total_negative_outstanding == 0) {
@@ -1217,7 +1213,7 @@ frappe.ui.form.on("Payment Entry", {
if (
frm.doc.party_type == "Customer" &&
!in_list(["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"], row.reference_doctype)
!["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"].includes(row.reference_doctype)
) {
frappe.model.set_value(row.doctype, row.name, "reference_doctype", null);
frappe.msgprint(
@@ -1231,7 +1227,7 @@ frappe.ui.form.on("Payment Entry", {
if (
frm.doc.party_type == "Supplier" &&
!in_list(["Purchase Order", "Purchase Invoice", "Journal Entry"], row.reference_doctype)
!["Purchase Order", "Purchase Invoice", "Journal Entry"].includes(row.reference_doctype)
) {
frappe.model.set_value(row.doctype, row.name, "against_voucher_type", null);
frappe.msgprint(
@@ -1327,7 +1323,7 @@ frappe.ui.form.on("Payment Entry", {
bank_account: function (frm) {
const field = frm.doc.payment_type == "Pay" ? "paid_from" : "paid_to";
if (frm.doc.bank_account && in_list(["Pay", "Receive"], frm.doc.payment_type)) {
if (frm.doc.bank_account && ["Pay", "Receive"].includes(frm.doc.payment_type)) {
frappe.call({
method: "erpnext.accounts.doctype.bank_account.bank_account.get_bank_account_details",
args: {

View File

@@ -404,7 +404,9 @@ class PaymentEntry(AccountsController):
ref_details = get_reference_details(
d.reference_doctype, d.reference_name, self.party_account_currency
)
if ref_exchange_rate:
# Only update exchange rate when the reference is Journal Entry
if ref_exchange_rate and d.reference_doctype == "Journal Entry":
ref_details.update({"exchange_rate": ref_exchange_rate})
for field, value in ref_details.items():
@@ -526,9 +528,9 @@ class PaymentEntry(AccountsController):
def get_valid_reference_doctypes(self):
if self.party_type == "Customer":
return ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning")
return ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning", "Payment Entry")
elif self.party_type == "Supplier":
return ("Purchase Order", "Purchase Invoice", "Journal Entry")
return ("Purchase Order", "Purchase Invoice", "Journal Entry", "Payment Entry")
elif self.party_type == "Shareholder":
return ("Journal Entry",)
elif self.party_type == "Employee":
@@ -1191,6 +1193,7 @@ class PaymentEntry(AccountsController):
"Journal Entry",
"Sales Order",
"Purchase Order",
"Payment Entry",
):
self.add_advance_gl_for_reference(gl_entries, ref)
@@ -1213,7 +1216,9 @@ class PaymentEntry(AccountsController):
if getdate(posting_date) < getdate(self.posting_date):
posting_date = self.posting_date
dr_or_cr = "credit" if invoice.reference_doctype in ["Sales Invoice", "Sales Order"] else "debit"
dr_or_cr = (
"credit" if invoice.reference_doctype in ["Sales Invoice", "Payment Entry"] else "debit"
)
args_dict["account"] = invoice.account
args_dict[dr_or_cr] = invoice.allocated_amount
args_dict[dr_or_cr + "_in_account_currency"] = invoice.allocated_amount
@@ -1660,7 +1665,7 @@ def get_outstanding_reference_documents(args, validate=False):
outstanding_invoices = get_outstanding_invoices(
args.get("party_type"),
args.get("party"),
party_account,
[party_account],
common_filter=common_filter,
posting_date=posting_and_due_date,
min_outstanding=args.get("outstanding_amt_greater_than"),

View File

@@ -1514,6 +1514,168 @@ class TestPaymentEntry(FrappeTestCase):
self.assertEqual(references[1].payment_term, "Basic Amount Receivable")
self.assertEqual(references[2].payment_term, "Tax Receivable")
def test_reverse_payment_reconciliation(self):
customer = create_customer(frappe.generate_hash(length=10), "INR")
pe = create_payment_entry(
party_type="Customer",
party=customer,
payment_type="Receive",
paid_from="Debtors - _TC",
paid_to="_Test Cash - _TC",
)
pe.submit()
reverse_pe = create_payment_entry(
party_type="Customer",
party=customer,
payment_type="Pay",
paid_from="_Test Cash - _TC",
paid_to="Debtors - _TC",
)
reverse_pe.submit()
pr = frappe.get_doc("Payment Reconciliation")
pr.company = "_Test Company"
pr.party_type = "Customer"
pr.party = customer
pr.receivable_payable_account = "Debtors - _TC"
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 1)
self.assertEqual(reverse_pe.name, pr.invoices[0].invoice_number)
self.assertEqual(pe.name, pr.payments[0].reference_name)
invoices = [x.as_dict() for x in pr.invoices]
payments = [pr.payments[0].as_dict()]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.reconcile()
self.assertEqual(len(pr.invoices), 0)
self.assertEqual(len(pr.payments), 0)
def test_advance_reverse_payment_reconciliation(self):
from erpnext.accounts.doctype.account.test_account import create_account
company = "_Test Company"
customer = create_customer(frappe.generate_hash(length=10), "INR")
advance_account = create_account(
parent_account="Current Assets - _TC",
account_name="Advances Received",
company=company,
account_type="Receivable",
)
frappe.db.set_value(
"Company",
company,
{
"book_advance_payments_in_separate_party_account": 1,
"default_advance_received_account": advance_account,
},
)
# Reverse Payment(essentially an Invoice)
reverse_pe = create_payment_entry(
party_type="Customer",
party=customer,
payment_type="Pay",
paid_from="_Test Cash - _TC",
paid_to=advance_account,
)
reverse_pe.save() # use save() to trigger set_liability_account()
reverse_pe.submit()
# Advance Payment
pe = create_payment_entry(
party_type="Customer",
party=customer,
payment_type="Receive",
paid_from=advance_account,
paid_to="_Test Cash - _TC",
)
pe.save() # use save() to trigger set_liability_account()
pe.submit()
# Partially reconcile advance against invoice
pr = frappe.get_doc("Payment Reconciliation")
pr.company = company
pr.party_type = "Customer"
pr.party = customer
pr.receivable_payable_account = "Debtors - _TC"
pr.default_advance_account = advance_account
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 1)
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.allocation[0].allocated_amount = 400
pr.reconcile()
# assert General and Payment Ledger entries post partial reconciliation
self.expected_gle = [
{"account": "Debtors - _TC", "debit": 0.0, "credit": 400.0},
{"account": advance_account, "debit": 400.0, "credit": 0.0},
{"account": advance_account, "debit": 0.0, "credit": 1000.0},
{"account": "_Test Cash - _TC", "debit": 1000.0, "credit": 0.0},
]
self.expected_ple = [
{
"account": advance_account,
"voucher_no": pe.name,
"against_voucher_no": pe.name,
"amount": -1000.0,
},
{
"account": "Debtors - _TC",
"voucher_no": pe.name,
"against_voucher_no": reverse_pe.name,
"amount": -400.0,
},
{
"account": advance_account,
"voucher_no": pe.name,
"against_voucher_no": pe.name,
"amount": 400.0,
},
]
self.voucher_no = pe.name
self.check_gl_entries()
self.check_pl_entries()
# Unreconcile
unrecon = (
frappe.get_doc(
{
"doctype": "Unreconcile Payment",
"company": company,
"voucher_type": pe.doctype,
"voucher_no": pe.name,
"allocations": [{"reference_doctype": reverse_pe.doctype, "reference_name": reverse_pe.name}],
}
)
.save()
.submit()
)
# assert General and Payment Ledger entries post unreconciliation
self.expected_gle = [
{"account": advance_account, "debit": 0.0, "credit": 1000.0},
{"account": "_Test Cash - _TC", "debit": 1000.0, "credit": 0.0},
]
self.expected_ple = [
{
"account": advance_account,
"voucher_no": pe.name,
"against_voucher_no": pe.name,
"amount": -1000.0,
},
]
self.voucher_no = pe.name
self.check_gl_entries()
self.check_pl_entries()
def create_payment_entry(**args):
payment_entry = frappe.new_doc("Payment Entry")

View File

@@ -340,10 +340,15 @@ class PaymentReconciliation(Document):
self.build_qb_filter_conditions(get_invoices=True)
accounts = [self.receivable_payable_account]
if self.default_advance_account:
accounts.append(self.default_advance_account)
non_reconciled_invoices = get_outstanding_invoices(
self.party_type,
self.party,
self.receivable_payable_account,
accounts,
common_filter=self.common_filter_conditions,
posting_date=self.ple_posting_date_filter,
min_outstanding=self.minimum_invoice_amount if self.minimum_invoice_amount else None,

View File

@@ -1130,6 +1130,17 @@ class TestPaymentReconciliation(FrappeTestCase):
self.assertEqual(pr.allocation[0].allocated_amount, 85)
self.assertEqual(pr.allocation[0].difference_amount, 0)
pr.reconcile()
si.reload()
self.assertEqual(si.outstanding_amount, 0)
# No Exchange Gain/Loss journal should be generated
exc_gain_loss_journals = frappe.db.get_all(
"Journal Entry Account",
filters={"reference_type": si.doctype, "reference_name": si.name, "docstatus": 1},
fields=["parent"],
)
self.assertEqual(exc_gain_loss_journals, [])
def test_reconciliation_purchase_invoice_against_return(self):
self.supplier = "_Test Supplier USD"
pi = self.create_purchase_invoice(qty=5, rate=50, do_not_submit=True)

View File

@@ -28,7 +28,7 @@ frappe.ui.form.on("Payment Request", "refresh", function (frm) {
if (
frm.doc.payment_request_type == "Inward" &&
frm.doc.payment_channel !== "Phone" &&
!in_list(["Initiated", "Paid"], frm.doc.status) &&
!["Initiated", "Paid"].includes(frm.doc.status) &&
!frm.doc.__islocal &&
frm.doc.docstatus == 1
) {

View File

@@ -89,10 +89,11 @@
<table class="table table-bordered">
<thead>
<tr>
<th style="width: 25%">30 Days</th>
<th style="width: 25%">60 Days</th>
<th style="width: 25%">90 Days</th>
<th style="width: 25%">120 Days</th>
<th style="width: 20%">0 - 30 Days</th>
<th style="width: 20%">30 - 60 Days</th>
<th style="width: 20%">60 - 90 Days</th>
<th style="width: 20%">90 - 120 Days</th>
<th style="width: 20%">Above 120 Days</th>
</tr>
</thead>
<tbody>
@@ -101,6 +102,7 @@
<td>{{ frappe.utils.fmt_money(ageing.range2, currency=filters.presentation_currency) }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range3, currency=filters.presentation_currency) }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range4, currency=filters.presentation_currency) }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range5, currency=filters.presentation_currency) }}</td>
</tr>
</tbody>
</table>

View File

@@ -3,6 +3,8 @@
frappe.provide("erpnext.accounts");
cur_frm.cscript.tax_table = "Purchase Taxes and Charges";
erpnext.accounts.payment_triggers.setup("Purchase Invoice");
erpnext.accounts.taxes.setup_tax_filters("Purchase Taxes and Charges");
erpnext.accounts.taxes.setup_tax_validations("Purchase Invoice");

View File

@@ -745,6 +745,7 @@
"fieldtype": "Currency",
"label": "Landed Cost Voucher Amount",
"no_copy": 1,
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
@@ -938,7 +939,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2024-02-04 14:11:52.742228",
"modified": "2024-03-19 19:09:47.210965",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",

View File

@@ -1,6 +1,7 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
cur_frm.cscript.tax_table = "Purchase Taxes and Charges";
erpnext.accounts.taxes.setup_tax_validations("Purchase Taxes and Charges Template");
erpnext.accounts.taxes.setup_tax_filters("Purchase Taxes and Charges");

View File

@@ -3,6 +3,8 @@
frappe.provide("erpnext.accounts");
cur_frm.cscript.tax_table = "Sales Taxes and Charges";
erpnext.accounts.taxes.setup_tax_validations("Sales Invoice");
erpnext.accounts.payment_triggers.setup("Sales Invoice");
erpnext.accounts.pos.setup("Sales Invoice");

View File

@@ -2170,7 +2170,8 @@
"description": "Credit Note will update it's own outstanding amount, even if \"Return Against\" is specified.",
"fieldname": "update_outstanding_for_self",
"fieldtype": "Check",
"label": "Update Outstanding for Self"
"label": "Update Outstanding for Self",
"no_copy": 1
}
],
"icon": "fa fa-file-text",
@@ -2183,7 +2184,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2024-03-11 14:20:34.874192",
"modified": "2024-03-15 16:44:17.778370",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -1571,6 +1571,12 @@ class TestSalesInvoice(FrappeTestCase):
self.assertEqual(frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount"), -1000)
self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 2500)
def test_zero_qty_return_invoice_with_stock_effect(self):
cr_note = create_sales_invoice(qty=-1, rate=300, is_return=1, do_not_submit=True)
cr_note.update_stock = True
cr_note.items[0].qty = 0
self.assertRaises(frappe.ValidationError, cr_note.save)
def test_return_invoice_with_account_mismatch(self):
debtors2 = create_account(
parent_account="Accounts Receivable - _TC",
@@ -3932,7 +3938,6 @@ def create_internal_supplier(supplier_name, represents_company, allowed_to_inter
)
supplier.append("companies", {"company": allowed_to_interact_with})
supplier.insert()
supplier_name = supplier.name
else:

View File

@@ -1,5 +1,6 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
cur_frm.cscript.tax_table = "Sales Taxes and Charges";
erpnext.accounts.taxes.setup_tax_validations("Sales Taxes and Charges Template");
erpnext.accounts.taxes.setup_tax_filters("Sales Taxes and Charges");

View File

@@ -1015,7 +1015,7 @@ def get_outstanding_invoices(
if account:
root_type, account_type = frappe.get_cached_value(
"Account", account, ["root_type", "account_type"]
"Account", account[0], ["root_type", "account_type"]
)
party_account_type = "Receivable" if root_type == "Asset" else "Payable"
party_account_type = account_type or party_account_type
@@ -1026,7 +1026,7 @@ def get_outstanding_invoices(
common_filter = common_filter or []
common_filter.append(ple.account_type == party_account_type)
common_filter.append(ple.account == account)
common_filter.append(ple.account.isin(account))
common_filter.append(ple.party_type == party_type)
common_filter.append(ple.party == party)

View File

@@ -79,7 +79,7 @@ frappe.ui.form.on("Asset", {
frm.toggle_display("next_depreciation_date", frm.doc.docstatus < 1);
if (frm.doc.docstatus == 1) {
if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) {
if (["Submitted", "Partially Depreciated", "Fully Depreciated"].includes(frm.doc.status)) {
frm.add_custom_button(
__("Transfer Asset"),
function () {
@@ -365,7 +365,7 @@ frappe.ui.form.on("Asset", {
if (v.journal_entry) {
asset_values.push(asset_value);
} else {
if (in_list(["Scrapped", "Sold"], frm.doc.status)) {
if (["Scrapped", "Sold"].includes(frm.doc.status)) {
asset_values.push(null);
} else {
asset_values.push(asset_value);
@@ -400,7 +400,7 @@ frappe.ui.form.on("Asset", {
});
}
if (in_list(["Scrapped", "Sold"], frm.doc.status)) {
if (["Scrapped", "Sold"].includes(frm.doc.status)) {
x_intervals.push(frappe.format(frm.doc.disposal_date, { fieldtype: "Date" }));
asset_values.push(0);
}

View File

@@ -4,6 +4,8 @@
frappe.provide("erpnext.buying");
frappe.provide("erpnext.accounts.dimensions");
cur_frm.cscript.tax_table = "Purchase Taxes and Charges";
erpnext.accounts.taxes.setup_tax_filters("Purchase Taxes and Charges");
erpnext.accounts.taxes.setup_tax_validations("Purchase Order");
erpnext.buying.setup_buying_controller();
@@ -289,7 +291,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
this.frm.fields_dict.items_section.wrapper.removeClass("hide-border");
}
if (!in_list(["Closed", "Delivered"], doc.status)) {
if (!["Closed", "Delivered"].includes(doc.status)) {
if (
this.frm.doc.status !== "Closed" &&
flt(this.frm.doc.per_received, 2) < 100 &&
@@ -334,7 +336,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
this.frm.page.set_inner_btn_group_as_primary(__("Status"));
}
} else if (in_list(["Closed", "Delivered"], doc.status)) {
} else if (["Closed", "Delivered"].includes(doc.status)) {
if (this.frm.has_perm("submit")) {
this.frm.add_custom_button(
__("Re-open"),
@@ -507,7 +509,6 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
target: me.frm,
setters: {
schedule_date: undefined,
status: undefined,
},
get_query_filters: {
material_request_type: "Purchase",

View File

@@ -485,7 +485,7 @@
"link_fieldname": "party"
}
],
"modified": "2023-10-19 16:55:15.148325",
"modified": "2024-03-13 11:14:06.516519",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",
@@ -544,7 +544,7 @@
}
],
"quick_entry": 1,
"search_fields": "supplier_name, supplier_group",
"search_fields": "supplier_group",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "ASC",

View File

@@ -154,44 +154,6 @@ class TestSupplier(FrappeTestCase):
# Rollback
address.delete()
def test_serach_fields_for_supplier(self):
from erpnext.controllers.queries import supplier_query
frappe.db.set_single_value("Buying Settings", "supp_master_name", "Naming Series")
supplier_name = create_supplier(supplier_name="Test Supplier 1").name
make_property_setter(
"Supplier", None, "search_fields", "supplier_group", "Data", for_doctype="Doctype"
)
data = supplier_query(
"Supplier", supplier_name, "name", 0, 20, filters={"name": supplier_name}, as_dict=True
)
self.assertEqual(data[0].name, supplier_name)
self.assertEqual(data[0].supplier_group, "Services")
self.assertTrue("supplier_type" not in data[0])
make_property_setter(
"Supplier",
None,
"search_fields",
"supplier_group, supplier_type",
"Data",
for_doctype="Doctype",
)
data = supplier_query(
"Supplier", supplier_name, "name", 0, 20, filters={"name": supplier_name}, as_dict=True
)
self.assertEqual(data[0].name, supplier_name)
self.assertEqual(data[0].supplier_group, "Services")
self.assertEqual(data[0].supplier_type, "Company")
self.assertTrue("supplier_type" in data[0])
frappe.db.set_single_value("Buying Settings", "supp_master_name", "Supplier Name")
def create_supplier(**args):
args = frappe._dict(args)

View File

@@ -77,7 +77,10 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
fieldname: "group_by",
label: __("Group by"),
fieldtype: "Select",
options: [__("Group by Supplier"), __("Group by Item")],
options: [
{ label: __("Group by Supplier"), value: "Group by Supplier" },
{ label: __("Group by Item"), value: "Group by Item" },
],
default: __("Group by Supplier"),
},
{

View File

@@ -89,6 +89,7 @@ force_item_fields = (
"weight_per_unit",
"weight_uom",
"total_weight",
"valuation_rate",
)
@@ -168,6 +169,13 @@ class AccountsController(TransactionBase):
if not self.get("is_return") and not self.get("is_debit_note"):
self.validate_qty_is_not_zero()
if (
self.doctype in ["Sales Invoice", "Purchase Invoice"]
and self.get("is_return")
and self.get("update_stock")
):
self.validate_zero_qty_for_return_invoices_with_stock()
if self.get("_action") and self._action != "update_after_submit":
self.set_missing_values(for_validate=True)
@@ -602,23 +610,31 @@ class AccountsController(TransactionBase):
)
def validate_due_date(self):
if self.get("is_pos"):
if self.get("is_pos") or self.doctype not in ["Sales Invoice", "Purchase Invoice"]:
return
from erpnext.accounts.party import validate_due_date
if self.doctype == "Sales Invoice":
posting_date = (
self.posting_date if self.doctype == "Sales Invoice" else (self.bill_date or self.posting_date)
)
# skip due date validation for records via Data Import
if frappe.flags.in_import and getdate(self.due_date) < getdate(posting_date):
self.due_date = posting_date
elif self.doctype == "Sales Invoice":
if not self.due_date:
frappe.throw(_("Due Date is mandatory"))
validate_due_date(
self.posting_date,
posting_date,
self.due_date,
self.payment_terms_template,
)
elif self.doctype == "Purchase Invoice":
validate_due_date(
self.bill_date or self.posting_date,
posting_date,
self.due_date,
self.bill_date,
self.payment_terms_template,
@@ -1044,6 +1060,18 @@ class AccountsController(TransactionBase):
else:
return flt(args.get(field, 0) / self.get("conversion_rate", 1))
def validate_zero_qty_for_return_invoices_with_stock(self):
rows = []
for item in self.items:
if not flt(item.qty):
rows.append(item)
if rows:
frappe.throw(
_(
"For Return Invoices with Stock effect, '0' qty Items are not allowed. Following rows are affected: {0}"
).format(frappe.bold(comma_and(["#" + str(x.idx) for x in rows])))
)
def validate_qty_is_not_zero(self):
if self.doctype == "Purchase Receipt":
return
@@ -2668,14 +2696,20 @@ def get_advance_journal_entries(
else:
q = q.where(journal_acc.debit_in_account_currency > 0)
reference_or_condition = []
if include_unallocated:
q = q.where((journal_acc.reference_name.isnull()) | (journal_acc.reference_name == ""))
reference_or_condition.append(journal_acc.reference_name.isnull())
reference_or_condition.append(journal_acc.reference_name == "")
if order_list:
q = q.where(
reference_or_condition.append(
(journal_acc.reference_type == order_doctype) & ((journal_acc.reference_name).isin(order_list))
)
if reference_or_condition:
q = q.where(Criterion.any(reference_or_condition))
q = q.orderby(journal_entry.posting_date)
journal_entries = q.run(as_dict=True)

View File

@@ -513,6 +513,14 @@ class BuyingController(SubcontractingController):
(not cint(self.is_return) and self.docstatus == 1)
or (cint(self.is_return) and self.docstatus == 2)
):
serial_and_batch_bundle = d.get("serial_and_batch_bundle")
if self.is_internal_transfer() and self.is_return and self.docstatus == 2:
serial_and_batch_bundle = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_detail_no": d.name, "warehouse": d.from_warehouse},
"serial_and_batch_bundle",
)
from_warehouse_sle = self.get_sl_entries(
d,
{
@@ -521,19 +529,24 @@ class BuyingController(SubcontractingController):
"outgoing_rate": d.rate,
"recalculate_rate": 1,
"dependant_sle_voucher_detail_no": d.name,
"serial_and_batch_bundle": serial_and_batch_bundle,
},
)
sl_entries.append(from_warehouse_sle)
type_of_transaction = "Inward"
if self.docstatus == 2:
type_of_transaction = "Outward"
sle = self.get_sl_entries(
d,
{
"actual_qty": flt(pr_qty),
"serial_and_batch_bundle": (
d.serial_and_batch_bundle
if not self.is_internal_transfer()
else self.get_package_for_target_warehouse(d)
if not self.is_internal_transfer() or self.is_return
else self.get_package_for_target_warehouse(d, type_of_transaction=type_of_transaction)
),
},
)
@@ -570,7 +583,17 @@ class BuyingController(SubcontractingController):
or (cint(self.is_return) and self.docstatus == 1)
):
from_warehouse_sle = self.get_sl_entries(
d, {"actual_qty": -1 * pr_qty, "warehouse": d.from_warehouse, "recalculate_rate": 1}
d,
{
"actual_qty": -1 * pr_qty,
"warehouse": d.from_warehouse,
"recalculate_rate": 1,
"serial_and_batch_bundle": (
self.get_package_for_target_warehouse(d, d.from_warehouse, "Inward")
if self.is_internal_transfer() and self.is_return
else None
),
},
)
sl_entries.append(from_warehouse_sle)
@@ -597,13 +620,15 @@ class BuyingController(SubcontractingController):
via_landed_cost_voucher=via_landed_cost_voucher,
)
def get_package_for_target_warehouse(self, item) -> str:
def get_package_for_target_warehouse(self, item, warehouse=None, type_of_transaction=None) -> str:
if not item.serial_and_batch_bundle:
return ""
if not warehouse:
warehouse = item.warehouse
return self.make_package_for_transfer(
item.serial_and_batch_bundle,
item.warehouse,
item.serial_and_batch_bundle, warehouse, type_of_transaction=type_of_transaction
)
def update_ordered_and_reserved_qty(self):

View File

@@ -85,79 +85,6 @@ def lead_query(doctype, txt, searchfield, start, page_len, filters):
{"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len},
)
# searches for customer
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def customer_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False):
doctype = "Customer"
conditions = []
cust_master_name = frappe.defaults.get_user_default("cust_master_name")
fields = ["name"]
if cust_master_name != "Customer Name":
fields.append("customer_name")
fields = get_fields(doctype, fields)
searchfields = frappe.get_meta(doctype).get_search_fields()
searchfields = " or ".join(field + " like %(txt)s" for field in searchfields)
return frappe.db.sql(
"""select {fields} from `tabCustomer`
where docstatus < 2
and ({scond}) and disabled=0
{fcond} {mcond}
order by
(case when locate(%(_txt)s, name) > 0 then locate(%(_txt)s, name) else 99999 end),
(case when locate(%(_txt)s, customer_name) > 0 then locate(%(_txt)s, customer_name) else 99999 end),
idx desc,
name, customer_name
limit %(page_len)s offset %(start)s""".format(
**{
"fields": ", ".join(fields),
"scond": searchfields,
"mcond": get_match_cond(doctype),
"fcond": get_filters_cond(doctype, filters, conditions).replace("%", "%%"),
}
),
{"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len},
as_dict=as_dict,
)
# searches for supplier
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def supplier_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False):
doctype = "Supplier"
supp_master_name = frappe.defaults.get_user_default("supp_master_name")
fields = ["name"]
if supp_master_name != "Supplier Name":
fields.append("supplier_name")
fields = get_fields(doctype, fields)
return frappe.db.sql(
"""select {field} from `tabSupplier`
where docstatus < 2
and ({key} like %(txt)s
or supplier_name like %(txt)s) and disabled=0
and (on_hold = 0 or (on_hold = 1 and CURRENT_DATE > release_date))
{mcond}
order by
(case when locate(%(_txt)s, name) > 0 then locate(%(_txt)s, name) else 99999 end),
(case when locate(%(_txt)s, supplier_name) > 0 then locate(%(_txt)s, supplier_name) else 99999 end),
idx desc,
name, supplier_name
limit %(page_len)s offset %(start)s""".format(
**{"field": ", ".join(fields), "key": searchfield, "mcond": get_match_cond(doctype)}
),
{"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len},
as_dict=as_dict,
)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs

View File

@@ -423,6 +423,15 @@ def make_return_doc(
]:
type_of_transaction = "Outward"
warehouse = source_doc.warehouse if qty_field == "stock_qty" else source_doc.rejected_warehouse
if source_parent.doctype in [
"Sales Invoice",
"POS Invoice",
"Delivery Note",
] and source_parent.get("is_internal_customer"):
type_of_transaction = "Outward"
warehouse = source_doc.target_warehouse
cls_obj = SerialBatchCreation(
{
"type_of_transaction": type_of_transaction,
@@ -432,7 +441,7 @@ def make_return_doc(
"returned_serial_nos": returned_serial_nos,
"voucher_type": source_parent.doctype,
"do_not_submit": True,
"warehouse": source_doc.warehouse,
"warehouse": warehouse,
"has_serial_no": item_details.has_serial_no,
"has_batch_no": item_details.has_batch_no,
}
@@ -575,11 +584,14 @@ def make_return_doc(
if not item_details.has_batch_no and not item_details.has_serial_no:
return
for qty_field in ["stock_qty", "rejected_qty"]:
if target_doc.get(qty_field) and not target_doc.get("use_serial_batch_fields"):
if not target_doc.get("use_serial_batch_fields"):
for qty_field in ["stock_qty", "rejected_qty"]:
if not target_doc.get(qty_field):
continue
update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field)
elif target_doc.get(qty_field) and target_doc.get("use_serial_batch_fields"):
update_non_bundled_serial_nos(source_doc, target_doc, source_parent)
elif target_doc.get("use_serial_batch_fields"):
update_non_bundled_serial_nos(source_doc, target_doc, source_parent)
def update_non_bundled_serial_nos(source_doc, target_doc, source_parent):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos

View File

@@ -442,8 +442,10 @@ class SellingController(StockController):
# Get incoming rate based on original item cost based on valuation method
qty = flt(d.get("stock_qty") or d.get("actual_qty"))
if not d.incoming_rate or (
get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return")
if (
not d.incoming_rate
or self.is_internal_transfer()
or (get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return"))
):
d.incoming_rate = get_incoming_rate(
{
@@ -458,6 +460,8 @@ class SellingController(StockController):
"voucher_no": self.name,
"voucher_detail_no": d.name,
"allow_zero_valuation": d.get("allow_zero_valuation"),
"batch_no": d.batch_no,
"serial_no": d.serial_no,
},
raise_error_if_no_rate=False,
)
@@ -530,13 +534,26 @@ class SellingController(StockController):
self.make_sl_entries(sl_entries)
def get_sle_for_source_warehouse(self, item_row):
serial_and_batch_bundle = item_row.serial_and_batch_bundle
if serial_and_batch_bundle and self.is_internal_transfer() and self.is_return:
if self.docstatus == 1:
serial_and_batch_bundle = self.make_package_for_transfer(
serial_and_batch_bundle, item_row.warehouse, type_of_transaction="Inward"
)
else:
serial_and_batch_bundle = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_detail_no": item_row.name, "warehouse": item_row.warehouse},
"serial_and_batch_bundle",
)
sle = self.get_sl_entries(
item_row,
{
"actual_qty": -1 * flt(item_row.qty),
"incoming_rate": item_row.incoming_rate,
"recalculate_rate": cint(self.is_return),
"serial_and_batch_bundle": item_row.serial_and_batch_bundle,
"serial_and_batch_bundle": serial_and_batch_bundle,
},
)
if item_row.target_warehouse and not cint(self.is_return):
@@ -557,9 +574,15 @@ class SellingController(StockController):
if item_row.warehouse:
sle.dependant_sle_voucher_detail_no = item_row.name
if item_row.serial_and_batch_bundle:
if item_row.serial_and_batch_bundle and not cint(self.is_return):
type_of_transaction = "Inward"
if cint(self.is_return):
type_of_transaction = "Outward"
sle["serial_and_batch_bundle"] = self.make_package_for_transfer(
item_row.serial_and_batch_bundle, item_row.target_warehouse
item_row.serial_and_batch_bundle,
item_row.target_warehouse,
type_of_transaction=type_of_transaction,
)
return sle

View File

@@ -236,6 +236,14 @@ class StockController(AccountsController):
qty = row.get("rejected_qty")
warehouse = row.get("rejected_warehouse")
if (
self.is_internal_transfer()
and self.doctype in ["Sales Invoice", "Delivery Note"]
and self.is_return
):
warehouse = row.get("target_warehouse") or row.get("warehouse")
type_of_transaction = "Outward"
bundle_details.update(
{
"qty": qty,
@@ -579,7 +587,7 @@ class StockController(AccountsController):
bundle_doc.warehouse = warehouse
bundle_doc.type_of_transaction = type_of_transaction
bundle_doc.voucher_type = self.doctype
bundle_doc.voucher_no = self.name
bundle_doc.voucher_no = "" if self.is_new() or self.docstatus == 2 else self.name
bundle_doc.is_cancelled = 0
for row in bundle_doc.entries:
@@ -595,6 +603,7 @@ class StockController(AccountsController):
bundle_doc.calculate_qty_and_amount()
bundle_doc.flags.ignore_permissions = True
bundle_doc.flags.ignore_validate = True
bundle_doc.save(ignore_permissions=True)
return bundle_doc.name

View File

@@ -31,18 +31,6 @@ class TestQueries(unittest.TestCase):
self.assertGreaterEqual(len(query(txt="_Test Lead")), 4)
self.assertEqual(len(query(txt="_Test Lead 4")), 1)
def test_customer_query(self):
query = add_default_params(queries.customer_query, "Customer")
self.assertGreaterEqual(len(query(txt="_Test Customer")), 7)
self.assertGreaterEqual(len(query(txt="_Test Customer USD")), 1)
def test_supplier_query(self):
query = add_default_params(queries.supplier_query, "Supplier")
self.assertGreaterEqual(len(query(txt="_Test Supplier")), 7)
self.assertGreaterEqual(len(query(txt="_Test Supplier USD")), 1)
def test_item_query(self):
query = add_default_params(queries.item_query, "Item")

View File

@@ -17,10 +17,6 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
}
onload() {
this.frm.set_query("customer", function (doc, cdt, cdn) {
return { query: "erpnext.controllers.queries.customer_query" };
});
this.frm.set_query("lead_owner", function (doc, cdt, cdn) {
return { query: "frappe.core.doctype.user.user.user_query" };
});

View File

@@ -288,9 +288,6 @@ has_website_permission = {
before_tests = "erpnext.setup.utils.before_tests"
standard_queries = {
"Customer": "erpnext.controllers.queries.customer_query",
}
period_closing_doctypes = [
"Sales Invoice",

View File

@@ -400,7 +400,7 @@ frappe.ui.form.on("BOM", {
},
rm_cost_as_per(frm) {
if (in_list(["Valuation Rate", "Last Purchase Rate"], frm.doc.rm_cost_as_per)) {
if (["Valuation Rate", "Last Purchase Rate"].includes(frm.doc.rm_cost_as_per)) {
frm.set_value("plc_conversion_rate", 1.0);
}
},

View File

@@ -129,7 +129,7 @@ frappe.ui.form.on("Production Plan", {
if (
frm.doc.mr_items &&
frm.doc.mr_items.length &&
!in_list(["Material Requested", "Closed"], frm.doc.status)
!["Material Requested", "Closed"].includes(frm.doc.status)
) {
frm.add_custom_button(
__("Material Request"),

View File

@@ -9,6 +9,8 @@ frappe.ui.form.on("Work Order", {
"Job Card": "Create Job Card",
};
frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"];
// Set query for warehouses
frm.set_query("wip_warehouse", function () {
return {
@@ -194,7 +196,7 @@ frappe.ui.form.on("Work Order", {
},
add_custom_button_to_return_components: function (frm) {
if (frm.doc.docstatus === 1 && in_list(["Closed", "Completed"], frm.doc.status)) {
if (frm.doc.docstatus === 1 && ["Closed", "Completed"].includes(frm.doc.status)) {
let non_consumed_items = frm.doc.required_items.filter((d) => {
return flt(d.consumed_qty) < flt(d.transferred_qty - d.returned_qty);
});
@@ -594,7 +596,7 @@ erpnext.work_order = {
);
}
if (doc.docstatus === 1 && !in_list(["Closed", "Completed"], doc.status)) {
if (doc.docstatus === 1 && !["Closed", "Completed"].includes(doc.status)) {
if (doc.status != "Stopped" && doc.status != "Completed") {
frm.add_custom_button(
__("Stop"),

View File

@@ -27,8 +27,6 @@ frappe.ui.form.on("Project", {
};
};
frm.set_query("customer", "erpnext.controllers.queries.customer_query");
frm.set_query("user", "users", function () {
return {
query: "erpnext.projects.doctype.project.project.get_users_for_project",

View File

@@ -20,7 +20,7 @@ frappe.ui.form.on("Communication", {
);
}
if (!in_list(["Lead", "Opportunity"], frm.doc.reference_doctype)) {
if (!["Lead", "Opportunity"].includes(frm.doc.reference_doctype)) {
frm.add_custom_button(
__("Lead"),
() => {

View File

@@ -11,7 +11,7 @@ erpnext.accounts.taxes = {
setup: function(frm) {
// set conditional display for rate column in taxes
$(frm.wrapper).on('grid-row-render', function(e, grid_row) {
if(in_list(['Sales Taxes and Charges', 'Purchase Taxes and Charges'], grid_row.doc.doctype)) {
if(['Sales Taxes and Charges', 'Purchase Taxes and Charges'].includes(grid_row.doc.doctype)) {
me.set_conditional_mandatory_rate_or_amount(grid_row);
}
});

View File

@@ -74,11 +74,6 @@ erpnext.buying = {
me.frm.set_query('billing_address', erpnext.queries.company_address_query);
erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype);
if(this.frm.fields_dict.supplier) {
this.frm.set_query("supplier", function() {
return{ query: "erpnext.controllers.queries.supplier_query" }});
}
this.frm.set_query("item_code", "items", function() {
if (me.frm.doc.is_subcontracted) {
var filters = {'supplier': me.frm.doc.supplier};
@@ -134,7 +129,7 @@ erpnext.buying = {
}
toggle_subcontracting_fields() {
if (in_list(['Purchase Receipt', 'Purchase Invoice'], this.frm.doc.doctype)) {
if (['Purchase Receipt', 'Purchase Invoice'].includes(this.frm.doc.doctype)) {
this.frm.fields_dict.supplied_items.grid.update_docfield_property('consumed_qty',
'read_only', this.frm.doc.__onload && this.frm.doc.__onload.backflush_based_on === 'BOM');

View File

@@ -9,7 +9,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
apply_pricing_rule_on_item(item) {
let effective_item_rate = item.price_list_rate;
let item_rate = item.rate;
if (in_list(["Sales Order", "Quotation"], item.parenttype) && item.blanket_order_rate) {
if (["Sales Order", "Quotation"].includes(item.parenttype) && item.blanket_order_rate) {
effective_item_rate = item.blanket_order_rate;
}
if (item.margin_type == "Percentage") {
@@ -26,7 +26,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
item.discount_amount = flt(item.rate_with_margin) * flt(item.discount_percentage) / 100;
}
if (item.discount_amount) {
if (item.discount_amount > 0) {
item_rate = flt((item.rate_with_margin) - (item.discount_amount), precision('rate', item));
item.discount_percentage = 100 * flt(item.discount_amount) / flt(item.rate_with_margin);
}
@@ -52,7 +52,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
// Advance calculation applicable to Sales/Purchase Invoice
if (
in_list(["Sales Invoice", "POS Invoice", "Purchase Invoice"], this.frm.doc.doctype)
["Sales Invoice", "POS Invoice", "Purchase Invoice"].includes(this.frm.doc.doctype)
&& this.frm.doc.docstatus < 2
&& !this.frm.doc.is_return
) {
@@ -60,7 +60,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
}
if (
in_list(["Sales Invoice", "POS Invoice"], this.frm.doc.doctype)
["Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype)
&& this.frm.doc.is_pos
&& this.frm.doc.is_return
) {
@@ -69,7 +69,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
}
// Sales person's commission
if (in_list(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"], this.frm.doc.doctype)) {
if (["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"].includes(this.frm.doc.doctype)) {
this.calculate_commission();
this.calculate_contribution();
}
@@ -562,7 +562,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
? this.frm.doc["taxes"][tax_count - 1].total + flt(this.frm.doc.rounding_adjustment)
: this.frm.doc.net_total);
if(in_list(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"], this.frm.doc.doctype)) {
if(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype)) {
this.frm.doc.base_grand_total = (this.frm.doc.total_taxes_and_charges) ?
flt(this.frm.doc.grand_total * this.frm.doc.conversion_rate) : this.frm.doc.base_net_total;
} else {
@@ -570,7 +570,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
this.frm.doc.taxes_and_charges_added = this.frm.doc.taxes_and_charges_deducted = 0.0;
if(tax_count) {
$.each(this.frm.doc["taxes"] || [], function(i, tax) {
if (in_list(["Valuation and Total", "Total"], tax.category)) {
if (["Valuation and Total", "Total"].includes(tax.category)) {
if(tax.add_deduct_tax == "Add") {
me.frm.doc.taxes_and_charges_added += flt(tax.tax_amount_after_discount_amount);
} else {
@@ -717,7 +717,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
var actual_taxes_dict = {};
$.each(this.frm.doc["taxes"] || [], function(i, tax) {
if (in_list(["Actual", "On Item Quantity"], tax.charge_type)) {
if (["Actual", "On Item Quantity"].includes(tax.charge_type)) {
var tax_amount = (tax.category == "Valuation") ? 0.0 : tax.tax_amount;
tax_amount *= (tax.add_deduct_tax == "Deduct") ? -1.0 : 1.0;
actual_taxes_dict[tax.idx] = tax_amount;
@@ -762,7 +762,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
// NOTE:
// paid_amount and write_off_amount is only for POS/Loyalty Point Redemption Invoice
// total_advance is only for non POS Invoice
if(in_list(["Sales Invoice", "POS Invoice"], this.frm.doc.doctype) && this.frm.doc.is_return){
if(["Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype) && this.frm.doc.is_return){
this.calculate_paid_amount();
}
@@ -770,7 +770,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
frappe.model.round_floats_in(this.frm.doc, ["grand_total", "total_advance", "write_off_amount"]);
if(in_list(["Sales Invoice", "POS Invoice", "Purchase Invoice"], this.frm.doc.doctype)) {
if(["Sales Invoice", "POS Invoice", "Purchase Invoice"].includes(this.frm.doc.doctype)) {
let grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total;
let base_grand_total = this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total;
@@ -793,7 +793,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
this.frm.refresh_field("base_paid_amount");
}
if(in_list(["Sales Invoice", "POS Invoice"], this.frm.doc.doctype)) {
if(["Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype)) {
let total_amount_for_payment = (this.frm.doc.redeem_loyalty_points && this.frm.doc.loyalty_amount)
? flt(total_amount_to_pay - this.frm.doc.loyalty_amount, precision("base_grand_total"))
: total_amount_to_pay;
@@ -897,7 +897,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
calculate_change_amount(){
this.frm.doc.change_amount = 0.0;
this.frm.doc.base_change_amount = 0.0;
if(in_list(["Sales Invoice", "POS Invoice"], this.frm.doc.doctype)
if(["Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype)
&& this.frm.doc.paid_amount > this.frm.doc.grand_total && !this.frm.doc.is_return) {
var payment_types = $.map(this.frm.doc.payments, function(d) { return d.type; });

View File

@@ -315,7 +315,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
setup_quality_inspection() {
if(!in_list(["Delivery Note", "Sales Invoice", "Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"], this.frm.doc.doctype)) {
if(!["Delivery Note", "Sales Invoice", "Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"].includes(this.frm.doc.doctype)) {
return;
}
@@ -327,7 +327,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
this.frm.page.set_inner_btn_group_as_primary(__('Create'));
}
const inspection_type = in_list(["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"], this.frm.doc.doctype)
const inspection_type = ["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"].includes(this.frm.doc.doctype)
? "Incoming" : "Outgoing";
let quality_inspection_field = this.frm.get_docfield("items", "quality_inspection");
@@ -359,7 +359,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
make_payment_request() {
let me = this;
const payment_request_type = (in_list(['Sales Order', 'Sales Invoice'], this.frm.doc.doctype))
const payment_request_type = (['Sales Order', 'Sales Invoice'].includes(this.frm.doc.doctype))
? "Inward" : "Outward";
frappe.call({
@@ -474,7 +474,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
setup_sms() {
var me = this;
let blacklist = ['Purchase Invoice', 'BOM'];
if(this.frm.doc.docstatus===1 && !in_list(["Lost", "Stopped", "Closed"], this.frm.doc.status)
if(this.frm.doc.docstatus===1 && !["Lost", "Stopped", "Closed"].includes(this.frm.doc.status)
&& !blacklist.includes(this.frm.doctype)) {
this.frm.page.add_menu_item(__('Send SMS'), function() { me.send_sms(); });
}
@@ -760,7 +760,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
on_submit() {
if (in_list(["Purchase Invoice", "Sales Invoice"], this.frm.doc.doctype)
if (["Purchase Invoice", "Sales Invoice"].includes(this.frm.doc.doctype)
&& !this.frm.doc.update_stock) {
return;
}
@@ -864,7 +864,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
var set_party_account = function(set_pricing) {
if (in_list(["Sales Invoice", "Purchase Invoice"], me.frm.doc.doctype)) {
if (["Sales Invoice", "Purchase Invoice"].includes(me.frm.doc.doctype)) {
if(me.frm.doc.doctype=="Sales Invoice") {
var party_type = "Customer";
var party_account_field = 'debit_to';
@@ -899,7 +899,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") &&
in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)) {
['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'].includes(this.frm.doctype)) {
erpnext.utils.get_shipping_address(this.frm, function() {
set_party_account(set_pricing);
});
@@ -1610,7 +1610,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
"doctype": me.frm.doc.doctype,
"name": me.frm.doc.name,
"is_return": cint(me.frm.doc.is_return),
"update_stock": in_list(['Sales Invoice', 'Purchase Invoice'], me.frm.doc.doctype) ? cint(me.frm.doc.update_stock) : 0,
"update_stock": ['Sales Invoice', 'Purchase Invoice'].includes(me.frm.doc.doctype) ? cint(me.frm.doc.update_stock) : 0,
"conversion_factor": me.frm.doc.conversion_factor,
"pos_profile": me.frm.doc.doctype == 'Sales Invoice' ? me.frm.doc.pos_profile : '',
"coupon_code": me.frm.doc.coupon_code
@@ -2256,7 +2256,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
get_method_for_payment() {
var method = "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry";
if(cur_frm.doc.__onload && cur_frm.doc.__onload.make_payment_via_journal_entry){
if(in_list(['Sales Invoice', 'Purchase Invoice'], cur_frm.doc.doctype)){
if(['Sales Invoice', 'Purchase Invoice'].includes( cur_frm.doc.doctype)){
method = "erpnext.accounts.doctype.journal_entry.journal_entry.get_payment_entry_against_invoice";
}else {
method= "erpnext.accounts.doctype.journal_entry.journal_entry.get_payment_entry_against_order";
@@ -2496,7 +2496,7 @@ erpnext.show_serial_batch_selector = function (frm, item_row, callback, on_close
}
frappe.require("assets/erpnext/js/utils/serial_no_batch_selector.js", function() {
if (in_list(["Sales Invoice", "Delivery Note"], frm.doc.doctype)) {
if (["Sales Invoice", "Delivery Note"].includes(frm.doc.doctype)) {
item_row.type_of_transaction = frm.doc.is_return ? "Inward" : "Outward";
} else {
item_row.type_of_transaction = frm.doc.is_return ? "Outward" : "Inward";

View File

@@ -218,7 +218,7 @@ erpnext.payments = class payments extends erpnext.stock.StockController {
update_paid_amount(update_write_off) {
var me = this;
if (in_list(["change_amount", "write_off_amount"], this.idx)) {
if (["change_amount", "write_off_amount"].includes(this.idx)) {
var value = me.selected_mode.val();
if (me.idx == "change_amount") {
me.change_amount(value);

View File

@@ -12,14 +12,6 @@ $.extend(erpnext.queries, {
return { query: "erpnext.controllers.queries.lead_query" };
},
customer: function () {
return { query: "erpnext.controllers.queries.customer_query" };
},
supplier: function () {
return { query: "erpnext.controllers.queries.supplier_query" };
},
item: function (filters) {
var args = { query: "erpnext.controllers.queries.item_query" };
if (filters) args["filters"] = filters;

View File

@@ -28,11 +28,11 @@ erpnext.SMSManager = function SMSManager(doc) {
"Purchase Receipt": "Items has been received against purchase receipt: " + doc.name,
};
if (in_list(["Sales Order", "Delivery Note", "Sales Invoice"], doc.doctype))
if (["Sales Order", "Delivery Note", "Sales Invoice"].includes(doc.doctype))
this.show(doc.contact_person, "Customer", doc.customer, "", default_msg[doc.doctype]);
else if (doc.doctype === "Quotation")
this.show(doc.contact_person, "Customer", doc.party_name, "", default_msg[doc.doctype]);
else if (in_list(["Purchase Order", "Purchase Receipt"], doc.doctype))
else if (["Purchase Order", "Purchase Receipt"].includes(doc.doctype))
this.show(doc.contact_person, "Supplier", doc.supplier, "", default_msg[doc.doctype]);
else if (doc.doctype == "Lead") this.show("", "", "", doc.mobile_no, default_msg[doc.doctype]);
else if (doc.doctype == "Opportunity")

View File

@@ -14,10 +14,10 @@ erpnext.utils.get_party_details = function (frm, method, args, callback) {
if (!args) {
if (
(frm.doctype != "Purchase Order" && frm.doc.customer) ||
(frm.doc.party_name && in_list(["Quotation", "Opportunity"], frm.doc.doctype))
(frm.doc.party_name && ["Quotation", "Opportunity"].includes(frm.doc.doctype))
) {
let party_type = "Customer";
if (frm.doc.quotation_to && in_list(["Lead", "Prospect"], frm.doc.quotation_to)) {
if (frm.doc.quotation_to && ["Lead", "Prospect"].includes(frm.doc.quotation_to)) {
party_type = frm.doc.quotation_to;
}

View File

@@ -303,7 +303,7 @@ erpnext.sales_common = {
if ((doc.packed_items || []).length) {
$(this.frm.fields_dict.packing_list.row.wrapper).toggle(true);
if (in_list(["Delivery Note", "Sales Invoice"], doc.doctype)) {
if (["Delivery Note", "Sales Invoice"].includes(doc.doctype)) {
var help_msg =
"<div class='alert alert-warning'>" +
__(
@@ -315,7 +315,7 @@ erpnext.sales_common = {
}
} else {
$(this.frm.fields_dict.packing_list.row.wrapper).toggle(false);
if (in_list(["Delivery Note", "Sales Invoice"], doc.doctype)) {
if (["Delivery Note", "Sales Invoice"].includes(doc.doctype)) {
frappe.meta.get_docfield(doc.doctype, "product_bundle_help", doc.name).options = "";
}
}
@@ -416,7 +416,7 @@ erpnext.sales_common = {
project() {
let me = this;
if (in_list(["Delivery Note", "Sales Invoice", "Sales Order"], this.frm.doc.doctype)) {
if (["Delivery Note", "Sales Invoice", "Sales Order"].includes(this.frm.doc.doctype)) {
if (this.frm.doc.project) {
frappe.call({
method: "erpnext.projects.doctype.project.project.get_cost_center_name",

View File

@@ -542,6 +542,10 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
frappe.throw(__("Please add atleast one Serial No / Batch No"));
}
if (!warehouse) {
frappe.throw(__("Please select a Warehouse"));
}
frappe
.call({
method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.add_serial_batch_ledgers",

View File

@@ -34,7 +34,7 @@ frappe.ui.form.on("Import Supplier Invoice", {
},
toggle_read_only_fields: function (frm) {
if (in_list(["File Import Completed", "Processing File Data"], frm.doc.status)) {
if (["File Import Completed", "Processing File Data"].includes(frm.doc.status)) {
cur_frm.set_read_only();
cur_frm.refresh_fields();
frm.set_df_property("import_invoices", "hidden", 1);

View File

@@ -583,7 +583,7 @@
"link_fieldname": "party"
}
],
"modified": "2023-12-28 13:15:36.298369",
"modified": "2024-03-16 19:41:47.971815",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",
@@ -661,7 +661,7 @@
}
],
"quick_entry": 1,
"search_fields": "customer_name,customer_group,territory, mobile_no,primary_address",
"search_fields": "customer_group,territory, mobile_no,primary_address",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",

View File

@@ -370,37 +370,6 @@ class TestCustomer(FrappeTestCase):
due_date = get_due_date("2017-01-22", "Customer", "_Test Customer")
self.assertEqual(due_date, "2017-01-22")
def test_serach_fields_for_customer(self):
from erpnext.controllers.queries import customer_query
frappe.db.set_single_value("Selling Settings", "cust_master_name", "Naming Series")
make_property_setter(
"Customer", None, "search_fields", "customer_group", "Data", for_doctype="Doctype"
)
data = customer_query(
"Customer", "_Test Customer", "", 0, 20, filters={"name": "_Test Customer"}, as_dict=True
)
self.assertEqual(data[0].name, "_Test Customer")
self.assertEqual(data[0].customer_group, "_Test Customer Group")
self.assertTrue("territory" not in data[0])
make_property_setter(
"Customer", None, "search_fields", "customer_group, territory", "Data", for_doctype="Doctype"
)
data = customer_query(
"Customer", "_Test Customer", "", 0, 20, filters={"name": "_Test Customer"}, as_dict=True
)
self.assertEqual(data[0].name, "_Test Customer")
self.assertEqual(data[0].customer_group, "_Test Customer Group")
self.assertEqual(data[0].territory, "_Test Territory")
self.assertTrue("territory" in data[0])
frappe.db.set_single_value("Selling Settings", "cust_master_name", "Customer Name")
def test_parse_full_name(self):
first, middle, last = parse_full_name("John")
self.assertEqual(first, "John")

View File

@@ -1,6 +1,8 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
cur_frm.cscript.tax_table = "Sales Taxes and Charges";
erpnext.accounts.taxes.setup_tax_validations("Sales Taxes and Charges Template");
erpnext.accounts.taxes.setup_tax_filters("Sales Taxes and Charges");
erpnext.pre_sales.set_as_lost("Quotation");

View File

@@ -30,6 +30,39 @@ class TestQuotation(FrappeTestCase):
self.assertTrue(sales_order.get("payment_schedule"))
def test_gross_profit(self):
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.get_item_details import insert_item_price
item_doc = make_item("_Test Item for Gross Profit", {"is_stock_item": 1})
item_code = item_doc.name
make_stock_entry(item_code=item_code, qty=10, rate=100, target="_Test Warehouse - _TC")
selling_price_list = frappe.get_all("Price List", filters={"selling": 1}, limit=1)[0].name
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 1)
insert_item_price(
frappe._dict(
{
"item_code": item_code,
"price_list": selling_price_list,
"price_list_rate": 300,
"rate": 300,
"conversion_factor": 1,
"discount_amount": 0.0,
"currency": frappe.db.get_value("Price List", selling_price_list, "currency"),
"uom": item_doc.stock_uom,
}
)
)
quotation = make_quotation(
item_code=item_code, qty=1, rate=300, selling_price_list=selling_price_list
)
self.assertEqual(quotation.items[0].valuation_rate, 100)
self.assertEqual(quotation.items[0].gross_profit, 200)
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0)
def test_maintain_rate_in_sales_cycle_is_enforced(self):
from erpnext.selling.doctype.quotation.quotation import make_sales_order

View File

@@ -1,6 +1,8 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
cur_frm.cscript.tax_table = "Sales Taxes and Charges";
erpnext.accounts.taxes.setup_tax_filters("Sales Taxes and Charges");
erpnext.accounts.taxes.setup_tax_validations("Sales Order");
erpnext.sales_common.setup_selling_controller();

View File

@@ -2091,6 +2091,40 @@ class TestSalesOrder(FrappeTestCase):
dn.submit()
dn.reload()
def test_auto_update_price_list(self):
item = make_item(
"_Test Auto Update Price List Item",
)
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 1)
so = make_sales_order(
item_code=item.name, currency="USD", qty=1, rate=100, price_list_rate=100, do_not_submit=True
)
so.save()
item_price = frappe.db.get_value("Item Price", {"item_code": item.name}, "price_list_rate")
self.assertEqual(item_price, 100)
so = make_sales_order(
item_code=item.name, currency="USD", qty=1, rate=200, price_list_rate=100, do_not_submit=True
)
so.save()
item_price = frappe.db.get_value("Item Price", {"item_code": item.name}, "price_list_rate")
self.assertEqual(item_price, 100)
frappe.db.set_single_value("Stock Settings", "update_existing_price_list_rate", 1)
so = make_sales_order(
item_code=item.name, currency="USD", qty=1, rate=200, price_list_rate=200, do_not_submit=True
)
so.save()
item_price = frappe.db.get_value("Item Price", {"item_code": item.name}, "price_list_rate")
self.assertEqual(item_price, 200)
frappe.db.set_single_value("Stock Settings", "update_existing_price_list_rate", 0)
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0)
def automatically_fetch_payment_terms(enable=1):
accounts_settings = frappe.get_doc("Accounts Settings")
@@ -2156,13 +2190,14 @@ def make_sales_order(**args):
return so
def create_dn_against_so(so, delivered_qty=0):
def create_dn_against_so(so, delivered_qty=0, do_not_submit=False):
frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 1)
dn = make_delivery_note(so)
dn.get("items")[0].qty = delivered_qty or 5
dn.insert()
dn.submit()
if not do_not_submit:
dn.submit()
return dn

View File

@@ -295,10 +295,10 @@ erpnext.PointOfSale.ItemCart = class {
<div class="customer-field"></div>
`);
const me = this;
const query = { query: "erpnext.controllers.queries.customer_query" };
const allowed_customer_group = this.allowed_customer_groups || [];
let filters = {};
if (allowed_customer_group.length) {
query.filters = {
filters = {
customer_group: ["in", allowed_customer_group],
};
}
@@ -308,7 +308,11 @@ erpnext.PointOfSale.ItemCart = class {
fieldtype: "Link",
options: "Customer",
placeholder: __("Search by customer name, phone, email."),
get_query: () => query,
get_query: function () {
return {
filters: filters,
};
},
onchange: function () {
if (this.value) {
const frm = me.events.get_frm();

View File

@@ -73,7 +73,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
const { status } = doc;
let indicator_color = "";
in_list(["Paid", "Consolidated"], status) && (indicator_color = "green");
["Paid", "Consolidated"].includes(status) && (indicator_color = "green");
status === "Draft" && (indicator_color = "red");
status === "Return" && (indicator_color = "grey");

View File

@@ -197,6 +197,8 @@ def prepare_data(
):
details[p_key] += r.get(qty_or_amount_field, 0)
details[variance_key] = details.get(p_key) - details.get(target_key)
else:
details[variance_key] = details.get(p_key) - details.get(target_key)
details["total_achieved"] += details.get(p_key)
details["total_variance"] = details.get("total_achieved") - details.get("total_target")
@@ -209,31 +211,32 @@ def get_actual_data(filters, sales_users_or_territory_data, date_field, sales_fi
parent_doc = frappe.qb.DocType(filters.get("doctype"))
child_doc = frappe.qb.DocType(filters.get("doctype") + " Item")
sales_team = frappe.qb.DocType("Sales Team")
query = (
frappe.qb.from_(parent_doc)
.inner_join(child_doc)
.on(child_doc.parent == parent_doc.name)
.inner_join(sales_team)
.on(sales_team.parent == parent_doc.name)
.select(
child_doc.item_group,
(child_doc.stock_qty * sales_team.allocated_percentage / 100).as_("stock_qty"),
(child_doc.base_net_amount * sales_team.allocated_percentage / 100).as_("base_net_amount"),
sales_team.sales_person,
parent_doc[date_field],
)
.where(
(parent_doc.docstatus == 1)
& (parent_doc[date_field].between(fiscal_year.year_start_date, fiscal_year.year_end_date))
)
)
query = frappe.qb.from_(parent_doc).inner_join(child_doc).on(child_doc.parent == parent_doc.name)
if sales_field == "sales_person":
query = query.where(sales_team.sales_person.isin(sales_users_or_territory_data))
sales_team = frappe.qb.DocType("Sales Team")
stock_qty = child_doc.stock_qty * sales_team.allocated_percentage / 100
net_amount = child_doc.base_net_amount * sales_team.allocated_percentage / 100
sales_field_col = sales_team[sales_field]
query = query.inner_join(sales_team).on(sales_team.parent == parent_doc.name)
else:
query = query.where(parent_doc[sales_field].isin(sales_users_or_territory_data))
stock_qty = child_doc.stock_qty
net_amount = child_doc.base_net_amount
sales_field_col = parent_doc[sales_field]
query = query.select(
child_doc.item_group,
parent_doc[date_field],
(stock_qty).as_("stock_qty"),
(net_amount).as_("base_net_amount"),
sales_field_col,
).where(
(parent_doc.docstatus == 1)
& (parent_doc[date_field].between(fiscal_year.year_start_date, fiscal_year.year_end_date))
& (sales_field_col.isin(sales_users_or_territory_data))
)
return query.run(as_dict=True)

View File

@@ -0,0 +1,57 @@
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import flt, nowdate
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.utils import get_fiscal_year
from erpnext.selling.report.sales_partner_target_variance_based_on_item_group.sales_partner_target_variance_based_on_item_group import (
execute,
)
from erpnext.selling.report.sales_person_target_variance_based_on_item_group.test_sales_person_target_variance_based_on_item_group import (
create_sales_target_doc,
create_target_distribution,
)
class TestSalesPartnerTargetVarianceBasedOnItemGroup(FrappeTestCase):
def setUp(self):
self.fiscal_year = get_fiscal_year(nowdate())[0]
def tearDown(self):
frappe.db.rollback()
def test_achieved_target_and_variance_for_partner(self):
# Create a Target Distribution
distribution = create_target_distribution(self.fiscal_year)
# Create Sales Partner with targets for the current fiscal year
sales_partner = create_sales_target_doc(
"Sales Partner", "partner_name", "Sales Partner 1", self.fiscal_year, distribution.name
)
# Create a Sales Invoice for the Partner
si = create_sales_invoice(
rate=1000,
qty=20,
do_not_submit=True,
)
si.sales_partner = sales_partner
si.commission_rate = 5
si.submit()
# Check Achieved Target and Variance for the Sales Partner
result = execute(
frappe._dict(
{
"fiscal_year": self.fiscal_year,
"doctype": "Sales Invoice",
"period": "Yearly",
"target_on": "Quantity",
}
)
)[1]
row = frappe._dict(result[0])
self.assertSequenceEqual(
[flt(value, 2) for value in (row.total_target, row.total_achieved, row.total_variance)],
[50, 20, -30],
)

View File

@@ -18,17 +18,17 @@ class TestSalesPersonTargetVarianceBasedOnItemGroup(FrappeTestCase):
def test_achieved_target_and_variance(self):
# Create a Target Distribution
distribution = frappe.new_doc("Monthly Distribution")
distribution.distribution_id = "Target Report Distribution"
distribution.fiscal_year = self.fiscal_year
distribution.get_months()
distribution.insert()
distribution = create_target_distribution(self.fiscal_year)
# Create sales people with targets
person_1 = create_sales_person_with_target("Sales Person 1", self.fiscal_year, distribution.name)
person_2 = create_sales_person_with_target("Sales Person 2", self.fiscal_year, distribution.name)
# Create sales people with targets for the current fiscal year
person_1 = create_sales_target_doc(
"Sales Person", "sales_person_name", "Sales Person 1", self.fiscal_year, distribution.name
)
person_2 = create_sales_target_doc(
"Sales Person", "sales_person_name", "Sales Person 2", self.fiscal_year, distribution.name
)
# Create a Sales Order with 50-50 contribution
# Create a Sales Order with 50-50 contribution between both Sales people
so = make_sales_order(
rate=1000,
qty=20,
@@ -69,10 +69,20 @@ class TestSalesPersonTargetVarianceBasedOnItemGroup(FrappeTestCase):
)
def create_sales_person_with_target(sales_person_name, fiscal_year, distribution_id):
sales_person = frappe.new_doc("Sales Person")
sales_person.sales_person_name = sales_person_name
sales_person.append(
def create_target_distribution(fiscal_year):
distribution = frappe.new_doc("Monthly Distribution")
distribution.distribution_id = "Target Report Distribution"
distribution.fiscal_year = fiscal_year
distribution.get_months()
return distribution.insert()
def create_sales_target_doc(
sales_field_dt, sales_field_name, sales_field_value, fiscal_year, distribution_id
):
sales_target_doc = frappe.new_doc(sales_field_dt)
sales_target_doc.set(sales_field_name, sales_field_value)
sales_target_doc.append(
"targets",
{
"fiscal_year": fiscal_year,
@@ -81,4 +91,6 @@ def create_sales_person_with_target(sales_person_name, fiscal_year, distribution
"distribution_id": distribution_id,
},
)
return sales_person.insert()
if sales_field_dt == "Sales Partner":
sales_target_doc.commission_rate = 5
return sales_target_doc.insert()

View File

@@ -4,6 +4,7 @@
"item_group": "Demo Item Group",
"item_code": "SKU001",
"item_name": "T-shirt",
"valuation_rate": 400.0,
"gst_hsn_code": "999512",
"image": "https://images.pexels.com/photos/1484808/pexels-photo-1484808.jpeg"
},
@@ -11,6 +12,7 @@
"doctype": "Item",
"item_group": "Demo Item Group",
"item_code": "SKU002",
"valuation_rate": 300.0,
"item_name": "Laptop",
"gst_hsn_code": "999512",
"image": "https://images.pexels.com/photos/3999538/pexels-photo-3999538.jpeg"
@@ -19,6 +21,7 @@
"doctype": "Item",
"item_group": "Demo Item Group",
"item_code": "SKU003",
"valuation_rate": 523.0,
"item_name": "Book",
"gst_hsn_code": "999512",
"image": "https://images.pexels.com/photos/2422178/pexels-photo-2422178.jpeg"
@@ -27,6 +30,7 @@
"doctype": "Item",
"item_group": "Demo Item Group",
"item_code": "SKU004",
"valuation_rate": 725.0,
"item_name": "Smartphone",
"gst_hsn_code": "999512",
"image": "https://images.pexels.com/photos/1647976/pexels-photo-1647976.jpeg"
@@ -35,6 +39,7 @@
"doctype": "Item",
"item_group": "Demo Item Group",
"item_code": "SKU005",
"valuation_rate": 222.0,
"item_name": "Sneakers",
"gst_hsn_code": "999512",
"image": "https://images.pexels.com/photos/1598505/pexels-photo-1598505.jpeg"
@@ -43,6 +48,7 @@
"doctype": "Item",
"item_group": "Demo Item Group",
"item_code": "SKU006",
"valuation_rate": 420.0,
"item_name": "Coffee Mug",
"gst_hsn_code": "999512",
"image": "https://images.pexels.com/photos/585753/pexels-photo-585753.jpeg"
@@ -51,6 +57,7 @@
"doctype": "Item",
"item_group": "Demo Item Group",
"item_code": "SKU007",
"valuation_rate": 375.0,
"item_name": "Television",
"gst_hsn_code": "999512",
"image": "https://images.pexels.com/photos/8059376/pexels-photo-8059376.jpeg"
@@ -59,6 +66,7 @@
"doctype": "Item",
"item_group": "Demo Item Group",
"item_code": "SKU008",
"valuation_rate": 333.0,
"item_name": "Backpack",
"gst_hsn_code": "999512",
"image": "https://images.pexels.com/photos/3731256/pexels-photo-3731256.jpeg"
@@ -67,6 +75,7 @@
"doctype": "Item",
"item_group": "Demo Item Group",
"item_code": "SKU009",
"valuation_rate": 700.0,
"item_name": "Headphones",
"gst_hsn_code": "999512",
"image": "https://images.pexels.com/photos/3587478/pexels-photo-3587478.jpeg"
@@ -75,6 +84,7 @@
"doctype": "Item",
"item_group": "Demo Item Group",
"item_code": "SKU010",
"valuation_rate": 500.0,
"item_name": "Camera",
"gst_hsn_code": "999512",
"image": "https://images.pexels.com/photos/51383/photo-camera-subject-photographer-51383.jpeg"

View File

@@ -8,7 +8,7 @@ frappe.ui.form.on("Closing Stock Balance", {
},
generate_closing_balance(frm) {
if (in_list(["Queued", "Failed"], frm.doc.status)) {
if (["Queued", "Failed"].includes(frm.doc.status)) {
frm.add_custom_button(__("Generate Closing Stock Balance"), () => {
frm.call({
method: "enqueue_job",

View File

@@ -123,7 +123,9 @@ class ClosingStockBalance(Document):
)
)
create_json_gz_file({"columns": columns, "data": data}, self.doctype, self.name)
create_json_gz_file(
{"columns": columns, "data": data}, self.doctype, self.name, "closing-stock-balance"
)
def get_prepared_data(self):
if attachments := get_attachments(self.doctype, self.name):

View File

@@ -3,6 +3,8 @@
cur_frm.add_fetch("customer", "tax_id", "tax_id");
cur_frm.cscript.tax_table = "Sales Taxes and Charges";
frappe.provide("erpnext.stock");
frappe.provide("erpnext.stock.delivery_note");
frappe.provide("erpnext.accounts.dimensions");

View File

@@ -251,6 +251,7 @@ class DeliveryNote(SellingController):
def validate(self):
self.validate_posting_time()
super(DeliveryNote, self).validate()
self.validate_references()
self.set_status()
self.so_required()
self.validate_proj_cust()
@@ -333,6 +334,7 @@ class DeliveryNote(SellingController):
"type_of_transaction": "Outward",
"serial_and_batch_bundle": bundle_id,
"item_code": item.get("item_code"),
"warehouse": item.get("warehouse"),
}
)
@@ -340,6 +342,58 @@ class DeliveryNote(SellingController):
item.serial_and_batch_bundle = cls_obj.serial_and_batch_bundle
def validate_references(self):
self.validate_sales_order_references()
self.validate_sales_invoice_references()
def validate_sales_order_references(self):
err_msg = ""
for item in self.items:
if (item.against_sales_order and not item.so_detail) or (
not item.against_sales_order and item.so_detail
):
if not item.against_sales_order:
err_msg += (
_("'Sales Order' reference ({1}) is missing in row {0}").format(
frappe.bold(item.idx), frappe.bold("against_sales_order")
)
+ "<br>"
)
else:
err_msg += (
_("'Sales Order Item' reference ({1}) is missing in row {0}").format(
frappe.bold(item.idx), frappe.bold("so_detail")
)
+ "<br>"
)
if err_msg:
frappe.throw(err_msg, title=_("References to Sales Orders are Incomplete"))
def validate_sales_invoice_references(self):
err_msg = ""
for item in self.items:
if (item.against_sales_invoice and not item.si_detail) or (
not item.against_sales_invoice and item.si_detail
):
if not item.against_sales_invoice:
err_msg += (
_("'Sales Invoice' reference ({1}) is missing in row {0}").format(
frappe.bold(item.idx), frappe.bold("against_sales_invoice")
)
+ "<br>"
)
else:
err_msg += (
_("'Sales Invoice Item' reference ({1}) is missing in row {0}").format(
frappe.bold(item.idx), frappe.bold("si_detail")
)
+ "<br>"
)
if err_msg:
frappe.throw(err_msg, title=_("References to Sales Invoices are Incomplete"))
def validate_proj_cust(self):
"""check for does customer belong to same project as entered.."""
if self.project and self.customer:

View File

@@ -813,6 +813,15 @@ class TestDeliveryNote(FrappeTestCase):
dn.cancel()
self.assertEqual(dn.status, "Cancelled")
def test_sales_order_reference_validation(self):
so = make_sales_order(po_no="12345")
dn = create_dn_against_so(so.name, delivered_qty=2, do_not_submit=True)
dn.items[0].against_sales_order = None
self.assertRaises(frappe.ValidationError, dn.save)
dn.reload()
dn.items[0].so_detail = None
self.assertRaises(frappe.ValidationError, dn.save)
def test_dn_billing_status_case1(self):
# SO -> DN -> SI
so = make_sales_order(po_no="12345")
@@ -1088,9 +1097,30 @@ class TestDeliveryNote(FrappeTestCase):
dn.load_from_db()
batch_no = get_batch_from_bundle(dn.packed_items[0].serial_and_batch_bundle)
packed_name = dn.packed_items[0].name
self.assertTrue(batch_no)
dn.cancel()
# Cancel the reposting entry
reposting_entries = frappe.get_all("Repost Item Valuation", filters={"docstatus": 1})
for entry in reposting_entries:
doc = frappe.get_doc("Repost Item Valuation", entry.name)
doc.cancel()
doc.delete()
frappe.db.set_single_value("Accounts Settings", "delete_linked_ledger_entries", 1)
dn.reload()
dn.delete()
bundle = frappe.db.get_value(
"Serial and Batch Bundle", {"voucher_detail_no": packed_name}, "name"
)
self.assertFalse(bundle)
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
frappe.db.set_single_value("Accounts Settings", "delete_linked_ledger_entries", 0)
def test_payment_terms_are_fetched_when_creating_sales_invoice(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import (

View File

@@ -1,9 +1,9 @@
frappe.listview_settings["Delivery Trip"] = {
add_fields: ["status"],
get_indicator: function (doc) {
if (in_list(["Cancelled", "Draft"], doc.status)) {
if (["Cancelled", "Draft"].includes(doc.status)) {
return [__(doc.status), "red", "status,=," + doc.status];
} else if (in_list(["In Transit", "Scheduled"], doc.status)) {
} else if (["In Transit", "Scheduled"].includes(doc.status)) {
return [__(doc.status), "orange", "status,=," + doc.status];
} else if (doc.status === "Completed") {
return [__(doc.status), "green", "status,=," + doc.status];

View File

@@ -406,14 +406,6 @@ $.extend(erpnext.item, {
};
};
frm.fields_dict.customer_items.grid.get_field("customer_name").get_query = function (doc, cdt, cdn) {
return { query: "erpnext.controllers.queries.customer_query" };
};
frm.fields_dict.supplier_items.grid.get_field("supplier").get_query = function (doc, cdt, cdn) {
return { query: "erpnext.controllers.queries.supplier_query" };
};
frm.fields_dict["item_defaults"].grid.get_field("default_warehouse").get_query = function (
doc,
cdt,

View File

@@ -184,7 +184,11 @@ class PickList(Document):
def delink_serial_and_batch_bundle(self):
for row in self.locations:
if row.serial_and_batch_bundle:
if (
row.serial_and_batch_bundle
and frappe.db.get_value("Serial and Batch Bundle", row.serial_and_batch_bundle, "docstatus")
== 1
):
frappe.db.set_value(
"Serial and Batch Bundle",
row.serial_and_batch_bundle,

View File

@@ -3,6 +3,8 @@
frappe.provide("erpnext.stock");
cur_frm.cscript.tax_table = "Purchase Taxes and Charges";
erpnext.accounts.taxes.setup_tax_filters("Purchase Taxes and Charges");
erpnext.accounts.taxes.setup_tax_validations("Purchase Receipt");
erpnext.buying.setup_buying_controller();

View File

@@ -2522,6 +2522,280 @@ class TestPurchaseReceipt(FrappeTestCase):
self.assertEqual(row.serial_no, "\n".join(serial_nos[:2]))
self.assertEqual(row.rejected_serial_no, serial_nos[2])
def test_internal_transfer_with_serial_batch_items_and_their_valuation(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
prepare_data_for_internal_transfer()
customer = "_Test Internal Customer 2"
company = "_Test Company with perpetual inventory"
batch_item_doc = make_item(
"_Test Batch Item For Stock Transfer",
{"has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "BT-BIFST-.####"},
)
serial_item_doc = make_item(
"_Test Serial No Item For Stock Transfer",
{"has_serial_no": 1, "serial_no_series": "BT-BIFST-.####"},
)
inward_entry = make_purchase_receipt(
item_code=batch_item_doc.name,
qty=10,
rate=150,
warehouse="Stores - TCP1",
company="_Test Company with perpetual inventory",
use_serial_batch_fields=1,
do_not_submit=1,
)
inward_entry.append(
"items",
{
"item_code": serial_item_doc.name,
"qty": 15,
"rate": 250,
"item_name": serial_item_doc.item_name,
"conversion_factor": 1.0,
"uom": serial_item_doc.stock_uom,
"stock_uom": serial_item_doc.stock_uom,
"warehouse": "Stores - TCP1",
"use_serial_batch_fields": 1,
},
)
inward_entry.submit()
inward_entry.reload()
for row in inward_entry.items:
self.assertTrue(row.serial_and_batch_bundle)
inter_transfer_dn = create_delivery_note(
item_code=inward_entry.items[0].item_code,
company=company,
customer=customer,
cost_center="Main - TCP1",
expense_account="Cost of Goods Sold - TCP1",
qty=10,
rate=500,
warehouse="Stores - TCP1",
target_warehouse="Work In Progress - TCP1",
batch_no=get_batch_from_bundle(inward_entry.items[0].serial_and_batch_bundle),
use_serial_batch_fields=1,
do_not_submit=1,
)
inter_transfer_dn.append(
"items",
{
"item_code": serial_item_doc.name,
"qty": 15,
"rate": 350,
"item_name": serial_item_doc.item_name,
"conversion_factor": 1.0,
"uom": serial_item_doc.stock_uom,
"stock_uom": serial_item_doc.stock_uom,
"warehouse": "Stores - TCP1",
"target_warehouse": "Work In Progress - TCP1",
"serial_no": "\n".join(
get_serial_nos_from_bundle(inward_entry.items[1].serial_and_batch_bundle)
),
"use_serial_batch_fields": 1,
},
)
inter_transfer_dn.submit()
inter_transfer_dn.reload()
for row in inter_transfer_dn.items:
if row.item_code == batch_item_doc.name:
self.assertEqual(row.rate, 150.0)
else:
self.assertEqual(row.rate, 250.0)
self.assertTrue(row.serial_and_batch_bundle)
inter_transfer_pr = make_inter_company_purchase_receipt(inter_transfer_dn.name)
for row in inter_transfer_pr.items:
row.from_warehouse = "Work In Progress - TCP1"
row.warehouse = "Stores - TCP1"
inter_transfer_pr.submit()
for row in inter_transfer_pr.items:
if row.item_code == batch_item_doc.name:
self.assertEqual(row.rate, 150.0)
else:
self.assertEqual(row.rate, 250.0)
self.assertTrue(row.serial_and_batch_bundle)
inter_transfer_pr_return = make_return_doc("Purchase Receipt", inter_transfer_pr.name)
inter_transfer_pr_return.submit()
inter_transfer_pr_return.reload()
for row in inter_transfer_pr_return.items:
self.assertTrue(row.serial_and_batch_bundle)
if row.item_code == serial_item_doc.name:
self.assertEqual(row.rate, 250.0)
serial_nos = get_serial_nos_from_bundle(row.serial_and_batch_bundle)
for sn in serial_nos:
serial_no_details = frappe.db.get_value("Serial No", sn, ["status", "warehouse"], as_dict=1)
self.assertTrue(serial_no_details.status == "Active")
self.assertEqual(serial_no_details.warehouse, "Work In Progress - TCP1")
inter_transfer_dn_return = make_return_doc("Delivery Note", inter_transfer_dn.name)
inter_transfer_dn_return.posting_date = today()
inter_transfer_dn_return.posting_time = nowtime()
for row in inter_transfer_dn_return.items:
row.target_warehouse = "Work In Progress - TCP1"
inter_transfer_dn_return.submit()
inter_transfer_dn_return.reload()
for row in inter_transfer_dn_return.items:
self.assertTrue(row.serial_and_batch_bundle)
def test_internal_transfer_with_serial_batch_items_without_user_serial_batch_fields(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0)
prepare_data_for_internal_transfer()
customer = "_Test Internal Customer 2"
company = "_Test Company with perpetual inventory"
batch_item_doc = make_item(
"_Test Batch Item For Stock Transfer USE SERIAL BATCH FIELDS",
{"has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "USBF-BT-BIFST-.####"},
)
serial_item_doc = make_item(
"_Test Serial No Item For Stock Transfer USE SERIAL BATCH FIELDS",
{"has_serial_no": 1, "serial_no_series": "USBF-BT-BIFST-.####"},
)
inward_entry = make_purchase_receipt(
item_code=batch_item_doc.name,
qty=10,
rate=150,
warehouse="Stores - TCP1",
company="_Test Company with perpetual inventory",
use_serial_batch_fields=0,
do_not_submit=1,
)
inward_entry.append(
"items",
{
"item_code": serial_item_doc.name,
"qty": 15,
"rate": 250,
"item_name": serial_item_doc.item_name,
"conversion_factor": 1.0,
"uom": serial_item_doc.stock_uom,
"stock_uom": serial_item_doc.stock_uom,
"warehouse": "Stores - TCP1",
"use_serial_batch_fields": 0,
},
)
inward_entry.submit()
inward_entry.reload()
for row in inward_entry.items:
self.assertTrue(row.serial_and_batch_bundle)
inter_transfer_dn = create_delivery_note(
item_code=inward_entry.items[0].item_code,
company=company,
customer=customer,
cost_center="Main - TCP1",
expense_account="Cost of Goods Sold - TCP1",
qty=10,
rate=500,
warehouse="Stores - TCP1",
target_warehouse="Work In Progress - TCP1",
batch_no=get_batch_from_bundle(inward_entry.items[0].serial_and_batch_bundle),
use_serial_batch_fields=0,
do_not_submit=1,
)
inter_transfer_dn.append(
"items",
{
"item_code": serial_item_doc.name,
"qty": 15,
"rate": 350,
"item_name": serial_item_doc.item_name,
"conversion_factor": 1.0,
"uom": serial_item_doc.stock_uom,
"stock_uom": serial_item_doc.stock_uom,
"warehouse": "Stores - TCP1",
"target_warehouse": "Work In Progress - TCP1",
"serial_no": "\n".join(
get_serial_nos_from_bundle(inward_entry.items[1].serial_and_batch_bundle)
),
"use_serial_batch_fields": 0,
},
)
inter_transfer_dn.submit()
inter_transfer_dn.reload()
for row in inter_transfer_dn.items:
if row.item_code == batch_item_doc.name:
self.assertEqual(row.rate, 150.0)
else:
self.assertEqual(row.rate, 250.0)
self.assertTrue(row.serial_and_batch_bundle)
inter_transfer_pr = make_inter_company_purchase_receipt(inter_transfer_dn.name)
for row in inter_transfer_pr.items:
row.from_warehouse = "Work In Progress - TCP1"
row.warehouse = "Stores - TCP1"
inter_transfer_pr.submit()
for row in inter_transfer_pr.items:
if row.item_code == batch_item_doc.name:
self.assertEqual(row.rate, 150.0)
else:
self.assertEqual(row.rate, 250.0)
self.assertTrue(row.serial_and_batch_bundle)
inter_transfer_pr_return = make_return_doc("Purchase Receipt", inter_transfer_pr.name)
inter_transfer_pr_return.submit()
inter_transfer_pr_return.reload()
for row in inter_transfer_pr_return.items:
self.assertTrue(row.serial_and_batch_bundle)
if row.item_code == serial_item_doc.name:
self.assertEqual(row.rate, 250.0)
serial_nos = get_serial_nos_from_bundle(row.serial_and_batch_bundle)
for sn in serial_nos:
serial_no_details = frappe.db.get_value("Serial No", sn, ["status", "warehouse"], as_dict=1)
self.assertTrue(serial_no_details.status == "Active")
self.assertEqual(serial_no_details.warehouse, "Work In Progress - TCP1")
inter_transfer_dn_return = make_return_doc("Delivery Note", inter_transfer_dn.name)
inter_transfer_dn_return.posting_date = today()
inter_transfer_dn_return.posting_time = nowtime()
for row in inter_transfer_dn_return.items:
row.target_warehouse = "Work In Progress - TCP1"
inter_transfer_dn_return.submit()
inter_transfer_dn_return.reload()
for row in inter_transfer_dn_return.items:
self.assertTrue(row.serial_and_batch_bundle)
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

@@ -113,6 +113,7 @@
{
"fieldname": "voucher_no",
"fieldtype": "Dynamic Link",
"in_standard_filter": 1,
"label": "Voucher No",
"no_copy": 1,
"options": "voucher_type",
@@ -250,7 +251,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-12-07 17:56:55.528563",
"modified": "2024-03-15 15:22:24.003486",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial and Batch Bundle",

View File

@@ -778,6 +778,10 @@ class SerialandBatchBundle(Document):
or_filters=or_filters,
)
if not vouchers and self.voucher_type == "Delivery Note":
frappe.db.set_value("Packed Item", self.voucher_detail_no, "serial_and_batch_bundle", None)
return
for voucher in vouchers:
if voucher.get("current_serial_and_batch_bundle"):
frappe.db.set_value(self.child_table, voucher.name, "current_serial_and_batch_bundle", None)
@@ -801,6 +805,7 @@ class SerialandBatchBundle(Document):
self.set_purchase_document_no()
def on_submit(self):
self.validate_batch_inventory()
self.validate_serial_nos_inventory()
def set_purchase_document_no(self):
@@ -827,6 +832,13 @@ class SerialandBatchBundle(Document):
if not self.has_batch_no:
return
if (
self.voucher_type == "Stock Reconciliation"
and self.type_of_transaction == "Outward"
and frappe.db.get_value("Stock Reconciliation Item", self.voucher_detail_no, "qty") > 0
):
return
batches = [d.batch_no for d in self.entries if d.batch_no]
if not batches:
return

View File

@@ -2606,6 +2606,7 @@ def move_sample_to_retention_warehouse(company, items):
"type_of_transaction": "Outward",
"serial_and_batch_bundle": item.get("serial_and_batch_bundle"),
"item_code": item.get("item_code"),
"warehouse": item.get("t_warehouse"),
}
)

View File

@@ -999,6 +999,7 @@ class TestStockEntry(FrappeTestCase):
"type_of_transaction": "Inward",
"serial_and_batch_bundle": s2.items[0].serial_and_batch_bundle,
"item_code": "_Test Serialized Item",
"warehouse": "_Test Warehouse - _TC",
}
)

View File

@@ -230,7 +230,7 @@
},
{
"fieldname": "stock_queue",
"fieldtype": "Text",
"fieldtype": "Long Text",
"label": "FIFO Stock Queue (qty, rate)",
"oldfieldname": "fcfs_stack",
"oldfieldtype": "Text",
@@ -360,7 +360,7 @@
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-02-07 09:18:13.999231",
"modified": "2024-03-13 09:56:13.021696",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Ledger Entry",

View File

@@ -58,7 +58,7 @@ class StockLedgerEntry(Document):
recalculate_rate: DF.Check
serial_and_batch_bundle: DF.Link | None
serial_no: DF.LongText | None
stock_queue: DF.Text | None
stock_queue: DF.LongText | None
stock_uom: DF.Link | None
stock_value: DF.Currency
stock_value_difference: DF.Currency

View File

@@ -154,7 +154,6 @@ class StockReconciliation(StockController):
{
"current_serial_and_batch_bundle": sn_doc.name,
"current_serial_no": "",
"batch_no": "",
}
)

View File

@@ -816,7 +816,9 @@ def get_price_list_rate(args, item_doc, out=None):
price_list_rate = get_price_list_rate_for(args, item_doc.variant_of)
# insert in database
if price_list_rate is None:
if price_list_rate is None or frappe.db.get_single_value(
"Stock Settings", "update_existing_price_list_rate"
):
if args.price_list and args.rate:
insert_item_price(args)
return out

View File

@@ -60,6 +60,7 @@ def execute(filters=None):
if filters.get("batch_no") or inventory_dimension_filters_applied:
actual_qty += flt(sle.actual_qty, precision)
stock_value += sle.stock_value_difference
batch_balance_dict[sle.batch_no] += sle.actual_qty
if sle.voucher_type == "Stock Reconciliation" and not sle.actual_qty:
actual_qty = sle.qty_after_transaction

View File

@@ -820,6 +820,10 @@ class SerialBatchCreation:
self.remove_returned_serial_nos(new_package)
new_package.docstatus = 0
new_package.warehouse = self.warehouse
new_package.voucher_no = ""
new_package.posting_date = today()
new_package.posting_time = nowtime()
new_package.type_of_transaction = self.type_of_transaction
new_package.returned_against = self.get("returned_against")
new_package.save()

View File

@@ -18,7 +18,7 @@
actual_qty = (frm.doc.doctype==="Sales Order"
? doc.projected_qty : doc.actual_qty);
if(flt(frm.doc.per_delivered, 2) < 100
&& in_list(["Sales Order Item", "Delivery Note Item"], doc.doctype)) {
&& ["Sales Order Item", "Delivery Note Item"].includes(doc.doctype)) {
if(actual_qty != undefined) {
if(actual_qty >= doc.qty) {
var color = "green";