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

chore: release v15
This commit is contained in:
ruthra kumar
2025-05-13 19:32:35 +05:30
committed by GitHub
30 changed files with 381 additions and 303 deletions

View File

@@ -21,7 +21,6 @@
"party_name", "party_name",
"book_advance_payments_in_separate_party_account", "book_advance_payments_in_separate_party_account",
"reconcile_on_advance_payment_date", "reconcile_on_advance_payment_date",
"advance_reconciliation_takes_effect_on",
"column_break_11", "column_break_11",
"bank_account", "bank_account",
"party_bank_account", "party_bank_account",
@@ -786,18 +785,9 @@
"options": "No\nYes", "options": "No\nYes",
"print_hide": 1, "print_hide": 1,
"search_index": 1 "search_index": 1
},
{
"default": "Oldest Of Invoice Or Advance",
"fetch_from": "company.reconciliation_takes_effect_on",
"fieldname": "advance_reconciliation_takes_effect_on",
"fieldtype": "Select",
"hidden": 1,
"label": "Advance Reconciliation Takes Effect On",
"no_copy": 1,
"options": "Advance Payment Date\nOldest Of Invoice Or Advance\nReconciliation Date"
} }
], ],
"grid_page_length": 50,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [ "links": [
@@ -809,7 +799,7 @@
"table_fieldname": "payment_entries" "table_fieldname": "payment_entries"
} }
], ],
"modified": "2025-03-24 16:18:19.920701", "modified": "2025-05-08 11:18:10.238085",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Entry", "name": "Payment Entry",
@@ -849,6 +839,7 @@
"write": 1 "write": 1
} }
], ],
"row_format": "Dynamic",
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",

View File

@@ -1492,9 +1492,12 @@ class PaymentEntry(AccountsController):
else: else:
# For backwards compatibility # For backwards compatibility
# Supporting reposting on payment entries reconciled before select field introduction # Supporting reposting on payment entries reconciled before select field introduction
if self.advance_reconciliation_takes_effect_on == "Advance Payment Date": reconciliation_takes_effect_on = frappe.get_cached_value(
"Company", self.company, "reconciliation_takes_effect_on"
)
if reconciliation_takes_effect_on == "Advance Payment Date":
posting_date = self.posting_date posting_date = self.posting_date
elif self.advance_reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance": elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
date_field = "posting_date" date_field = "posting_date"
if invoice.reference_doctype in ["Sales Order", "Purchase Order"]: if invoice.reference_doctype in ["Sales Order", "Purchase Order"]:
date_field = "transaction_date" date_field = "transaction_date"
@@ -1504,7 +1507,7 @@ class PaymentEntry(AccountsController):
if getdate(posting_date) < getdate(self.posting_date): if getdate(posting_date) < getdate(self.posting_date):
posting_date = self.posting_date posting_date = self.posting_date
elif self.advance_reconciliation_takes_effect_on == "Reconciliation Date": elif reconciliation_takes_effect_on == "Reconciliation Date":
posting_date = nowdate() posting_date = nowdate()
frappe.db.set_value("Payment Entry Reference", invoice.name, "reconcile_effect_on", posting_date) frappe.db.set_value("Payment Entry Reference", invoice.name, "reconcile_effect_on", posting_date)
@@ -2361,7 +2364,7 @@ def get_outstanding_reference_documents(args, validate=False):
accounts = get_party_account( accounts = get_party_account(
args.get("party_type"), args.get("party"), args.get("company"), include_advance=True args.get("party_type"), args.get("party"), args.get("company"), include_advance=True
) )
advance_account = accounts[1] if len(accounts) >= 1 else None advance_account = accounts[1] if len(accounts) > 1 else None
if party_account == advance_account: if party_account == advance_account:
party_account = accounts[0] party_account = accounts[0]

View File

@@ -133,7 +133,12 @@ class PeriodClosingVoucher(AccountsController):
self.make_gl_entries() self.make_gl_entries()
def on_cancel(self): def on_cancel(self):
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry") self.ignore_linked_doctypes = (
"GL Entry",
"Stock Ledger Entry",
"Payment Ledger Entry",
"Account Closing Balance",
)
self.block_if_future_closing_voucher_exists() self.block_if_future_closing_voucher_exists()
self.db_set("gle_processing_status", "In Progress") self.db_set("gle_processing_status", "In Progress")
self.cancel_gl_entries() self.cancel_gl_entries()

View File

