mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-16 03:29:16 +00:00
Merge pull request #45703 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -19,10 +19,15 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onload: function (frm) {
|
onload: function (frm) {
|
||||||
|
if (!frm.doc.company) {
|
||||||
|
frm.set_value("company", frappe.defaults.get_default("company"));
|
||||||
|
}
|
||||||
|
|
||||||
// Set default filter dates
|
// Set default filter dates
|
||||||
let today = frappe.datetime.get_today();
|
let today = frappe.datetime.get_today();
|
||||||
frm.doc.bank_statement_from_date = frappe.datetime.add_months(today, -1);
|
frm.doc.bank_statement_from_date = frappe.datetime.add_months(today, -1);
|
||||||
frm.doc.bank_statement_to_date = today;
|
frm.doc.bank_statement_to_date = today;
|
||||||
|
|
||||||
frm.trigger("bank_account");
|
frm.trigger("bank_account");
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -98,7 +103,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
|||||||
|
|
||||||
make_reconciliation_tool(frm) {
|
make_reconciliation_tool(frm) {
|
||||||
frm.get_field("reconciliation_tool_cards").$wrapper.empty();
|
frm.get_field("reconciliation_tool_cards").$wrapper.empty();
|
||||||
if (frm.doc.bank_account && frm.doc.bank_statement_to_date) {
|
if (frm.doc.company && frm.doc.bank_account && frm.doc.bank_statement_to_date) {
|
||||||
frm.trigger("get_cleared_balance").then(() => {
|
frm.trigger("get_cleared_balance").then(() => {
|
||||||
if (
|
if (
|
||||||
frm.doc.bank_account &&
|
frm.doc.bank_account &&
|
||||||
@@ -114,7 +119,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
get_account_opening_balance(frm) {
|
get_account_opening_balance(frm) {
|
||||||
if (frm.doc.bank_account && frm.doc.bank_statement_from_date) {
|
if (frm.doc.company && frm.doc.bank_account && frm.doc.bank_statement_from_date) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance",
|
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance",
|
||||||
args: {
|
args: {
|
||||||
@@ -130,7 +135,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
get_cleared_balance(frm) {
|
get_cleared_balance(frm) {
|
||||||
if (frm.doc.bank_account && frm.doc.bank_statement_to_date) {
|
if (frm.doc.company && frm.doc.bank_account && frm.doc.bank_statement_to_date) {
|
||||||
return frappe.call({
|
return frappe.call({
|
||||||
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance",
|
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance",
|
||||||
args: {
|
args: {
|
||||||
|
|||||||
@@ -543,7 +543,7 @@ class PaymentEntry(AccountsController):
|
|||||||
if d.reference_doctype not in valid_reference_doctypes:
|
if d.reference_doctype not in valid_reference_doctypes:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Reference Doctype must be one of {0}").format(
|
_("Reference Doctype must be one of {0}").format(
|
||||||
comma_or(_(d) for d in valid_reference_doctypes)
|
comma_or([_(d) for d in valid_reference_doctypes])
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1798,7 +1798,7 @@ class PaymentEntry(AccountsController):
|
|||||||
paid_amount -= sum(flt(d.amount, precision) for d in self.deductions)
|
paid_amount -= sum(flt(d.amount, precision) for d in self.deductions)
|
||||||
|
|
||||||
for ref in self.references:
|
for ref in self.references:
|
||||||
reference_outstanding_amount = ref.outstanding_amount
|
reference_outstanding_amount = flt(ref.outstanding_amount)
|
||||||
abs_outstanding_amount = abs(reference_outstanding_amount)
|
abs_outstanding_amount = abs(reference_outstanding_amount)
|
||||||
|
|
||||||
if reference_outstanding_amount > 0:
|
if reference_outstanding_amount > 0:
|
||||||
@@ -2243,10 +2243,17 @@ def get_outstanding_reference_documents(args, validate=False):
|
|||||||
outstanding_invoices = []
|
outstanding_invoices = []
|
||||||
negative_outstanding_invoices = []
|
negative_outstanding_invoices = []
|
||||||
|
|
||||||
|
party_account = args.get("party_account")
|
||||||
|
|
||||||
|
# get party account if advance account is set.
|
||||||
if args.get("book_advance_payments_in_separate_party_account"):
|
if args.get("book_advance_payments_in_separate_party_account"):
|
||||||
party_account = get_party_account(args.get("party_type"), args.get("party"), args.get("company"))
|
accounts = get_party_account(
|
||||||
else:
|
args.get("party_type"), args.get("party"), args.get("company"), include_advance=True
|
||||||
party_account = args.get("party_account")
|
)
|
||||||
|
advance_account = accounts[1] if len(accounts) >= 1 else None
|
||||||
|
|
||||||
|
if party_account == advance_account:
|
||||||
|
party_account = accounts[0]
|
||||||
|
|
||||||
if args.get("get_outstanding_invoices"):
|
if args.get("get_outstanding_invoices"):
|
||||||
outstanding_invoices = get_outstanding_invoices(
|
outstanding_invoices = get_outstanding_invoices(
|
||||||
@@ -2826,6 +2833,7 @@ def get_payment_entry(
|
|||||||
pe.paid_amount = paid_amount
|
pe.paid_amount = paid_amount
|
||||||
pe.received_amount = received_amount
|
pe.received_amount = received_amount
|
||||||
pe.letter_head = doc.get("letter_head")
|
pe.letter_head = doc.get("letter_head")
|
||||||
|
pe.bank_account = frappe.db.get_value("Bank Account", {"is_company_account": 1, "is_default": 1}, "name")
|
||||||
|
|
||||||
if dt in ["Purchase Order", "Sales Order", "Sales Invoice", "Purchase Invoice"]:
|
if dt in ["Purchase Order", "Sales Order", "Sales Invoice", "Purchase Invoice"]:
|
||||||
pe.project = doc.get("project") or reduce(
|
pe.project = doc.get("project") or reduce(
|
||||||
|
|||||||
@@ -224,9 +224,6 @@ class TestPOSClosingEntry(unittest.TestCase):
|
|||||||
item_code=item_code, qty=5, rate=300, use_serial_batch_fields=1, batch_no=batch_no
|
item_code=item_code, qty=5, rate=300, use_serial_batch_fields=1, batch_no=batch_no
|
||||||
)
|
)
|
||||||
|
|
||||||
batch_qty = frappe.db.get_value("Batch", batch_no, "batch_qty")
|
|
||||||
self.assertEqual(batch_qty, 10)
|
|
||||||
|
|
||||||
batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code)
|
batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code)
|
||||||
self.assertEqual(batch_qty_with_pos, 0.0)
|
self.assertEqual(batch_qty_with_pos, 0.0)
|
||||||
|
|
||||||
@@ -256,9 +253,6 @@ class TestPOSClosingEntry(unittest.TestCase):
|
|||||||
pcv_doc.reload()
|
pcv_doc.reload()
|
||||||
pcv_doc.cancel()
|
pcv_doc.cancel()
|
||||||
|
|
||||||
batch_qty = frappe.db.get_value("Batch", batch_no, "batch_qty")
|
|
||||||
self.assertEqual(batch_qty, 10)
|
|
||||||
|
|
||||||
batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code)
|
batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code)
|
||||||
self.assertEqual(batch_qty_with_pos, 0.0)
|
self.assertEqual(batch_qty_with_pos, 0.0)
|
||||||
|
|
||||||
|
|||||||
@@ -321,9 +321,12 @@ def get_recipients_and_cc(customer, doc):
|
|||||||
recipients = []
|
recipients = []
|
||||||
for clist in doc.customers:
|
for clist in doc.customers:
|
||||||
if clist.customer == customer:
|
if clist.customer == customer:
|
||||||
recipients.append(clist.billing_email)
|
if clist.billing_email:
|
||||||
|
for email in clist.billing_email.split(","):
|
||||||
|
recipients.append(email.strip())
|
||||||
if doc.primary_mandatory and clist.primary_email:
|
if doc.primary_mandatory and clist.primary_email:
|
||||||
recipients.append(clist.primary_email)
|
for email in clist.primary_email.split(","):
|
||||||
|
recipients.append(email.strip())
|
||||||
cc = []
|
cc = []
|
||||||
if doc.cc_to != "":
|
if doc.cc_to != "":
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -332,6 +332,8 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
|||||||
|
|
||||||
if (this.frm.doc.__onload && this.frm.doc.__onload.load_after_mapping) return;
|
if (this.frm.doc.__onload && this.frm.doc.__onload.load_after_mapping) return;
|
||||||
|
|
||||||
|
let payment_terms_template = this.frm.doc.payment_terms_template;
|
||||||
|
|
||||||
erpnext.utils.get_party_details(
|
erpnext.utils.get_party_details(
|
||||||
this.frm,
|
this.frm,
|
||||||
"erpnext.accounts.party.get_party_details",
|
"erpnext.accounts.party.get_party_details",
|
||||||
@@ -352,6 +354,12 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
|||||||
me.frm.doc.tax_withholding_category = me.frm.supplier_tds;
|
me.frm.doc.tax_withholding_category = me.frm.supplier_tds;
|
||||||
me.frm.set_df_property("apply_tds", "read_only", me.frm.supplier_tds ? 0 : 1);
|
me.frm.set_df_property("apply_tds", "read_only", me.frm.supplier_tds ? 0 : 1);
|
||||||
me.frm.set_df_property("tax_withholding_category", "hidden", me.frm.supplier_tds ? 0 : 1);
|
me.frm.set_df_property("tax_withholding_category", "hidden", me.frm.supplier_tds ? 0 : 1);
|
||||||
|
|
||||||
|
// while duplicating, don't change payment terms
|
||||||
|
if (me.frm.doc.__run_link_triggers === false) {
|
||||||
|
me.frm.set_value("payment_terms_template", payment_terms_template);
|
||||||
|
me.frm.refresh_field("payment_terms_template");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -368,6 +376,18 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tax_withholding_category(frm) {
|
||||||
|
var me = this;
|
||||||
|
let filtered_taxes = (me.frm.doc.taxes || []).filter((row) => !row.is_tax_withholding_account);
|
||||||
|
me.frm.clear_table("taxes");
|
||||||
|
|
||||||
|
filtered_taxes.forEach((row) => {
|
||||||
|
me.frm.add_child("taxes", row);
|
||||||
|
});
|
||||||
|
|
||||||
|
me.frm.refresh_field("taxes");
|
||||||
|
}
|
||||||
|
|
||||||
credit_to() {
|
credit_to() {
|
||||||
var me = this;
|
var me = this;
|
||||||
if (this.frm.doc.credit_to) {
|
if (this.frm.doc.credit_to) {
|
||||||
|
|||||||
@@ -65,9 +65,10 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
|||||||
refresh(doc, dt, dn) {
|
refresh(doc, dt, dn) {
|
||||||
const me = this;
|
const me = this;
|
||||||
super.refresh();
|
super.refresh();
|
||||||
if (cur_frm.msgbox && cur_frm.msgbox.$wrapper.is(":visible")) {
|
|
||||||
|
if (this.frm?.msgbox && this.frm.msgbox.$wrapper.is(":visible")) {
|
||||||
// hide new msgbox
|
// hide new msgbox
|
||||||
cur_frm.msgbox.hide();
|
this.frm.msgbox.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.frm.toggle_reqd("due_date", !this.frm.doc.is_return);
|
this.frm.toggle_reqd("due_date", !this.frm.doc.is_return);
|
||||||
|
|||||||
@@ -634,9 +634,7 @@ class Subscription(Document):
|
|||||||
"""
|
"""
|
||||||
invoice = frappe.get_all(
|
invoice = frappe.get_all(
|
||||||
self.invoice_document_type,
|
self.invoice_document_type,
|
||||||
{
|
{"subscription": self.name, "docstatus": ("<", 2)},
|
||||||
"subscription": self.name,
|
|
||||||
},
|
|
||||||
limit=1,
|
limit=1,
|
||||||
order_by="to_date desc",
|
order_by="to_date desc",
|
||||||
pluck="name",
|
pluck="name",
|
||||||
@@ -675,6 +673,7 @@ class Subscription(Document):
|
|||||||
self.invoice_document_type,
|
self.invoice_document_type,
|
||||||
{
|
{
|
||||||
"subscription": self.name,
|
"subscription": self.name,
|
||||||
|
"docstatus": 1,
|
||||||
"status": ["!=", "Paid"],
|
"status": ["!=", "Paid"],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"add_total_row": 1,
|
"add_total_row": 0,
|
||||||
"columns": [],
|
"columns": [],
|
||||||
"creation": "2013-02-25 17:03:34",
|
"creation": "2013-02-25 17:03:34",
|
||||||
"disable_prepared_report": 0,
|
"disable_prepared_report": 0,
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
"filters": [],
|
"filters": [],
|
||||||
"idx": 3,
|
"idx": 3,
|
||||||
"is_standard": "Yes",
|
"is_standard": "Yes",
|
||||||
"modified": "2022-02-11 10:18:36.956558",
|
"modified": "2025-01-27 18:40:24.493829",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Gross Profit",
|
"name": "Gross Profit",
|
||||||
|
|||||||
@@ -178,7 +178,14 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
|
|||||||
# removing Item Code and Item Name columns
|
# removing Item Code and Item Name columns
|
||||||
del columns[4:6]
|
del columns[4:6]
|
||||||
|
|
||||||
|
total_base_amount = 0
|
||||||
|
total_buying_amount = 0
|
||||||
|
|
||||||
for src in gross_profit_data.si_list:
|
for src in gross_profit_data.si_list:
|
||||||
|
if src.indent == 1:
|
||||||
|
total_base_amount += src.base_amount or 0.0
|
||||||
|
total_buying_amount += src.buying_amount or 0.0
|
||||||
|
|
||||||
row = frappe._dict()
|
row = frappe._dict()
|
||||||
row.indent = src.indent
|
row.indent = src.indent
|
||||||
row.parent_invoice = src.parent_invoice
|
row.parent_invoice = src.parent_invoice
|
||||||
@@ -189,6 +196,27 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
|
|||||||
|
|
||||||
data.append(row)
|
data.append(row)
|
||||||
|
|
||||||
|
total_gross_profit = total_base_amount - total_buying_amount
|
||||||
|
data.append(
|
||||||
|
frappe._dict(
|
||||||
|
{
|
||||||
|
"sales_invoice": "Total",
|
||||||
|
"qty": None,
|
||||||
|
"avg._selling_rate": None,
|
||||||
|
"valuation_rate": None,
|
||||||
|
"selling_amount": total_base_amount,
|
||||||
|
"buying_amount": total_buying_amount,
|
||||||
|
"gross_profit": total_gross_profit,
|
||||||
|
"gross_profit_%": flt(
|
||||||
|
(total_gross_profit / total_base_amount) * 100.0,
|
||||||
|
cint(frappe.db.get_default("currency_precision")) or 3,
|
||||||
|
)
|
||||||
|
if total_base_amount
|
||||||
|
else 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_columns, data):
|
def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_columns, data):
|
||||||
for src in gross_profit_data.grouped_data:
|
for src in gross_profit_data.grouped_data:
|
||||||
|
|||||||
@@ -605,3 +605,33 @@ class TestGrossProfit(FrappeTestCase):
|
|||||||
item_from_sinv2 = [x for x in data if x.parent_invoice == sinv2.name]
|
item_from_sinv2 = [x for x in data if x.parent_invoice == sinv2.name]
|
||||||
self.assertEqual(len(item_from_sinv2), 1)
|
self.assertEqual(len(item_from_sinv2), 1)
|
||||||
self.assertEqual(1800, item_from_sinv2[0].valuation_rate)
|
self.assertEqual(1800, item_from_sinv2[0].valuation_rate)
|
||||||
|
|
||||||
|
def test_gross_profit_groupby_invoices(self):
|
||||||
|
create_sales_invoice(
|
||||||
|
qty=1,
|
||||||
|
rate=100,
|
||||||
|
company=self.company,
|
||||||
|
customer=self.customer,
|
||||||
|
item_code=self.item,
|
||||||
|
item_name=self.item,
|
||||||
|
cost_center=self.cost_center,
|
||||||
|
warehouse=self.warehouse,
|
||||||
|
debit_to=self.debit_to,
|
||||||
|
parent_cost_center=self.cost_center,
|
||||||
|
update_stock=0,
|
||||||
|
currency="INR",
|
||||||
|
income_account=self.income_account,
|
||||||
|
expense_account=self.expense_account,
|
||||||
|
)
|
||||||
|
|
||||||
|
filters = frappe._dict(
|
||||||
|
company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
|
||||||
|
)
|
||||||
|
|
||||||
|
_, data = execute(filters=filters)
|
||||||
|
total = data[-1]
|
||||||
|
|
||||||
|
self.assertEqual(total.selling_amount, 100.0)
|
||||||
|
self.assertEqual(total.buying_amount, 0.0)
|
||||||
|
self.assertEqual(total.gross_profit, 100.0)
|
||||||
|
self.assertEqual(total.get("gross_profit_%"), 100.0)
|
||||||
|
|||||||
@@ -1069,7 +1069,7 @@ def make_new_active_asset_depr_schedules_and_cancel_current_ones(
|
|||||||
|
|
||||||
new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
|
new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
|
||||||
if asset_doc.flags.decrease_in_asset_value_due_to_value_adjustment and not value_after_depreciation:
|
if asset_doc.flags.decrease_in_asset_value_due_to_value_adjustment and not value_after_depreciation:
|
||||||
value_after_depreciation = row.value_after_depreciation + difference_amount
|
value_after_depreciation = row.value_after_depreciation - difference_amount
|
||||||
|
|
||||||
if asset_doc.flags.increase_in_asset_value_due_to_repair and row.depreciation_method in (
|
if asset_doc.flags.increase_in_asset_value_due_to_repair and row.depreciation_method in (
|
||||||
"Written Down Value",
|
"Written Down Value",
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ frappe.provide("erpnext.accounts.dimensions");
|
|||||||
|
|
||||||
frappe.ui.form.on("Asset Value Adjustment", {
|
frappe.ui.form.on("Asset Value Adjustment", {
|
||||||
setup: function (frm) {
|
setup: function (frm) {
|
||||||
frm.add_fetch("company", "cost_center", "cost_center");
|
|
||||||
frm.set_query("cost_center", function () {
|
frm.set_query("cost_center", function () {
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
@@ -22,6 +21,14 @@ frappe.ui.form.on("Asset Value Adjustment", {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
frm.set_query("difference_account", function () {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
company: frm.doc.company,
|
||||||
|
is_group: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onload: function (frm) {
|
onload: function (frm) {
|
||||||
@@ -37,7 +44,7 @@ frappe.ui.form.on("Asset Value Adjustment", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
asset: function (frm) {
|
asset: function (frm) {
|
||||||
frm.trigger("set_current_asset_value");
|
frm.trigger("set_acc_dimension");
|
||||||
},
|
},
|
||||||
|
|
||||||
finance_book: function (frm) {
|
finance_book: function (frm) {
|
||||||
@@ -60,4 +67,15 @@ frappe.ui.form.on("Asset Value Adjustment", {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
set_acc_dimension: function (frm) {
|
||||||
|
if (frm.doc.asset) {
|
||||||
|
frm.call({
|
||||||
|
method: "erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment.get_value_of_accounting_dimensions",
|
||||||
|
args: {
|
||||||
|
asset_name: frm.doc.asset,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"new_asset_value",
|
"new_asset_value",
|
||||||
"column_break_11",
|
"column_break_11",
|
||||||
"difference_amount",
|
"difference_amount",
|
||||||
|
"difference_account",
|
||||||
"journal_entry",
|
"journal_entry",
|
||||||
"accounting_dimensions_section",
|
"accounting_dimensions_section",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
@@ -54,6 +55,7 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Journal Entry",
|
"label": "Journal Entry",
|
||||||
"options": "Journal Entry",
|
"options": "Journal Entry",
|
||||||
|
"no_copy": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -79,6 +81,7 @@
|
|||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "New Asset Value",
|
"label": "New Asset Value",
|
||||||
|
"no_copy": 1,
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -120,12 +123,20 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_11",
|
"fieldname": "column_break_11",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "difference_account",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Difference Account",
|
||||||
|
"no_copy": 1,
|
||||||
|
"options": "Account",
|
||||||
|
"reqd": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-01-22 14:10:23.085181",
|
"modified": "2024-08-13 16:21:18.639208",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Assets",
|
"module": "Assets",
|
||||||
"name": "Asset Value Adjustment",
|
"name": "Asset Value Adjustment",
|
||||||
@@ -182,4 +193,4 @@
|
|||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"title_field": "asset",
|
"title_field": "asset",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class AssetValueAdjustment(Document):
|
|||||||
cost_center: DF.Link | None
|
cost_center: DF.Link | None
|
||||||
current_asset_value: DF.Currency
|
current_asset_value: DF.Currency
|
||||||
date: DF.Date
|
date: DF.Date
|
||||||
|
difference_account: DF.Link
|
||||||
difference_amount: DF.Currency
|
difference_amount: DF.Currency
|
||||||
finance_book: DF.Link | None
|
finance_book: DF.Link | None
|
||||||
journal_entry: DF.Link | None
|
journal_entry: DF.Link | None
|
||||||
@@ -47,6 +48,7 @@ class AssetValueAdjustment(Document):
|
|||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.make_depreciation_entry()
|
self.make_depreciation_entry()
|
||||||
|
self.set_value_after_depreciation()
|
||||||
self.update_asset(self.new_asset_value)
|
self.update_asset(self.new_asset_value)
|
||||||
add_asset_activity(
|
add_asset_activity(
|
||||||
self.asset,
|
self.asset,
|
||||||
@@ -76,7 +78,10 @@ class AssetValueAdjustment(Document):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def set_difference_amount(self):
|
def set_difference_amount(self):
|
||||||
self.difference_amount = flt(self.current_asset_value - self.new_asset_value)
|
self.difference_amount = flt(self.new_asset_value - self.current_asset_value)
|
||||||
|
|
||||||
|
def set_value_after_depreciation(self):
|
||||||
|
frappe.db.set_value("Asset", self.asset, "value_after_depreciation", self.new_asset_value)
|
||||||
|
|
||||||
def set_current_asset_value(self):
|
def set_current_asset_value(self):
|
||||||
if not self.current_asset_value and self.asset:
|
if not self.current_asset_value and self.asset:
|
||||||
@@ -85,7 +90,7 @@ class AssetValueAdjustment(Document):
|
|||||||
def make_depreciation_entry(self):
|
def make_depreciation_entry(self):
|
||||||
asset = frappe.get_doc("Asset", self.asset)
|
asset = frappe.get_doc("Asset", self.asset)
|
||||||
(
|
(
|
||||||
_,
|
fixed_asset_account,
|
||||||
accumulated_depreciation_account,
|
accumulated_depreciation_account,
|
||||||
depreciation_expense_account,
|
depreciation_expense_account,
|
||||||
) = get_depreciation_accounts(asset.asset_category, asset.company)
|
) = get_depreciation_accounts(asset.asset_category, asset.company)
|
||||||
@@ -95,28 +100,41 @@ class AssetValueAdjustment(Document):
|
|||||||
)
|
)
|
||||||
|
|
||||||
je = frappe.new_doc("Journal Entry")
|
je = frappe.new_doc("Journal Entry")
|
||||||
je.voucher_type = "Depreciation Entry"
|
je.voucher_type = "Journal Entry"
|
||||||
je.naming_series = depreciation_series
|
je.naming_series = depreciation_series
|
||||||
je.posting_date = self.date
|
je.posting_date = self.date
|
||||||
je.company = self.company
|
je.company = self.company
|
||||||
je.remark = f"Depreciation Entry against {self.asset} worth {self.difference_amount}"
|
je.remark = f"Revaluation Entry against {self.asset} worth {self.difference_amount}"
|
||||||
je.finance_book = self.finance_book
|
je.finance_book = self.finance_book
|
||||||
|
|
||||||
credit_entry = {
|
entry_template = {
|
||||||
"account": accumulated_depreciation_account,
|
"cost_center": self.cost_center or depreciation_cost_center,
|
||||||
"credit_in_account_currency": self.difference_amount,
|
|
||||||
"cost_center": depreciation_cost_center or self.cost_center,
|
|
||||||
"reference_type": "Asset",
|
"reference_type": "Asset",
|
||||||
"reference_name": self.asset,
|
"reference_name": asset.name,
|
||||||
}
|
}
|
||||||
|
|
||||||
debit_entry = {
|
if self.difference_amount < 0:
|
||||||
"account": depreciation_expense_account,
|
credit_entry = {
|
||||||
"debit_in_account_currency": self.difference_amount,
|
"account": fixed_asset_account,
|
||||||
"cost_center": depreciation_cost_center or self.cost_center,
|
"credit_in_account_currency": -self.difference_amount,
|
||||||
"reference_type": "Asset",
|
**entry_template,
|
||||||
"reference_name": self.asset,
|
}
|
||||||
}
|
debit_entry = {
|
||||||
|
"account": self.difference_account,
|
||||||
|
"debit_in_account_currency": -self.difference_amount,
|
||||||
|
**entry_template,
|
||||||
|
}
|
||||||
|
elif self.difference_amount > 0:
|
||||||
|
credit_entry = {
|
||||||
|
"account": self.difference_account,
|
||||||
|
"credit_in_account_currency": self.difference_amount,
|
||||||
|
**entry_template,
|
||||||
|
}
|
||||||
|
debit_entry = {
|
||||||
|
"account": fixed_asset_account,
|
||||||
|
"debit_in_account_currency": self.difference_amount,
|
||||||
|
**entry_template,
|
||||||
|
}
|
||||||
|
|
||||||
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
|
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
|
||||||
|
|
||||||
@@ -179,3 +197,9 @@ class AssetValueAdjustment(Document):
|
|||||||
)
|
)
|
||||||
asset.flags.ignore_validate_update_after_submit = True
|
asset.flags.ignore_validate_update_after_submit = True
|
||||||
asset.save()
|
asset.save()
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_value_of_accounting_dimensions(asset_name):
|
||||||
|
dimension_fields = [*frappe.get_list("Accounting Dimension", pluck="fieldname"), "cost_center"]
|
||||||
|
return frappe.db.get_value("Asset", asset_name, fieldname=dimension_fields, as_dict=True)
|
||||||
|
|||||||
@@ -93,8 +93,8 @@ class TestAssetValueAdjustment(unittest.TestCase):
|
|||||||
self.assertEqual(first_asset_depr_schedule.status, "Cancelled")
|
self.assertEqual(first_asset_depr_schedule.status, "Cancelled")
|
||||||
|
|
||||||
expected_gle = (
|
expected_gle = (
|
||||||
("_Test Accumulated Depreciations - _TC", 0.0, 4625.29),
|
("_Test Difference Account - _TC", 4625.29, 0.0),
|
||||||
("_Test Depreciations - _TC", 4625.29, 0.0),
|
("_Test Fixed Asset - _TC", 0.0, 4625.29),
|
||||||
)
|
)
|
||||||
|
|
||||||
gle = frappe.db.sql(
|
gle = frappe.db.sql(
|
||||||
@@ -177,8 +177,8 @@ class TestAssetValueAdjustment(unittest.TestCase):
|
|||||||
|
|
||||||
# Test gl entry creted from asset value adjustemnet
|
# Test gl entry creted from asset value adjustemnet
|
||||||
expected_gle = (
|
expected_gle = (
|
||||||
("_Test Accumulated Depreciations - _TC", 0.0, 5625.29),
|
("_Test Difference Account - _TC", 5625.29, 0.0),
|
||||||
("_Test Depreciations - _TC", 5625.29, 0.0),
|
("_Test Fixed Asset - _TC", 0.0, 5625.29),
|
||||||
)
|
)
|
||||||
|
|
||||||
gle = frappe.db.sql(
|
gle = frappe.db.sql(
|
||||||
@@ -259,6 +259,39 @@ class TestAssetValueAdjustment(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(schedules, expected_schedules)
|
self.assertEqual(schedules, expected_schedules)
|
||||||
|
|
||||||
|
def test_difference_amount(self):
|
||||||
|
pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=120000.0, location="Test Location")
|
||||||
|
|
||||||
|
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name")
|
||||||
|
asset_doc = frappe.get_doc("Asset", asset_name)
|
||||||
|
asset_doc.calculate_depreciation = 1
|
||||||
|
asset_doc.available_for_use_date = "2023-01-15"
|
||||||
|
asset_doc.purchase_date = "2023-01-15"
|
||||||
|
|
||||||
|
asset_doc.append(
|
||||||
|
"finance_books",
|
||||||
|
{
|
||||||
|
"expected_value_after_useful_life": 200,
|
||||||
|
"depreciation_method": "Straight Line",
|
||||||
|
"total_number_of_depreciations": 12,
|
||||||
|
"frequency_of_depreciation": 1,
|
||||||
|
"depreciation_start_date": "2023-01-31",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
asset_doc.submit()
|
||||||
|
|
||||||
|
adj_doc = make_asset_value_adjustment(
|
||||||
|
asset=asset_doc.name,
|
||||||
|
current_asset_value=54000,
|
||||||
|
new_asset_value=50000.0,
|
||||||
|
date="2023-08-21",
|
||||||
|
)
|
||||||
|
adj_doc.submit()
|
||||||
|
difference_amount = adj_doc.new_asset_value - adj_doc.current_asset_value
|
||||||
|
self.assertEqual(difference_amount, -4000)
|
||||||
|
asset_doc.load_from_db()
|
||||||
|
self.assertEqual(asset_doc.value_after_depreciation, 50000.0)
|
||||||
|
|
||||||
|
|
||||||
def make_asset_value_adjustment(**args):
|
def make_asset_value_adjustment(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
@@ -272,7 +305,22 @@ def make_asset_value_adjustment(**args):
|
|||||||
"new_asset_value": args.new_asset_value,
|
"new_asset_value": args.new_asset_value,
|
||||||
"current_asset_value": args.current_asset_value,
|
"current_asset_value": args.current_asset_value,
|
||||||
"cost_center": args.cost_center or "Main - _TC",
|
"cost_center": args.cost_center or "Main - _TC",
|
||||||
|
"difference_account": make_difference_account(),
|
||||||
}
|
}
|
||||||
).insert()
|
).insert()
|
||||||
|
|
||||||
return doc
|
return doc
|
||||||
|
|
||||||
|
|
||||||
|
def make_difference_account(**args):
|
||||||
|
account = "_Test Difference Account - _TC"
|
||||||
|
if not frappe.db.exists("Account", account):
|
||||||
|
acc = frappe.new_doc("Account")
|
||||||
|
acc.account_name = "_Test Difference Account"
|
||||||
|
acc.parent_account = "Direct Income - _TC"
|
||||||
|
acc.company = "_Test Company"
|
||||||
|
acc.is_group = 0
|
||||||
|
acc.insert()
|
||||||
|
return acc.name
|
||||||
|
else:
|
||||||
|
return account
|
||||||
|
|||||||
@@ -367,7 +367,11 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (is_drop_ship && doc.status != "Delivered") {
|
if (is_drop_ship && doc.status != "Delivered") {
|
||||||
this.frm.add_custom_button(__("Delivered"), this.delivered_by_supplier, __("Status"));
|
this.frm.add_custom_button(
|
||||||
|
__("Delivered"),
|
||||||
|
this.delivered_by_supplier.bind(this),
|
||||||
|
__("Status")
|
||||||
|
);
|
||||||
|
|
||||||
this.frm.page.set_inner_btn_group_as_primary(__("Status"));
|
this.frm.page.set_inner_btn_group_as_primary(__("Status"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -821,6 +821,9 @@ class AccountsController(TransactionBase):
|
|||||||
and item.get("use_serial_batch_fields")
|
and item.get("use_serial_batch_fields")
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
|
if fieldname == "batch_no" and not item.batch_no:
|
||||||
|
item.set("rate", ret.get("rate"))
|
||||||
|
item.set("price_list_rate", ret.get("price_list_rate"))
|
||||||
item.set(fieldname, value)
|
item.set(fieldname, value)
|
||||||
|
|
||||||
elif fieldname in ["cost_center", "conversion_factor"] and not item.get(
|
elif fieldname in ["cost_center", "conversion_factor"] and not item.get(
|
||||||
|
|||||||
@@ -807,7 +807,27 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
item_group = item_group_doc.parent_item_group
|
item_group = item_group_doc.parent_item_group
|
||||||
|
|
||||||
if not taxes:
|
if not taxes:
|
||||||
return frappe.get_all("Item Tax Template", filters={"disabled": 0, "company": company}, as_list=True)
|
or_filters = []
|
||||||
|
if txt:
|
||||||
|
search_fields = ["name"]
|
||||||
|
|
||||||
|
tax_template_doc = frappe.get_meta("Item Tax Template")
|
||||||
|
|
||||||
|
if title_field := tax_template_doc.title_field:
|
||||||
|
search_fields.append(title_field)
|
||||||
|
if tax_template_doc.search_fields:
|
||||||
|
search_fields.extend(tax_template_doc.get_search_fields())
|
||||||
|
|
||||||
|
for f in search_fields:
|
||||||
|
or_filters.append([doctype, f.strip(), "like", f"%{txt}%"])
|
||||||
|
|
||||||
|
return frappe.get_list(
|
||||||
|
"Item Tax Template",
|
||||||
|
filters={"disabled": 0, "company": company},
|
||||||
|
or_filters=or_filters,
|
||||||
|
as_list=True,
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
valid_from = filters.get("valid_from")
|
valid_from = filters.get("valid_from")
|
||||||
valid_from = valid_from[1] if isinstance(valid_from, list) else valid_from
|
valid_from = valid_from[1] if isinstance(valid_from, list) else valid_from
|
||||||
@@ -820,7 +840,8 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
}
|
}
|
||||||
|
|
||||||
taxes = _get_item_tax_template(args, taxes, for_validate=True)
|
taxes = _get_item_tax_template(args, taxes, for_validate=True)
|
||||||
return [(d,) for d in set(taxes)]
|
txt = txt.lower()
|
||||||
|
return [(d,) for d in set(taxes) if not txt or txt in d.lower()]
|
||||||
|
|
||||||
|
|
||||||
def get_fields(doctype, fields=None):
|
def get_fields(doctype, fields=None):
|
||||||
|
|||||||
@@ -333,7 +333,7 @@ class SellingController(StockController):
|
|||||||
"batch_no": p.batch_no if self.docstatus == 2 else None,
|
"batch_no": p.batch_no if self.docstatus == 2 else None,
|
||||||
"uom": p.uom,
|
"uom": p.uom,
|
||||||
"serial_and_batch_bundle": p.serial_and_batch_bundle
|
"serial_and_batch_bundle": p.serial_and_batch_bundle
|
||||||
or get_serial_and_batch_bundle(p, self),
|
or get_serial_and_batch_bundle(p, self, d),
|
||||||
"name": d.name,
|
"name": d.name,
|
||||||
"target_warehouse": p.target_warehouse,
|
"target_warehouse": p.target_warehouse,
|
||||||
"company": self.company,
|
"company": self.company,
|
||||||
@@ -799,7 +799,7 @@ def set_default_income_account_for_item(obj):
|
|||||||
set_item_default(d.item_code, obj.company, "income_account", d.income_account)
|
set_item_default(d.item_code, obj.company, "income_account", d.income_account)
|
||||||
|
|
||||||
|
|
||||||
def get_serial_and_batch_bundle(child, parent):
|
def get_serial_and_batch_bundle(child, parent, delivery_note_child=None):
|
||||||
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
||||||
|
|
||||||
if child.get("use_serial_batch_fields"):
|
if child.get("use_serial_batch_fields"):
|
||||||
@@ -819,7 +819,7 @@ def get_serial_and_batch_bundle(child, parent):
|
|||||||
"warehouse": child.warehouse,
|
"warehouse": child.warehouse,
|
||||||
"voucher_type": parent.doctype,
|
"voucher_type": parent.doctype,
|
||||||
"voucher_no": parent.name if parent.docstatus < 2 else None,
|
"voucher_no": parent.name if parent.docstatus < 2 else None,
|
||||||
"voucher_detail_no": child.name,
|
"voucher_detail_no": delivery_note_child.name if delivery_note_child else child.name,
|
||||||
"posting_date": parent.posting_date,
|
"posting_date": parent.posting_date,
|
||||||
"posting_time": parent.posting_time,
|
"posting_time": parent.posting_time,
|
||||||
"qty": child.qty,
|
"qty": child.qty,
|
||||||
|
|||||||
@@ -216,6 +216,10 @@ class StockController(AccountsController):
|
|||||||
if self.doctype == "Asset Capitalization":
|
if self.doctype == "Asset Capitalization":
|
||||||
table_name = "stock_items"
|
table_name = "stock_items"
|
||||||
|
|
||||||
|
parent_details = frappe._dict()
|
||||||
|
if table_name == "packed_items":
|
||||||
|
parent_details = self.get_parent_details_for_packed_items()
|
||||||
|
|
||||||
for row in self.get(table_name):
|
for row in self.get(table_name):
|
||||||
if row.serial_and_batch_bundle and (row.serial_no or row.batch_no):
|
if row.serial_and_batch_bundle and (row.serial_no or row.batch_no):
|
||||||
self.validate_serial_nos_and_batches_with_bundle(row)
|
self.validate_serial_nos_and_batches_with_bundle(row)
|
||||||
@@ -246,13 +250,20 @@ class StockController(AccountsController):
|
|||||||
}
|
}
|
||||||
|
|
||||||
if row.get("qty") or row.get("consumed_qty") or row.get("stock_qty"):
|
if row.get("qty") or row.get("consumed_qty") or row.get("stock_qty"):
|
||||||
self.update_bundle_details(bundle_details, table_name, row)
|
self.update_bundle_details(bundle_details, table_name, row, parent_details=parent_details)
|
||||||
self.create_serial_batch_bundle(bundle_details, row)
|
self.create_serial_batch_bundle(bundle_details, row)
|
||||||
|
|
||||||
if row.get("rejected_qty"):
|
if row.get("rejected_qty"):
|
||||||
self.update_bundle_details(bundle_details, table_name, row, is_rejected=True)
|
self.update_bundle_details(bundle_details, table_name, row, is_rejected=True)
|
||||||
self.create_serial_batch_bundle(bundle_details, row)
|
self.create_serial_batch_bundle(bundle_details, row)
|
||||||
|
|
||||||
|
def get_parent_details_for_packed_items(self):
|
||||||
|
parent_details = frappe._dict()
|
||||||
|
for row in self.get("items"):
|
||||||
|
parent_details[row.name] = row
|
||||||
|
|
||||||
|
return parent_details
|
||||||
|
|
||||||
def make_bundle_for_sales_purchase_return(self, table_name=None):
|
def make_bundle_for_sales_purchase_return(self, table_name=None):
|
||||||
if not self.get("is_return"):
|
if not self.get("is_return"):
|
||||||
return
|
return
|
||||||
@@ -387,7 +398,7 @@ class StockController(AccountsController):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def update_bundle_details(self, bundle_details, table_name, row, is_rejected=False):
|
def update_bundle_details(self, bundle_details, table_name, row, is_rejected=False, parent_details=None):
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
|
|
||||||
# Since qty field is different for different doctypes
|
# Since qty field is different for different doctypes
|
||||||
@@ -429,6 +440,11 @@ class StockController(AccountsController):
|
|||||||
warehouse = row.get("target_warehouse") or row.get("warehouse")
|
warehouse = row.get("target_warehouse") or row.get("warehouse")
|
||||||
type_of_transaction = "Outward"
|
type_of_transaction = "Outward"
|
||||||
|
|
||||||
|
if table_name == "packed_items":
|
||||||
|
if not warehouse:
|
||||||
|
warehouse = parent_details[row.parent_detail_docname].warehouse
|
||||||
|
bundle_details["voucher_detail_no"] = parent_details[row.parent_detail_docname].name
|
||||||
|
|
||||||
bundle_details.update(
|
bundle_details.update(
|
||||||
{
|
{
|
||||||
"qty": qty,
|
"qty": qty,
|
||||||
@@ -921,9 +937,11 @@ class StockController(AccountsController):
|
|||||||
row.db_set(dimension.source_fieldname, sl_dict[dimension.target_fieldname])
|
row.db_set(dimension.source_fieldname, sl_dict[dimension.target_fieldname])
|
||||||
|
|
||||||
def make_sl_entries(self, sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
|
def make_sl_entries(self, sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
|
||||||
|
from erpnext.stock.serial_batch_bundle import update_batch_qty
|
||||||
from erpnext.stock.stock_ledger import make_sl_entries
|
from erpnext.stock.stock_ledger import make_sl_entries
|
||||||
|
|
||||||
make_sl_entries(sl_entries, allow_negative_stock, via_landed_cost_voucher)
|
make_sl_entries(sl_entries, allow_negative_stock, via_landed_cost_voucher)
|
||||||
|
update_batch_qty(self.doctype, self.name, via_landed_cost_voucher=via_landed_cost_voucher)
|
||||||
|
|
||||||
def make_gl_entries_on_cancel(self):
|
def make_gl_entries_on_cancel(self):
|
||||||
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
|
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
|
||||||
@@ -1546,6 +1564,27 @@ def repost_required_for_queue(doc: StockController) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def check_item_quality_inspection(doctype, items):
|
||||||
|
if isinstance(items, str):
|
||||||
|
items = json.loads(items)
|
||||||
|
|
||||||
|
inspection_fieldname_map = {
|
||||||
|
"Purchase Receipt": "inspection_required_before_purchase",
|
||||||
|
"Purchase Invoice": "inspection_required_before_purchase",
|
||||||
|
"Subcontracting Receipt": "inspection_required_before_purchase",
|
||||||
|
"Sales Invoice": "inspection_required_before_delivery",
|
||||||
|
"Delivery Note": "inspection_required_before_delivery",
|
||||||
|
}
|
||||||
|
|
||||||
|
items_to_remove = []
|
||||||
|
for item in items:
|
||||||
|
if not frappe.db.get_value("Item", item.get("item_code"), inspection_fieldname_map.get(doctype)):
|
||||||
|
items_to_remove.append(item)
|
||||||
|
items = [item for item in items if item not in items_to_remove]
|
||||||
|
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def make_quality_inspections(doctype, docname, items):
|
def make_quality_inspections(doctype, docname, items):
|
||||||
if isinstance(items, str):
|
if isinstance(items, str):
|
||||||
|
|||||||
@@ -113,11 +113,10 @@ class SubcontractingController(StockController):
|
|||||||
)
|
)
|
||||||
item.sc_conversion_factor = service_item_qty / item.qty
|
item.sc_conversion_factor = service_item_qty / item.qty
|
||||||
|
|
||||||
if (
|
if self.doctype not in "Subcontracting Receipt" and item.qty > flt(
|
||||||
self.doctype not in "Subcontracting Receipt"
|
get_pending_sco_qty(self.purchase_order).get(item.purchase_order_item)
|
||||||
and item.qty
|
/ item.sc_conversion_factor,
|
||||||
> flt(get_pending_sco_qty(self.purchase_order).get(item.purchase_order_item))
|
frappe.get_precision("Purchase Order Item", "qty"),
|
||||||
/ item.sc_conversion_factor
|
|
||||||
):
|
):
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_(
|
_(
|
||||||
|
|||||||
@@ -377,7 +377,7 @@
|
|||||||
"depends_on": "eval:!doc.__islocal",
|
"depends_on": "eval:!doc.__islocal",
|
||||||
"fieldname": "notes_tab",
|
"fieldname": "notes_tab",
|
||||||
"fieldtype": "Tab Break",
|
"fieldtype": "Tab Break",
|
||||||
"label": "Comments"
|
"label": "Notes"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
@@ -516,7 +516,7 @@
|
|||||||
"idx": 5,
|
"idx": 5,
|
||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-12-01 18:46:49.468526",
|
"modified": "2025-01-31 13:40:08.094759",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "CRM",
|
"module": "CRM",
|
||||||
"name": "Lead",
|
"name": "Lead",
|
||||||
@@ -584,4 +584,4 @@
|
|||||||
"states": [],
|
"states": [],
|
||||||
"subject_field": "title",
|
"subject_field": "title",
|
||||||
"title_field": "title"
|
"title_field": "title"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ def create_custom_fields_for_frappe_crm():
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def create_prospect_against_crm_deal():
|
def create_prospect_against_crm_deal():
|
||||||
frappe.only_for("System Manager")
|
|
||||||
doc = frappe.form_dict
|
doc = frappe.form_dict
|
||||||
prospect = frappe.get_doc(
|
prospect = frappe.get_doc(
|
||||||
{
|
{
|
||||||
@@ -152,7 +151,6 @@ def contact_exists(email, mobile_no):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def create_customer(customer_data=None):
|
def create_customer(customer_data=None):
|
||||||
frappe.only_for("System Manager")
|
|
||||||
if not customer_data:
|
if not customer_data:
|
||||||
customer_data = frappe.form_dict
|
customer_data = frappe.form_dict
|
||||||
|
|
||||||
|
|||||||
@@ -562,6 +562,8 @@ accounting_dimension_doctypes = [
|
|||||||
"Payment Reconciliation",
|
"Payment Reconciliation",
|
||||||
"Payment Reconciliation Allocation",
|
"Payment Reconciliation Allocation",
|
||||||
"Payment Request",
|
"Payment Request",
|
||||||
|
"Asset Movement Item",
|
||||||
|
"Asset Depreciation Schedule",
|
||||||
]
|
]
|
||||||
|
|
||||||
get_matching_queries = (
|
get_matching_queries = (
|
||||||
|
|||||||
@@ -982,7 +982,9 @@ class JobCard(Document):
|
|||||||
if self.time_logs:
|
if self.time_logs:
|
||||||
self.status = "Work In Progress"
|
self.status = "Work In Progress"
|
||||||
|
|
||||||
if self.docstatus == 1 and (self.for_quantity <= self.total_completed_qty or not self.items):
|
if self.docstatus == 1 and (
|
||||||
|
self.for_quantity <= (self.total_completed_qty + self.process_loss_qty) or not self.items
|
||||||
|
):
|
||||||
self.status = "Completed"
|
self.status = "Completed"
|
||||||
|
|
||||||
if update_status:
|
if update_status:
|
||||||
|
|||||||
@@ -516,6 +516,7 @@ class TestJobCard(FrappeTestCase):
|
|||||||
self.assertEqual(jc.status, status)
|
self.assertEqual(jc.status, status)
|
||||||
|
|
||||||
jc = frappe.new_doc("Job Card")
|
jc = frappe.new_doc("Job Card")
|
||||||
|
jc.process_loss_qty = 0
|
||||||
jc.for_quantity = 2
|
jc.for_quantity = 2
|
||||||
jc.transferred_qty = 1
|
jc.transferred_qty = 1
|
||||||
jc.total_completed_qty = 0
|
jc.total_completed_qty = 0
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
"bom_section",
|
"bom_section",
|
||||||
"update_bom_costs_automatically",
|
"update_bom_costs_automatically",
|
||||||
"column_break_lhyt",
|
"column_break_lhyt",
|
||||||
"manufacture_sub_assembly_in_operation",
|
|
||||||
"section_break_6",
|
"section_break_6",
|
||||||
"default_wip_warehouse",
|
"default_wip_warehouse",
|
||||||
"default_fg_warehouse",
|
"default_fg_warehouse",
|
||||||
@@ -223,13 +222,6 @@
|
|||||||
"fieldname": "column_break_lhyt",
|
"fieldname": "column_break_lhyt",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"description": "If enabled then system will manufacture Sub-assembly against the Job Card (operation).",
|
|
||||||
"fieldname": "manufacture_sub_assembly_in_operation",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Manufacture Sub-assembly in Operation"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"depends_on": "eval:doc.backflush_raw_materials_based_on == \"BOM\"",
|
"depends_on": "eval:doc.backflush_raw_materials_based_on == \"BOM\"",
|
||||||
@@ -249,7 +241,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-01-13 12:07:03.089977",
|
"modified": "2025-02-05 16:11:11.639916",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Manufacturing Settings",
|
"name": "Manufacturing Settings",
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ class ManufacturingSettings(Document):
|
|||||||
get_rm_cost_from_consumption_entry: DF.Check
|
get_rm_cost_from_consumption_entry: DF.Check
|
||||||
job_card_excess_transfer: DF.Check
|
job_card_excess_transfer: DF.Check
|
||||||
make_serial_no_batch_from_work_order: DF.Check
|
make_serial_no_batch_from_work_order: DF.Check
|
||||||
manufacture_sub_assembly_in_operation: DF.Check
|
|
||||||
material_consumption: DF.Check
|
material_consumption: DF.Check
|
||||||
mins_between_operations: DF.Int
|
mins_between_operations: DF.Int
|
||||||
overproduction_percentage_for_sales_order: DF.Percent
|
overproduction_percentage_for_sales_order: DF.Percent
|
||||||
|
|||||||
@@ -562,6 +562,28 @@ frappe.ui.form.on("Production Plan Sales Order", {
|
|||||||
frappe.ui.form.on("Production Plan Sub Assembly Item", {
|
frappe.ui.form.on("Production Plan Sub Assembly Item", {
|
||||||
fg_warehouse(frm, cdt, cdn) {
|
fg_warehouse(frm, cdt, cdn) {
|
||||||
erpnext.utils.copy_value_in_all_rows(frm.doc, cdt, cdn, "sub_assembly_items", "fg_warehouse");
|
erpnext.utils.copy_value_in_all_rows(frm.doc, cdt, cdn, "sub_assembly_items", "fg_warehouse");
|
||||||
|
|
||||||
|
let row = locals[cdt][cdn];
|
||||||
|
if (row.fg_warehouse && row.production_item) {
|
||||||
|
let child_row = {
|
||||||
|
item_code: row.production_item,
|
||||||
|
warehouse: row.fg_warehouse,
|
||||||
|
};
|
||||||
|
|
||||||
|
frappe.call({
|
||||||
|
method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_bin_details",
|
||||||
|
args: {
|
||||||
|
row: child_row,
|
||||||
|
company: frm.doc.company,
|
||||||
|
for_warehouse: row.fg_warehouse,
|
||||||
|
},
|
||||||
|
callback: function (r) {
|
||||||
|
if (r.message && r.message.length) {
|
||||||
|
frappe.model.set_value(cdt, cdn, "actual_qty", r.message[0].actual_qty);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -937,8 +937,14 @@ class ProductionPlan(Document):
|
|||||||
|
|
||||||
bom_data = []
|
bom_data = []
|
||||||
|
|
||||||
warehouse = (self.sub_assembly_warehouse) if self.skip_available_sub_assembly_item else None
|
get_sub_assembly_items(
|
||||||
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty, self.company, warehouse=warehouse)
|
row.bom_no,
|
||||||
|
bom_data,
|
||||||
|
row.planned_qty,
|
||||||
|
self.company,
|
||||||
|
warehouse=self.sub_assembly_warehouse,
|
||||||
|
skip_available_sub_assembly_item=self.skip_available_sub_assembly_item,
|
||||||
|
)
|
||||||
self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
|
self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
|
||||||
sub_assembly_items_store.extend(bom_data)
|
sub_assembly_items_store.extend(bom_data)
|
||||||
|
|
||||||
@@ -1729,14 +1735,23 @@ def get_item_data(item_code):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, company, warehouse=None, indent=0):
|
def get_sub_assembly_items(
|
||||||
|
bom_no,
|
||||||
|
bom_data,
|
||||||
|
to_produce_qty,
|
||||||
|
company,
|
||||||
|
warehouse=None,
|
||||||
|
indent=0,
|
||||||
|
skip_available_sub_assembly_item=False,
|
||||||
|
):
|
||||||
data = get_bom_children(parent=bom_no)
|
data = get_bom_children(parent=bom_no)
|
||||||
for d in data:
|
for d in data:
|
||||||
if d.expandable:
|
if d.expandable:
|
||||||
parent_item_code = frappe.get_cached_value("BOM", bom_no, "item")
|
parent_item_code = frappe.get_cached_value("BOM", bom_no, "item")
|
||||||
stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty)
|
stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty)
|
||||||
|
|
||||||
if warehouse:
|
bin_details = frappe._dict()
|
||||||
|
if skip_available_sub_assembly_item:
|
||||||
bin_details = get_bin_details(d, company, for_warehouse=warehouse)
|
bin_details = get_bin_details(d, company, for_warehouse=warehouse)
|
||||||
|
|
||||||
for _bin_dict in bin_details:
|
for _bin_dict in bin_details:
|
||||||
@@ -1746,11 +1761,14 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, company, warehouse=
|
|||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
stock_qty = stock_qty - _bin_dict.projected_qty
|
stock_qty = stock_qty - _bin_dict.projected_qty
|
||||||
|
elif warehouse:
|
||||||
|
bin_details = get_bin_details(d, company, for_warehouse=warehouse)
|
||||||
|
|
||||||
if stock_qty > 0:
|
if stock_qty > 0:
|
||||||
bom_data.append(
|
bom_data.append(
|
||||||
frappe._dict(
|
frappe._dict(
|
||||||
{
|
{
|
||||||
|
"actual_qty": bin_details[0].get("actual_qty", 0) if bin_details else 0,
|
||||||
"parent_item_code": parent_item_code,
|
"parent_item_code": parent_item_code,
|
||||||
"description": d.description,
|
"description": d.description,
|
||||||
"production_item": d.item_code,
|
"production_item": d.item_code,
|
||||||
@@ -1768,7 +1786,13 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, company, warehouse=
|
|||||||
|
|
||||||
if d.value:
|
if d.value:
|
||||||
get_sub_assembly_items(
|
get_sub_assembly_items(
|
||||||
d.value, bom_data, stock_qty, company, warehouse, indent=indent + 1
|
d.value,
|
||||||
|
bom_data,
|
||||||
|
stock_qty,
|
||||||
|
company,
|
||||||
|
warehouse,
|
||||||
|
indent=indent + 1,
|
||||||
|
skip_available_sub_assembly_item=skip_available_sub_assembly_item,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -397,7 +397,10 @@ frappe.ui.form.on("Work Order", {
|
|||||||
message = title;
|
message = title;
|
||||||
// pending qty
|
// pending qty
|
||||||
if (!frm.doc.skip_transfer) {
|
if (!frm.doc.skip_transfer) {
|
||||||
var pending_complete = frm.doc.material_transferred_for_manufacturing - frm.doc.produced_qty;
|
var pending_complete =
|
||||||
|
frm.doc.material_transferred_for_manufacturing -
|
||||||
|
frm.doc.produced_qty -
|
||||||
|
frm.doc.process_loss_qty;
|
||||||
if (pending_complete) {
|
if (pending_complete) {
|
||||||
var width = (pending_complete / frm.doc.qty) * 100 - added_min;
|
var width = (pending_complete / frm.doc.qty) * 100 - added_min;
|
||||||
title = __("{0} items in progress", [pending_complete]);
|
title = __("{0} items in progress", [pending_complete]);
|
||||||
@@ -409,6 +412,16 @@ frappe.ui.form.on("Work Order", {
|
|||||||
message = message + ". " + title;
|
message = message + ". " + title;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (frm.doc.process_loss_qty) {
|
||||||
|
var process_loss_width = (frm.doc.process_loss_qty / frm.doc.qty) * 100;
|
||||||
|
title = __("{0} items lost during process.", [frm.doc.process_loss_qty]);
|
||||||
|
bars.push({
|
||||||
|
title: title,
|
||||||
|
width: process_loss_width + "%",
|
||||||
|
progress_class: "progress-bar-danger",
|
||||||
|
});
|
||||||
|
message = message + ". " + title;
|
||||||
|
}
|
||||||
frm.dashboard.add_progress(__("Status"), bars, message);
|
frm.dashboard.add_progress(__("Status"), bars, message);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1519,7 +1519,9 @@ def close_work_order(work_order, status):
|
|||||||
work_order = frappe.get_doc("Work Order", work_order)
|
work_order = frappe.get_doc("Work Order", work_order)
|
||||||
if work_order.get("operations"):
|
if work_order.get("operations"):
|
||||||
job_cards = frappe.get_list(
|
job_cards = frappe.get_list(
|
||||||
"Job Card", filters={"work_order": work_order.name, "status": "Work In Progress"}, pluck="name"
|
"Job Card",
|
||||||
|
filters={"work_order": work_order.name, "status": "Work In Progress", "docstatus": 1},
|
||||||
|
pluck="name",
|
||||||
)
|
)
|
||||||
|
|
||||||
if job_cards:
|
if job_cards:
|
||||||
|
|||||||
@@ -391,3 +391,5 @@ erpnext.patches.v15_0.rename_manufacturing_settings_field
|
|||||||
erpnext.patches.v15_0.migrate_checkbox_to_select_for_reconciliation_effect
|
erpnext.patches.v15_0.migrate_checkbox_to_select_for_reconciliation_effect
|
||||||
erpnext.patches.v15_0.sync_auto_reconcile_config
|
erpnext.patches.v15_0.sync_auto_reconcile_config
|
||||||
execute:frappe.db.set_single_value("Accounts Settings", "exchange_gain_loss_posting_date", "Payment")
|
execute:frappe.db.set_single_value("Accounts Settings", "exchange_gain_loss_posting_date", "Payment")
|
||||||
|
erpnext.patches.v14_0.disable_add_row_in_gross_profit
|
||||||
|
erpnext.patches.v15_0.set_difference_amount_in_asset_value_adjustment
|
||||||
|
|||||||
5
erpnext/patches/v14_0/disable_add_row_in_gross_profit.py
Normal file
5
erpnext/patches/v14_0/disable_add_row_in_gross_profit.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.db.set_value("Report", "Gross Profit", "add_total_row", 0)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
AssetValueAdjustment = frappe.qb.DocType("Asset Value Adjustment")
|
||||||
|
|
||||||
|
frappe.qb.update(AssetValueAdjustment).set(
|
||||||
|
AssetValueAdjustment.difference_amount,
|
||||||
|
AssetValueAdjustment.new_asset_value - AssetValueAdjustment.current_asset_value,
|
||||||
|
).where(AssetValueAdjustment.docstatus != 2).run()
|
||||||
@@ -2367,29 +2367,39 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
primary_action_label: __("Create")
|
primary_action_label: __("Create")
|
||||||
});
|
});
|
||||||
|
|
||||||
this.frm.doc.items.forEach(item => {
|
frappe.call({
|
||||||
if (this.has_inspection_required(item)) {
|
method: "erpnext.controllers.stock_controller.check_item_quality_inspection",
|
||||||
let dialog_items = dialog.fields_dict.items;
|
args: {
|
||||||
dialog_items.df.data.push({
|
doctype: this.frm.doc.doctype,
|
||||||
"item_code": item.item_code,
|
items: this.frm.doc.items
|
||||||
"item_name": item.item_name,
|
},
|
||||||
"qty": item.qty,
|
freeze: true,
|
||||||
"description": item.description,
|
callback: function (r) {
|
||||||
"serial_no": item.serial_no,
|
r.message.forEach(item => {
|
||||||
"batch_no": item.batch_no,
|
if (me.has_inspection_required(item)) {
|
||||||
"sample_size": item.sample_quantity,
|
let dialog_items = dialog.fields_dict.items;
|
||||||
"child_row_reference": item.name,
|
dialog_items.df.data.push({
|
||||||
|
"item_code": item.item_code,
|
||||||
|
"item_name": item.item_name,
|
||||||
|
"qty": item.qty,
|
||||||
|
"description": item.description,
|
||||||
|
"serial_no": item.serial_no,
|
||||||
|
"batch_no": item.batch_no,
|
||||||
|
"sample_size": item.sample_quantity,
|
||||||
|
"child_row_reference": item.name,
|
||||||
|
});
|
||||||
|
dialog_items.grid.refresh();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
dialog_items.grid.refresh();
|
|
||||||
|
data = dialog.fields_dict.items.df.data;
|
||||||
|
if (!data.length) {
|
||||||
|
frappe.msgprint(__("All items in this document already have a linked Quality Inspection."));
|
||||||
|
} else {
|
||||||
|
dialog.show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
data = dialog.fields_dict.items.df.data;
|
|
||||||
if (!data.length) {
|
|
||||||
frappe.msgprint(__("All items in this document already have a linked Quality Inspection."));
|
|
||||||
} else {
|
|
||||||
dialog.show();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
has_inspection_required(item) {
|
has_inspection_required(item) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(10, minmax(0, 1fr));
|
grid-template-columns: repeat(10, minmax(0, 1fr));
|
||||||
gap: var(--margin-md);
|
gap: var(--margin-md);
|
||||||
|
padding: 1%;
|
||||||
|
|
||||||
section {
|
section {
|
||||||
min-height: 45rem;
|
min-height: 45rem;
|
||||||
|
|||||||
@@ -485,6 +485,7 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
pos_profile: this.pos_profile,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -681,7 +682,7 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
i.item_code === item_code &&
|
i.item_code === item_code &&
|
||||||
(!has_batch_no || (has_batch_no && i.batch_no === batch_no)) &&
|
(!has_batch_no || (has_batch_no && i.batch_no === batch_no)) &&
|
||||||
i.uom === uom &&
|
i.uom === uom &&
|
||||||
i.rate === flt(rate)
|
i.price_list_rate === flt(rate)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -357,7 +357,9 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
|||||||
|
|
||||||
this.add_summary_btns(condition_btns_map);
|
this.add_summary_btns(condition_btns_map);
|
||||||
|
|
||||||
this.print_receipt_on_order_complete();
|
if (after_submission) {
|
||||||
|
this.print_receipt_on_order_complete();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
attach_document_info(doc) {
|
attach_document_info(doc) {
|
||||||
|
|||||||
@@ -462,7 +462,7 @@ erpnext.PointOfSale.Payment = class {
|
|||||||
this.$payment_modes.find(".cash-shortcuts").remove();
|
this.$payment_modes.find(".cash-shortcuts").remove();
|
||||||
let shortcuts_html = shortcuts
|
let shortcuts_html = shortcuts
|
||||||
.map((s) => {
|
.map((s) => {
|
||||||
return `<div class="shortcut" data-value="${s}">${format_currency(s, currency, 0)}</div>`;
|
return `<div class="shortcut" data-value="${s}">${format_currency(s, currency)}</div>`;
|
||||||
})
|
})
|
||||||
.join("");
|
.join("");
|
||||||
|
|
||||||
|
|||||||
@@ -871,5 +871,6 @@
|
|||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"title_field": "employee_name"
|
"title_field": "employee_name",
|
||||||
}
|
"track_changes": 1
|
||||||
|
}
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ class DeprecatedBatchNoValuation:
|
|||||||
if not self.non_batchwise_balance_qty:
|
if not self.non_batchwise_balance_qty:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if self.non_batchwise_balance_qty.get(batch_no) == 0:
|
if not self.non_batchwise_balance_qty.get(batch_no):
|
||||||
self.batch_avg_rate[batch_no] = 0.0
|
self.batch_avg_rate[batch_no] = 0.0
|
||||||
self.stock_value_differece[batch_no] = 0.0
|
self.stock_value_differece[batch_no] = 0.0
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -455,10 +455,14 @@ def get_available_batches(kwargs):
|
|||||||
|
|
||||||
batches = get_auto_batch_nos(kwargs)
|
batches = get_auto_batch_nos(kwargs)
|
||||||
for batch in batches:
|
for batch in batches:
|
||||||
if batch.get("batch_no") not in batchwise_qty:
|
key = batch.get("batch_no")
|
||||||
batchwise_qty[batch.get("batch_no")] = batch.get("qty")
|
if kwargs.get("based_on_warehouse"):
|
||||||
|
key = (batch.get("batch_no"), batch.get("warehouse"))
|
||||||
|
|
||||||
|
if key not in batchwise_qty:
|
||||||
|
batchwise_qty[key] = batch.get("qty")
|
||||||
else:
|
else:
|
||||||
batchwise_qty[batch.get("batch_no")] += batch.get("qty")
|
batchwise_qty[key] += batch.get("qty")
|
||||||
|
|
||||||
return batchwise_qty
|
return batchwise_qty
|
||||||
|
|
||||||
|
|||||||
@@ -116,7 +116,6 @@ class ClosingStockBalance(Document):
|
|||||||
"item_group": self.item_group,
|
"item_group": self.item_group,
|
||||||
"warehouse_type": self.warehouse_type,
|
"warehouse_type": self.warehouse_type,
|
||||||
"include_uom": self.include_uom,
|
"include_uom": self.include_uom,
|
||||||
"ignore_closing_balance": 1,
|
|
||||||
"show_variant_attributes": 1,
|
"show_variant_attributes": 1,
|
||||||
"show_stock_ageing_data": 1,
|
"show_stock_ageing_data": 1,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1132,7 +1132,7 @@ def make_packing_slip(source_name, target_doc=None):
|
|||||||
"batch_no": "batch_no",
|
"batch_no": "batch_no",
|
||||||
"description": "description",
|
"description": "description",
|
||||||
"qty": "qty",
|
"qty": "qty",
|
||||||
"stock_uom": "stock_uom",
|
"uom": "stock_uom",
|
||||||
"name": "dn_detail",
|
"name": "dn_detail",
|
||||||
},
|
},
|
||||||
"postprocess": update_item,
|
"postprocess": update_item,
|
||||||
|
|||||||
@@ -756,6 +756,7 @@
|
|||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Incoming Rate",
|
"label": "Incoming Rate",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
|
"options": "Company:company:default_currency",
|
||||||
"precision": "6",
|
"precision": "6",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
@@ -933,7 +934,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-11-21 17:37:37.441498",
|
"modified": "2025-02-05 14:28:32.322181",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Delivery Note Item",
|
"name": "Delivery Note Item",
|
||||||
|
|||||||
@@ -159,11 +159,10 @@ class PackingSlip(StatusUpdater):
|
|||||||
self.from_case_no = self.get_recommended_case_no()
|
self.from_case_no = self.get_recommended_case_no()
|
||||||
|
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
stock_uom, weight_per_unit, weight_uom = frappe.db.get_value(
|
weight_per_unit, weight_uom = frappe.db.get_value(
|
||||||
"Item", item.item_code, ["stock_uom", "weight_per_unit", "weight_uom"]
|
"Item", item.item_code, ["weight_per_unit", "weight_uom"]
|
||||||
)
|
)
|
||||||
|
|
||||||
item.stock_uom = stock_uom
|
|
||||||
if weight_per_unit and not item.net_weight:
|
if weight_per_unit and not item.net_weight:
|
||||||
item.net_weight = weight_per_unit
|
item.net_weight = weight_per_unit
|
||||||
if weight_uom and not item.weight_uom:
|
if weight_uom and not item.weight_uom:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ frappe.ui.form.on("Serial and Batch Bundle", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
before_submit(frm) {
|
before_submit(frm) {
|
||||||
frappe.throw(__("User cannot submitted the Serial and Batch Bundle manually"));
|
frappe.throw(__("The user cannot submit the Serial and Batch Bundle manually"));
|
||||||
},
|
},
|
||||||
|
|
||||||
refresh(frm) {
|
refresh(frm) {
|
||||||
|
|||||||
@@ -84,6 +84,9 @@ class SerialandBatchBundle(Document):
|
|||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
|
if self.docstatus == 1 and self.voucher_detail_no:
|
||||||
|
self.validate_voucher_detail_no()
|
||||||
|
|
||||||
self.reset_serial_batch_bundle()
|
self.reset_serial_batch_bundle()
|
||||||
self.set_batch_no()
|
self.set_batch_no()
|
||||||
self.validate_serial_and_batch_no()
|
self.validate_serial_and_batch_no()
|
||||||
@@ -101,9 +104,36 @@ class SerialandBatchBundle(Document):
|
|||||||
self.set_is_outward()
|
self.set_is_outward()
|
||||||
self.calculate_total_qty()
|
self.calculate_total_qty()
|
||||||
self.set_warehouse()
|
self.set_warehouse()
|
||||||
self.set_incoming_rate()
|
|
||||||
|
if self.voucher_type != "Stock Entry" or not self.voucher_no or self.docstatus == 1:
|
||||||
|
self.set_incoming_rate()
|
||||||
|
|
||||||
self.calculate_qty_and_amount()
|
self.calculate_qty_and_amount()
|
||||||
|
|
||||||
|
def validate_voucher_detail_no(self):
|
||||||
|
if self.type_of_transaction not in ["Inward", "Outward"] or self.voucher_type in [
|
||||||
|
"Installation Note",
|
||||||
|
"Job Card",
|
||||||
|
"Maintenance Schedule",
|
||||||
|
"Pick List",
|
||||||
|
]:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.voucher_type == "POS Invoice":
|
||||||
|
if not frappe.db.exists("POS Invoice Item", self.voucher_detail_no):
|
||||||
|
frappe.throw(
|
||||||
|
_("The serial and batch bundle {0} not linked to {1} {2}").format(
|
||||||
|
bold(self.name), self.voucher_type, bold(self.voucher_no)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
elif not frappe.db.exists("Stock Ledger Entry", {"voucher_detail_no": self.voucher_detail_no}):
|
||||||
|
frappe.throw(
|
||||||
|
_("The serial and batch bundle {0} not linked to {1} {2}").format(
|
||||||
|
bold(self.name), self.voucher_type, bold(self.voucher_no)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def allow_existing_serial_nos(self):
|
def allow_existing_serial_nos(self):
|
||||||
if self.type_of_transaction == "Outward" or not self.has_serial_no:
|
if self.type_of_transaction == "Outward" or not self.has_serial_no:
|
||||||
return
|
return
|
||||||
@@ -1026,7 +1056,6 @@ class SerialandBatchBundle(Document):
|
|||||||
self.set_purchase_document_no()
|
self.set_purchase_document_no()
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.validate_batch_inventory()
|
|
||||||
self.validate_serial_nos_inventory()
|
self.validate_serial_nos_inventory()
|
||||||
|
|
||||||
def set_purchase_document_no(self):
|
def set_purchase_document_no(self):
|
||||||
@@ -1053,25 +1082,9 @@ class SerialandBatchBundle(Document):
|
|||||||
self.validate_batch_inventory()
|
self.validate_batch_inventory()
|
||||||
|
|
||||||
def validate_batch_inventory(self):
|
def validate_batch_inventory(self):
|
||||||
if (
|
|
||||||
self.voucher_type in ["Purchase Invoice", "Purchase Receipt"]
|
|
||||||
and frappe.db.get_value(self.voucher_type, self.voucher_no, "docstatus") == 1
|
|
||||||
):
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.voucher_type in ["Sales Invoice", "Delivery Note"] and self.type_of_transaction == "Inward":
|
|
||||||
return
|
|
||||||
|
|
||||||
if not self.has_batch_no:
|
if not self.has_batch_no:
|
||||||
return
|
return
|
||||||
|
|
||||||
if (
|
|
||||||
self.voucher_type == "Stock Reconciliation"
|
|
||||||
and self.type_of_transaction == "Outward"
|
|
||||||
and frappe.db.get_value("Stock Reconciliation Item", self.voucher_detail_no, "qty") > 0
|
|
||||||
):
|
|
||||||
return
|
|
||||||
|
|
||||||
batches = [d.batch_no for d in self.entries if d.batch_no]
|
batches = [d.batch_no for d in self.entries if d.batch_no]
|
||||||
if not batches:
|
if not batches:
|
||||||
return
|
return
|
||||||
@@ -2440,6 +2453,9 @@ def get_stock_ledgers_batches(kwargs):
|
|||||||
else:
|
else:
|
||||||
query = query.where(stock_ledger_entry[field] == kwargs.get(field))
|
query = query.where(stock_ledger_entry[field] == kwargs.get(field))
|
||||||
|
|
||||||
|
if not kwargs.get("for_stock_levels"):
|
||||||
|
query = query.where((batch_table.expiry_date >= today()) | (batch_table.expiry_date.isnull()))
|
||||||
|
|
||||||
if kwargs.get("posting_date"):
|
if kwargs.get("posting_date"):
|
||||||
if kwargs.get("posting_time") is None:
|
if kwargs.get("posting_time") is None:
|
||||||
kwargs.posting_time = nowtime()
|
kwargs.posting_time = nowtime()
|
||||||
|
|||||||
@@ -755,6 +755,80 @@ class TestSerialandBatchBundle(FrappeTestCase):
|
|||||||
"Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", original_value
|
"Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", original_value
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_voucher_detail_no(self):
|
||||||
|
item_code = make_item(
|
||||||
|
"Test Voucher Detail No 1",
|
||||||
|
properties={
|
||||||
|
"is_stock_item": 1,
|
||||||
|
"has_batch_no": 1,
|
||||||
|
"create_new_batch": 1,
|
||||||
|
"batch_number_series": "TST-VDN-.#####",
|
||||||
|
},
|
||||||
|
).name
|
||||||
|
|
||||||
|
se = make_stock_entry(
|
||||||
|
item_code=item_code,
|
||||||
|
qty=10,
|
||||||
|
target="_Test Warehouse - _TC",
|
||||||
|
rate=500,
|
||||||
|
use_serial_batch_fields=True,
|
||||||
|
do_not_submit=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not frappe.db.exists("Batch", "TST-ACSBBO-TACSB-00001"):
|
||||||
|
frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Batch",
|
||||||
|
"batch_id": "TST-ACSBBO-TACSB-00001",
|
||||||
|
"item": item_code,
|
||||||
|
"company": "_Test Company",
|
||||||
|
}
|
||||||
|
).insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
bundle_doc = make_serial_batch_bundle(
|
||||||
|
{
|
||||||
|
"item_code": item_code,
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
"voucher_type": "Stock Entry",
|
||||||
|
"posting_date": today(),
|
||||||
|
"posting_time": nowtime(),
|
||||||
|
"qty": 10,
|
||||||
|
"batches": frappe._dict({"TST-ACSBBO-TACSB-00001": 10}),
|
||||||
|
"type_of_transaction": "Inward",
|
||||||
|
"do_not_submit": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
se.append(
|
||||||
|
"items",
|
||||||
|
{
|
||||||
|
"item_code": item_code,
|
||||||
|
"t_warehouse": "_Test Warehouse - _TC",
|
||||||
|
"stock_uom": "Nos",
|
||||||
|
"stock_qty": 10,
|
||||||
|
"conversion_factor": 1,
|
||||||
|
"uom": "Nos",
|
||||||
|
"basic_rate": 500,
|
||||||
|
"qty": 10,
|
||||||
|
"use_serial_batch_fields": 0,
|
||||||
|
"serial_and_batch_bundle": bundle_doc.name,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
se.save()
|
||||||
|
|
||||||
|
bundle_doc = frappe.get_doc("Serial and Batch Bundle", bundle_doc.name)
|
||||||
|
self.assertEqual(bundle_doc.voucher_detail_no, se.items[1].name)
|
||||||
|
|
||||||
|
se.remove(se.items[1])
|
||||||
|
se.save()
|
||||||
|
self.assertTrue(len(se.items) == 1)
|
||||||
|
se.submit()
|
||||||
|
|
||||||
|
bundle_doc.reload()
|
||||||
|
self.assertTrue(bundle_doc.docstatus == 0)
|
||||||
|
self.assertRaises(frappe.ValidationError, bundle_doc.submit)
|
||||||
|
|
||||||
|
|
||||||
def get_batch_from_bundle(bundle):
|
def get_batch_from_bundle(bundle):
|
||||||
from erpnext.stock.serial_batch_bundle import get_batch_nos
|
from erpnext.stock.serial_batch_bundle import get_batch_nos
|
||||||
|
|||||||
@@ -206,13 +206,21 @@ def update_stock(ctx, out, doc=None):
|
|||||||
filter_batches(batches, doc)
|
filter_batches(batches, doc)
|
||||||
|
|
||||||
for batch_no, batch_qty in batches.items():
|
for batch_no, batch_qty in batches.items():
|
||||||
|
rate = get_batch_based_item_price(
|
||||||
|
{"price_list": doc.selling_price_list, "uom": out.uom, "batch_no": batch_no},
|
||||||
|
out.item_code,
|
||||||
|
)
|
||||||
if batch_qty >= qty:
|
if batch_qty >= qty:
|
||||||
out.update({"batch_no": batch_no, "actual_batch_qty": qty})
|
out.update({"batch_no": batch_no, "actual_batch_qty": qty})
|
||||||
|
if rate:
|
||||||
|
out.update({"rate": rate, "price_list_rate": rate})
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
qty -= batch_qty
|
qty -= batch_qty
|
||||||
|
|
||||||
out.update({"batch_no": batch_no, "actual_batch_qty": batch_qty})
|
out.update({"batch_no": batch_no, "actual_batch_qty": qty})
|
||||||
|
if rate:
|
||||||
|
out.update({"rate": rate, "price_list_rate": rate})
|
||||||
|
|
||||||
if out.has_serial_no and out.has_batch_no and has_incorrect_serial_nos(ctx, out):
|
if out.has_serial_no and out.has_batch_no and has_incorrect_serial_nos(ctx, out):
|
||||||
kwargs["batches"] = [ctx.get("batch_no")] if ctx.get("batch_no") else [out.get("batch_no")]
|
kwargs["batches"] = [ctx.get("batch_no")] if ctx.get("batch_no") else [out.get("batch_no")]
|
||||||
|
|||||||
@@ -151,6 +151,8 @@ def get_stock_ledger_entries_for_batch_bundle(filters):
|
|||||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||||
batch_package = frappe.qb.DocType("Serial and Batch Entry")
|
batch_package = frappe.qb.DocType("Serial and Batch Entry")
|
||||||
|
|
||||||
|
to_date = get_datetime(filters.to_date + " 23:59:59")
|
||||||
|
|
||||||
query = (
|
query = (
|
||||||
frappe.qb.from_(sle)
|
frappe.qb.from_(sle)
|
||||||
.inner_join(batch_package)
|
.inner_join(batch_package)
|
||||||
@@ -166,7 +168,7 @@ def get_stock_ledger_entries_for_batch_bundle(filters):
|
|||||||
(sle.docstatus < 2)
|
(sle.docstatus < 2)
|
||||||
& (sle.is_cancelled == 0)
|
& (sle.is_cancelled == 0)
|
||||||
& (sle.has_batch_no == 1)
|
& (sle.has_batch_no == 1)
|
||||||
& (sle.posting_date <= filters["to_date"])
|
& (sle.posting_datetime <= to_date)
|
||||||
)
|
)
|
||||||
.groupby(sle.voucher_no, batch_package.batch_no, batch_package.warehouse)
|
.groupby(sle.voucher_no, batch_package.batch_no, batch_package.warehouse)
|
||||||
.orderby(sle.item_code, sle.warehouse)
|
.orderby(sle.item_code, sle.warehouse)
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.query_reports["Incorrect Serial and Batch Bundle"] = {
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
fieldname: "item_code",
|
||||||
|
label: __("Item Code"),
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Item",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "warehouse",
|
||||||
|
label: __("Warehouse"),
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Warehouse",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
get_datatable_options(options) {
|
||||||
|
return Object.assign(options, {
|
||||||
|
checkboxColumn: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onload(report) {
|
||||||
|
report.page.add_inner_button(__("Remove SABB Entry"), () => {
|
||||||
|
let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows();
|
||||||
|
let selected_rows = indexes.map((i) => frappe.query_report.data[i]);
|
||||||
|
|
||||||
|
if (!selected_rows.length) {
|
||||||
|
frappe.throw(__("Please select a row to create a Reposting Entry"));
|
||||||
|
} else {
|
||||||
|
frappe.call({
|
||||||
|
method: "erpnext.stock.report.incorrect_serial_and_batch_bundle.incorrect_serial_and_batch_bundle.remove_sabb_entry",
|
||||||
|
freeze: true,
|
||||||
|
args: {
|
||||||
|
selected_rows: selected_rows,
|
||||||
|
},
|
||||||
|
callback: function (r) {
|
||||||
|
frappe.query_report.refresh();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"add_total_row": 0,
|
||||||
|
"columns": [],
|
||||||
|
"creation": "2025-02-03 15:39:44.521366",
|
||||||
|
"disabled": 0,
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Report",
|
||||||
|
"filters": [],
|
||||||
|
"idx": 0,
|
||||||
|
"is_standard": "Yes",
|
||||||
|
"json": "{}",
|
||||||
|
"letter_head": "Test",
|
||||||
|
"letterhead": null,
|
||||||
|
"modified": "2025-02-03 15:39:47.613040",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Stock",
|
||||||
|
"name": "Incorrect Serial and Batch Bundle",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"prepared_report": 0,
|
||||||
|
"ref_doctype": "Serial and Batch Bundle",
|
||||||
|
"report_name": "Incorrect Serial and Batch Bundle",
|
||||||
|
"report_type": "Script Report",
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"role": "Stock User"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Purchase Manager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Delivery Manager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "System Manager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Delivery User"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Manufacturing User"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Purchase User"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Stock Manager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Manufacturing Manager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Maintenance User"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timeout": 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
|
||||||
|
|
||||||
|
def execute(filters: dict | None = None):
|
||||||
|
"""Return columns and data for the report.
|
||||||
|
|
||||||
|
This is the main entry point for the report. It accepts the filters as a
|
||||||
|
dictionary and should return columns and data. It is called by the framework
|
||||||
|
every time the report is refreshed or a filter is updated.
|
||||||
|
"""
|
||||||
|
columns = get_columns()
|
||||||
|
data = get_data(filters)
|
||||||
|
|
||||||
|
return columns, data
|
||||||
|
|
||||||
|
|
||||||
|
def get_columns() -> list[dict]:
|
||||||
|
"""Return columns for the report.
|
||||||
|
|
||||||
|
One field definition per column, just like a DocType field definition.
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"label": _("Serial and Batch Bundle"),
|
||||||
|
"fieldname": "name",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Serial and Batch Bundle",
|
||||||
|
"width": 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": _("Voucher Type"),
|
||||||
|
"fieldname": "voucher_type",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"width": 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": _("Voucher No"),
|
||||||
|
"fieldname": "voucher_no",
|
||||||
|
"fieldtype": "Dynamic Link",
|
||||||
|
"options": "voucher_type",
|
||||||
|
"width": 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": _("Voucher Detail No"),
|
||||||
|
"fieldname": "voucher_detail_no",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"width": 200,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_data(filters) -> list[list]:
|
||||||
|
"""Return data for the report.
|
||||||
|
|
||||||
|
The report data is a list of rows, with each row being a list of cell values.
|
||||||
|
"""
|
||||||
|
|
||||||
|
SABB = frappe.qb.DocType("Serial And Batch Bundle")
|
||||||
|
SLE = frappe.qb.DocType("Stock Ledger Entry")
|
||||||
|
ignore_voycher_types = [
|
||||||
|
"Installation Note",
|
||||||
|
"Job Card",
|
||||||
|
"Maintenance Schedule",
|
||||||
|
"Pick List",
|
||||||
|
]
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(SABB)
|
||||||
|
.left_join(SLE)
|
||||||
|
.on(SABB.name == SLE.serial_and_batch_bundle)
|
||||||
|
.select(
|
||||||
|
SABB.name,
|
||||||
|
SABB.voucher_type,
|
||||||
|
SABB.voucher_no,
|
||||||
|
SABB.voucher_detail_no,
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
(SLE.serial_and_batch_bundle.isnull())
|
||||||
|
& (SABB.docstatus == 1)
|
||||||
|
& (SABB.is_cancelled == 0)
|
||||||
|
& (SABB.voucher_type.notin(ignore_voycher_types))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for field in filters:
|
||||||
|
query = query.where(SABB[field] == filters[field])
|
||||||
|
|
||||||
|
data = query.run(as_dict=1)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def remove_sabb_entry(selected_rows):
|
||||||
|
if isinstance(selected_rows, str):
|
||||||
|
selected_rows = frappe.parse_json(selected_rows)
|
||||||
|
|
||||||
|
for row in selected_rows:
|
||||||
|
doc = frappe.get_doc("Serial and Batch Bundle", row.get("name"))
|
||||||
|
doc.cancel()
|
||||||
|
doc.delete()
|
||||||
|
|
||||||
|
frappe.msgprint(_("Selected Serial and Batch Bundle entries have been removed."))
|
||||||
@@ -7,7 +7,7 @@ from operator import itemgetter
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import cint, date_diff, flt
|
from frappe.utils import cint, date_diff, flt, get_datetime
|
||||||
|
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
|
|
||||||
@@ -424,6 +424,7 @@ class FIFOSlots:
|
|||||||
def __get_stock_ledger_entries(self) -> Iterator[dict]:
|
def __get_stock_ledger_entries(self) -> Iterator[dict]:
|
||||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||||
item = self.__get_item_query() # used as derived table in sle query
|
item = self.__get_item_query() # used as derived table in sle query
|
||||||
|
to_date = get_datetime(self.filters.get("to_date") + " 23:59:59")
|
||||||
|
|
||||||
sle_query = (
|
sle_query = (
|
||||||
frappe.qb.from_(sle)
|
frappe.qb.from_(sle)
|
||||||
@@ -450,7 +451,7 @@ class FIFOSlots:
|
|||||||
.where(
|
.where(
|
||||||
(sle.item_code == item.name)
|
(sle.item_code == item.name)
|
||||||
& (sle.company == self.filters.get("company"))
|
& (sle.company == self.filters.get("company"))
|
||||||
& (sle.posting_date <= self.filters.get("to_date"))
|
& (sle.posting_datetime <= to_date)
|
||||||
& (sle.is_cancelled != 1)
|
& (sle.is_cancelled != 1)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -467,7 +468,7 @@ class FIFOSlots:
|
|||||||
if warehouses:
|
if warehouses:
|
||||||
sle_query = sle_query.where(sle.warehouse.isin(warehouses))
|
sle_query = sle_query.where(sle.warehouse.isin(warehouses))
|
||||||
|
|
||||||
sle_query = sle_query.orderby(sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty)
|
sle_query = sle_query.orderby(sle.posting_datetime, sle.creation)
|
||||||
|
|
||||||
return sle_query.run(as_dict=True, as_iterator=True)
|
return sle_query.run(as_dict=True, as_iterator=True)
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from collections import defaultdict
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.query_builder.functions import CombineDatetime, Sum
|
from frappe.query_builder.functions import CombineDatetime, Sum
|
||||||
from frappe.utils import cint, flt
|
from frappe.utils import cint, flt, get_datetime
|
||||||
|
|
||||||
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
|
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
@@ -367,6 +367,9 @@ def get_columns(filters):
|
|||||||
|
|
||||||
|
|
||||||
def get_stock_ledger_entries(filters, items):
|
def get_stock_ledger_entries(filters, items):
|
||||||
|
from_date = get_datetime(filters.from_date + " 00:00:00")
|
||||||
|
to_date = get_datetime(filters.to_date + " 23:59:59")
|
||||||
|
|
||||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||||
query = (
|
query = (
|
||||||
frappe.qb.from_(sle)
|
frappe.qb.from_(sle)
|
||||||
@@ -390,12 +393,8 @@ def get_stock_ledger_entries(filters, items):
|
|||||||
sle.serial_no,
|
sle.serial_no,
|
||||||
sle.project,
|
sle.project,
|
||||||
)
|
)
|
||||||
.where(
|
.where((sle.docstatus < 2) & (sle.is_cancelled == 0) & (sle.posting_datetime[from_date:to_date]))
|
||||||
(sle.docstatus < 2)
|
.orderby(sle.posting_datetime)
|
||||||
& (sle.is_cancelled == 0)
|
|
||||||
& (sle.posting_date[filters.from_date : filters.to_date])
|
|
||||||
)
|
|
||||||
.orderby(CombineDatetime(sle.posting_date, sle.posting_time))
|
|
||||||
.orderby(sle.creation)
|
.orderby(sle.creation)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -302,9 +302,6 @@ class SerialBatchBundle:
|
|||||||
):
|
):
|
||||||
self.set_batch_no_in_serial_nos()
|
self.set_batch_no_in_serial_nos()
|
||||||
|
|
||||||
if self.item_details.has_batch_no == 1:
|
|
||||||
self.update_batch_qty()
|
|
||||||
|
|
||||||
if self.sle.is_cancelled and self.sle.serial_and_batch_bundle:
|
if self.sle.is_cancelled and self.sle.serial_and_batch_bundle:
|
||||||
self.cancel_serial_and_batch_bundle()
|
self.cancel_serial_and_batch_bundle()
|
||||||
|
|
||||||
@@ -410,26 +407,6 @@ class SerialBatchBundle:
|
|||||||
.where(sn_table.name.isin(serial_nos))
|
.where(sn_table.name.isin(serial_nos))
|
||||||
).run()
|
).run()
|
||||||
|
|
||||||
def update_batch_qty(self):
|
|
||||||
from erpnext.stock.doctype.batch.batch import get_available_batches
|
|
||||||
|
|
||||||
batches = get_batch_nos(self.sle.serial_and_batch_bundle)
|
|
||||||
if not self.sle.serial_and_batch_bundle and self.sle.batch_no:
|
|
||||||
batches = frappe._dict({self.sle.batch_no: self.sle.actual_qty})
|
|
||||||
|
|
||||||
batches_qty = get_available_batches(
|
|
||||||
frappe._dict(
|
|
||||||
{
|
|
||||||
"item_code": self.item_code,
|
|
||||||
"batch_no": list(batches.keys()),
|
|
||||||
"consider_negative_batches": 1,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
for batch_no in batches:
|
|
||||||
frappe.db.set_value("Batch", batch_no, "batch_qty", batches_qty.get(batch_no, 0))
|
|
||||||
|
|
||||||
|
|
||||||
def get_serial_nos(serial_and_batch_bundle, serial_nos=None):
|
def get_serial_nos(serial_and_batch_bundle, serial_nos=None):
|
||||||
if not serial_and_batch_bundle:
|
if not serial_and_batch_bundle:
|
||||||
@@ -657,7 +634,6 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
|
|||||||
|
|
||||||
parent = frappe.qb.DocType("Serial and Batch Bundle")
|
parent = frappe.qb.DocType("Serial and Batch Bundle")
|
||||||
child = frappe.qb.DocType("Serial and Batch Entry")
|
child = frappe.qb.DocType("Serial and Batch Entry")
|
||||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
|
||||||
|
|
||||||
timestamp_condition = ""
|
timestamp_condition = ""
|
||||||
if self.sle.posting_date:
|
if self.sle.posting_date:
|
||||||
@@ -690,14 +666,6 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
|
|||||||
& (parent.docstatus == 1)
|
& (parent.docstatus == 1)
|
||||||
& (parent.is_cancelled == 0)
|
& (parent.is_cancelled == 0)
|
||||||
& (parent.type_of_transaction.isin(["Inward", "Outward"]))
|
& (parent.type_of_transaction.isin(["Inward", "Outward"]))
|
||||||
& (
|
|
||||||
ExistsCriterion(
|
|
||||||
frappe.qb.from_(sle)
|
|
||||||
.select(sle.name)
|
|
||||||
.where((parent.name == sle.serial_and_batch_bundle) & (sle.is_cancelled == 0))
|
|
||||||
)
|
|
||||||
| (parent.voucher_type == "POS Invoice")
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.groupby(child.batch_no)
|
.groupby(child.batch_no)
|
||||||
)
|
)
|
||||||
@@ -1258,3 +1226,53 @@ def get_serial_nos_batch(serial_nos):
|
|||||||
as_list=1,
|
as_list=1,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def update_batch_qty(voucher_type, voucher_no, via_landed_cost_voucher=False):
|
||||||
|
from erpnext.stock.doctype.batch.batch import get_available_batches
|
||||||
|
|
||||||
|
batches = get_distinct_batches(voucher_type, voucher_no)
|
||||||
|
if not batches:
|
||||||
|
return
|
||||||
|
|
||||||
|
precision = frappe.get_precision("Batch", "batch_qty")
|
||||||
|
batch_data = get_available_batches(
|
||||||
|
frappe._dict({"batch_no": batches, "consider_negative_batches": 1, "based_on_warehouse": True})
|
||||||
|
)
|
||||||
|
batchwise_qty = defaultdict(float)
|
||||||
|
|
||||||
|
for (batch_no, warehouse), qty in batch_data.items():
|
||||||
|
if not via_landed_cost_voucher and flt(qty, precision) < 0:
|
||||||
|
throw_negative_batch_validation(batch_no, warehouse, qty)
|
||||||
|
|
||||||
|
batchwise_qty[batch_no] += qty
|
||||||
|
|
||||||
|
for batch_no in batches:
|
||||||
|
qty = flt(batchwise_qty.get(batch_no, 0), precision)
|
||||||
|
frappe.db.set_value("Batch", batch_no, "batch_qty", qty)
|
||||||
|
|
||||||
|
|
||||||
|
def throw_negative_batch_validation(batch_no, warehouse, qty):
|
||||||
|
frappe.throw(
|
||||||
|
_("The Batch {0} has negative quantity {1} in warehouse {2}. Please correct the quantity.").format(
|
||||||
|
bold(batch_no), bold(qty), bold(warehouse)
|
||||||
|
),
|
||||||
|
title=_("Negative Batch Quantity"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_distinct_batches(voucher_type, voucher_no):
|
||||||
|
bundles = frappe.get_all(
|
||||||
|
"Serial and Batch Bundle",
|
||||||
|
filters={"voucher_no": voucher_no, "voucher_type": voucher_type},
|
||||||
|
pluck="name",
|
||||||
|
)
|
||||||
|
if not bundles:
|
||||||
|
return
|
||||||
|
|
||||||
|
return frappe.get_all(
|
||||||
|
"Serial and Batch Entry",
|
||||||
|
filters={"parent": ("in", bundles), "batch_no": ("is", "set")},
|
||||||
|
group_by="batch_no",
|
||||||
|
pluck="batch_no",
|
||||||
|
)
|
||||||
|
|||||||
@@ -405,23 +405,13 @@ def create_json_gz_file(data, doc, file_name=None) -> str:
|
|||||||
compressed_content = gzip.compress(encoded_content)
|
compressed_content = gzip.compress(encoded_content)
|
||||||
|
|
||||||
if not file_name:
|
if not file_name:
|
||||||
json_filename = f"{scrub(doc.doctype)}-{scrub(doc.name)}.json.gz"
|
return create_file(doc, compressed_content)
|
||||||
_file = frappe.get_doc(
|
|
||||||
{
|
|
||||||
"doctype": "File",
|
|
||||||
"file_name": json_filename,
|
|
||||||
"attached_to_doctype": doc.doctype,
|
|
||||||
"attached_to_name": doc.name,
|
|
||||||
"attached_to_field": "reposting_data_file",
|
|
||||||
"content": compressed_content,
|
|
||||||
"is_private": 1,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
_file.save(ignore_permissions=True)
|
|
||||||
|
|
||||||
return _file.file_url
|
|
||||||
else:
|
else:
|
||||||
file_doc = frappe.get_doc("File", file_name)
|
file_doc = frappe.get_doc("File", file_name)
|
||||||
|
if "/frappe_s3_attachment." in file_doc.file_url:
|
||||||
|
file_doc.delete()
|
||||||
|
return create_file(doc, compressed_content)
|
||||||
|
|
||||||
path = file_doc.get_full_path()
|
path = file_doc.get_full_path()
|
||||||
|
|
||||||
with open(path, "wb") as f:
|
with open(path, "wb") as f:
|
||||||
@@ -430,6 +420,24 @@ def create_json_gz_file(data, doc, file_name=None) -> str:
|
|||||||
return doc.reposting_data_file
|
return doc.reposting_data_file
|
||||||
|
|
||||||
|
|
||||||
|
def create_file(doc, compressed_content):
|
||||||
|
json_filename = f"{scrub(doc.doctype)}-{scrub(doc.name)}.json.gz"
|
||||||
|
_file = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "File",
|
||||||
|
"file_name": json_filename,
|
||||||
|
"attached_to_doctype": doc.doctype,
|
||||||
|
"attached_to_name": doc.name,
|
||||||
|
"attached_to_field": "reposting_data_file",
|
||||||
|
"content": compressed_content,
|
||||||
|
"is_private": 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
_file.save(ignore_permissions=True)
|
||||||
|
|
||||||
|
return _file.file_url
|
||||||
|
|
||||||
|
|
||||||
def get_items_to_be_repost(voucher_type=None, voucher_no=None, doc=None, reposting_data=None):
|
def get_items_to_be_repost(voucher_type=None, voucher_no=None, doc=None, reposting_data=None):
|
||||||
if not reposting_data and doc and doc.reposting_data_file:
|
if not reposting_data and doc and doc.reposting_data_file:
|
||||||
reposting_data = get_reposting_data(doc.reposting_data_file)
|
reposting_data = get_reposting_data(doc.reposting_data_file)
|
||||||
|
|||||||
@@ -34,3 +34,66 @@ class TestGetItemDetail(FrappeTestCase):
|
|||||||
)
|
)
|
||||||
details = get_item_details(args)
|
details = get_item_details(args)
|
||||||
self.assertEqual(details.get("price_list_rate"), 100)
|
self.assertEqual(details.get("price_list_rate"), 100)
|
||||||
|
|
||||||
|
# making this test in get_item_details test file as feat/fix is present in that method
|
||||||
|
def test_fetch_price_from_list_rate_on_doc_save(self):
|
||||||
|
# create item
|
||||||
|
item = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Item",
|
||||||
|
"item_code": "Test Item with Batch",
|
||||||
|
"item_name": "Test Item with Batch",
|
||||||
|
"item_group": "All Item Groups",
|
||||||
|
"is_stock_item": 1,
|
||||||
|
"has_batch_no": 1,
|
||||||
|
}
|
||||||
|
).insert()
|
||||||
|
|
||||||
|
# create batch
|
||||||
|
frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Batch",
|
||||||
|
"batch_id": "BATCH01",
|
||||||
|
"item": item,
|
||||||
|
}
|
||||||
|
).insert()
|
||||||
|
|
||||||
|
# create item price
|
||||||
|
frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Item Price",
|
||||||
|
"price_list": "Standard Selling",
|
||||||
|
"item_code": item.item_code,
|
||||||
|
"price_list_rate": 50,
|
||||||
|
"batch_no": "BATCH01",
|
||||||
|
}
|
||||||
|
).insert()
|
||||||
|
|
||||||
|
# create purchase receipt to have some stock for delivery
|
||||||
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||||
|
|
||||||
|
make_purchase_receipt(
|
||||||
|
item_code=item.item_code,
|
||||||
|
warehouse="_Test Warehouse - _TC",
|
||||||
|
qty=100,
|
||||||
|
rate=100,
|
||||||
|
batch_no="BATCH01",
|
||||||
|
)
|
||||||
|
|
||||||
|
# creating sales order just to create delivery note from it
|
||||||
|
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||||
|
|
||||||
|
so = make_sales_order(item_code=item.item_code, qty=2, rate=75)
|
||||||
|
|
||||||
|
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
|
||||||
|
|
||||||
|
dn = make_delivery_note(so.name)
|
||||||
|
|
||||||
|
# Test 1 : On creation of DN, item's batch won't be fetched and rate will remaing the same as in SO
|
||||||
|
self.assertIsNone(dn.items[0].batch_no)
|
||||||
|
self.assertEqual(dn.items[0].rate, 75)
|
||||||
|
|
||||||
|
# Test 2 : On saving the DN, item's batch will be fetched and rate will be updated from Item Price
|
||||||
|
dn.save()
|
||||||
|
self.assertEqual(dn.items[0].batch_no, "BATCH01")
|
||||||
|
self.assertEqual(dn.items[0].rate, 50)
|
||||||
|
|||||||
Reference in New Issue
Block a user