mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-25 16:04:46 +00:00
Merge pull request #35807 from frappe/version-14-hotfix
chore: release v14
This commit is contained in:
@@ -37,7 +37,7 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
|
|||||||
|
|
||||||
validate_rounding_loss: function(frm) {
|
validate_rounding_loss: function(frm) {
|
||||||
let allowance = frm.doc.rounding_loss_allowance;
|
let allowance = frm.doc.rounding_loss_allowance;
|
||||||
if (!(allowance > 0 && allowance < 1)) {
|
if (!(allowance >= 0 && allowance < 1)) {
|
||||||
frappe.throw(__("Rounding Loss Allowance should be between 0 and 1"));
|
frappe.throw(__("Rounding Loss Allowance should be between 0 and 1"));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -100,15 +100,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0.05",
|
"default": "0.05",
|
||||||
"description": "Only values between 0 and 1 are allowed. \nEx: If allowance is set at 0.07, accounts that have balance of 0.07 in either of the currencies will be considered as zero balance account",
|
"description": "Only values between [0,1) are allowed. Like {0.00, 0.04, 0.09, ...}\nEx: If allowance is set at 0.07, accounts that have balance of 0.07 in either of the currencies will be considered as zero balance account",
|
||||||
"fieldname": "rounding_loss_allowance",
|
"fieldname": "rounding_loss_allowance",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Rounding Loss Allowance"
|
"label": "Rounding Loss Allowance",
|
||||||
|
"precision": "9"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-06-12 21:02:09.818208",
|
"modified": "2023-06-20 07:29:06.972434",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Exchange Rate Revaluation",
|
"name": "Exchange Rate Revaluation",
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class ExchangeRateRevaluation(Document):
|
|||||||
self.set_total_gain_loss()
|
self.set_total_gain_loss()
|
||||||
|
|
||||||
def validate_rounding_loss_allowance(self):
|
def validate_rounding_loss_allowance(self):
|
||||||
if not (self.rounding_loss_allowance > 0 and self.rounding_loss_allowance < 1):
|
if not (self.rounding_loss_allowance >= 0 and self.rounding_loss_allowance < 1):
|
||||||
frappe.throw(_("Rounding Loss Allowance should be between 0 and 1"))
|
frappe.throw(_("Rounding Loss Allowance should be between 0 and 1"))
|
||||||
|
|
||||||
def set_total_gain_loss(self):
|
def set_total_gain_loss(self):
|
||||||
@@ -373,6 +373,24 @@ class ExchangeRateRevaluation(Document):
|
|||||||
"credit": 0,
|
"credit": 0,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
journal_entry_accounts.append(journal_account)
|
||||||
|
|
||||||
|
journal_entry_accounts.append(
|
||||||
|
{
|
||||||
|
"account": unrealized_exchange_gain_loss_account,
|
||||||
|
"balance": get_balance_on(unrealized_exchange_gain_loss_account),
|
||||||
|
"debit": 0,
|
||||||
|
"credit": 0,
|
||||||
|
"debit_in_account_currency": abs(d.gain_loss) if d.gain_loss < 0 else 0,
|
||||||
|
"credit_in_account_currency": abs(d.gain_loss) if d.gain_loss > 0 else 0,
|
||||||
|
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||||
|
"exchange_rate": 1,
|
||||||
|
"reference_type": "Exchange Rate Revaluation",
|
||||||
|
"reference_name": self.name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
elif d.get("balance_in_base_currency") and not d.get("new_balance_in_base_currency"):
|
elif d.get("balance_in_base_currency") and not d.get("new_balance_in_base_currency"):
|
||||||
# Base currency has balance
|
# Base currency has balance
|
||||||
dr_or_cr = "credit" if d.get("balance_in_base_currency") > 0 else "debit"
|
dr_or_cr = "credit" if d.get("balance_in_base_currency") > 0 else "debit"
|
||||||
@@ -388,22 +406,22 @@ class ExchangeRateRevaluation(Document):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
journal_entry_accounts.append(journal_account)
|
journal_entry_accounts.append(journal_account)
|
||||||
|
|
||||||
journal_entry_accounts.append(
|
journal_entry_accounts.append(
|
||||||
{
|
{
|
||||||
"account": unrealized_exchange_gain_loss_account,
|
"account": unrealized_exchange_gain_loss_account,
|
||||||
"balance": get_balance_on(unrealized_exchange_gain_loss_account),
|
"balance": get_balance_on(unrealized_exchange_gain_loss_account),
|
||||||
"debit": abs(self.gain_loss_booked) if self.gain_loss_booked < 0 else 0,
|
"debit": abs(d.gain_loss) if d.gain_loss < 0 else 0,
|
||||||
"credit": abs(self.gain_loss_booked) if self.gain_loss_booked > 0 else 0,
|
"credit": abs(d.gain_loss) if d.gain_loss > 0 else 0,
|
||||||
"debit_in_account_currency": abs(self.gain_loss_booked) if self.gain_loss_booked < 0 else 0,
|
"debit_in_account_currency": 0,
|
||||||
"credit_in_account_currency": self.gain_loss_booked if self.gain_loss_booked > 0 else 0,
|
"credit_in_account_currency": 0,
|
||||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||||
"exchange_rate": 1,
|
"exchange_rate": 1,
|
||||||
"reference_type": "Exchange Rate Revaluation",
|
"reference_type": "Exchange Rate Revaluation",
|
||||||
"reference_name": self.name,
|
"reference_name": self.name,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
journal_entry.set("accounts", journal_entry_accounts)
|
journal_entry.set("accounts", journal_entry_accounts)
|
||||||
journal_entry.set_total_debit_credit()
|
journal_entry.set_total_debit_credit()
|
||||||
|
|||||||
@@ -92,6 +92,7 @@
|
|||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "New Exchange Rate",
|
"label": "New Exchange Rate",
|
||||||
|
"precision": "9",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -147,7 +148,7 @@
|
|||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-12-29 19:38:52.915295",
|
"modified": "2023-06-20 07:21:40.743460",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Exchange Rate Revaluation Account",
|
"name": "Exchange Rate Revaluation Account",
|
||||||
|
|||||||
@@ -613,7 +613,7 @@ frappe.ui.form.on('Payment Entry', {
|
|||||||
frm.events.set_unallocated_amount(frm);
|
frm.events.set_unallocated_amount(frm);
|
||||||
},
|
},
|
||||||
|
|
||||||
get_outstanding_invoice: function(frm) {
|
get_outstanding_invoices_or_orders: function(frm, get_outstanding_invoices, get_orders_to_be_billed) {
|
||||||
const today = frappe.datetime.get_today();
|
const today = frappe.datetime.get_today();
|
||||||
const fields = [
|
const fields = [
|
||||||
{fieldtype:"Section Break", label: __("Posting Date")},
|
{fieldtype:"Section Break", label: __("Posting Date")},
|
||||||
@@ -643,12 +643,29 @@ frappe.ui.form.on('Payment Entry', {
|
|||||||
{fieldtype:"Check", label: __("Allocate Payment Amount"), fieldname:"allocate_payment_amount", default:1},
|
{fieldtype:"Check", label: __("Allocate Payment Amount"), fieldname:"allocate_payment_amount", default:1},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let btn_text = "";
|
||||||
|
|
||||||
|
if (get_outstanding_invoices) {
|
||||||
|
btn_text = "Get Outstanding Invoices";
|
||||||
|
}
|
||||||
|
else if (get_orders_to_be_billed) {
|
||||||
|
btn_text = "Get Outstanding Orders";
|
||||||
|
}
|
||||||
|
|
||||||
frappe.prompt(fields, function(filters){
|
frappe.prompt(fields, function(filters){
|
||||||
frappe.flags.allocate_payment_amount = true;
|
frappe.flags.allocate_payment_amount = true;
|
||||||
frm.events.validate_filters_data(frm, filters);
|
frm.events.validate_filters_data(frm, filters);
|
||||||
frm.doc.cost_center = filters.cost_center;
|
frm.doc.cost_center = filters.cost_center;
|
||||||
frm.events.get_outstanding_documents(frm, filters);
|
frm.events.get_outstanding_documents(frm, filters, get_outstanding_invoices, get_orders_to_be_billed);
|
||||||
}, __("Filters"), __("Get Outstanding Documents"));
|
}, __("Filters"), __(btn_text));
|
||||||
|
},
|
||||||
|
|
||||||
|
get_outstanding_invoices: function(frm) {
|
||||||
|
frm.events.get_outstanding_invoices_or_orders(frm, true, false);
|
||||||
|
},
|
||||||
|
|
||||||
|
get_outstanding_orders: function(frm) {
|
||||||
|
frm.events.get_outstanding_invoices_or_orders(frm, false, true);
|
||||||
},
|
},
|
||||||
|
|
||||||
validate_filters_data: function(frm, filters) {
|
validate_filters_data: function(frm, filters) {
|
||||||
@@ -674,7 +691,7 @@ frappe.ui.form.on('Payment Entry', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
get_outstanding_documents: function(frm, filters) {
|
get_outstanding_documents: function(frm, filters, get_outstanding_invoices, get_orders_to_be_billed) {
|
||||||
frm.clear_table("references");
|
frm.clear_table("references");
|
||||||
|
|
||||||
if(!frm.doc.party) {
|
if(!frm.doc.party) {
|
||||||
@@ -698,6 +715,13 @@ frappe.ui.form.on('Payment Entry', {
|
|||||||
args[key] = filters[key];
|
args[key] = filters[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (get_outstanding_invoices) {
|
||||||
|
args["get_outstanding_invoices"] = true;
|
||||||
|
}
|
||||||
|
else if (get_orders_to_be_billed) {
|
||||||
|
args["get_orders_to_be_billed"] = true;
|
||||||
|
}
|
||||||
|
|
||||||
frappe.flags.allocate_payment_amount = filters['allocate_payment_amount'];
|
frappe.flags.allocate_payment_amount = filters['allocate_payment_amount'];
|
||||||
|
|
||||||
return frappe.call({
|
return frappe.call({
|
||||||
|
|||||||
@@ -48,7 +48,8 @@
|
|||||||
"base_received_amount",
|
"base_received_amount",
|
||||||
"base_received_amount_after_tax",
|
"base_received_amount_after_tax",
|
||||||
"section_break_14",
|
"section_break_14",
|
||||||
"get_outstanding_invoice",
|
"get_outstanding_invoices",
|
||||||
|
"get_outstanding_orders",
|
||||||
"references",
|
"references",
|
||||||
"section_break_34",
|
"section_break_34",
|
||||||
"total_allocated_amount",
|
"total_allocated_amount",
|
||||||
@@ -355,12 +356,6 @@
|
|||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Reference"
|
"label": "Reference"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"depends_on": "eval:doc.docstatus==0",
|
|
||||||
"fieldname": "get_outstanding_invoice",
|
|
||||||
"fieldtype": "Button",
|
|
||||||
"label": "Get Outstanding Invoice"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "references",
|
"fieldname": "references",
|
||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
@@ -728,12 +723,24 @@
|
|||||||
"fieldname": "section_break_60",
|
"fieldname": "section_break_60",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"hide_border": 1
|
"hide_border": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:doc.docstatus==0",
|
||||||
|
"fieldname": "get_outstanding_invoices",
|
||||||
|
"fieldtype": "Button",
|
||||||
|
"label": "Get Outstanding Invoices"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:doc.docstatus==0",
|
||||||
|
"fieldname": "get_outstanding_orders",
|
||||||
|
"fieldtype": "Button",
|
||||||
|
"label": "Get Outstanding Orders"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-02-14 04:52:30.478523",
|
"modified": "2023-06-19 11:38:04.387219",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Entry",
|
"name": "Payment Entry",
|
||||||
|
|||||||
@@ -172,6 +172,8 @@ class PaymentEntry(AccountsController):
|
|||||||
"payment_type": self.payment_type,
|
"payment_type": self.payment_type,
|
||||||
"party": self.party,
|
"party": self.party,
|
||||||
"party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to,
|
"party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to,
|
||||||
|
"get_outstanding_invoices": True,
|
||||||
|
"get_orders_to_be_billed": True,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -196,7 +198,7 @@ class PaymentEntry(AccountsController):
|
|||||||
):
|
):
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_(
|
_(
|
||||||
"{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' button to get the latest outstanding amount."
|
"{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts."
|
||||||
).format(d.reference_doctype, d.reference_name)
|
).format(d.reference_doctype, d.reference_name)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1356,62 +1358,75 @@ def get_outstanding_reference_documents(args):
|
|||||||
condition += " and company = {0}".format(frappe.db.escape(args.get("company")))
|
condition += " and company = {0}".format(frappe.db.escape(args.get("company")))
|
||||||
common_filter.append(ple.company == args.get("company"))
|
common_filter.append(ple.company == args.get("company"))
|
||||||
|
|
||||||
outstanding_invoices = get_outstanding_invoices(
|
outstanding_invoices = []
|
||||||
args.get("party_type"),
|
|
||||||
args.get("party"),
|
|
||||||
args.get("party_account"),
|
|
||||||
common_filter=common_filter,
|
|
||||||
posting_date=posting_and_due_date,
|
|
||||||
min_outstanding=args.get("outstanding_amt_greater_than"),
|
|
||||||
max_outstanding=args.get("outstanding_amt_less_than"),
|
|
||||||
accounting_dimensions=accounting_dimensions_filter,
|
|
||||||
)
|
|
||||||
|
|
||||||
outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices)
|
|
||||||
|
|
||||||
for d in outstanding_invoices:
|
|
||||||
d["exchange_rate"] = 1
|
|
||||||
if party_account_currency != company_currency:
|
|
||||||
if d.voucher_type in frappe.get_hooks("invoice_doctypes"):
|
|
||||||
d["exchange_rate"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "conversion_rate")
|
|
||||||
elif d.voucher_type == "Journal Entry":
|
|
||||||
d["exchange_rate"] = get_exchange_rate(
|
|
||||||
party_account_currency, company_currency, d.posting_date
|
|
||||||
)
|
|
||||||
if d.voucher_type in ("Purchase Invoice"):
|
|
||||||
d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no")
|
|
||||||
|
|
||||||
# Get all SO / PO which are not fully billed or against which full advance not paid
|
|
||||||
orders_to_be_billed = []
|
|
||||||
orders_to_be_billed = get_orders_to_be_billed(
|
|
||||||
args.get("posting_date"),
|
|
||||||
args.get("party_type"),
|
|
||||||
args.get("party"),
|
|
||||||
args.get("company"),
|
|
||||||
party_account_currency,
|
|
||||||
company_currency,
|
|
||||||
filters=args,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get negative outstanding sales /purchase invoices
|
|
||||||
negative_outstanding_invoices = []
|
negative_outstanding_invoices = []
|
||||||
if args.get("party_type") != "Employee" and not args.get("voucher_no"):
|
|
||||||
negative_outstanding_invoices = get_negative_outstanding_invoices(
|
if args.get("get_outstanding_invoices"):
|
||||||
|
outstanding_invoices = get_outstanding_invoices(
|
||||||
args.get("party_type"),
|
args.get("party_type"),
|
||||||
args.get("party"),
|
args.get("party"),
|
||||||
args.get("party_account"),
|
args.get("party_account"),
|
||||||
|
common_filter=common_filter,
|
||||||
|
posting_date=posting_and_due_date,
|
||||||
|
min_outstanding=args.get("outstanding_amt_greater_than"),
|
||||||
|
max_outstanding=args.get("outstanding_amt_less_than"),
|
||||||
|
accounting_dimensions=accounting_dimensions_filter,
|
||||||
|
)
|
||||||
|
|
||||||
|
outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices)
|
||||||
|
|
||||||
|
for d in outstanding_invoices:
|
||||||
|
d["exchange_rate"] = 1
|
||||||
|
if party_account_currency != company_currency:
|
||||||
|
if d.voucher_type in frappe.get_hooks("invoice_doctypes"):
|
||||||
|
d["exchange_rate"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "conversion_rate")
|
||||||
|
elif d.voucher_type == "Journal Entry":
|
||||||
|
d["exchange_rate"] = get_exchange_rate(
|
||||||
|
party_account_currency, company_currency, d.posting_date
|
||||||
|
)
|
||||||
|
if d.voucher_type in ("Purchase Invoice"):
|
||||||
|
d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no")
|
||||||
|
|
||||||
|
# Get negative outstanding sales /purchase invoices
|
||||||
|
if args.get("party_type") != "Employee" and not args.get("voucher_no"):
|
||||||
|
negative_outstanding_invoices = get_negative_outstanding_invoices(
|
||||||
|
args.get("party_type"),
|
||||||
|
args.get("party"),
|
||||||
|
args.get("party_account"),
|
||||||
|
party_account_currency,
|
||||||
|
company_currency,
|
||||||
|
condition=condition,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get all SO / PO which are not fully billed or against which full advance not paid
|
||||||
|
orders_to_be_billed = []
|
||||||
|
if args.get("get_orders_to_be_billed"):
|
||||||
|
orders_to_be_billed = get_orders_to_be_billed(
|
||||||
|
args.get("posting_date"),
|
||||||
|
args.get("party_type"),
|
||||||
|
args.get("party"),
|
||||||
|
args.get("company"),
|
||||||
party_account_currency,
|
party_account_currency,
|
||||||
company_currency,
|
company_currency,
|
||||||
condition=condition,
|
filters=args,
|
||||||
)
|
)
|
||||||
|
|
||||||
data = negative_outstanding_invoices + outstanding_invoices + orders_to_be_billed
|
data = negative_outstanding_invoices + outstanding_invoices + orders_to_be_billed
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
|
if args.get("get_outstanding_invoices") and args.get("get_orders_to_be_billed"):
|
||||||
|
ref_document_type = "invoices or orders"
|
||||||
|
elif args.get("get_outstanding_invoices"):
|
||||||
|
ref_document_type = "invoices"
|
||||||
|
elif args.get("get_orders_to_be_billed"):
|
||||||
|
ref_document_type = "orders"
|
||||||
|
|
||||||
frappe.msgprint(
|
frappe.msgprint(
|
||||||
_(
|
_(
|
||||||
"No outstanding invoices found for the {0} {1} which qualify the filters you have specified."
|
"No outstanding {0} found for the {1} {2} which qualify the filters you have specified."
|
||||||
).format(_(args.get("party_type")).lower(), frappe.bold(args.get("party")))
|
).format(
|
||||||
|
ref_document_type, _(args.get("party_type")).lower(), frappe.bold(args.get("party"))
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|||||||
@@ -637,13 +637,6 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
|||||||
gle_filters={"account": "Stock In Hand - TCP1"},
|
gle_filters={"account": "Stock In Hand - TCP1"},
|
||||||
)
|
)
|
||||||
|
|
||||||
# assert loss booked in COGS
|
|
||||||
self.assertGLEs(
|
|
||||||
return_pi,
|
|
||||||
[{"credit": 0, "debit": 200}],
|
|
||||||
gle_filters={"account": "Cost of Goods Sold - TCP1"},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_return_with_lcv(self):
|
def test_return_with_lcv(self):
|
||||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||||
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
|
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
|
||||||
@@ -1662,6 +1655,21 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
|||||||
|
|
||||||
self.assertTrue(return_pi.docstatus == 1)
|
self.assertTrue(return_pi.docstatus == 1)
|
||||||
|
|
||||||
|
def test_gl_entries_for_standalone_debit_note(self):
|
||||||
|
make_purchase_invoice(qty=5, rate=500, update_stock=True)
|
||||||
|
|
||||||
|
returned_inv = make_purchase_invoice(qty=-5, rate=5, update_stock=True, is_return=True)
|
||||||
|
|
||||||
|
# override the rate with valuation rate
|
||||||
|
sle = frappe.get_all(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
fields=["stock_value_difference", "actual_qty"],
|
||||||
|
filters={"voucher_no": returned_inv.name},
|
||||||
|
)[0]
|
||||||
|
|
||||||
|
rate = flt(sle.stock_value_difference) / flt(sle.actual_qty)
|
||||||
|
self.assertAlmostEqual(returned_inv.items[0].rate, rate)
|
||||||
|
|
||||||
|
|
||||||
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||||
gl_entries = frappe.db.sql(
|
gl_entries = frappe.db.sql(
|
||||||
|
|||||||
@@ -1012,10 +1012,16 @@ class SalesInvoice(SellingController):
|
|||||||
|
|
||||||
def check_prev_docstatus(self):
|
def check_prev_docstatus(self):
|
||||||
for d in self.get("items"):
|
for d in self.get("items"):
|
||||||
if d.sales_order and frappe.db.get_value("Sales Order", d.sales_order, "docstatus") != 1:
|
if (
|
||||||
|
d.sales_order
|
||||||
|
and frappe.db.get_value("Sales Order", d.sales_order, "docstatus", cache=True) != 1
|
||||||
|
):
|
||||||
frappe.throw(_("Sales Order {0} is not submitted").format(d.sales_order))
|
frappe.throw(_("Sales Order {0} is not submitted").format(d.sales_order))
|
||||||
|
|
||||||
if d.delivery_note and frappe.db.get_value("Delivery Note", d.delivery_note, "docstatus") != 1:
|
if (
|
||||||
|
d.delivery_note
|
||||||
|
and frappe.db.get_value("Delivery Note", d.delivery_note, "docstatus", cache=True) != 1
|
||||||
|
):
|
||||||
throw(_("Delivery Note {0} is not submitted").format(d.delivery_note))
|
throw(_("Delivery Note {0} is not submitted").format(d.delivery_note))
|
||||||
|
|
||||||
def make_gl_entries(self, gl_entries=None, from_repost=False):
|
def make_gl_entries(self, gl_entries=None, from_repost=False):
|
||||||
|
|||||||
@@ -483,18 +483,22 @@ def get_gl_entries_on_asset_disposal(
|
|||||||
},
|
},
|
||||||
item=asset,
|
item=asset,
|
||||||
),
|
),
|
||||||
asset.get_gl_dict(
|
|
||||||
{
|
|
||||||
"account": accumulated_depr_account,
|
|
||||||
"debit_in_account_currency": accumulated_depr_amount,
|
|
||||||
"debit": accumulated_depr_amount,
|
|
||||||
"cost_center": depreciation_cost_center,
|
|
||||||
"posting_date": date,
|
|
||||||
},
|
|
||||||
item=asset,
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if accumulated_depr_amount:
|
||||||
|
gl_entries.append(
|
||||||
|
asset.get_gl_dict(
|
||||||
|
{
|
||||||
|
"account": accumulated_depr_account,
|
||||||
|
"debit_in_account_currency": accumulated_depr_amount,
|
||||||
|
"debit": accumulated_depr_amount,
|
||||||
|
"cost_center": depreciation_cost_center,
|
||||||
|
"posting_date": date,
|
||||||
|
},
|
||||||
|
item=asset,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
profit_amount = flt(selling_amount) - flt(value_after_depreciation)
|
profit_amount = flt(selling_amount) - flt(value_after_depreciation)
|
||||||
if profit_amount:
|
if profit_amount:
|
||||||
get_profit_gl_entries(
|
get_profit_gl_entries(
|
||||||
|
|||||||
@@ -19,56 +19,6 @@ frappe.query_reports["Fixed Asset Register"] = {
|
|||||||
options: "\nIn Location\nDisposed",
|
options: "\nIn Location\nDisposed",
|
||||||
default: 'In Location'
|
default: 'In Location'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname":"filter_based_on",
|
|
||||||
"label": __("Period Based On"),
|
|
||||||
"fieldtype": "Select",
|
|
||||||
"options": ["Fiscal Year", "Date Range"],
|
|
||||||
"default": "Fiscal Year",
|
|
||||||
"reqd": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname":"from_date",
|
|
||||||
"label": __("Start Date"),
|
|
||||||
"fieldtype": "Date",
|
|
||||||
"default": frappe.datetime.add_months(frappe.datetime.nowdate(), -12),
|
|
||||||
"depends_on": "eval: doc.filter_based_on == 'Date Range'",
|
|
||||||
"reqd": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname":"to_date",
|
|
||||||
"label": __("End Date"),
|
|
||||||
"fieldtype": "Date",
|
|
||||||
"default": frappe.datetime.nowdate(),
|
|
||||||
"depends_on": "eval: doc.filter_based_on == 'Date Range'",
|
|
||||||
"reqd": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname":"from_fiscal_year",
|
|
||||||
"label": __("Start Year"),
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"options": "Fiscal Year",
|
|
||||||
"default": frappe.defaults.get_user_default("fiscal_year"),
|
|
||||||
"depends_on": "eval: doc.filter_based_on == 'Fiscal Year'",
|
|
||||||
"reqd": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname":"to_fiscal_year",
|
|
||||||
"label": __("End Year"),
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"options": "Fiscal Year",
|
|
||||||
"default": frappe.defaults.get_user_default("fiscal_year"),
|
|
||||||
"depends_on": "eval: doc.filter_based_on == 'Fiscal Year'",
|
|
||||||
"reqd": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname":"date_based_on",
|
|
||||||
"label": __("Date Based On"),
|
|
||||||
"fieldtype": "Select",
|
|
||||||
"options": ["Purchase Date", "Available For Use Date"],
|
|
||||||
"default": "Purchase Date",
|
|
||||||
"reqd": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
fieldname:"asset_category",
|
fieldname:"asset_category",
|
||||||
label: __("Asset Category"),
|
label: __("Asset Category"),
|
||||||
@@ -89,22 +39,67 @@ frappe.query_reports["Fixed Asset Register"] = {
|
|||||||
default: "--Select a group--",
|
default: "--Select a group--",
|
||||||
reqd: 1
|
reqd: 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
fieldname:"finance_book",
|
|
||||||
label: __("Finance Book"),
|
|
||||||
fieldtype: "Link",
|
|
||||||
options: "Finance Book",
|
|
||||||
depends_on: "eval: doc.filter_by_finance_book == 1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname:"filter_by_finance_book",
|
|
||||||
label: __("Filter by Finance Book"),
|
|
||||||
fieldtype: "Check"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
fieldname:"only_existing_assets",
|
fieldname:"only_existing_assets",
|
||||||
label: __("Only existing assets"),
|
label: __("Only existing assets"),
|
||||||
fieldtype: "Check"
|
fieldtype: "Check"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fieldname:"finance_book",
|
||||||
|
label: __("Finance Book"),
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Finance Book",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "include_default_book_assets",
|
||||||
|
"label": __("Include Default Book Assets"),
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"default": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"filter_based_on",
|
||||||
|
"label": __("Period Based On"),
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"options": ["--Select a period--", "Fiscal Year", "Date Range"],
|
||||||
|
"default": "--Select a period--",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"from_date",
|
||||||
|
"label": __("Start Date"),
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"default": frappe.datetime.add_months(frappe.datetime.nowdate(), -12),
|
||||||
|
"depends_on": "eval: doc.filter_based_on == 'Date Range'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"to_date",
|
||||||
|
"label": __("End Date"),
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"default": frappe.datetime.nowdate(),
|
||||||
|
"depends_on": "eval: doc.filter_based_on == 'Date Range'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"from_fiscal_year",
|
||||||
|
"label": __("Start Year"),
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Fiscal Year",
|
||||||
|
"default": frappe.defaults.get_user_default("fiscal_year"),
|
||||||
|
"depends_on": "eval: doc.filter_based_on == 'Fiscal Year'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"to_fiscal_year",
|
||||||
|
"label": __("End Year"),
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Fiscal Year",
|
||||||
|
"default": frappe.defaults.get_user_default("fiscal_year"),
|
||||||
|
"depends_on": "eval: doc.filter_based_on == 'Fiscal Year'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"date_based_on",
|
||||||
|
"label": __("Date Based On"),
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"options": ["Purchase Date", "Available For Use Date"],
|
||||||
|
"default": "Purchase Date",
|
||||||
|
"depends_on": "eval: doc.filter_based_on == 'Date Range' || doc.filter_based_on == 'Fiscal Year'",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.query_builder.functions import Sum
|
from frappe.query_builder.functions import IfNull, Sum
|
||||||
from frappe.utils import cstr, flt, formatdate, getdate
|
from frappe.utils import cstr, flt, formatdate, getdate
|
||||||
|
|
||||||
from erpnext.accounts.report.financial_statements import (
|
from erpnext.accounts.report.financial_statements import (
|
||||||
@@ -13,7 +15,6 @@ from erpnext.accounts.report.financial_statements import (
|
|||||||
validate_fiscal_year,
|
validate_fiscal_year,
|
||||||
)
|
)
|
||||||
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
|
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
|
||||||
from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts
|
|
||||||
|
|
||||||
|
|
||||||
def execute(filters=None):
|
def execute(filters=None):
|
||||||
@@ -64,11 +65,9 @@ def get_conditions(filters):
|
|||||||
|
|
||||||
|
|
||||||
def get_data(filters):
|
def get_data(filters):
|
||||||
|
|
||||||
data = []
|
data = []
|
||||||
|
|
||||||
conditions = get_conditions(filters)
|
conditions = get_conditions(filters)
|
||||||
depreciation_amount_map = get_finance_book_value_map(filters)
|
|
||||||
pr_supplier_map = get_purchase_receipt_supplier_map()
|
pr_supplier_map = get_purchase_receipt_supplier_map()
|
||||||
pi_supplier_map = get_purchase_invoice_supplier_map()
|
pi_supplier_map = get_purchase_invoice_supplier_map()
|
||||||
|
|
||||||
@@ -102,20 +101,27 @@ def get_data(filters):
|
|||||||
]
|
]
|
||||||
assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields)
|
assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields)
|
||||||
|
|
||||||
assets_linked_to_fb = None
|
assets_linked_to_fb = get_assets_linked_to_fb(filters)
|
||||||
|
|
||||||
if filters.filter_by_finance_book:
|
company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book")
|
||||||
assets_linked_to_fb = frappe.db.get_all(
|
|
||||||
doctype="Asset Finance Book",
|
if filters.include_default_book_assets and company_fb:
|
||||||
filters={"finance_book": filters.finance_book or ("is", "not set")},
|
finance_book = company_fb
|
||||||
pluck="parent",
|
elif filters.finance_book:
|
||||||
)
|
finance_book = filters.finance_book
|
||||||
|
else:
|
||||||
|
finance_book = None
|
||||||
|
|
||||||
|
depreciation_amount_map = get_asset_depreciation_amount_map(filters, finance_book)
|
||||||
|
|
||||||
for asset in assets_record:
|
for asset in assets_record:
|
||||||
if assets_linked_to_fb and asset.asset_id not in assets_linked_to_fb:
|
if assets_linked_to_fb and asset.asset_id not in assets_linked_to_fb:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
asset_value = get_asset_value_after_depreciation(asset.asset_id, filters.finance_book)
|
asset_value = get_asset_value_after_depreciation(
|
||||||
|
asset.asset_id, finance_book
|
||||||
|
) or get_asset_value_after_depreciation(asset.asset_id)
|
||||||
|
|
||||||
row = {
|
row = {
|
||||||
"asset_id": asset.asset_id,
|
"asset_id": asset.asset_id,
|
||||||
"asset_name": asset.asset_name,
|
"asset_name": asset.asset_name,
|
||||||
@@ -126,7 +132,7 @@ def get_data(filters):
|
|||||||
or pi_supplier_map.get(asset.purchase_invoice),
|
or pi_supplier_map.get(asset.purchase_invoice),
|
||||||
"gross_purchase_amount": asset.gross_purchase_amount,
|
"gross_purchase_amount": asset.gross_purchase_amount,
|
||||||
"opening_accumulated_depreciation": asset.opening_accumulated_depreciation,
|
"opening_accumulated_depreciation": asset.opening_accumulated_depreciation,
|
||||||
"depreciated_amount": get_depreciation_amount_of_asset(asset, depreciation_amount_map, filters),
|
"depreciated_amount": get_depreciation_amount_of_asset(asset, depreciation_amount_map),
|
||||||
"available_for_use_date": asset.available_for_use_date,
|
"available_for_use_date": asset.available_for_use_date,
|
||||||
"location": asset.location,
|
"location": asset.location,
|
||||||
"asset_category": asset.asset_category,
|
"asset_category": asset.asset_category,
|
||||||
@@ -140,14 +146,23 @@ def get_data(filters):
|
|||||||
|
|
||||||
def prepare_chart_data(data, filters):
|
def prepare_chart_data(data, filters):
|
||||||
labels_values_map = {}
|
labels_values_map = {}
|
||||||
date_field = frappe.scrub(filters.date_based_on)
|
if filters.filter_based_on not in ("Date Range", "Fiscal Year"):
|
||||||
|
filters_filter_based_on = "Date Range"
|
||||||
|
date_field = "purchase_date"
|
||||||
|
filters_from_date = min(data, key=lambda a: a.get(date_field)).get(date_field)
|
||||||
|
filters_to_date = max(data, key=lambda a: a.get(date_field)).get(date_field)
|
||||||
|
else:
|
||||||
|
filters_filter_based_on = filters.filter_based_on
|
||||||
|
date_field = frappe.scrub(filters.date_based_on)
|
||||||
|
filters_from_date = filters.from_date
|
||||||
|
filters_to_date = filters.to_date
|
||||||
|
|
||||||
period_list = get_period_list(
|
period_list = get_period_list(
|
||||||
filters.from_fiscal_year,
|
filters.from_fiscal_year,
|
||||||
filters.to_fiscal_year,
|
filters.to_fiscal_year,
|
||||||
filters.from_date,
|
filters_from_date,
|
||||||
filters.to_date,
|
filters_to_date,
|
||||||
filters.filter_based_on,
|
filters_filter_based_on,
|
||||||
"Monthly",
|
"Monthly",
|
||||||
company=filters.company,
|
company=filters.company,
|
||||||
ignore_fiscal_year=True,
|
ignore_fiscal_year=True,
|
||||||
@@ -184,57 +199,76 @@ def prepare_chart_data(data, filters):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_depreciation_amount_of_asset(asset, depreciation_amount_map, filters):
|
def get_assets_linked_to_fb(filters):
|
||||||
if asset.calculate_depreciation:
|
afb = frappe.qb.DocType("Asset Finance Book")
|
||||||
depr_amount = depreciation_amount_map.get(asset.asset_id) or 0.0
|
|
||||||
else:
|
|
||||||
depr_amount = get_manual_depreciation_amount_of_asset(asset, filters)
|
|
||||||
|
|
||||||
return flt(depr_amount, 2)
|
query = frappe.qb.from_(afb).select(
|
||||||
|
afb.parent,
|
||||||
|
|
||||||
def get_finance_book_value_map(filters):
|
|
||||||
date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date
|
|
||||||
|
|
||||||
return frappe._dict(
|
|
||||||
frappe.db.sql(
|
|
||||||
""" Select
|
|
||||||
parent, SUM(depreciation_amount)
|
|
||||||
FROM `tabDepreciation Schedule`
|
|
||||||
WHERE
|
|
||||||
parentfield='schedules'
|
|
||||||
AND schedule_date<=%s
|
|
||||||
AND journal_entry IS NOT NULL
|
|
||||||
AND ifnull(finance_book, '')=%s
|
|
||||||
GROUP BY parent""",
|
|
||||||
(date, cstr(filters.finance_book or "")),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if filters.include_default_book_assets:
|
||||||
|
company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book")
|
||||||
|
|
||||||
def get_manual_depreciation_amount_of_asset(asset, filters):
|
if filters.finance_book and company_fb and cstr(filters.finance_book) != cstr(company_fb):
|
||||||
|
frappe.throw(_("To use a different finance book, please uncheck 'Include Default Book Assets'"))
|
||||||
|
|
||||||
|
query = query.where(
|
||||||
|
(afb.finance_book.isin([cstr(filters.finance_book), cstr(company_fb), ""]))
|
||||||
|
| (afb.finance_book.isnull())
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
query = query.where(
|
||||||
|
(afb.finance_book.isin([cstr(filters.finance_book), ""])) | (afb.finance_book.isnull())
|
||||||
|
)
|
||||||
|
|
||||||
|
assets_linked_to_fb = list(chain(*query.run(as_list=1)))
|
||||||
|
|
||||||
|
return assets_linked_to_fb
|
||||||
|
|
||||||
|
|
||||||
|
def get_depreciation_amount_of_asset(asset, depreciation_amount_map):
|
||||||
|
return depreciation_amount_map.get(asset.asset_id) or 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def get_asset_depreciation_amount_map(filters, finance_book):
|
||||||
date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date
|
date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date
|
||||||
|
|
||||||
(_, _, depreciation_expense_account) = get_depreciation_accounts(asset)
|
asset = frappe.qb.DocType("Asset")
|
||||||
|
|
||||||
gle = frappe.qb.DocType("GL Entry")
|
gle = frappe.qb.DocType("GL Entry")
|
||||||
|
aca = frappe.qb.DocType("Asset Category Account")
|
||||||
|
company = frappe.qb.DocType("Company")
|
||||||
|
|
||||||
result = (
|
query = (
|
||||||
frappe.qb.from_(gle)
|
frappe.qb.from_(gle)
|
||||||
.select(Sum(gle.debit))
|
.join(asset)
|
||||||
.where(gle.against_voucher == asset.asset_id)
|
.on(gle.against_voucher == asset.name)
|
||||||
.where(gle.account == depreciation_expense_account)
|
.join(aca)
|
||||||
|
.on((aca.parent == asset.asset_category) & (aca.company_name == asset.company))
|
||||||
|
.join(company)
|
||||||
|
.on(company.name == asset.company)
|
||||||
|
.select(asset.name.as_("asset"), Sum(gle.debit).as_("depreciation_amount"))
|
||||||
|
.where(
|
||||||
|
gle.account == IfNull(aca.depreciation_expense_account, company.depreciation_expense_account)
|
||||||
|
)
|
||||||
.where(gle.debit != 0)
|
.where(gle.debit != 0)
|
||||||
.where(gle.is_cancelled == 0)
|
.where(gle.is_cancelled == 0)
|
||||||
.where(gle.posting_date <= date)
|
.where(asset.docstatus == 1)
|
||||||
).run()
|
.groupby(asset.name)
|
||||||
|
)
|
||||||
|
|
||||||
if result and result[0] and result[0][0]:
|
if finance_book:
|
||||||
depr_amount = result[0][0]
|
query = query.where(
|
||||||
|
(gle.finance_book.isin([cstr(finance_book), ""])) | (gle.finance_book.isnull())
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
depr_amount = 0
|
query = query.where((gle.finance_book.isin([""])) | (gle.finance_book.isnull()))
|
||||||
|
|
||||||
return depr_amount
|
if filters.filter_based_on in ("Date Range", "Fiscal Year"):
|
||||||
|
query = query.where(gle.posting_date <= date)
|
||||||
|
|
||||||
|
asset_depr_amount_map = query.run()
|
||||||
|
|
||||||
|
return dict(asset_depr_amount_map)
|
||||||
|
|
||||||
|
|
||||||
def get_purchase_receipt_supplier_map():
|
def get_purchase_receipt_supplier_map():
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ class BuyingController(SubcontractingController):
|
|||||||
return _("From {0} | {1} {2}").format(self.supplier_name, self.currency, self.grand_total)
|
return _("From {0} | {1} {2}").format(self.supplier_name, self.currency, self.grand_total)
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
|
self.set_rate_for_standalone_debit_note()
|
||||||
|
|
||||||
super(BuyingController, self).validate()
|
super(BuyingController, self).validate()
|
||||||
if getattr(self, "supplier", None) and not self.supplier_name:
|
if getattr(self, "supplier", None) and not self.supplier_name:
|
||||||
self.supplier_name = frappe.db.get_value("Supplier", self.supplier, "supplier_name")
|
self.supplier_name = frappe.db.get_value("Supplier", self.supplier, "supplier_name")
|
||||||
@@ -72,6 +74,30 @@ class BuyingController(SubcontractingController):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def set_rate_for_standalone_debit_note(self):
|
||||||
|
if self.get("is_return") and self.get("update_stock") and not self.return_against:
|
||||||
|
for row in self.items:
|
||||||
|
|
||||||
|
# override the rate with valuation rate
|
||||||
|
row.rate = get_incoming_rate(
|
||||||
|
{
|
||||||
|
"item_code": row.item_code,
|
||||||
|
"warehouse": row.warehouse,
|
||||||
|
"posting_date": self.get("posting_date"),
|
||||||
|
"posting_time": self.get("posting_time"),
|
||||||
|
"qty": row.qty,
|
||||||
|
"serial_and_batch_bundle": row.get("serial_and_batch_bundle"),
|
||||||
|
"company": self.company,
|
||||||
|
"voucher_type": self.doctype,
|
||||||
|
"voucher_no": self.name,
|
||||||
|
},
|
||||||
|
raise_error_if_no_rate=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
row.discount_percentage = 0.0
|
||||||
|
row.discount_amount = 0.0
|
||||||
|
row.margin_rate_or_amount = 0.0
|
||||||
|
|
||||||
def set_missing_values(self, for_validate=False):
|
def set_missing_values(self, for_validate=False):
|
||||||
super(BuyingController, self).set_missing_values(for_validate)
|
super(BuyingController, self).set_missing_values(for_validate)
|
||||||
|
|
||||||
@@ -445,7 +471,7 @@ class BuyingController(SubcontractingController):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if d.warehouse:
|
if d.warehouse:
|
||||||
pr_qty = flt(d.qty) * flt(d.conversion_factor)
|
pr_qty = flt(flt(d.qty) * flt(d.conversion_factor), d.precision("stock_qty"))
|
||||||
|
|
||||||
if pr_qty:
|
if pr_qty:
|
||||||
|
|
||||||
@@ -507,7 +533,7 @@ class BuyingController(SubcontractingController):
|
|||||||
d,
|
d,
|
||||||
{
|
{
|
||||||
"warehouse": d.rejected_warehouse,
|
"warehouse": d.rejected_warehouse,
|
||||||
"actual_qty": flt(d.rejected_qty) * flt(d.conversion_factor),
|
"actual_qty": flt(flt(d.rejected_qty) * flt(d.conversion_factor), d.precision("stock_qty")),
|
||||||
"serial_no": cstr(d.rejected_serial_no).strip(),
|
"serial_no": cstr(d.rejected_serial_no).strip(),
|
||||||
"incoming_rate": 0.0,
|
"incoming_rate": 0.0,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -617,6 +617,9 @@ def get_filters(
|
|||||||
if reference_voucher_detail_no:
|
if reference_voucher_detail_no:
|
||||||
filters["voucher_detail_no"] = reference_voucher_detail_no
|
filters["voucher_detail_no"] = reference_voucher_detail_no
|
||||||
|
|
||||||
|
if item_row and item_row.get("warehouse"):
|
||||||
|
filters["warehouse"] = item_row.get("warehouse")
|
||||||
|
|
||||||
return filters
|
return filters
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
import frappe.defaults
|
import frappe.defaults
|
||||||
from frappe import _, throw
|
from frappe import _, bold, throw
|
||||||
from frappe.contacts.doctype.address.address import get_address_display
|
from frappe.contacts.doctype.address.address import get_address_display
|
||||||
from frappe.contacts.doctype.contact.contact import get_contact_name
|
from frappe.contacts.doctype.contact.contact import get_contact_name
|
||||||
from frappe.utils import cint, cstr, flt, get_fullname
|
from frappe.utils import cint, cstr, flt, get_fullname
|
||||||
@@ -201,6 +201,11 @@ def get_shopping_cart_menu(context=None):
|
|||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def add_new_address(doc):
|
def add_new_address(doc):
|
||||||
doc = frappe.parse_json(doc)
|
doc = frappe.parse_json(doc)
|
||||||
|
address_title = doc.get("address_title")
|
||||||
|
if frappe.db.exists("Address", {"address_title": address_title}):
|
||||||
|
msg = f"The address with the title {bold(address_title)} already exists. Please change the title accordingly."
|
||||||
|
frappe.throw(_(msg), title=_("Address Already Exists"))
|
||||||
|
|
||||||
doc.update({"doctype": "Address"})
|
doc.update({"doctype": "Address"})
|
||||||
address = frappe.get_doc(doc)
|
address = frappe.get_doc(doc)
|
||||||
address.save(ignore_permissions=True)
|
address.save(ignore_permissions=True)
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ def get_next_attribute_and_values(item_code, selected_attributes):
|
|||||||
product_info = get_item_variant_price_dict(exact_match[0], cart_settings)
|
product_info = get_item_variant_price_dict(exact_match[0], cart_settings)
|
||||||
|
|
||||||
if product_info:
|
if product_info:
|
||||||
|
product_info["is_stock_item"] = frappe.get_cached_value("Item", exact_match[0], "is_stock_item")
|
||||||
product_info["allow_items_not_in_stock"] = cint(cart_settings.allow_items_not_in_stock)
|
product_info["allow_items_not_in_stock"] = cint(cart_settings.allow_items_not_in_stock)
|
||||||
else:
|
else:
|
||||||
product_info = None
|
product_info = None
|
||||||
|
|||||||
@@ -290,8 +290,8 @@ def get_last_accrual_date(loan, posting_date):
|
|||||||
# interest for last interest accrual date is already booked, so add 1 day
|
# interest for last interest accrual date is already booked, so add 1 day
|
||||||
last_disbursement_date = get_last_disbursement_date(loan, posting_date)
|
last_disbursement_date = get_last_disbursement_date(loan, posting_date)
|
||||||
|
|
||||||
if last_disbursement_date and getdate(last_disbursement_date) > getdate(
|
if last_disbursement_date and getdate(last_disbursement_date) > add_days(
|
||||||
last_interest_accrual_date
|
getdate(last_interest_accrual_date), 1
|
||||||
):
|
):
|
||||||
last_interest_accrual_date = last_disbursement_date
|
last_interest_accrual_date = last_disbursement_date
|
||||||
|
|
||||||
|
|||||||
@@ -1657,6 +1657,61 @@ class TestWorkOrder(FrappeTestCase):
|
|||||||
job_card2.time_logs = []
|
job_card2.time_logs = []
|
||||||
job_card2.save()
|
job_card2.save()
|
||||||
|
|
||||||
|
def test_make_serial_no_batch_from_work_order_for_serial_no(self):
|
||||||
|
item_code = "Test Serial No Item For Work Order"
|
||||||
|
warehouse = "_Test Warehouse - _TC"
|
||||||
|
raw_materials = [
|
||||||
|
"Test RM Item 1 for Serial No Item In Work Order",
|
||||||
|
]
|
||||||
|
|
||||||
|
make_item(
|
||||||
|
item_code,
|
||||||
|
{
|
||||||
|
"has_stock_item": 1,
|
||||||
|
"has_serial_no": 1,
|
||||||
|
"serial_no_series": "TSNIFWO-.#####",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
for rm_item in raw_materials:
|
||||||
|
make_item(
|
||||||
|
rm_item,
|
||||||
|
{
|
||||||
|
"has_stock_item": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
test_stock_entry.make_stock_entry(item_code=rm_item, target=warehouse, qty=10, basic_rate=100)
|
||||||
|
|
||||||
|
bom = make_bom(item=item_code, raw_materials=raw_materials)
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order", 1)
|
||||||
|
|
||||||
|
wo_order = make_wo_order_test_record(
|
||||||
|
item=item_code,
|
||||||
|
bom_no=bom.name,
|
||||||
|
qty=5,
|
||||||
|
skip_transfer=1,
|
||||||
|
from_wip_warehouse=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
serial_nos = frappe.get_all(
|
||||||
|
"Serial No",
|
||||||
|
filters={"item_code": item_code, "work_order": wo_order.name},
|
||||||
|
)
|
||||||
|
|
||||||
|
serial_nos = [d.name for d in serial_nos]
|
||||||
|
self.assertEqual(len(serial_nos), 5)
|
||||||
|
|
||||||
|
stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 5))
|
||||||
|
|
||||||
|
stock_entry.submit()
|
||||||
|
for row in stock_entry.items:
|
||||||
|
if row.is_finished_item:
|
||||||
|
self.assertEqual(sorted(get_serial_nos(row.serial_no)), sorted(get_serial_nos(serial_nos)))
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order", 0)
|
||||||
|
|
||||||
|
|
||||||
def prepare_data_for_workstation_type_check():
|
def prepare_data_for_workstation_type_check():
|
||||||
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
|
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
|
||||||
@@ -1886,6 +1941,7 @@ def make_wo_order_test_record(**args):
|
|||||||
wo_order.sales_order = args.sales_order or None
|
wo_order.sales_order = args.sales_order or None
|
||||||
wo_order.planned_start_date = args.planned_start_date or now()
|
wo_order.planned_start_date = args.planned_start_date or now()
|
||||||
wo_order.transfer_material_against = args.transfer_material_against or "Work Order"
|
wo_order.transfer_material_against = args.transfer_material_against or "Work Order"
|
||||||
|
wo_order.from_wip_warehouse = args.from_wip_warehouse or None
|
||||||
|
|
||||||
if args.source_warehouse:
|
if args.source_warehouse:
|
||||||
for item in wo_order.get("required_items"):
|
for item in wo_order.get("required_items"):
|
||||||
|
|||||||
@@ -33,10 +33,9 @@ def get_data(filters: Filters) -> Data:
|
|||||||
wo.name,
|
wo.name,
|
||||||
wo.status,
|
wo.status,
|
||||||
wo.production_item,
|
wo.production_item,
|
||||||
wo.qty,
|
|
||||||
wo.produced_qty,
|
wo.produced_qty,
|
||||||
wo.process_loss_qty,
|
wo.process_loss_qty,
|
||||||
(wo.produced_qty - wo.process_loss_qty).as_("actual_produced_qty"),
|
wo.qty.as_("qty_to_manufacture"),
|
||||||
Sum(se.total_incoming_value).as_("total_fg_value"),
|
Sum(se.total_incoming_value).as_("total_fg_value"),
|
||||||
Sum(se.total_outgoing_value).as_("total_rm_value"),
|
Sum(se.total_outgoing_value).as_("total_rm_value"),
|
||||||
)
|
)
|
||||||
@@ -44,6 +43,7 @@ def get_data(filters: Filters) -> Data:
|
|||||||
(wo.process_loss_qty > 0)
|
(wo.process_loss_qty > 0)
|
||||||
& (wo.company == filters.company)
|
& (wo.company == filters.company)
|
||||||
& (se.docstatus == 1)
|
& (se.docstatus == 1)
|
||||||
|
& (se.purpose == "Manufacture")
|
||||||
& (se.posting_date.between(filters.from_date, filters.to_date))
|
& (se.posting_date.between(filters.from_date, filters.to_date))
|
||||||
)
|
)
|
||||||
.groupby(se.work_order)
|
.groupby(se.work_order)
|
||||||
@@ -79,20 +79,30 @@ def get_columns() -> Columns:
|
|||||||
"width": "100",
|
"width": "100",
|
||||||
},
|
},
|
||||||
{"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": "100"},
|
{"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": "100"},
|
||||||
|
{
|
||||||
|
"label": _("Qty To Manufacture"),
|
||||||
|
"fieldname": "qty_to_manufacture",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"width": "150",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": _("Manufactured Qty"),
|
"label": _("Manufactured Qty"),
|
||||||
"fieldname": "produced_qty",
|
"fieldname": "produced_qty",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"width": "150",
|
"width": "150",
|
||||||
},
|
},
|
||||||
{"label": _("Loss Qty"), "fieldname": "process_loss_qty", "fieldtype": "Float", "width": "150"},
|
|
||||||
{
|
{
|
||||||
"label": _("Actual Manufactured Qty"),
|
"label": _("Process Loss Qty"),
|
||||||
"fieldname": "actual_produced_qty",
|
"fieldname": "process_loss_qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"width": "150",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": _("Process Loss Value"),
|
||||||
|
"fieldname": "total_pl_value",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"width": "150",
|
"width": "150",
|
||||||
},
|
},
|
||||||
{"label": _("Loss Value"), "fieldname": "total_pl_value", "fieldtype": "Float", "width": "150"},
|
|
||||||
{"label": _("FG Value"), "fieldname": "total_fg_value", "fieldtype": "Float", "width": "150"},
|
{"label": _("FG Value"), "fieldname": "total_fg_value", "fieldtype": "Float", "width": "150"},
|
||||||
{
|
{
|
||||||
"label": _("Raw Material Value"),
|
"label": _("Raw Material Value"),
|
||||||
@@ -105,5 +115,5 @@ def get_columns() -> Columns:
|
|||||||
|
|
||||||
def update_data_with_total_pl_value(data: Data) -> None:
|
def update_data_with_total_pl_value(data: Data) -> None:
|
||||||
for row in data:
|
for row in data:
|
||||||
value_per_unit_fg = row["total_fg_value"] / row["actual_produced_qty"]
|
value_per_unit_fg = row["total_fg_value"] / row["qty_to_manufacture"]
|
||||||
row["total_pl_value"] = row["process_loss_qty"] * value_per_unit_fg
|
row["total_pl_value"] = row["process_loss_qty"] * value_per_unit_fg
|
||||||
|
|||||||
@@ -333,3 +333,4 @@ erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
|||||||
erpnext.patches.v14_0.update_company_in_ldc
|
erpnext.patches.v14_0.update_company_in_ldc
|
||||||
erpnext.patches.v14_0.set_packed_qty_in_draft_delivery_notes
|
erpnext.patches.v14_0.set_packed_qty_in_draft_delivery_notes
|
||||||
erpnext.patches.v14_0.cleanup_workspaces
|
erpnext.patches.v14_0.cleanup_workspaces
|
||||||
|
erpnext.patches.v14_0.enable_allow_existing_serial_no
|
||||||
|
|||||||
9
erpnext/patches/v14_0/enable_allow_existing_serial_no.py
Normal file
9
erpnext/patches/v14_0/enable_allow_existing_serial_no.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.reload_doc("stock", "doctype", frappe.scrub("Stock Settings"))
|
||||||
|
frappe.db.set_single_value("Stock Settings", "allow_existing_serial_no", 1, update_modified=False)
|
||||||
@@ -123,6 +123,15 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
this.frm.set_query("batch_no", "items", function(doc, cdt, cdn) {
|
this.frm.set_query("batch_no", "items", function(doc, cdt, cdn) {
|
||||||
return me.set_query_for_batch(doc, cdt, cdn);
|
return me.set_query_for_batch(doc, cdt, cdn);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let batch_field = this.frm.get_docfield('items', 'batch_no');
|
||||||
|
if (batch_field) {
|
||||||
|
batch_field.get_route_options_for_new_doc = (row) => {
|
||||||
|
return {
|
||||||
|
'item': row.doc.item_code
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(
|
if(
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ def search_by_term(search_term, warehouse, price_list):
|
|||||||
)
|
)
|
||||||
|
|
||||||
item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse)
|
item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse)
|
||||||
item_stock_qty = item_stock_qty // item.get("conversion_factor")
|
|
||||||
item_stock_qty = item_stock_qty // item.get("conversion_factor", 1)
|
item_stock_qty = item_stock_qty // item.get("conversion_factor", 1)
|
||||||
item.update({"actual_qty": item_stock_qty})
|
item.update({"actual_qty": item_stock_qty})
|
||||||
|
|
||||||
@@ -59,7 +58,7 @@ def search_by_term(search_term, warehouse, price_list):
|
|||||||
"price_list": price_list,
|
"price_list": price_list,
|
||||||
"item_code": item_code,
|
"item_code": item_code,
|
||||||
},
|
},
|
||||||
fields=["uom", "stock_uom", "currency", "price_list_rate"],
|
fields=["uom", "currency", "price_list_rate"],
|
||||||
)
|
)
|
||||||
|
|
||||||
def __sort(p):
|
def __sort(p):
|
||||||
|
|||||||
@@ -1222,7 +1222,8 @@
|
|||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Pick List",
|
"label": "Pick List",
|
||||||
"options": "Pick List",
|
"options": "Pick List",
|
||||||
"read_only": 1
|
"read_only": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
@@ -1398,7 +1399,7 @@
|
|||||||
"idx": 146,
|
"idx": 146,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-04-21 11:15:23.931084",
|
"modified": "2023-06-16 14:58:55.066602",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Delivery Note",
|
"name": "Delivery Note",
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class PickList(Document):
|
|||||||
for location in self.get("locations"):
|
for location in self.get("locations"):
|
||||||
if (
|
if (
|
||||||
location.sales_order
|
location.sales_order
|
||||||
and frappe.db.get_value("Sales Order", location.sales_order, "per_picked") == 100
|
and frappe.db.get_value("Sales Order", location.sales_order, "per_picked", cache=True) == 100
|
||||||
):
|
):
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Row #{}: item {} has been picked already.").format(location.idx, location.item_code)
|
_("Row #{}: item {} has been picked already.").format(location.idx, location.item_code)
|
||||||
@@ -360,6 +360,7 @@ class PickList(Document):
|
|||||||
(pi_item.item_code.isin([x.item_code for x in items]))
|
(pi_item.item_code.isin([x.item_code for x in items]))
|
||||||
& ((pi_item.picked_qty > 0) | (pi_item.stock_qty > 0))
|
& ((pi_item.picked_qty > 0) | (pi_item.stock_qty > 0))
|
||||||
& (pi.status != "Completed")
|
& (pi.status != "Completed")
|
||||||
|
& (pi.status != "Cancelled")
|
||||||
& (pi_item.docstatus != 2)
|
& (pi_item.docstatus != 2)
|
||||||
)
|
)
|
||||||
.groupby(
|
.groupby(
|
||||||
@@ -473,7 +474,7 @@ def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus)
|
|||||||
)
|
)
|
||||||
qty = stock_qty / (item_doc.conversion_factor or 1)
|
qty = stock_qty / (item_doc.conversion_factor or 1)
|
||||||
|
|
||||||
uom_must_be_whole_number = frappe.db.get_value("UOM", item_doc.uom, "must_be_whole_number")
|
uom_must_be_whole_number = frappe.get_cached_value("UOM", item_doc.uom, "must_be_whole_number")
|
||||||
if uom_must_be_whole_number:
|
if uom_must_be_whole_number:
|
||||||
qty = floor(qty)
|
qty = floor(qty)
|
||||||
stock_qty = qty * item_doc.conversion_factor
|
stock_qty = qty * item_doc.conversion_factor
|
||||||
|
|||||||
@@ -149,7 +149,8 @@
|
|||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Sales Order Item",
|
"label": "Sales Order Item",
|
||||||
"read_only": 1
|
"read_only": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "serial_no_and_batch_section",
|
"fieldname": "serial_no_and_batch_section",
|
||||||
@@ -191,7 +192,7 @@
|
|||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-04-22 05:27:38.497997",
|
"modified": "2023-06-16 14:05:51.719959",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Pick List Item",
|
"name": "Pick List Item",
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ class PurchaseReceipt(BuyingController):
|
|||||||
self.set_status()
|
self.set_status()
|
||||||
|
|
||||||
self.po_required()
|
self.po_required()
|
||||||
|
self.validate_items_quality_inspection()
|
||||||
self.validate_with_previous_doc()
|
self.validate_with_previous_doc()
|
||||||
self.validate_uom_is_integer("uom", ["qty", "received_qty"])
|
self.validate_uom_is_integer("uom", ["qty", "received_qty"])
|
||||||
self.validate_uom_is_integer("stock_uom", "stock_qty")
|
self.validate_uom_is_integer("stock_uom", "stock_qty")
|
||||||
@@ -197,6 +198,26 @@ class PurchaseReceipt(BuyingController):
|
|||||||
if not d.purchase_order:
|
if not d.purchase_order:
|
||||||
frappe.throw(_("Purchase Order number required for Item {0}").format(d.item_code))
|
frappe.throw(_("Purchase Order number required for Item {0}").format(d.item_code))
|
||||||
|
|
||||||
|
def validate_items_quality_inspection(self):
|
||||||
|
for item in self.get("items"):
|
||||||
|
if item.quality_inspection:
|
||||||
|
qi = frappe.db.get_value(
|
||||||
|
"Quality Inspection",
|
||||||
|
item.quality_inspection,
|
||||||
|
["reference_type", "reference_name", "item_code"],
|
||||||
|
as_dict=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if qi.reference_type != self.doctype or qi.reference_name != self.name:
|
||||||
|
msg = f"""Row #{item.idx}: Please select a valid Quality Inspection with Reference Type
|
||||||
|
{frappe.bold(self.doctype)} and Reference Name {frappe.bold(self.name)}."""
|
||||||
|
frappe.throw(_(msg))
|
||||||
|
|
||||||
|
if qi.item_code != item.item_code:
|
||||||
|
msg = f"""Row #{item.idx}: Please select a valid Quality Inspection with Item Code
|
||||||
|
{frappe.bold(item.item_code)}."""
|
||||||
|
frappe.throw(_(msg))
|
||||||
|
|
||||||
def get_already_received_qty(self, po, po_detail):
|
def get_already_received_qty(self, po, po_detail):
|
||||||
qty = frappe.db.sql(
|
qty = frappe.db.sql(
|
||||||
"""select sum(qty) from `tabPurchase Receipt Item`
|
"""select sum(qty) from `tabPurchase Receipt Item`
|
||||||
|
|||||||
@@ -1751,6 +1751,52 @@ class TestPurchaseReceipt(FrappeTestCase):
|
|||||||
pr.items[0].delivery_note_item = delivery_note_item
|
pr.items[0].delivery_note_item = delivery_note_item
|
||||||
pr.save()
|
pr.save()
|
||||||
|
|
||||||
|
def test_purchase_return_valuation_with_rejected_qty(self):
|
||||||
|
item_code = "_Test Item Return Valuation"
|
||||||
|
create_item(item_code)
|
||||||
|
|
||||||
|
warehouse = create_warehouse("_Test Warehouse Return Valuation")
|
||||||
|
rejected_warehouse = create_warehouse("_Test Rejected Warehouse Return Valuation")
|
||||||
|
|
||||||
|
# Step 1: Create Purchase Receipt with valuation rate 100
|
||||||
|
make_purchase_receipt(
|
||||||
|
item_code=item_code,
|
||||||
|
warehouse=warehouse,
|
||||||
|
qty=10,
|
||||||
|
rate=100,
|
||||||
|
rejected_qty=2,
|
||||||
|
rejected_warehouse=rejected_warehouse,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 2: Create One more Purchase Receipt with valuation rate 200
|
||||||
|
pr = make_purchase_receipt(
|
||||||
|
item_code=item_code,
|
||||||
|
warehouse=warehouse,
|
||||||
|
qty=10,
|
||||||
|
rate=200,
|
||||||
|
rejected_qty=2,
|
||||||
|
rejected_warehouse=rejected_warehouse,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 3: Create Purchase Return for 2 qty
|
||||||
|
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_return
|
||||||
|
|
||||||
|
pr_return = make_purchase_return(pr.name)
|
||||||
|
pr_return.items[0].qty = 2 * -1
|
||||||
|
pr_return.items[0].received_qty = 2 * -1
|
||||||
|
pr_return.items[0].rejected_qty = 0
|
||||||
|
pr_return.items[0].rejected_warehouse = ""
|
||||||
|
pr_return.save()
|
||||||
|
pr_return.submit()
|
||||||
|
|
||||||
|
data = frappe.get_all(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
filters={"voucher_no": pr_return.name, "docstatus": 1},
|
||||||
|
fields=["SUM(stock_value_difference) as stock_value_difference"],
|
||||||
|
)[0]
|
||||||
|
|
||||||
|
self.assertEqual(abs(data["stock_value_difference"]), 400.00)
|
||||||
|
|
||||||
|
|
||||||
def prepare_data_for_internal_transfer():
|
def prepare_data_for_internal_transfer():
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||||
|
|||||||
@@ -307,6 +307,11 @@ def validate_serial_no(sle, item_det):
|
|||||||
allow_existing_serial_no = cint(
|
allow_existing_serial_no = cint(
|
||||||
frappe.get_cached_value("Stock Settings", "None", "allow_existing_serial_no")
|
frappe.get_cached_value("Stock Settings", "None", "allow_existing_serial_no")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
work_order = None
|
||||||
|
if sle.voucher_no and sle.voucher_type == "Stock Entry":
|
||||||
|
work_order = frappe.get_cached_value("Stock Entry", sle.voucher_no, "work_order")
|
||||||
|
|
||||||
for serial_no in serial_nos:
|
for serial_no in serial_nos:
|
||||||
if frappe.db.exists("Serial No", serial_no):
|
if frappe.db.exists("Serial No", serial_no):
|
||||||
sr = frappe.db.get_value(
|
sr = frappe.db.get_value(
|
||||||
@@ -324,6 +329,7 @@ def validate_serial_no(sle, item_det):
|
|||||||
"purchase_document_no",
|
"purchase_document_no",
|
||||||
"company",
|
"company",
|
||||||
"status",
|
"status",
|
||||||
|
"work_order",
|
||||||
],
|
],
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
@@ -335,6 +341,9 @@ def validate_serial_no(sle, item_det):
|
|||||||
SerialNoItemError,
|
SerialNoItemError,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if sr.work_order and work_order and sr.work_order == work_order:
|
||||||
|
allow_existing_serial_no = True
|
||||||
|
|
||||||
if not allow_existing_serial_no and sle.voucher_type in [
|
if not allow_existing_serial_no and sle.voucher_type in [
|
||||||
"Stock Entry",
|
"Stock Entry",
|
||||||
"Purchase Receipt",
|
"Purchase Receipt",
|
||||||
|
|||||||
@@ -101,6 +101,14 @@ frappe.ui.form.on('Stock Entry', {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let batch_field = frm.get_docfield('items', 'batch_no');
|
||||||
|
if (batch_field) {
|
||||||
|
batch_field.get_route_options_for_new_doc = (row) => {
|
||||||
|
return {
|
||||||
|
'item': row.doc.item_code
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
frm.add_fetch("bom_no", "inspection_required", "inspection_required");
|
frm.add_fetch("bom_no", "inspection_required", "inspection_required");
|
||||||
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
||||||
|
|||||||
@@ -125,7 +125,8 @@
|
|||||||
"oldfieldname": "purpose",
|
"oldfieldname": "purpose",
|
||||||
"oldfieldtype": "Select",
|
"oldfieldtype": "Select",
|
||||||
"options": "Material Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor",
|
"options": "Material Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor",
|
||||||
"read_only": 1
|
"read_only": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "company",
|
"fieldname": "company",
|
||||||
@@ -577,7 +578,8 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Pick List",
|
"label": "Pick List",
|
||||||
"options": "Pick List",
|
"options": "Pick List",
|
||||||
"read_only": 1
|
"read_only": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "print_settings_col_break",
|
"fieldname": "print_settings_col_break",
|
||||||
@@ -677,7 +679,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-06-09 15:46:28.418339",
|
"modified": "2023-06-19 18:23:40.748114",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Entry",
|
"name": "Stock Entry",
|
||||||
@@ -738,7 +740,6 @@
|
|||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "Stock Manager",
|
"role": "Stock Manager",
|
||||||
"set_user_permissions": 1,
|
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"submit": 1,
|
"submit": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
|
|||||||
@@ -475,7 +475,7 @@ def add_additional_uom_columns(columns, result, include_uom, conversion_factors)
|
|||||||
|
|
||||||
for row_idx, row in enumerate(result):
|
for row_idx, row in enumerate(result):
|
||||||
for convertible_col, data in convertible_column_map.items():
|
for convertible_col, data in convertible_column_map.items():
|
||||||
conversion_factor = conversion_factors[row.get("item_code")] or 1
|
conversion_factor = conversion_factors.get(row.get("item_code")) or 1.0
|
||||||
for_type = data.for_type
|
for_type = data.for_type
|
||||||
value_before_conversion = row.get(convertible_col)
|
value_before_conversion = row.get(convertible_col)
|
||||||
if for_type == "rate":
|
if for_type == "rate":
|
||||||
|
|||||||
@@ -219,7 +219,8 @@ class ItemConfigure {
|
|||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
${available_qty === 0 ? '<span class="text-danger">(' + __('Out of Stock') + ')</span>' : ''}
|
${available_qty === 0 && product_info && product_info?.is_stock_item
|
||||||
|
? '<span class="text-danger">(' + __('Out of Stock') + ')</span>' : ''}
|
||||||
|
|
||||||
</div></div>
|
</div></div>
|
||||||
<a href data-action="btn_clear_values" data-item-code="${one_item}">
|
<a href data-action="btn_clear_values" data-item-code="${one_item}">
|
||||||
@@ -236,7 +237,8 @@ class ItemConfigure {
|
|||||||
</div>`;
|
</div>`;
|
||||||
/* eslint-disable indent */
|
/* eslint-disable indent */
|
||||||
|
|
||||||
if (!product_info?.allow_items_not_in_stock && available_qty === 0) {
|
if (!product_info?.allow_items_not_in_stock && available_qty === 0
|
||||||
|
&& product_info && product_info?.is_stock_item) {
|
||||||
item_add_to_cart = '';
|
item_add_to_cart = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user