mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-14 20:35:09 +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) {
|
||||
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"));
|
||||
}
|
||||
},
|
||||
|
||||
@@ -100,15 +100,16 @@
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"fieldtype": "Float",
|
||||
"label": "Rounding Loss Allowance"
|
||||
"label": "Rounding Loss Allowance",
|
||||
"precision": "9"
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-06-12 21:02:09.818208",
|
||||
"modified": "2023-06-20 07:29:06.972434",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Exchange Rate Revaluation",
|
||||
|
||||
@@ -22,7 +22,7 @@ class ExchangeRateRevaluation(Document):
|
||||
self.set_total_gain_loss()
|
||||
|
||||
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"))
|
||||
|
||||
def set_total_gain_loss(self):
|
||||
@@ -373,6 +373,24 @@ class ExchangeRateRevaluation(Document):
|
||||
"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"):
|
||||
# Base currency has balance
|
||||
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(
|
||||
{
|
||||
"account": 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,
|
||||
"credit": abs(self.gain_loss_booked) if self.gain_loss_booked > 0 else 0,
|
||||
"debit_in_account_currency": abs(self.gain_loss_booked) if self.gain_loss_booked < 0 else 0,
|
||||
"credit_in_account_currency": self.gain_loss_booked if self.gain_loss_booked > 0 else 0,
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
"exchange_rate": 1,
|
||||
"reference_type": "Exchange Rate Revaluation",
|
||||
"reference_name": self.name,
|
||||
}
|
||||
)
|
||||
journal_entry_accounts.append(
|
||||
{
|
||||
"account": unrealized_exchange_gain_loss_account,
|
||||
"balance": get_balance_on(unrealized_exchange_gain_loss_account),
|
||||
"debit": abs(d.gain_loss) if d.gain_loss < 0 else 0,
|
||||
"credit": abs(d.gain_loss) if d.gain_loss > 0 else 0,
|
||||
"debit_in_account_currency": 0,
|
||||
"credit_in_account_currency": 0,
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
"exchange_rate": 1,
|
||||
"reference_type": "Exchange Rate Revaluation",
|
||||
"reference_name": self.name,
|
||||
}
|
||||
)
|
||||
|
||||
journal_entry.set("accounts", journal_entry_accounts)
|
||||
journal_entry.set_total_debit_credit()
|
||||
|
||||
@@ -92,6 +92,7 @@
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "New Exchange Rate",
|
||||
"precision": "9",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -147,7 +148,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-12-29 19:38:52.915295",
|
||||
"modified": "2023-06-20 07:21:40.743460",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Exchange Rate Revaluation Account",
|
||||
|
||||
@@ -613,7 +613,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
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 fields = [
|
||||
{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},
|
||||
];
|
||||
|
||||
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.flags.allocate_payment_amount = true;
|
||||
frm.events.validate_filters_data(frm, filters);
|
||||
frm.doc.cost_center = filters.cost_center;
|
||||
frm.events.get_outstanding_documents(frm, filters);
|
||||
}, __("Filters"), __("Get Outstanding Documents"));
|
||||
frm.events.get_outstanding_documents(frm, filters, get_outstanding_invoices, get_orders_to_be_billed);
|
||||
}, __("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) {
|
||||
@@ -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");
|
||||
|
||||
if(!frm.doc.party) {
|
||||
@@ -698,6 +715,13 @@ frappe.ui.form.on('Payment Entry', {
|
||||
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'];
|
||||
|
||||
return frappe.call({
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"base_received_amount",
|
||||
"base_received_amount_after_tax",
|
||||
"section_break_14",
|
||||
"get_outstanding_invoice",
|
||||
"get_outstanding_invoices",
|
||||
"get_outstanding_orders",
|
||||
"references",
|
||||
"section_break_34",
|
||||
"total_allocated_amount",
|
||||
@@ -355,12 +356,6 @@
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Reference"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus==0",
|
||||
"fieldname": "get_outstanding_invoice",
|
||||
"fieldtype": "Button",
|
||||
"label": "Get Outstanding Invoice"
|
||||
},
|
||||
{
|
||||
"fieldname": "references",
|
||||
"fieldtype": "Table",
|
||||
@@ -728,12 +723,24 @@
|
||||
"fieldname": "section_break_60",
|
||||
"fieldtype": "Section Break",
|
||||
"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,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-02-14 04:52:30.478523",
|
||||
"modified": "2023-06-19 11:38:04.387219",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
|
||||
@@ -172,6 +172,8 @@ class PaymentEntry(AccountsController):
|
||||
"payment_type": self.payment_type,
|
||||
"party": self.party,
|
||||
"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(
|
||||
_(
|
||||
"{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)
|
||||
)
|
||||
|
||||
@@ -1356,62 +1358,75 @@ def get_outstanding_reference_documents(args):
|
||||
condition += " and company = {0}".format(frappe.db.escape(args.get("company")))
|
||||
common_filter.append(ple.company == args.get("company"))
|
||||
|
||||
outstanding_invoices = get_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
|
||||
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"),
|
||||
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,
|
||||
company_currency,
|
||||
condition=condition,
|
||||
filters=args,
|
||||
)
|
||||
|
||||
data = negative_outstanding_invoices + outstanding_invoices + orders_to_be_billed
|
||||
|
||||
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(
|
||||
_(
|
||||
"No outstanding invoices found for the {0} {1} which qualify the filters you have specified."
|
||||
).format(_(args.get("party_type")).lower(), frappe.bold(args.get("party")))
|
||||
"No outstanding {0} found for the {1} {2} which qualify the filters you have specified."
|
||||
).format(
|
||||
ref_document_type, _(args.get("party_type")).lower(), frappe.bold(args.get("party"))
|
||||
)
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
@@ -637,13 +637,6 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
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):
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
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)
|
||||
|
||||
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):
|
||||
gl_entries = frappe.db.sql(
|
||||
|
||||
@@ -1012,10 +1012,16 @@ class SalesInvoice(SellingController):
|
||||
|
||||
def check_prev_docstatus(self):
|
||||
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))
|
||||
|
||||
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))
|
||||
|
||||
def make_gl_entries(self, gl_entries=None, from_repost=False):
|
||||
|
||||
@@ -483,18 +483,22 @@ def get_gl_entries_on_asset_disposal(
|
||||
},
|
||||
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)
|
||||
if profit_amount:
|
||||
get_profit_gl_entries(
|
||||
|
||||
@@ -19,56 +19,6 @@ frappe.query_reports["Fixed Asset Register"] = {
|
||||
options: "\nIn Location\nDisposed",
|
||||
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",
|
||||
label: __("Asset Category"),
|
||||
@@ -89,22 +39,67 @@ frappe.query_reports["Fixed Asset Register"] = {
|
||||
default: "--Select a group--",
|
||||
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",
|
||||
label: __("Only existing assets"),
|
||||
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
|
||||
|
||||
|
||||
from itertools import chain
|
||||
|
||||
import frappe
|
||||
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 erpnext.accounts.report.financial_statements import (
|
||||
@@ -13,7 +15,6 @@ from erpnext.accounts.report.financial_statements import (
|
||||
validate_fiscal_year,
|
||||
)
|
||||
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):
|
||||
@@ -64,11 +65,9 @@ def get_conditions(filters):
|
||||
|
||||
|
||||
def get_data(filters):
|
||||
|
||||
data = []
|
||||
|
||||
conditions = get_conditions(filters)
|
||||
depreciation_amount_map = get_finance_book_value_map(filters)
|
||||
pr_supplier_map = get_purchase_receipt_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_linked_to_fb = None
|
||||
assets_linked_to_fb = get_assets_linked_to_fb(filters)
|
||||
|
||||
if filters.filter_by_finance_book:
|
||||
assets_linked_to_fb = frappe.db.get_all(
|
||||
doctype="Asset Finance Book",
|
||||
filters={"finance_book": filters.finance_book or ("is", "not set")},
|
||||
pluck="parent",
|
||||
)
|
||||
company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book")
|
||||
|
||||
if filters.include_default_book_assets and company_fb:
|
||||
finance_book = company_fb
|
||||
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:
|
||||
if assets_linked_to_fb and asset.asset_id not in assets_linked_to_fb:
|
||||
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 = {
|
||||
"asset_id": asset.asset_id,
|
||||
"asset_name": asset.asset_name,
|
||||
@@ -126,7 +132,7 @@ def get_data(filters):
|
||||
or pi_supplier_map.get(asset.purchase_invoice),
|
||||
"gross_purchase_amount": asset.gross_purchase_amount,
|
||||
"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,
|
||||
"location": asset.location,
|
||||
"asset_category": asset.asset_category,
|
||||
@@ -140,14 +146,23 @@ def get_data(filters):
|
||||
|
||||
def prepare_chart_data(data, filters):
|
||||
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(
|
||||
filters.from_fiscal_year,
|
||||
filters.to_fiscal_year,
|
||||
filters.from_date,
|
||||
filters.to_date,
|
||||
filters.filter_based_on,
|
||||
filters_from_date,
|
||||
filters_to_date,
|
||||
filters_filter_based_on,
|
||||
"Monthly",
|
||||
company=filters.company,
|
||||
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):
|
||||
if asset.calculate_depreciation:
|
||||
depr_amount = depreciation_amount_map.get(asset.asset_id) or 0.0
|
||||
else:
|
||||
depr_amount = get_manual_depreciation_amount_of_asset(asset, filters)
|
||||
def get_assets_linked_to_fb(filters):
|
||||
afb = frappe.qb.DocType("Asset Finance Book")
|
||||
|
||||
return flt(depr_amount, 2)
|
||||
|
||||
|
||||
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 "")),
|
||||
)
|
||||
query = frappe.qb.from_(afb).select(
|
||||
afb.parent,
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
(_, _, depreciation_expense_account) = get_depreciation_accounts(asset)
|
||||
|
||||
asset = frappe.qb.DocType("Asset")
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
aca = frappe.qb.DocType("Asset Category Account")
|
||||
company = frappe.qb.DocType("Company")
|
||||
|
||||
result = (
|
||||
query = (
|
||||
frappe.qb.from_(gle)
|
||||
.select(Sum(gle.debit))
|
||||
.where(gle.against_voucher == asset.asset_id)
|
||||
.where(gle.account == depreciation_expense_account)
|
||||
.join(asset)
|
||||
.on(gle.against_voucher == asset.name)
|
||||
.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.is_cancelled == 0)
|
||||
.where(gle.posting_date <= date)
|
||||
).run()
|
||||
.where(asset.docstatus == 1)
|
||||
.groupby(asset.name)
|
||||
)
|
||||
|
||||
if result and result[0] and result[0][0]:
|
||||
depr_amount = result[0][0]
|
||||
if finance_book:
|
||||
query = query.where(
|
||||
(gle.finance_book.isin([cstr(finance_book), ""])) | (gle.finance_book.isnull())
|
||||
)
|
||||
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():
|
||||
|
||||
@@ -30,6 +30,8 @@ class BuyingController(SubcontractingController):
|
||||
return _("From {0} | {1} {2}").format(self.supplier_name, self.currency, self.grand_total)
|
||||
|
||||
def validate(self):
|
||||
self.set_rate_for_standalone_debit_note()
|
||||
|
||||
super(BuyingController, self).validate()
|
||||
if getattr(self, "supplier", None) and not self.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):
|
||||
super(BuyingController, self).set_missing_values(for_validate)
|
||||
|
||||
@@ -445,7 +471,7 @@ class BuyingController(SubcontractingController):
|
||||
continue
|
||||
|
||||
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:
|
||||
|
||||
@@ -507,7 +533,7 @@ class BuyingController(SubcontractingController):
|
||||
d,
|
||||
{
|
||||
"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(),
|
||||
"incoming_rate": 0.0,
|
||||
},
|
||||
|
||||
@@ -617,6 +617,9 @@ def get_filters(
|
||||
if 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
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import frappe
|
||||
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.contact.contact import get_contact_name
|
||||
from frappe.utils import cint, cstr, flt, get_fullname
|
||||
@@ -201,6 +201,11 @@ def get_shopping_cart_menu(context=None):
|
||||
@frappe.whitelist()
|
||||
def add_new_address(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"})
|
||||
address = frappe.get_doc(doc)
|
||||
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)
|
||||
|
||||
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)
|
||||
else:
|
||||
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
|
||||
last_disbursement_date = get_last_disbursement_date(loan, posting_date)
|
||||
|
||||
if last_disbursement_date and getdate(last_disbursement_date) > getdate(
|
||||
last_interest_accrual_date
|
||||
if last_disbursement_date and getdate(last_disbursement_date) > add_days(
|
||||
getdate(last_interest_accrual_date), 1
|
||||
):
|
||||
last_interest_accrual_date = last_disbursement_date
|
||||
|
||||
|
||||
@@ -1657,6 +1657,61 @@ class TestWorkOrder(FrappeTestCase):
|
||||
job_card2.time_logs = []
|
||||
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():
|
||||
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.planned_start_date = args.planned_start_date or now()
|
||||
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:
|
||||
for item in wo_order.get("required_items"):
|
||||
|
||||
@@ -33,10 +33,9 @@ def get_data(filters: Filters) -> Data:
|
||||
wo.name,
|
||||
wo.status,
|
||||
wo.production_item,
|
||||
wo.qty,
|
||||
wo.produced_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_outgoing_value).as_("total_rm_value"),
|
||||
)
|
||||
@@ -44,6 +43,7 @@ def get_data(filters: Filters) -> Data:
|
||||
(wo.process_loss_qty > 0)
|
||||
& (wo.company == filters.company)
|
||||
& (se.docstatus == 1)
|
||||
& (se.purpose == "Manufacture")
|
||||
& (se.posting_date.between(filters.from_date, filters.to_date))
|
||||
)
|
||||
.groupby(se.work_order)
|
||||
@@ -79,20 +79,30 @@ def get_columns() -> Columns:
|
||||
"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"),
|
||||
"fieldname": "produced_qty",
|
||||
"fieldtype": "Float",
|
||||
"width": "150",
|
||||
},
|
||||
{"label": _("Loss Qty"), "fieldname": "process_loss_qty", "fieldtype": "Float", "width": "150"},
|
||||
{
|
||||
"label": _("Actual Manufactured Qty"),
|
||||
"fieldname": "actual_produced_qty",
|
||||
"label": _("Process Loss Qty"),
|
||||
"fieldname": "process_loss_qty",
|
||||
"fieldtype": "Float",
|
||||
"width": "150",
|
||||
},
|
||||
{
|
||||
"label": _("Process Loss Value"),
|
||||
"fieldname": "total_pl_value",
|
||||
"fieldtype": "Float",
|
||||
"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": _("Raw Material Value"),
|
||||
@@ -105,5 +115,5 @@ def get_columns() -> Columns:
|
||||
|
||||
def update_data_with_total_pl_value(data: Data) -> None:
|
||||
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
|
||||
|
||||
@@ -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.set_packed_qty_in_draft_delivery_notes
|
||||
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) {
|
||||
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(
|
||||
|
||||
@@ -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 = item_stock_qty // item.get("conversion_factor")
|
||||
item_stock_qty = item_stock_qty // item.get("conversion_factor", 1)
|
||||
item.update({"actual_qty": item_stock_qty})
|
||||
|
||||
@@ -59,7 +58,7 @@ def search_by_term(search_term, warehouse, price_list):
|
||||
"price_list": price_list,
|
||||
"item_code": item_code,
|
||||
},
|
||||
fields=["uom", "stock_uom", "currency", "price_list_rate"],
|
||||
fields=["uom", "currency", "price_list_rate"],
|
||||
)
|
||||
|
||||
def __sort(p):
|
||||
|
||||
@@ -1222,7 +1222,8 @@
|
||||
"hidden": 1,
|
||||
"label": "Pick List",
|
||||
"options": "Pick List",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -1398,7 +1399,7 @@
|
||||
"idx": 146,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-04-21 11:15:23.931084",
|
||||
"modified": "2023-06-16 14:58:55.066602",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Delivery Note",
|
||||
|
||||
@@ -36,7 +36,7 @@ class PickList(Document):
|
||||
for location in self.get("locations"):
|
||||
if (
|
||||
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(
|
||||
_("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.picked_qty > 0) | (pi_item.stock_qty > 0))
|
||||
& (pi.status != "Completed")
|
||||
& (pi.status != "Cancelled")
|
||||
& (pi_item.docstatus != 2)
|
||||
)
|
||||
.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)
|
||||
|
||||
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:
|
||||
qty = floor(qty)
|
||||
stock_qty = qty * item_doc.conversion_factor
|
||||
|
||||
@@ -149,7 +149,8 @@
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Sales Order Item",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "serial_no_and_batch_section",
|
||||
@@ -191,7 +192,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-04-22 05:27:38.497997",
|
||||
"modified": "2023-06-16 14:05:51.719959",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Pick List Item",
|
||||
|
||||
@@ -124,6 +124,7 @@ class PurchaseReceipt(BuyingController):
|
||||
self.set_status()
|
||||
|
||||
self.po_required()
|
||||
self.validate_items_quality_inspection()
|
||||
self.validate_with_previous_doc()
|
||||
self.validate_uom_is_integer("uom", ["qty", "received_qty"])
|
||||
self.validate_uom_is_integer("stock_uom", "stock_qty")
|
||||
@@ -197,6 +198,26 @@ class PurchaseReceipt(BuyingController):
|
||||
if not d.purchase_order:
|
||||
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):
|
||||
qty = frappe.db.sql(
|
||||
"""select sum(qty) from `tabPurchase Receipt Item`
|
||||
|
||||
@@ -1751,6 +1751,52 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
pr.items[0].delivery_note_item = delivery_note_item
|
||||
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():
|
||||
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(
|
||||
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:
|
||||
if frappe.db.exists("Serial No", serial_no):
|
||||
sr = frappe.db.get_value(
|
||||
@@ -324,6 +329,7 @@ def validate_serial_no(sle, item_det):
|
||||
"purchase_document_no",
|
||||
"company",
|
||||
"status",
|
||||
"work_order",
|
||||
],
|
||||
as_dict=1,
|
||||
)
|
||||
@@ -335,6 +341,9 @@ def validate_serial_no(sle, item_det):
|
||||
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 [
|
||||
"Stock Entry",
|
||||
"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");
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
||||
|
||||
@@ -125,7 +125,8 @@
|
||||
"oldfieldname": "purpose",
|
||||
"oldfieldtype": "Select",
|
||||
"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",
|
||||
@@ -577,7 +578,8 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Pick List",
|
||||
"options": "Pick List",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "print_settings_col_break",
|
||||
@@ -677,7 +679,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-06-09 15:46:28.418339",
|
||||
"modified": "2023-06-19 18:23:40.748114",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Entry",
|
||||
@@ -738,7 +740,6 @@
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Stock Manager",
|
||||
"set_user_permissions": 1,
|
||||
"share": 1,
|
||||
"submit": 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 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
|
||||
value_before_conversion = row.get(convertible_col)
|
||||
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>
|
||||
<a href data-action="btn_clear_values" data-item-code="${one_item}">
|
||||
@@ -236,7 +237,8 @@ class ItemConfigure {
|
||||
</div>`;
|
||||
/* 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 = '';
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user