mirror of
https://github.com/frappe/erpnext.git
synced 2026-02-21 18:36:30 +00:00
Merge pull request #47528 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -21,7 +21,6 @@
|
||||
"party_name",
|
||||
"book_advance_payments_in_separate_party_account",
|
||||
"reconcile_on_advance_payment_date",
|
||||
"advance_reconciliation_takes_effect_on",
|
||||
"column_break_11",
|
||||
"bank_account",
|
||||
"party_bank_account",
|
||||
@@ -786,18 +785,9 @@
|
||||
"options": "No\nYes",
|
||||
"print_hide": 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,
|
||||
"is_submittable": 1,
|
||||
"links": [
|
||||
@@ -809,7 +799,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2025-03-24 16:18:19.920701",
|
||||
"modified": "2025-05-08 11:18:10.238085",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
@@ -849,6 +839,7 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
|
||||
@@ -1492,9 +1492,12 @@ class PaymentEntry(AccountsController):
|
||||
else:
|
||||
# For backwards compatibility
|
||||
# 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
|
||||
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"
|
||||
if invoice.reference_doctype in ["Sales Order", "Purchase Order"]:
|
||||
date_field = "transaction_date"
|
||||
@@ -1504,7 +1507,7 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
if getdate(posting_date) < getdate(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()
|
||||
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(
|
||||
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:
|
||||
party_account = accounts[0]
|
||||
|
||||
@@ -133,7 +133,12 @@ class PeriodClosingVoucher(AccountsController):
|
||||
self.make_gl_entries()
|
||||
|
||||
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.db_set("gle_processing_status", "In Progress")
|
||||
self.cancel_gl_entries()
|
||||
|
||||
@@ -1695,6 +1695,9 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
# Configure Buying Settings to allow rate change
|
||||
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
|
||||
pr = make_purchase_receipt(
|
||||
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)
|
||||
|
||||
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):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -563,17 +563,20 @@ def get_account_type_map(company):
|
||||
|
||||
|
||||
def get_result_as_list(data, filters):
|
||||
balance, _balance_in_account_currency = 0, 0
|
||||
balance = 0
|
||||
|
||||
for d in data:
|
||||
if not d.get("posting_date"):
|
||||
balance, _balance_in_account_currency = 0, 0
|
||||
balance = 0
|
||||
|
||||
balance = get_balance(d, balance, "debit", "credit")
|
||||
|
||||
d["balance"] = balance
|
||||
|
||||
d["account_currency"] = filters.account_currency
|
||||
|
||||
d["presentation_currency"] = filters.presentation_currency
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -599,11 +602,8 @@ def get_columns(filters):
|
||||
if filters.get("presentation_currency"):
|
||||
currency = filters["presentation_currency"]
|
||||
else:
|
||||
if filters.get("company"):
|
||||
currency = get_company_currency(filters["company"])
|
||||
else:
|
||||
company = get_default_company()
|
||||
currency = get_company_currency(company)
|
||||
company = filters.get("company") or get_default_company()
|
||||
filters["presentation_currency"] = currency = get_company_currency(company)
|
||||
|
||||
columns = [
|
||||
{
|
||||
@@ -624,19 +624,22 @@ def get_columns(filters):
|
||||
{
|
||||
"label": _("Debit ({0})").format(currency),
|
||||
"fieldname": "debit",
|
||||
"fieldtype": "Float",
|
||||
"fieldtype": "Currency",
|
||||
"options": "presentation_currency",
|
||||
"width": 130,
|
||||
},
|
||||
{
|
||||
"label": _("Credit ({0})").format(currency),
|
||||
"fieldname": "credit",
|
||||
"fieldtype": "Float",
|
||||
"fieldtype": "Currency",
|
||||
"options": "presentation_currency",
|
||||
"width": 130,
|
||||
},
|
||||
{
|
||||
"label": _("Balance ({0})").format(currency),
|
||||
"fieldname": "balance",
|
||||
"fieldtype": "Float",
|
||||
"fieldtype": "Currency",
|
||||
"options": "presentation_currency",
|
||||
"width": 130,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -35,7 +35,6 @@ def execute(filters=None):
|
||||
filters=filters,
|
||||
accumulated_values=filters.accumulated_values,
|
||||
ignore_closing_entries=True,
|
||||
ignore_accumulated_values_for_fy=True,
|
||||
)
|
||||
|
||||
expense = get_data(
|
||||
@@ -46,7 +45,6 @@ def execute(filters=None):
|
||||
filters=filters,
|
||||
accumulated_values=filters.accumulated_values,
|
||||
ignore_closing_entries=True,
|
||||
ignore_accumulated_values_for_fy=True,
|
||||
)
|
||||
|
||||
net_profit_loss = get_net_profit_loss(
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
# MIT License. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.desk.query_report import export_query
|
||||
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.report.financial_statements import get_period_list
|
||||
@@ -57,7 +58,7 @@ class TestProfitAndLossStatement(AccountsTestMixin, FrappeTestCase):
|
||||
period_end_date=fy.year_end_date,
|
||||
filter_based_on="Fiscal Year",
|
||||
periodicity="Monthly",
|
||||
accumulated_vallues=True,
|
||||
accumulated_values=False,
|
||||
)
|
||||
|
||||
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):
|
||||
self.assertEqual(acc[current_period_key], 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))
|
||||
|
||||
@@ -713,10 +713,13 @@ def update_reference_in_payment_entry(
|
||||
update_advance_paid = []
|
||||
|
||||
# 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.advance_reconciliation_takes_effect_on == "Advance Payment Date":
|
||||
if reconciliation_takes_effect_on == "Advance Payment 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"
|
||||
if d.against_voucher_type in ["Sales Order", "Purchase Order"]:
|
||||
date_field = "transaction_date"
|
||||
@@ -724,7 +727,7 @@ def update_reference_in_payment_entry(
|
||||
|
||||
if getdate(reconcile_on) < getdate(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()
|
||||
|
||||
reference_details.update({"reconcile_effect_on": reconcile_on})
|
||||
|
||||
@@ -122,6 +122,7 @@ class Asset(AccountsController):
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.validate_category()
|
||||
self.validate_precision()
|
||||
self.set_purchase_doc_row_item()
|
||||
self.validate_asset_values()
|
||||
@@ -343,6 +344,17 @@ class Asset(AccountsController):
|
||||
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):
|
||||
if self.gross_purchase_amount:
|
||||
self.gross_purchase_amount = flt(
|
||||
|
||||
@@ -330,45 +330,6 @@ def _make_journal_entry_for_depreciation(
|
||||
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):
|
||||
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)
|
||||
accumulated_depr_amount = flt(asset.gross_purchase_amount) - flt(value_after_depreciation)
|
||||
|
||||
fixed_asset_account, accumulated_depr_account, _ = get_asset_accounts(
|
||||
asset.asset_category, asset.company, accumulated_depr_amount
|
||||
fixed_asset_account, accumulated_depr_account, _ = get_depreciation_accounts(
|
||||
asset.asset_category, 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
|
||||
@@ -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
|
||||
|
||||
non_depreciable_category = frappe.db.get_value(
|
||||
"Asset Category", asset_category, "non_depreciable_category"
|
||||
)
|
||||
|
||||
accounts = frappe.db.get_value(
|
||||
"Asset Category Account",
|
||||
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:
|
||||
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(
|
||||
"Company", company, ["accumulated_depreciation_account", "depreciation_expense_account"]
|
||||
)
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"column_break_3",
|
||||
"depreciation_options",
|
||||
"enable_cwip_accounting",
|
||||
"non_depreciable_category",
|
||||
"finance_book_detail",
|
||||
"finance_books",
|
||||
"section_break_2",
|
||||
@@ -63,10 +64,16 @@
|
||||
"fieldname": "enable_cwip_accounting",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Capital Work in Progress Accounting"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "non_depreciable_category",
|
||||
"fieldtype": "Check",
|
||||
"label": "Non Depreciable Category"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2021-02-24 15:05:38.621803",
|
||||
"modified": "2025-05-13 15:33:03.791814",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Category",
|
||||
@@ -111,8 +118,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,15 +17,14 @@ class AssetCategory(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.assets.doctype.asset_category_account.asset_category_account import (
|
||||
AssetCategoryAccount,
|
||||
)
|
||||
from erpnext.assets.doctype.asset_category_account.asset_category_account import AssetCategoryAccount
|
||||
from erpnext.assets.doctype.asset_finance_book.asset_finance_book import AssetFinanceBook
|
||||
|
||||
accounts: DF.Table[AssetCategoryAccount]
|
||||
asset_category_name: DF.Data
|
||||
enable_cwip_accounting: DF.Check
|
||||
finance_books: DF.Table[AssetFinanceBook]
|
||||
non_depreciable_category: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
|
||||
@@ -111,6 +111,13 @@ frappe.ui.form.on("Opportunity", {
|
||||
},
|
||||
__("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) {
|
||||
@@ -152,7 +159,7 @@ frappe.ui.form.on("Opportunity", {
|
||||
|
||||
currency: function (frm) {
|
||||
let company_currency = erpnext.get_currency(frm.doc.company);
|
||||
if (company_currency != frm.doc.company) {
|
||||
if (company_currency != frm.doc.currency) {
|
||||
frappe.call({
|
||||
method: "erpnext.setup.utils.get_exchange_rate",
|
||||
args: {
|
||||
@@ -278,7 +285,6 @@ erpnext.crm.Opportunity = class Opportunity extends frappe.ui.form.Controller {
|
||||
}
|
||||
|
||||
this.setup_queries();
|
||||
this.frm.trigger("currency");
|
||||
}
|
||||
|
||||
refresh() {
|
||||
|
||||
@@ -2665,6 +2665,81 @@ class TestWorkOrder(FrappeTestCase):
|
||||
)
|
||||
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):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2013-01-10 16:34:16",
|
||||
"creation": "2025-04-09 12:09:40.634472",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"engine": "InnoDB",
|
||||
|
||||
@@ -624,19 +624,30 @@ class WorkOrder(Document):
|
||||
enable_capacity_planning = not cint(manufacturing_settings_doc.disable_capacity_planning)
|
||||
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
|
||||
while qty > 0:
|
||||
qty = split_qty_based_on_batch_size(self, row, qty)
|
||||
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
|
||||
if 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):
|
||||
self.set_operation_start_end_time(index, row)
|
||||
def prepare_data_for_job_card(self, row, idx, plan_days, enable_capacity_planning):
|
||||
self.set_operation_start_end_time(row, idx)
|
||||
|
||||
job_card_doc = create_job_card(
|
||||
self, row, auto_create=True, enable_capacity_planning=enable_capacity_planning
|
||||
@@ -661,12 +672,24 @@ class WorkOrder(Document):
|
||||
|
||||
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
|
||||
`planned_start_date`, else add time diff to end time of earlier operation."""
|
||||
if idx == 0:
|
||||
# first operation at 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:
|
||||
row.planned_start_time = (
|
||||
get_datetime(self.operations[idx - 1].planned_end_time) + get_mins_between_operations()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2014-10-16 14:35:41.950175",
|
||||
"creation": "2025-04-09 12:12:19.824560",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
@@ -102,13 +102,15 @@
|
||||
"fieldname": "planned_start_time",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Planned Start Time",
|
||||
"no_copy": 1
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "planned_end_time",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Planned End Time",
|
||||
"no_copy": 1
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_10",
|
||||
@@ -191,7 +193,6 @@
|
||||
{
|
||||
"fieldname": "sequence_id",
|
||||
"fieldtype": "Int",
|
||||
"hidden": 1,
|
||||
"label": "Sequence ID",
|
||||
"print_hide": 1
|
||||
},
|
||||
@@ -219,10 +220,11 @@
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-06-09 14:03:01.612909",
|
||||
"modified": "2025-04-09 16:21:47.110564",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Work Order Operation",
|
||||
@@ -232,4 +234,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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_asset_status_to_work_in_progress
|
||||
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
|
||||
execute:frappe.db.set_single_value("Accounts Settings", "exchange_gain_loss_posting_date", "Payment")
|
||||
erpnext.patches.v14_0.disable_add_row_in_gross_profit
|
||||
|
||||
@@ -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')"""
|
||||
)
|
||||
@@ -535,7 +535,7 @@ def get_timesheets_list(doctype, txt, filters, limit_start, limit_page_length=20
|
||||
table.name,
|
||||
child_table.activity_type,
|
||||
table.status,
|
||||
table.total_billable_hours,
|
||||
child_table.billing_hours,
|
||||
(table.sales_invoice | child_table.sales_invoice).as_("sales_invoice"),
|
||||
child_table.project,
|
||||
)
|
||||
|
||||
@@ -47,7 +47,7 @@ frappe.ui.form.on("Event", {
|
||||
frm.add_custom_button(
|
||||
__("Add Sales Partners"),
|
||||
function () {
|
||||
new frappe.desk.eventParticipants(frm, "Sales Partners");
|
||||
new frappe.desk.eventParticipants(frm, "Sales Partner");
|
||||
},
|
||||
__("Add Participants")
|
||||
);
|
||||
|
||||
@@ -147,10 +147,8 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te
|
||||
|
||||
bin_join_selection, bin_join_condition = "", ""
|
||||
if hide_unavailable_items:
|
||||
bin_join_selection = ", `tabBin` bin"
|
||||
bin_join_condition = (
|
||||
"AND bin.warehouse = %(warehouse)s AND bin.item_code = item.name AND bin.actual_qty > 0"
|
||||
)
|
||||
bin_join_selection = "LEFT JOIN `tabBin` bin ON bin.item_code = item.name"
|
||||
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)"
|
||||
|
||||
items_data = frappe.db.sql(
|
||||
"""
|
||||
|
||||
@@ -214,13 +214,12 @@ def get_or_create_account(company_name, account):
|
||||
default_root_type = "Liability"
|
||||
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(
|
||||
"Account",
|
||||
filters={"company": company_name, "root_type": root_type},
|
||||
or_filters={
|
||||
"account_name": account.get("account_name"),
|
||||
"account_number": account.get("account_number"),
|
||||
},
|
||||
"Account", filters={"company": company_name, "root_type": root_type}, or_filters=or_filters
|
||||
)
|
||||
|
||||
if existing_accounts:
|
||||
|
||||
@@ -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):
|
||||
# Update Billing % based on pending accepted qty
|
||||
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
|
||||
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"))
|
||||
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)
|
||||
pr_doc.db_set("per_billed", percent_billed)
|
||||
|
||||
@@ -342,7 +342,7 @@ def remove_attached_file(docname):
|
||||
if file_name := frappe.db.get_value(
|
||||
"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):
|
||||
|
||||
@@ -6,7 +6,7 @@ import json
|
||||
from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe import _, bold
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import (
|
||||
@@ -526,6 +526,14 @@ class StockEntry(StockController):
|
||||
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):
|
||||
"""perform various (sometimes conditional) validations on warehouse"""
|
||||
|
||||
|
||||
@@ -203,9 +203,19 @@ class StockReconciliation(StockController):
|
||||
)
|
||||
)
|
||||
|
||||
if self.docstatus == 1:
|
||||
bundle.voucher_no = self.name
|
||||
bundle.submit()
|
||||
if (
|
||||
self.docstatus == 1
|
||||
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(
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
label: __("Voucher #"),
|
||||
fieldtype: "Data",
|
||||
},
|
||||
{
|
||||
fieldname: "project",
|
||||
label: __("Project"),
|
||||
fieldtype: "Link",
|
||||
options: "Project",
|
||||
},
|
||||
{
|
||||
fieldname: "include_uom",
|
||||
label: __("Include UOM"),
|
||||
fieldtype: "Link",
|
||||
options: "UOM",
|
||||
},
|
||||
{
|
||||
fieldname: "valuation_field_type",
|
||||
label: __("Valuation Field Type"),
|
||||
|
||||
@@ -3,108 +3,62 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Sum
|
||||
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.stock_reconciliation.stock_reconciliation import get_stock_balance_for
|
||||
from erpnext.stock.report.stock_ledger.stock_ledger import (
|
||||
check_inventory_dimension_filters_applied,
|
||||
get_item_details,
|
||||
get_item_group_condition,
|
||||
get_opening_balance,
|
||||
get_opening_balance_from_batch,
|
||||
get_stock_ledger_entries,
|
||||
)
|
||||
from erpnext.stock.utils import (
|
||||
is_reposting_item_valuation_in_progress,
|
||||
update_included_uom_in_report,
|
||||
)
|
||||
from erpnext.stock.utils import is_reposting_item_valuation_in_progress
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
is_reposting_item_valuation_in_progress()
|
||||
include_uom = filters.get("include_uom")
|
||||
columns = get_columns(filters)
|
||||
items = get_items(filters)
|
||||
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"))
|
||||
data, conversion_factors = process_stock_ledger_entries(
|
||||
filters, sl_entries, item_details, opening_row, actual_qty, stock_value, precision
|
||||
)
|
||||
data = process_stock_ledger_entries(sl_entries, item_details, opening_row, precision)
|
||||
|
||||
update_included_uom_in_report(columns, data, include_uom, conversion_factors)
|
||||
return columns, data
|
||||
|
||||
|
||||
def get_opening_balance_data(filters, columns, sl_entries):
|
||||
if filters.get("batch_no"):
|
||||
opening_row = get_opening_balance_from_batch(filters, columns, sl_entries)
|
||||
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
|
||||
opening_row = get_opening_balance(filters, columns, sl_entries)
|
||||
return opening_row
|
||||
|
||||
|
||||
def process_stock_ledger_entries(
|
||||
filters, sl_entries, item_details, opening_row, actual_qty, stock_value, precision
|
||||
):
|
||||
def process_stock_ledger_entries(sl_entries, item_details, opening_row, precision):
|
||||
data = []
|
||||
conversion_factors = []
|
||||
|
||||
if 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"):
|
||||
batch_balance_dict[filters.batch_no] = [actual_qty, stock_value]
|
||||
|
||||
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]
|
||||
)
|
||||
if not available_serial_nos:
|
||||
return [], []
|
||||
|
||||
for sle in sl_entries:
|
||||
update_stock_ledger_entry(
|
||||
sle, item_details, filters, actual_qty, stock_value, batch_balance_dict, precision
|
||||
)
|
||||
update_stock_ledger_entry(sle, item_details, precision)
|
||||
update_available_serial_nos(available_serial_nos, sle)
|
||||
data.append(sle)
|
||||
|
||||
if filters.get("include_uom"):
|
||||
conversion_factors.append(item_details[sle.item_code].conversion_factor)
|
||||
|
||||
return data, conversion_factors
|
||||
return data
|
||||
|
||||
|
||||
def update_stock_ledger_entry(
|
||||
sle, item_details, filters, actual_qty, stock_value, batch_balance_dict, precision
|
||||
):
|
||||
def update_stock_ledger_entry(sle, item_details, precision):
|
||||
item_detail = item_details[sle.item_code]
|
||||
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)})
|
||||
|
||||
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)
|
||||
)
|
||||
key = (sle.item_code, sle.warehouse)
|
||||
sle.serial_no = "\n".join(serial_nos) if serial_nos else ""
|
||||
if key not in available_serial_nos:
|
||||
stock_balance = get_stock_balance_for(
|
||||
sle.item_code, sle.warehouse, sle.posting_date, sle.posting_time
|
||||
)
|
||||
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)
|
||||
available_serial_nos.setdefault(key, serial_nos)
|
||||
sle.balance_serial_no = "\n".join(serial_nos)
|
||||
return
|
||||
|
||||
existing_serial_no = available_serial_nos[key]
|
||||
@@ -151,25 +102,14 @@ def get_columns(filters):
|
||||
},
|
||||
{"label": _("Item Name"), "fieldname": "item_name", "width": 100},
|
||||
{
|
||||
"label": _("Stock UOM"),
|
||||
"label": _("UOM"),
|
||||
"fieldname": "stock_uom",
|
||||
"fieldtype": "Link",
|
||||
"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(
|
||||
[
|
||||
{
|
||||
@@ -201,20 +141,11 @@ def get_columns(filters):
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"label": _("Item Group"),
|
||||
"fieldname": "item_group",
|
||||
"fieldtype": "Link",
|
||||
"options": "Item Group",
|
||||
"width": 100,
|
||||
"label": _("Serial No (In/Out)"),
|
||||
"fieldname": "serial_no",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"label": _("Brand"),
|
||||
"fieldname": "brand",
|
||||
"fieldtype": "Link",
|
||||
"options": "Brand",
|
||||
"width": 100,
|
||||
},
|
||||
{"label": _("Description"), "fieldname": "description", "width": 200},
|
||||
{"label": _("Balance Serial No"), "fieldname": "balance_serial_no", "width": 150},
|
||||
{
|
||||
"label": _("Incoming Rate"),
|
||||
"fieldname": "incoming_rate",
|
||||
@@ -257,28 +188,6 @@ def get_columns(filters):
|
||||
"width": 110,
|
||||
"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"),
|
||||
"fieldname": "serial_and_batch_bundle",
|
||||
@@ -286,12 +195,12 @@ def get_columns(filters):
|
||||
"options": "Serial and Batch Bundle",
|
||||
"width": 100,
|
||||
},
|
||||
{"label": _("Balance Serial No"), "fieldname": "balance_serial_no", "width": 100},
|
||||
{"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 110},
|
||||
{
|
||||
"label": _("Project"),
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"options": "Project",
|
||||
"label": _("Voucher #"),
|
||||
"fieldname": "voucher_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "voucher_type",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
@@ -310,19 +219,8 @@ def get_columns(filters):
|
||||
def get_items(filters):
|
||||
item = frappe.qb.DocType("Item")
|
||||
query = frappe.qb.from_(item).select(item.name).where(item.has_serial_no == 1)
|
||||
conditions = []
|
||||
|
||||
if item_code := filters.get("item_code"):
|
||||
conditions.append(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)
|
||||
query = query.where(item.name == item_code)
|
||||
|
||||
return query.run(pluck=True)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{{ doc.name }}
|
||||
</span>
|
||||
</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.sales_invoice or '' }} </div>
|
||||
<div class="col-xs-2 small"> {{ _(doc.activity_type) }} </div>
|
||||
|
||||
Reference in New Issue
Block a user