mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-28 02:58:30 +00:00
Merge pull request #40538 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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");
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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" };
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"),
|
||||
() => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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; });
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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],
|
||||
)
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -154,7 +154,6 @@ class StockReconciliation(StockController):
|
||||
{
|
||||
"current_serial_and_batch_bundle": sn_doc.name,
|
||||
"current_serial_no": "",
|
||||
"batch_no": "",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user