@@ -1695,6 +1695,9 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
# Configure Buying Settings to allow rate change # Configure Buying Settings to allow rate change
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0) frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
# Configure Accounts Settings to allow 300% over billing
frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", 300)
# Create PR: rate = 1000, qty = 5 # Create PR: rate = 1000, qty = 5
pr = make_purchase_receipt( pr = make_purchase_receipt(
item_code="_Test Non Stock Item", rate=1000, posting_date=add_days(nowdate(), -2) item_code="_Test Non Stock Item", rate=1000, posting_date=add_days(nowdate(), -2)
@@ -2756,6 +2759,43 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
self.assertEqual(invoice.grand_total, 300) self.assertEqual(invoice.grand_total, 300)
def test_pr_pi_over_billing(self):
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as make_purchase_invoice_from_pr,
)
# Configure Buying Settings to allow rate change
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
pr = make_purchase_receipt(qty=10, rate=10)
pi = make_purchase_invoice_from_pr(pr.name)
pi.items[0].rate = 12
# Test 1 - This will fail because over billing is not allowed
self.assertRaises(frappe.ValidationError, pi.submit)
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1)
# Test 2 - This will now submit because over billing allowance is ignored when set_landed_cost_based_on_purchase_invoice_rate is checked
pi.submit()
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0)
frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", 20)
pi.cancel()
pi = make_purchase_invoice_from_pr(pr.name)
pi.items[0].rate = 12
# Test 3 - This will now submit because over billing is allowed upto 20%
pi.submit()
pi.reload()
pi.cancel()
pi = make_purchase_invoice_from_pr(pr.name)
pi.items[0].rate = 13
# Test 4 - Since this PI is overbilled by 130% and only 120% is allowed, it will fail
self.assertRaises(frappe.ValidationError, pi.submit)
def set_advance_flag(company, flag, default_account): def set_advance_flag(company, flag, default_account):
frappe.db.set_value( frappe.db.set_value(

View File

@@ -563,17 +563,20 @@ def get_account_type_map(company):
def get_result_as_list(data, filters): def get_result_as_list(data, filters):
balance, _balance_in_account_currency = 0, 0 balance = 0
for d in data: for d in data:
if not d.get("posting_date"): if not d.get("posting_date"):
balance, _balance_in_account_currency = 0, 0 balance = 0
balance = get_balance(d, balance, "debit", "credit") balance = get_balance(d, balance, "debit", "credit")
d["balance"] = balance d["balance"] = balance
d["account_currency"] = filters.account_currency d["account_currency"] = filters.account_currency
d["presentation_currency"] = filters.presentation_currency
return data return data
@@ -599,11 +602,8 @@ def get_columns(filters):
if filters.get("presentation_currency"): if filters.get("presentation_currency"):
currency = filters["presentation_currency"] currency = filters["presentation_currency"]
else: else:
if filters.get("company"): company = filters.get("company") or get_default_company()
currency = get_company_currency(filters["company"]) filters["presentation_currency"] = currency = get_company_currency(company)
else:
company = get_default_company()
currency = get_company_currency(company)
columns = [ columns = [
{ {
@@ -624,19 +624,22 @@ def get_columns(filters):
{ {
"label": _("Debit ({0})").format(currency), "label": _("Debit ({0})").format(currency),
"fieldname": "debit", "fieldname": "debit",
"fieldtype": "Float", "fieldtype": "Currency",
"options": "presentation_currency",
"width": 130, "width": 130,
}, },
{ {
"label": _("Credit ({0})").format(currency), "label": _("Credit ({0})").format(currency),
"fieldname": "credit", "fieldname": "credit",
"fieldtype": "Float", "fieldtype": "Currency",
"options": "presentation_currency",
"width": 130, "width": 130,
}, },
{ {
"label": _("Balance ({0})").format(currency), "label": _("Balance ({0})").format(currency),
"fieldname": "balance", "fieldname": "balance",
"fieldtype": "Float", "fieldtype": "Currency",
"options": "presentation_currency",
"width": 130, "width": 130,
}, },
] ]

View File

@@ -35,7 +35,6 @@ def execute(filters=None):
filters=filters, filters=filters,
accumulated_values=filters.accumulated_values, accumulated_values=filters.accumulated_values,
ignore_closing_entries=True, ignore_closing_entries=True,
ignore_accumulated_values_for_fy=True,
) )
expense = get_data( expense = get_data(
@@ -46,7 +45,6 @@ def execute(filters=None):
filters=filters, filters=filters,
accumulated_values=filters.accumulated_values, accumulated_values=filters.accumulated_values,
ignore_closing_entries=True, ignore_closing_entries=True,
ignore_accumulated_values_for_fy=True,
) )
net_profit_loss = get_net_profit_loss( net_profit_loss = get_net_profit_loss(

View File

@@ -2,8 +2,9 @@
# MIT License. See license.txt # MIT License. See license.txt
import frappe import frappe
from frappe.desk.query_report import export_query
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase
from frappe.utils import getdate, today from frappe.utils import add_days, getdate, today
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.financial_statements import get_period_list from erpnext.accounts.report.financial_statements import get_period_list
@@ -57,7 +58,7 @@ class TestProfitAndLossStatement(AccountsTestMixin, FrappeTestCase):
period_end_date=fy.year_end_date, period_end_date=fy.year_end_date,
filter_based_on="Fiscal Year", filter_based_on="Fiscal Year",
periodicity="Monthly", periodicity="Monthly",
accumulated_vallues=True, accumulated_values=False,
) )
def test_profit_and_loss_output_and_summary(self): def test_profit_and_loss_output_and_summary(self):
@@ -90,3 +91,82 @@ class TestProfitAndLossStatement(AccountsTestMixin, FrappeTestCase):
with self.subTest(current_period_key=current_period_key): with self.subTest(current_period_key=current_period_key):
self.assertEqual(acc[current_period_key], 150) self.assertEqual(acc[current_period_key], 150)
self.assertEqual(acc["total"], 150) self.assertEqual(acc["total"], 150)
def test_p_and_l_export(self):
self.create_sales_invoice(qty=1, rate=150)
filters = self.get_report_filters()
frappe.local.form_dict = frappe._dict(
{
"report_name": "Profit and Loss Statement",
"file_format_type": "CSV",
"filters": filters,
"visible_idx": [0, 1, 2, 3, 4, 5, 6],
}
)
export_query()
contents = frappe.response["filecontent"].decode()
sales_account = frappe.db.get_value("Company", self.company, "default_income_account")
self.assertIn(sales_account, contents)
def test_accumulate_filter(self):
# ensure 2 fiscal years
cur_fy = self.get_fiscal_year()
find_for = add_days(cur_fy.year_start_date, -1)
_x = frappe.db.get_all(
"Fiscal Year",
filters={"disabled": 0, "year_start_date": ("<=", find_for), "year_end_date": (">=", find_for)},
)[0]
prev_fy = frappe.get_doc("Fiscal Year", _x.name)
prev_fy.append("companies", {"company": self.company})
prev_fy.save()
# make SI on both of them
prev_fy_si = self.create_sales_invoice(qty=1, rate=450, do_not_submit=True)
prev_fy_si.posting_date = add_days(prev_fy.year_end_date, -1)
prev_fy_si.save().submit()
income_acc = prev_fy_si.items[0].income_account
self.create_sales_invoice(qty=1, rate=120)
# Unaccumualted
filters = frappe._dict(
company=self.company,
from_fiscal_year=prev_fy.name,
to_fiscal_year=cur_fy.name,
period_start_date=prev_fy.year_start_date,
period_end_date=cur_fy.year_end_date,
filter_based_on="Date Range",
periodicity="Yearly",
accumulated_values=False,
)
result = execute(filters)
columns = [result[0][2], result[0][3]]
expected = {
"account": income_acc,
columns[0].get("fieldname"): 450.0,
columns[1].get("fieldname"): 120.0,
}
actual = [x for x in result[1] if x.get("account") == income_acc]
self.assertEqual(len(actual), 1)
actual = actual[0]
for key in expected.keys():
with self.subTest(key=key):
self.assertEqual(expected.get(key), actual.get(key))
# accumualted
filters.update({"accumulated_values": True})
expected = {
"account": income_acc,
columns[0].get("fieldname"): 450.0,
columns[1].get("fieldname"): 570.0,
}
result = execute(filters)
columns = [result[0][2], result[0][3]]
actual = [x for x in result[1] if x.get("account") == income_acc]
self.assertEqual(len(actual), 1)
actual = actual[0]
for key in expected.keys():
with self.subTest(key=key):
self.assertEqual(expected.get(key), actual.get(key))

View File

@@ -713,10 +713,13 @@ def update_reference_in_payment_entry(
update_advance_paid = [] update_advance_paid = []
# Update Reconciliation effect date in reference # Update Reconciliation effect date in reference
reconciliation_takes_effect_on = frappe.get_cached_value(
"Company", payment_entry.company, "reconciliation_takes_effect_on"
)
if payment_entry.book_advance_payments_in_separate_party_account: if payment_entry.book_advance_payments_in_separate_party_account:
if payment_entry.advance_reconciliation_takes_effect_on == "Advance Payment Date": if reconciliation_takes_effect_on == "Advance Payment Date":
reconcile_on = payment_entry.posting_date reconcile_on = payment_entry.posting_date
elif payment_entry.advance_reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance": elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
date_field = "posting_date" date_field = "posting_date"
if d.against_voucher_type in ["Sales Order", "Purchase Order"]: if d.against_voucher_type in ["Sales Order", "Purchase Order"]:
date_field = "transaction_date" date_field = "transaction_date"
@@ -724,7 +727,7 @@ def update_reference_in_payment_entry(
if getdate(reconcile_on) < getdate(payment_entry.posting_date): if getdate(reconcile_on) < getdate(payment_entry.posting_date):
reconcile_on = payment_entry.posting_date reconcile_on = payment_entry.posting_date
elif payment_entry.advance_reconciliation_takes_effect_on == "Reconciliation Date": elif reconciliation_takes_effect_on == "Reconciliation Date":
reconcile_on = nowdate() reconcile_on = nowdate()
reference_details.update({"reconcile_effect_on": reconcile_on}) reference_details.update({"reconcile_effect_on": reconcile_on})

View File

@@ -122,6 +122,7 @@ class Asset(AccountsController):
# end: auto-generated types # end: auto-generated types
def validate(self): def validate(self):
self.validate_category()
self.validate_precision() self.validate_precision()
self.set_purchase_doc_row_item() self.set_purchase_doc_row_item()
self.validate_asset_values() self.validate_asset_values()
@@ -343,6 +344,17 @@ class Asset(AccountsController):
title=_("Missing Finance Book"), title=_("Missing Finance Book"),
) )
def validate_category(self):
non_depreciable_category = frappe.db.get_value(
"Asset Category", self.asset_category, "non_depreciable_category"
)
if self.calculate_depreciation and non_depreciable_category:
frappe.throw(
_(
"This asset category is marked as non-depreciable. Please disable depreciation calculation or choose a different category."
)
)
def validate_precision(self): def validate_precision(self):
if self.gross_purchase_amount: if self.gross_purchase_amount:
self.gross_purchase_amount = flt( self.gross_purchase_amount = flt(

View File

@@ -330,45 +330,6 @@ def _make_journal_entry_for_depreciation(
row.db_update() row.db_update()
def get_depreciation_accounts(asset_category, company):
fixed_asset_account = accumulated_depreciation_account = depreciation_expense_account = None
accounts = frappe.db.get_value(
"Asset Category Account",
filters={"parent": asset_category, "company_name": company},
fieldname=[
"fixed_asset_account",
"accumulated_depreciation_account",
"depreciation_expense_account",
],
as_dict=1,
)
if accounts:
fixed_asset_account = accounts.fixed_asset_account
accumulated_depreciation_account = accounts.accumulated_depreciation_account
depreciation_expense_account = accounts.depreciation_expense_account
if not accumulated_depreciation_account or not depreciation_expense_account:
accounts = frappe.get_cached_value(
"Company", company, ["accumulated_depreciation_account", "depreciation_expense_account"]
)
if not accumulated_depreciation_account:
accumulated_depreciation_account = accounts[0]
if not depreciation_expense_account:
depreciation_expense_account = accounts[1]
if not fixed_asset_account or not accumulated_depreciation_account or not depreciation_expense_account:
frappe.throw(
_("Please set Depreciation related Accounts in Asset Category {0} or Company {1}").format(
asset_category, company
)
)
return fixed_asset_account, accumulated_depreciation_account, depreciation_expense_account
def get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation_expense_account): def get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation_expense_account):
root_type = frappe.get_value("Account", depreciation_expense_account, "root_type") root_type = frappe.get_value("Account", depreciation_expense_account, "root_type")
@@ -721,8 +682,8 @@ def get_asset_details(asset, finance_book=None):
value_after_depreciation = asset.get_value_after_depreciation(finance_book) value_after_depreciation = asset.get_value_after_depreciation(finance_book)
accumulated_depr_amount = flt(asset.gross_purchase_amount) - flt(value_after_depreciation) accumulated_depr_amount = flt(asset.gross_purchase_amount) - flt(value_after_depreciation)
fixed_asset_account, accumulated_depr_account, _ = get_asset_accounts( fixed_asset_account, accumulated_depr_account, _ = get_depreciation_accounts(
asset.asset_category, asset.company, accumulated_depr_amount asset.asset_category, asset.company
) )
disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company) disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company)
depreciation_cost_center = asset.cost_center or depreciation_cost_center depreciation_cost_center = asset.cost_center or depreciation_cost_center
@@ -738,9 +699,13 @@ def get_asset_details(asset, finance_book=None):
) )
def get_asset_accounts(asset_category, company, accumulated_depr_amount): def get_depreciation_accounts(asset_category, company):
fixed_asset_account = accumulated_depreciation_account = depreciation_expense_account = None fixed_asset_account = accumulated_depreciation_account = depreciation_expense_account = None
non_depreciable_category = frappe.db.get_value(
"Asset Category", asset_category, "non_depreciable_category"
)
accounts = frappe.db.get_value( accounts = frappe.db.get_value(
"Asset Category Account", "Asset Category Account",
filters={"parent": asset_category, "company_name": company}, filters={"parent": asset_category, "company_name": company},
@@ -760,7 +725,7 @@ def get_asset_accounts(asset_category, company, accumulated_depr_amount):
if not fixed_asset_account: if not fixed_asset_account:
frappe.throw(_("Please set Fixed Asset Account in Asset Category {0}").format(asset_category)) frappe.throw(_("Please set Fixed Asset Account in Asset Category {0}").format(asset_category))
if accumulated_depr_amount: if not non_depreciable_category:
accounts = frappe.get_cached_value( accounts = frappe.get_cached_value(
"Company", company, ["accumulated_depreciation_account", "depreciation_expense_account"] "Company", company, ["accumulated_depreciation_account", "depreciation_expense_account"]
) )

View File

@@ -12,6 +12,7 @@
"column_break_3", "column_break_3",
"depreciation_options", "depreciation_options",
"enable_cwip_accounting", "enable_cwip_accounting",
"non_depreciable_category",
"finance_book_detail", "finance_book_detail",
"finance_books", "finance_books",
"section_break_2", "section_break_2",
@@ -63,10 +64,16 @@
"fieldname": "enable_cwip_accounting", "fieldname": "enable_cwip_accounting",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Enable Capital Work in Progress Accounting" "label": "Enable Capital Work in Progress Accounting"
},
{
"default": "0",
"fieldname": "non_depreciable_category",
"fieldtype": "Check",
"label": "Non Depreciable Category"
} }
], ],
"links": [], "links": [],
"modified": "2021-02-24 15:05:38.621803", "modified": "2025-05-13 15:33:03.791814",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Category", "name": "Asset Category",
@@ -111,6 +118,7 @@
"write": 1 "write": 1
} }
], ],
"row_format": "Dynamic",
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",

View File

@@ -17,15 +17,14 @@ class AssetCategory(Document):
if TYPE_CHECKING: if TYPE_CHECKING:
from frappe.types import DF from frappe.types import DF
from erpnext.assets.doctype.asset_category_account.asset_category_account import ( from erpnext.assets.doctype.asset_category_account.asset_category_account import AssetCategoryAccount
AssetCategoryAccount,
)
from erpnext.assets.doctype.asset_finance_book.asset_finance_book import AssetFinanceBook from erpnext.assets.doctype.asset_finance_book.asset_finance_book import AssetFinanceBook
accounts: DF.Table[AssetCategoryAccount] accounts: DF.Table[AssetCategoryAccount]
asset_category_name: DF.Data asset_category_name: DF.Data
enable_cwip_accounting: DF.Check enable_cwip_accounting: DF.Check
finance_books: DF.Table[AssetFinanceBook] finance_books: DF.Table[AssetFinanceBook]
non_depreciable_category: DF.Check
# end: auto-generated types # end: auto-generated types
def validate(self): def validate(self):

View File

@@ -111,6 +111,13 @@ frappe.ui.form.on("Opportunity", {
}, },
__("Create") __("Create")
); );
let company_currency = erpnext.get_currency(frm.doc.company);
if (company_currency != frm.doc.currency) {
frm.add_custom_button(__("Fetch Latest Exchange Rate"), function () {
frm.trigger("currency");
});
}
} }
if (!frm.doc.__islocal && frm.perm[0].write && frm.doc.docstatus == 0) { if (!frm.doc.__islocal && frm.perm[0].write && frm.doc.docstatus == 0) {
@@ -152,7 +159,7 @@ frappe.ui.form.on("Opportunity", {
currency: function (frm) { currency: function (frm) {
let company_currency = erpnext.get_currency(frm.doc.company); let company_currency = erpnext.get_currency(frm.doc.company);
if (company_currency != frm.doc.company) { if (company_currency != frm.doc.currency) {
frappe.call({ frappe.call({
method: "erpnext.setup.utils.get_exchange_rate", method: "erpnext.setup.utils.get_exchange_rate",
args: { args: {
@@ -278,7 +285,6 @@ erpnext.crm.Opportunity = class Opportunity extends frappe.ui.form.Controller {
} }
this.setup_queries(); this.setup_queries();
this.frm.trigger("currency");
} }
refresh() { refresh() {

View File

@@ -2665,6 +2665,81 @@ class TestWorkOrder(FrappeTestCase):
) )
frappe.db.set_single_value("Stock Settings", "pick_serial_and_batch_based_on", original_based_on) frappe.db.set_single_value("Stock Settings", "pick_serial_and_batch_based_on", original_based_on)
def test_operations_time_planning_calculation(self):
from erpnext.manufacturing.doctype.routing.test_routing import create_routing, setup_operations
operations = [
{"operation": "Test Operation A", "workstation": "Test Workstation A", "time_in_mins": 1},
{"operation": "Test Operation B", "workstation": "Test Workstation A", "time_in_mins": 4},
{"operation": "Test Operation C", "workstation": "Test Workstation A", "time_in_mins": 3},
{"operation": "Test Operation D", "workstation": "Test Workstation A", "time_in_mins": 2},
]
setup_operations(operations)
routing_doc = create_routing(routing_name="Testing Route", operations=operations)
bom = make_bom(
item="_Test FG Item", raw_materials=["_Test Item"], with_operations=1, routing=routing_doc.name
)
wo = make_wo_order_test_record(
item="_Test FG Item",
bom_no=bom.name,
qty=5,
source_warehouse="_Test Warehouse 1 - _TC",
skip_transfer=1,
fg_warehouse="_Test Warehouse 2 - _TC",
)
# Initial check
self.assertEqual(wo.operations[0].operation, "Test Operation A")
self.assertEqual(wo.operations[1].operation, "Test Operation B")
self.assertEqual(wo.operations[2].operation, "Test Operation C")
self.assertEqual(wo.operations[3].operation, "Test Operation D")
wo = frappe.copy_doc(wo)
wo.operations[3].sequence_id = 2
wo.submit()
# Test 2 : Sort line items in child table based on sequence ID
self.assertEqual(wo.operations[0].operation, "Test Operation A")
self.assertEqual(wo.operations[1].operation, "Test Operation B")
self.assertEqual(wo.operations[2].operation, "Test Operation D")
self.assertEqual(wo.operations[3].operation, "Test Operation C")
wo = frappe.copy_doc(wo)
wo.operations[3].sequence_id = 1
wo.submit()
self.assertEqual(wo.operations[0].operation, "Test Operation A")
self.assertEqual(wo.operations[1].operation, "Test Operation C")
self.assertEqual(wo.operations[2].operation, "Test Operation B")
self.assertEqual(wo.operations[3].operation, "Test Operation D")
wo = frappe.copy_doc(wo)
wo.operations[0].sequence_id = 3
wo.submit()
self.assertEqual(wo.operations[0].operation, "Test Operation C")
self.assertEqual(wo.operations[1].operation, "Test Operation B")
self.assertEqual(wo.operations[2].operation, "Test Operation D")
self.assertEqual(wo.operations[3].operation, "Test Operation A")
wo = frappe.copy_doc(wo)
wo.operations[1].sequence_id = 0
# Test 3 - Error should be thrown if any one operation does not have sequence id but others do
self.assertRaises(frappe.ValidationError, wo.submit)
workstation = frappe.get_doc("Workstation", "Test Workstation A")
workstation.production_capacity = 4
workstation.save()
wo = frappe.copy_doc(wo)
wo.operations[1].sequence_id = 2
wo.submit()
# Test 4 - If Sequence ID is same then planned start time for both operations should be same
self.assertEqual(wo.operations[1].planned_start_time, wo.operations[2].planned_start_time)
def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse): def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse):
from erpnext.stock.doctype.stock_entry.test_stock_entry import ( from erpnext.stock.doctype.stock_entry.test_stock_entry import (

View File

@@ -2,7 +2,7 @@
"actions": [], "actions": [],
"allow_import": 1, "allow_import": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
"creation": "2013-01-10 16:34:16", "creation": "2025-04-09 12:09:40.634472",
"doctype": "DocType", "doctype": "DocType",
"document_type": "Setup", "document_type": "Setup",
"engine": "InnoDB", "engine": "InnoDB",

View File

@@ -624,19 +624,30 @@ class WorkOrder(Document):
enable_capacity_planning = not cint(manufacturing_settings_doc.disable_capacity_planning) enable_capacity_planning = not cint(manufacturing_settings_doc.disable_capacity_planning)
plan_days = cint(manufacturing_settings_doc.capacity_planning_for_days) or 30 plan_days = cint(manufacturing_settings_doc.capacity_planning_for_days) or 30
for index, row in enumerate(self.operations): if all([op.sequence_id for op in self.operations]):
self.operations = sorted(self.operations, key=lambda op: op.sequence_id)
for idx, op in enumerate(self.operations):
op.idx = idx + 1
elif any([op.sequence_id for op in self.operations]):
frappe.throw(
_(
"Row #{0}: Incorrect Sequence ID. If any single operation has a Sequence ID then all other operations must have one too."
).format(next((op.idx for op in self.operations if not op.sequence_id), None))
)
for idx, row in enumerate(self.operations):
qty = self.qty qty = self.qty
while qty > 0: while qty > 0:
qty = split_qty_based_on_batch_size(self, row, qty) qty = split_qty_based_on_batch_size(self, row, qty)
if row.job_card_qty > 0: if row.job_card_qty > 0:
self.prepare_data_for_job_card(row, index, plan_days, enable_capacity_planning) self.prepare_data_for_job_card(row, idx, plan_days, enable_capacity_planning)
planned_end_date = self.operations and self.operations[-1].planned_end_time planned_end_date = self.operations and self.operations[-1].planned_end_time
if planned_end_date: if planned_end_date:
self.db_set("planned_end_date", planned_end_date) self.db_set("planned_end_date", planned_end_date)
def prepare_data_for_job_card(self, row, index, plan_days, enable_capacity_planning): def prepare_data_for_job_card(self, row, idx, plan_days, enable_capacity_planning):
self.set_operation_start_end_time(index, row) self.set_operation_start_end_time(row, idx)
job_card_doc = create_job_card( job_card_doc = create_job_card(
self, row, auto_create=True, enable_capacity_planning=enable_capacity_planning self, row, auto_create=True, enable_capacity_planning=enable_capacity_planning
@@ -661,12 +672,24 @@ class WorkOrder(Document):
row.db_update() row.db_update()
def set_operation_start_end_time(self, idx, row): def set_operation_start_end_time(self, row, idx):
"""Set start and end time for given operation. If first operation, set start as """Set start and end time for given operation. If first operation, set start as
`planned_start_date`, else add time diff to end time of earlier operation.""" `planned_start_date`, else add time diff to end time of earlier operation."""
if idx == 0: if idx == 0:
# first operation at planned_start date # first operation at planned_start date
row.planned_start_time = self.planned_start_date row.planned_start_time = self.planned_start_date
elif self.operations[idx - 1].sequence_id:
if self.operations[idx - 1].sequence_id == row.sequence_id:
row.planned_start_time = self.operations[idx - 1].planned_start_time
else:
last_ops_with_same_sequence_ids = sorted(
[op for op in self.operations if op.sequence_id == self.operations[idx - 1].sequence_id],
key=lambda op: get_datetime(op.planned_end_time),
)
row.planned_start_time = (
get_datetime(last_ops_with_same_sequence_ids[-1].planned_end_time)
+ get_mins_between_operations()
)
else: else:
row.planned_start_time = ( row.planned_start_time = (
get_datetime(self.operations[idx - 1].planned_end_time) + get_mins_between_operations() get_datetime(self.operations[idx - 1].planned_end_time) + get_mins_between_operations()

View File

@@ -1,6 +1,6 @@
{ {
"actions": [], "actions": [],
"creation": "2014-10-16 14:35:41.950175", "creation": "2025-04-09 12:12:19.824560",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
@@ -102,13 +102,15 @@
"fieldname": "planned_start_time", "fieldname": "planned_start_time",
"fieldtype": "Datetime", "fieldtype": "Datetime",
"label": "Planned Start Time", "label": "Planned Start Time",
"no_copy": 1 "no_copy": 1,
"read_only": 1
}, },
{ {
"fieldname": "planned_end_time", "fieldname": "planned_end_time",
"fieldtype": "Datetime", "fieldtype": "Datetime",
"label": "Planned End Time", "label": "Planned End Time",
"no_copy": 1 "no_copy": 1,
"read_only": 1
}, },
{ {
"fieldname": "column_break_10", "fieldname": "column_break_10",
@@ -191,7 +193,6 @@
{ {
"fieldname": "sequence_id", "fieldname": "sequence_id",
"fieldtype": "Int", "fieldtype": "Int",
"hidden": 1,
"label": "Sequence ID", "label": "Sequence ID",
"print_hide": 1 "print_hide": 1
}, },
@@ -219,10 +220,11 @@
"read_only": 1 "read_only": 1
} }
], ],
"grid_page_length": 50,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-06-09 14:03:01.612909", "modified": "2025-04-09 16:21:47.110564",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Work Order Operation", "name": "Work Order Operation",

View File

@@ -389,7 +389,6 @@ erpnext.patches.v15_0.enable_allow_existing_serial_no
erpnext.patches.v15_0.update_cc_in_process_statement_of_accounts erpnext.patches.v15_0.update_cc_in_process_statement_of_accounts
erpnext.patches.v15_0.update_asset_status_to_work_in_progress erpnext.patches.v15_0.update_asset_status_to_work_in_progress
erpnext.patches.v15_0.rename_manufacturing_settings_field erpnext.patches.v15_0.rename_manufacturing_settings_field
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.v14_0.disable_add_row_in_gross_profit

View File

@@ -1,18 +0,0 @@
import frappe
def execute():
"""
A New select field 'reconciliation_takes_effect_on' has been added to control Advance Payment Reconciliation dates.
Migrate old checkbox configuration to new select field on 'Company' and 'Payment Entry'
"""
companies = frappe.db.get_all("Company", fields=["name", "reconciliation_takes_effect_on"])
for x in companies:
new_value = (
"Advance Payment Date" if x.reconcile_on_advance_payment_date else "Oldest Of Invoice Or Advance"
)
frappe.db.set_value("Company", x.name, "reconciliation_takes_effect_on", new_value)
frappe.db.sql(
"""update `tabPayment Entry` set advance_reconciliation_takes_effect_on = if(reconcile_on_advance_payment_date = 0, 'Oldest Of Invoice Or Advance', 'Advance Payment Date')"""
)

View File

@@ -535,7 +535,7 @@ def get_timesheets_list(doctype, txt, filters, limit_start, limit_page_length=20
table.name, table.name,
child_table.activity_type, child_table.activity_type,
table.status, table.status,
table.total_billable_hours, child_table.billing_hours,
(table.sales_invoice | child_table.sales_invoice).as_("sales_invoice"), (table.sales_invoice | child_table.sales_invoice).as_("sales_invoice"),
child_table.project, child_table.project,
) )

View File

@@ -47,7 +47,7 @@ frappe.ui.form.on("Event", {
frm.add_custom_button( frm.add_custom_button(
__("Add Sales Partners"), __("Add Sales Partners"),
function () { function () {
new frappe.desk.eventParticipants(frm, "Sales Partners"); new frappe.desk.eventParticipants(frm, "Sales Partner");
}, },
__("Add Participants") __("Add Participants")
); );

View File

@@ -147,10 +147,8 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te
bin_join_selection, bin_join_condition = "", "" bin_join_selection, bin_join_condition = "", ""
if hide_unavailable_items: if hide_unavailable_items:
bin_join_selection = ", `tabBin` bin" bin_join_selection = "LEFT JOIN `tabBin` bin ON bin.item_code = item.name"
bin_join_condition = ( bin_join_condition = "AND item.is_stock_item = 0 OR (item.is_stock_item = 1 AND bin.warehouse = %(warehouse)s AND bin.actual_qty > 0)"
"AND bin.warehouse = %(warehouse)s AND bin.item_code = item.name AND bin.actual_qty > 0"
)
items_data = frappe.db.sql( items_data = frappe.db.sql(
""" """

View File

@@ -214,13 +214,12 @@ def get_or_create_account(company_name, account):
default_root_type = "Liability" default_root_type = "Liability"
root_type = account.get("root_type", default_root_type) root_type = account.get("root_type", default_root_type)
or_filters = {"account_name": account.get("account_name")}
if account.get("account_number"):
or_filters.update({"account_number": account.get("account_number")})
existing_accounts = frappe.get_all( existing_accounts = frappe.get_all(
"Account", "Account", filters={"company": company_name, "root_type": root_type}, or_filters=or_filters
filters={"company": company_name, "root_type": root_type},
or_filters={
"account_name": account.get("account_name"),
"account_number": account.get("account_number"),
},
) )
if existing_accounts: if existing_accounts:

View File

@@ -1081,6 +1081,7 @@ def get_billed_amount_against_po(po_items):
def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate=False): def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate=False):
# Update Billing % based on pending accepted qty # Update Billing % based on pending accepted qty
buying_settings = frappe.get_single("Buying Settings") buying_settings = frappe.get_single("Buying Settings")
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
total_amount, total_billed_amount = 0, 0 total_amount, total_billed_amount = 0, 0
item_wise_returned_qty = get_item_wise_returned_qty(pr_doc) item_wise_returned_qty = get_item_wise_returned_qty(pr_doc)
@@ -1119,6 +1120,14 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate
adjusted_amt = flt(adjusted_amt * flt(pr_doc.conversion_rate), item.precision("amount")) adjusted_amt = flt(adjusted_amt * flt(pr_doc.conversion_rate), item.precision("amount"))
item.db_set("amount_difference_with_purchase_invoice", adjusted_amt, update_modified=False) item.db_set("amount_difference_with_purchase_invoice", adjusted_amt, update_modified=False)
elif item.billed_amt > item.amount:
per_over_billed = (flt(item.billed_amt / item.amount, 2) * 100) - 100
if per_over_billed > over_billing_allowance:
frappe.throw(
_("Over Billing Allowance exceeded for Purchase Receipt Item {0} ({1}) by {2}%").format(
item.name, frappe.bold(item.item_code), per_over_billed - over_billing_allowance
)
)
percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6) percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6)
pr_doc.db_set("per_billed", percent_billed) pr_doc.db_set("per_billed", percent_billed)

View File

@@ -342,7 +342,7 @@ def remove_attached_file(docname):
if file_name := frappe.db.get_value( if file_name := frappe.db.get_value(
"File", {"attached_to_name": docname, "attached_to_doctype": "Repost Item Valuation"}, "name" "File", {"attached_to_name": docname, "attached_to_doctype": "Repost Item Valuation"}, "name"
): ):
frappe.delete_doc("File", file_name, ignore_permissions=True, delete_permanently=True) frappe.delete_doc("File", file_name, ignore_permissions=True, delete_permanently=True, force=True)
def repost_sl_entries(doc): def repost_sl_entries(doc):

View File

@@ -6,7 +6,7 @@ import json
from collections import defaultdict from collections import defaultdict
import frappe import frappe
from frappe import _ from frappe import _, bold
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.query_builder.functions import Sum from frappe.query_builder.functions import Sum
from frappe.utils import ( from frappe.utils import (
@@ -526,6 +526,14 @@ class StockEntry(StockController):
OpeningEntryAccountError, OpeningEntryAccountError,
) )
if self.purpose != "Material Issue" and acc_details.account_type == "Cost of Goods Sold":
frappe.msgprint(
_(
"At row {0}: You have selected the Difference Account {1}, which is a Cost of Goods Sold type account. Please select a different account"
).format(d.idx, bold(get_link_to_form("Account", d.expense_account))),
title=_("Warning : Cost of Goods Sold Account"),
)
def validate_warehouse(self): def validate_warehouse(self):
"""perform various (sometimes conditional) validations on warehouse""" """perform various (sometimes conditional) validations on warehouse"""

View File

@@ -203,9 +203,19 @@ class StockReconciliation(StockController):
) )
) )
if self.docstatus == 1: if (
bundle.voucher_no = self.name self.docstatus == 1
bundle.submit() and item.current_serial_and_batch_bundle
and frappe.db.get_value(
"Serial and Batch Bundle", item.current_serial_and_batch_bundle, "docstatus"
)
== 0
):
sabb_doc = frappe.get_doc(
"Serial and Batch Bundle", item.current_serial_and_batch_bundle
)
sabb_doc.voucher_no = self.name
sabb_doc.submit()
item.db_set( item.db_set(
{ {

View File

@@ -51,49 +51,11 @@ frappe.query_reports["Available Serial No"] = {
}; };
}, },
}, },
{
fieldname: "item_group",
label: __("Item Group"),
fieldtype: "Link",
options: "Item Group",
},
{
fieldname: "batch_no",
label: __("Batch No"),
fieldtype: "Link",
options: "Batch",
on_change() {
const batch_no = frappe.query_report.get_filter_value("batch_no");
if (batch_no) {
frappe.query_report.set_filter_value("segregate_serial_batch_bundle", 1);
} else {
frappe.query_report.set_filter_value("segregate_serial_batch_bundle", 0);
}
},
},
{
fieldname: "brand",
label: __("Brand"),
fieldtype: "Link",
options: "Brand",
},
{ {
fieldname: "voucher_no", fieldname: "voucher_no",
label: __("Voucher #"), label: __("Voucher #"),
fieldtype: "Data", fieldtype: "Data",
}, },
{
fieldname: "project",
label: __("Project"),
fieldtype: "Link",
options: "Project",
},
{
fieldname: "include_uom",
label: __("Include UOM"),
fieldtype: "Link",
options: "UOM",
},
{ {
fieldname: "valuation_field_type", fieldname: "valuation_field_type",
label: __("Valuation Field Type"), label: __("Valuation Field Type"),

View File

@@ -3,108 +3,62 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.query_builder.functions import Sum
from frappe.utils import cint, flt from frappe.utils import cint, flt
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, get_serial_nos_from_sle_list from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, get_serial_nos_from_sle_list
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import get_stock_balance_for
from erpnext.stock.report.stock_ledger.stock_ledger import ( from erpnext.stock.report.stock_ledger.stock_ledger import (
check_inventory_dimension_filters_applied,
get_item_details, get_item_details,
get_item_group_condition,
get_opening_balance, get_opening_balance,
get_opening_balance_from_batch,
get_stock_ledger_entries, get_stock_ledger_entries,
) )
from erpnext.stock.utils import ( from erpnext.stock.utils import is_reposting_item_valuation_in_progress
is_reposting_item_valuation_in_progress,
update_included_uom_in_report,
)
def execute(filters=None): def execute(filters=None):
is_reposting_item_valuation_in_progress() is_reposting_item_valuation_in_progress()
include_uom = filters.get("include_uom")
columns = get_columns(filters) columns = get_columns(filters)
items = get_items(filters) items = get_items(filters)
sl_entries = get_stock_ledger_entries(filters, items) sl_entries = get_stock_ledger_entries(filters, items)
item_details = get_item_details(items, sl_entries, include_uom) item_details = get_item_details(items, sl_entries, False)
opening_row, actual_qty, stock_value = get_opening_balance_data(filters, columns, sl_entries) opening_row = get_opening_balance_data(filters, columns, sl_entries)
precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
data, conversion_factors = process_stock_ledger_entries( data = process_stock_ledger_entries(sl_entries, item_details, opening_row, precision)
filters, sl_entries, item_details, opening_row, actual_qty, stock_value, precision
)
update_included_uom_in_report(columns, data, include_uom, conversion_factors)
return columns, data return columns, data
def get_opening_balance_data(filters, columns, sl_entries): def get_opening_balance_data(filters, columns, sl_entries):
if filters.get("batch_no"): opening_row = get_opening_balance(filters, columns, sl_entries)
opening_row = get_opening_balance_from_batch(filters, columns, sl_entries) return opening_row
else:
opening_row = get_opening_balance(filters, columns, sl_entries)
actual_qty = opening_row.get("qty_after_transaction") if opening_row else 0
stock_value = opening_row.get("stock_value") if opening_row else 0
return opening_row, actual_qty, stock_value
def process_stock_ledger_entries( def process_stock_ledger_entries(sl_entries, item_details, opening_row, precision):
filters, sl_entries, item_details, opening_row, actual_qty, stock_value, precision
):
data = [] data = []
conversion_factors = []
if opening_row: if opening_row:
data.append(opening_row) data.append(opening_row)
conversion_factors.append(0)
batch_balance_dict = frappe._dict({}) available_serial_nos = {}
if sabb_list := [sle.serial_and_batch_bundle for sle in sl_entries if sle.serial_and_batch_bundle]:
available_serial_nos = get_serial_nos_from_sle_list(sabb_list)
if actual_qty and filters.get("batch_no"): if not available_serial_nos:
batch_balance_dict[filters.batch_no] = [actual_qty, stock_value] return [], []
available_serial_nos = get_serial_nos_from_sle_list(
[sle.serial_and_batch_bundle for sle in sl_entries if sle.serial_and_batch_bundle]
)
for sle in sl_entries: for sle in sl_entries:
update_stock_ledger_entry( update_stock_ledger_entry(sle, item_details, precision)
sle, item_details, filters, actual_qty, stock_value, batch_balance_dict, precision
)
update_available_serial_nos(available_serial_nos, sle) update_available_serial_nos(available_serial_nos, sle)
data.append(sle) data.append(sle)
if filters.get("include_uom"): return data
conversion_factors.append(item_details[sle.item_code].conversion_factor)
return data, conversion_factors
def update_stock_ledger_entry( def update_stock_ledger_entry(sle, item_details, precision):
sle, item_details, filters, actual_qty, stock_value, batch_balance_dict, precision
):
item_detail = item_details[sle.item_code] item_detail = item_details[sle.item_code]
sle.update(item_detail) sle.update(item_detail)
if filters.get("batch_no") or check_inventory_dimension_filters_applied(filters):
actual_qty += flt(sle.actual_qty, precision)
stock_value += sle.stock_value_difference
if sle.batch_no:
batch_balance_dict.setdefault(sle.batch_no, [0, 0])
batch_balance_dict[sle.batch_no][0] += sle.actual_qty
if sle.voucher_type == "Stock Reconciliation" and not sle.actual_qty:
actual_qty = sle.qty_after_transaction
stock_value = sle.stock_value
sle.update({"qty_after_transaction": actual_qty, "stock_value": stock_value})
sle.update({"in_qty": max(sle.actual_qty, 0), "out_qty": min(sle.actual_qty, 0)}) sle.update({"in_qty": max(sle.actual_qty, 0), "out_qty": min(sle.actual_qty, 0)})
if sle.actual_qty: if sle.actual_qty:
@@ -120,13 +74,10 @@ def update_available_serial_nos(available_serial_nos, sle):
else available_serial_nos.get(sle.serial_and_batch_bundle) else available_serial_nos.get(sle.serial_and_batch_bundle)
) )
key = (sle.item_code, sle.warehouse) key = (sle.item_code, sle.warehouse)
sle.serial_no = "\n".join(serial_nos) if serial_nos else ""
if key not in available_serial_nos: if key not in available_serial_nos:
stock_balance = get_stock_balance_for( available_serial_nos.setdefault(key, serial_nos)
sle.item_code, sle.warehouse, sle.posting_date, sle.posting_time sle.balance_serial_no = "\n".join(serial_nos)
)
serials = get_serial_nos(stock_balance["serial_nos"]) if stock_balance["serial_nos"] else []
available_serial_nos.setdefault(key, serials)
sle.balance_serial_no = "\n".join(serials)
return return
existing_serial_no = available_serial_nos[key] existing_serial_no = available_serial_nos[key]
@@ -151,25 +102,14 @@ def get_columns(filters):
}, },
{"label": _("Item Name"), "fieldname": "item_name", "width": 100}, {"label": _("Item Name"), "fieldname": "item_name", "width": 100},
{ {
"label": _("Stock UOM"), "label": _("UOM"),
"fieldname": "stock_uom", "fieldname": "stock_uom",
"fieldtype": "Link", "fieldtype": "Link",
"options": "UOM", "options": "UOM",
"width": 90, "width": 60,
}, },
] ]
for dimension in get_inventory_dimensions():
columns.append(
{
"label": _(dimension.doctype),
"fieldname": dimension.fieldname,
"fieldtype": "Link",
"options": dimension.doctype,
"width": 110,
}
)
columns.extend( columns.extend(
[ [
{ {
@@ -201,20 +141,11 @@ def get_columns(filters):
"width": 150, "width": 150,
}, },
{ {
"label": _("Item Group"), "label": _("Serial No (In/Out)"),
"fieldname": "item_group", "fieldname": "serial_no",
"fieldtype": "Link", "width": 150,
"options": "Item Group",
"width": 100,
}, },
{ {"label": _("Balance Serial No"), "fieldname": "balance_serial_no", "width": 150},
"label": _("Brand"),
"fieldname": "brand",
"fieldtype": "Link",
"options": "Brand",
"width": 100,
},
{"label": _("Description"), "fieldname": "description", "width": 200},
{ {
"label": _("Incoming Rate"), "label": _("Incoming Rate"),
"fieldname": "incoming_rate", "fieldname": "incoming_rate",
@@ -257,28 +188,6 @@ def get_columns(filters):
"width": 110, "width": 110,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
}, },
{"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 110},
{
"label": _("Voucher #"),
"fieldname": "voucher_no",
"fieldtype": "Dynamic Link",
"options": "voucher_type",
"width": 100,
},
{
"label": _("Batch"),
"fieldname": "batch_no",
"fieldtype": "Link",
"options": "Batch",
"width": 100,
},
{
"label": _("Serial No"),
"fieldname": "serial_no",
"fieldtype": "Link",
"options": "Serial No",
"width": 100,
},
{ {
"label": _("Serial and Batch Bundle"), "label": _("Serial and Batch Bundle"),
"fieldname": "serial_and_batch_bundle", "fieldname": "serial_and_batch_bundle",
@@ -286,12 +195,12 @@ def get_columns(filters):
"options": "Serial and Batch Bundle", "options": "Serial and Batch Bundle",
"width": 100, "width": 100,
}, },
{"label": _("Balance Serial No"), "fieldname": "balance_serial_no", "width": 100}, {"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 110},
{ {
"label": _("Project"), "label": _("Voucher #"),
"fieldname": "project", "fieldname": "voucher_no",
"fieldtype": "Link", "fieldtype": "Dynamic Link",
"options": "Project", "options": "voucher_type",
"width": 100, "width": 100,
}, },
{ {
@@ -310,19 +219,8 @@ def get_columns(filters):
def get_items(filters): def get_items(filters):
item = frappe.qb.DocType("Item") item = frappe.qb.DocType("Item")
query = frappe.qb.from_(item).select(item.name).where(item.has_serial_no == 1) query = frappe.qb.from_(item).select(item.name).where(item.has_serial_no == 1)
conditions = []
if item_code := filters.get("item_code"): if item_code := filters.get("item_code"):
conditions.append(item.name == item_code) query = query.where(item.name == item_code)
else:
if brand := filters.get("brand"):
conditions.append(item.brand == brand)
if item_group := filters.get("item_group"):
if condition := get_item_group_condition(item_group, item):
conditions.append(condition)
if conditions:
for condition in conditions:
query = query.where(condition)
return query.run(pluck=True) return query.run(pluck=True)

View File

@@ -5,7 +5,7 @@
{{ doc.name }} {{ doc.name }}
</span> </span>
</div> </div>
<div class="col-xs-2 small"> {{ doc.total_billable_hours }} </div> <div class="col-xs-2 small"> {{ doc.billing_hours }} </div>
<div class="col-xs-2 small"> {{ doc.project or '' }} </div> <div class="col-xs-2 small"> {{ doc.project or '' }} </div>
<div class="col-xs-2 small"> {{ doc.sales_invoice or '' }} </div> <div class="col-xs-2 small"> {{ doc.sales_invoice or '' }} </div>
<div class="col-xs-2 small"> {{ _(doc.activity_type) }} </div> <div class="col-xs-2 small"> {{ _(doc.activity_type) }} </div>