mirror of
https://github.com/frappe/erpnext.git
synced 2026-02-17 16:45:02 +00:00
Merge branch 'develop' into fix-no-account-in-gl-entry
This commit is contained in:
@@ -42,6 +42,7 @@
|
||||
"show_payment_schedule_in_print",
|
||||
"item_price_settings_section",
|
||||
"maintain_same_internal_transaction_rate",
|
||||
"fetch_valuation_rate_for_internal_transaction",
|
||||
"column_break_feyo",
|
||||
"maintain_same_rate_action",
|
||||
"role_to_override_stop_action",
|
||||
@@ -644,6 +645,12 @@
|
||||
"fieldname": "drop_ar_procedures",
|
||||
"fieldtype": "Button",
|
||||
"label": "Drop Procedures"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "fetch_valuation_rate_for_internal_transaction",
|
||||
"fieldtype": "Check",
|
||||
"label": "Fetch Valuation Rate for Internal Transaction"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -652,7 +659,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-23 15:55:33.346398",
|
||||
"modified": "2025-07-18 13:56:47.192437",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -49,6 +49,7 @@ class AccountsSettings(Document):
|
||||
enable_immutable_ledger: DF.Check
|
||||
enable_party_matching: DF.Check
|
||||
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
|
||||
fetch_valuation_rate_for_internal_transaction: DF.Check
|
||||
frozen_accounts_modifier: DF.Link | None
|
||||
general_ledger_remarks_length: DF.Int
|
||||
ignore_account_closing_balance: DF.Check
|
||||
|
||||
@@ -462,4 +462,9 @@ def rename_temporarily_named_docs(doctype):
|
||||
f"UPDATE `tab{doctype}` SET name = %s, to_rename = 0, modified = %s where name = %s",
|
||||
(newname, now(), oldname),
|
||||
)
|
||||
|
||||
for hook_type in ("on_gle_rename", "on_sle_rename"):
|
||||
for hook in frappe.get_hooks(hook_type):
|
||||
frappe.call(hook, newname=newname, oldname=oldname)
|
||||
|
||||
frappe.db.commit()
|
||||
|
||||
@@ -1145,9 +1145,7 @@ class JournalEntry(AccountsController):
|
||||
|
||||
def set_print_format_fields(self):
|
||||
bank_amount = party_amount = total_amount = 0.0
|
||||
currency = (
|
||||
bank_account_currency
|
||||
) = party_account_currency = pay_to_recd_from = self.pay_to_recd_from = None
|
||||
currency = bank_account_currency = party_account_currency = pay_to_recd_from = None
|
||||
party_type = None
|
||||
for d in self.get("accounts"):
|
||||
if d.party_type in ["Customer", "Supplier"] and d.party:
|
||||
|
||||
@@ -579,6 +579,18 @@ class TestJournalEntry(IntegrationTestCase):
|
||||
]
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_pay_to_recd_from(self):
|
||||
jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, save=False)
|
||||
jv.pay_to_recd_from = "_Test Receiver"
|
||||
jv.save()
|
||||
self.assertEqual(jv.pay_to_recd_from, "_Test Receiver")
|
||||
|
||||
jv.pay_to_recd_from = "_Test Receiver 2"
|
||||
jv.save()
|
||||
jv.submit()
|
||||
|
||||
self.assertEqual(jv.pay_to_recd_from, "_Test Receiver 2")
|
||||
|
||||
|
||||
def make_journal_entry(
|
||||
account1,
|
||||
|
||||
@@ -30,24 +30,6 @@
|
||||
"project",
|
||||
"dimension_col_break",
|
||||
"cost_center",
|
||||
"customer_po_details",
|
||||
"po_no",
|
||||
"column_break_23",
|
||||
"po_date",
|
||||
"address_and_contact",
|
||||
"customer_address",
|
||||
"address_display",
|
||||
"contact_person",
|
||||
"contact_display",
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"territory",
|
||||
"col_break4",
|
||||
"shipping_address_name",
|
||||
"shipping_address",
|
||||
"company_address",
|
||||
"company_address_display",
|
||||
"company_contact_person",
|
||||
"currency_and_price_list",
|
||||
"currency",
|
||||
"conversion_rate",
|
||||
@@ -91,14 +73,6 @@
|
||||
"base_total_taxes_and_charges",
|
||||
"column_break_47",
|
||||
"total_taxes_and_charges",
|
||||
"loyalty_points_redemption",
|
||||
"loyalty_points",
|
||||
"loyalty_amount",
|
||||
"redeem_loyalty_points",
|
||||
"column_break_77",
|
||||
"loyalty_program",
|
||||
"loyalty_redemption_account",
|
||||
"loyalty_redemption_cost_center",
|
||||
"section_break_49",
|
||||
"coupon_code",
|
||||
"apply_discount_on",
|
||||
@@ -118,13 +92,7 @@
|
||||
"in_words",
|
||||
"total_advance",
|
||||
"outstanding_amount",
|
||||
"advances_section",
|
||||
"allocate_advances_automatically",
|
||||
"get_advances",
|
||||
"advances",
|
||||
"payment_schedule_section",
|
||||
"payment_terms_template",
|
||||
"payment_schedule",
|
||||
"payments_tab",
|
||||
"payments_section",
|
||||
"cash_bank_account",
|
||||
"payments",
|
||||
@@ -137,6 +105,10 @@
|
||||
"column_break_90",
|
||||
"change_amount",
|
||||
"account_for_change_amount",
|
||||
"advances_section",
|
||||
"allocate_advances_automatically",
|
||||
"get_advances",
|
||||
"advances",
|
||||
"column_break4",
|
||||
"write_off_amount",
|
||||
"base_write_off_amount",
|
||||
@@ -144,9 +116,41 @@
|
||||
"column_break_74",
|
||||
"write_off_account",
|
||||
"write_off_cost_center",
|
||||
"loyalty_points_redemption",
|
||||
"loyalty_points",
|
||||
"loyalty_amount",
|
||||
"redeem_loyalty_points",
|
||||
"column_break_77",
|
||||
"loyalty_program",
|
||||
"loyalty_redemption_account",
|
||||
"loyalty_redemption_cost_center",
|
||||
"contact_and_address_tab",
|
||||
"address_and_contact",
|
||||
"customer_address",
|
||||
"address_display",
|
||||
"contact_person",
|
||||
"contact_display",
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"territory",
|
||||
"col_break4",
|
||||
"shipping_address_name",
|
||||
"shipping_address",
|
||||
"company_address",
|
||||
"company_address_display",
|
||||
"company_contact_person",
|
||||
"terms_tab",
|
||||
"payment_schedule_section",
|
||||
"payment_terms_template",
|
||||
"payment_schedule",
|
||||
"terms_section_break",
|
||||
"tc_name",
|
||||
"terms",
|
||||
"more_info_tab",
|
||||
"customer_po_details",
|
||||
"po_no",
|
||||
"column_break_23",
|
||||
"po_date",
|
||||
"edit_printing_settings",
|
||||
"letter_head",
|
||||
"group_same_items",
|
||||
@@ -398,7 +402,6 @@
|
||||
"label": "Customer's Purchase Order Date"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "address_and_contact",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Address and Contact"
|
||||
@@ -1050,7 +1053,6 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval:(!doc.is_pos && !doc.is_return)",
|
||||
"fieldname": "payment_schedule_section",
|
||||
"fieldtype": "Section Break",
|
||||
@@ -1130,8 +1132,10 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "section_break_88",
|
||||
"fieldtype": "Section Break"
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Changes"
|
||||
},
|
||||
{
|
||||
"depends_on": "is_pos",
|
||||
@@ -1218,7 +1222,6 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "terms",
|
||||
"fieldname": "terms_section_break",
|
||||
"fieldtype": "Section Break",
|
||||
@@ -1570,12 +1573,32 @@
|
||||
"label": "Company Contact Person",
|
||||
"options": "Contact",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "payments_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Payments"
|
||||
},
|
||||
{
|
||||
"fieldname": "contact_and_address_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Address & Contact"
|
||||
},
|
||||
{
|
||||
"fieldname": "terms_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Terms"
|
||||
},
|
||||
{
|
||||
"fieldname": "more_info_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "More Info"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-17 16:51:40.886083",
|
||||
"modified": "2025-07-18 16:50:30.516162",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice",
|
||||
|
||||
@@ -86,7 +86,7 @@ def set_gl_entries_by_account(dimension_list, filters, account, gl_entries_by_ac
|
||||
"finance_book": cstr(filters.get("finance_book")),
|
||||
}
|
||||
|
||||
gl_filters["dimensions"] = set(dimension_list)
|
||||
gl_filters["dimensions"] = tuple(set(dimension_list))
|
||||
|
||||
if filters.get("include_default_book_entries"):
|
||||
gl_filters["company_fb"] = frappe.get_cached_value("Company", filters.company, "default_finance_book")
|
||||
@@ -179,7 +179,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name, dimension_list):
|
||||
def get_condition(dimension):
|
||||
conditions = []
|
||||
|
||||
conditions.append(f"{frappe.scrub(dimension)} in (%(dimensions)s)")
|
||||
conditions.append(f"{frappe.scrub(dimension)} in %(dimensions)s")
|
||||
|
||||
return " and {}".format(" and ".join(conditions)) if conditions else ""
|
||||
|
||||
|
||||
@@ -210,7 +210,7 @@ def get_gl_entries(filters, accounting_dimensions):
|
||||
)
|
||||
|
||||
if filters.get("presentation_currency"):
|
||||
return convert_to_presentation_currency(gl_entries, currency_map)
|
||||
return convert_to_presentation_currency(gl_entries, currency_map, filters)
|
||||
else:
|
||||
return gl_entries
|
||||
|
||||
|
||||
@@ -178,7 +178,7 @@ def get_columns(additional_table_columns, filters):
|
||||
"fieldname": "invoice",
|
||||
"fieldtype": "Link",
|
||||
"options": "Purchase Invoice",
|
||||
"width": 120,
|
||||
"width": 150,
|
||||
},
|
||||
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120},
|
||||
]
|
||||
@@ -310,8 +310,8 @@ def apply_conditions(query, pi, pii, filters):
|
||||
|
||||
def get_items(filters, additional_table_columns):
|
||||
doctype = "Purchase Invoice"
|
||||
pi = frappe.qb.DocType(doctype)
|
||||
pii = frappe.qb.DocType(f"{doctype} Item")
|
||||
pi = frappe.qb.DocType(doctype).as_("invoice")
|
||||
pii = frappe.qb.DocType(f"{doctype} Item").as_("invoice_item")
|
||||
Item = frappe.qb.DocType("Item")
|
||||
query = (
|
||||
frappe.qb.from_(pi)
|
||||
@@ -331,6 +331,7 @@ def get_items(filters, additional_table_columns):
|
||||
pi.unrealized_profit_loss_account,
|
||||
pii.item_code,
|
||||
pii.description,
|
||||
pii.item_name,
|
||||
pii.item_group,
|
||||
pii.item_name.as_("pi_item_name"),
|
||||
pii.item_group.as_("pi_item_group"),
|
||||
@@ -374,7 +375,7 @@ def get_items(filters, additional_table_columns):
|
||||
if match_conditions:
|
||||
query += " and " + match_conditions
|
||||
|
||||
query = apply_order_by_conditions(query, pi, pii, filters)
|
||||
query = apply_order_by_conditions(query, filters)
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
|
||||
|
||||
@@ -199,7 +199,7 @@ def get_columns(additional_table_columns, filters):
|
||||
"fieldname": "invoice",
|
||||
"fieldtype": "Link",
|
||||
"options": "Sales Invoice",
|
||||
"width": 120,
|
||||
"width": 150,
|
||||
},
|
||||
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120},
|
||||
]
|
||||
@@ -395,15 +395,15 @@ def apply_conditions(query, si, sii, sip, filters, additional_conditions=None):
|
||||
return query
|
||||
|
||||
|
||||
def apply_order_by_conditions(query, si, ii, filters):
|
||||
def apply_order_by_conditions(query, filters):
|
||||
if not filters.get("group_by"):
|
||||
query += f" order by {si.posting_date} desc, {ii.item_group} desc"
|
||||
query += "order by invoice.posting_date desc, invoice_item.item_group desc"
|
||||
elif filters.get("group_by") == "Invoice":
|
||||
query += f" order by {ii.parent} desc"
|
||||
query += "order by invoice_item.parent desc"
|
||||
elif filters.get("group_by") == "Item":
|
||||
query += f" order by {ii.item_code}"
|
||||
query += "order by invoice_item.item_code"
|
||||
elif filters.get("group_by") == "Item Group":
|
||||
query += f" order by {ii.item_group}"
|
||||
query += "order by invoice_item.item_group"
|
||||
elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"):
|
||||
filter_field = frappe.scrub(filters.get("group_by"))
|
||||
query += f" order by {filter_field} desc"
|
||||
@@ -413,9 +413,9 @@ def apply_order_by_conditions(query, si, ii, filters):
|
||||
|
||||
def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
doctype = "Sales Invoice"
|
||||
si = frappe.qb.DocType(doctype)
|
||||
sip = frappe.qb.DocType(f"{doctype} Payment")
|
||||
sii = frappe.qb.DocType(f"{doctype} Item")
|
||||
si = frappe.qb.DocType("Sales Invoice").as_("invoice")
|
||||
sii = frappe.qb.DocType("Sales Invoice Item").as_("invoice_item")
|
||||
sip = frappe.qb.DocType("Sales Invoice Payment")
|
||||
item = frappe.qb.DocType("Item")
|
||||
|
||||
query = (
|
||||
@@ -493,7 +493,7 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
if match_conditions:
|
||||
query += " and " + match_conditions
|
||||
|
||||
query = apply_order_by_conditions(query, si, sii, filters)
|
||||
query = apply_order_by_conditions(query, filters)
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
|
||||
@@ -763,25 +763,13 @@ def add_total_row(
|
||||
def get_display_value(filters, group_by_field, item):
|
||||
if filters.get("group_by") == "Item":
|
||||
if item.get("item_code") != item.get("item_name"):
|
||||
value = (
|
||||
cstr(item.get("item_code"))
|
||||
+ "<br><br>"
|
||||
+ "<span style='font-weight: normal'>"
|
||||
+ cstr(item.get("item_name"))
|
||||
+ "</span>"
|
||||
)
|
||||
value = f"{item.get('item_code')}: {item.get('item_name')}"
|
||||
else:
|
||||
value = item.get("item_code", "")
|
||||
elif filters.get("group_by") in ("Customer", "Supplier"):
|
||||
party = frappe.scrub(filters.get("group_by"))
|
||||
if item.get(party) != item.get(party + "_name"):
|
||||
value = (
|
||||
item.get(party)
|
||||
+ "<br><br>"
|
||||
+ "<span style='font-weight: normal'>"
|
||||
+ item.get(party + "_name")
|
||||
+ "</span>"
|
||||
)
|
||||
value = f"{item.get(party)}: {item.get(party + '_name')}"
|
||||
else:
|
||||
value = item.get(party)
|
||||
else:
|
||||
|
||||
@@ -86,7 +86,7 @@ def get_rate_as_at(date, from_currency, to_currency):
|
||||
return rate
|
||||
|
||||
|
||||
def convert_to_presentation_currency(gl_entries, currency_info):
|
||||
def convert_to_presentation_currency(gl_entries, currency_info, filters=None):
|
||||
"""
|
||||
Take a list of GL Entries and change the 'debit' and 'credit' values to currencies
|
||||
in `currency_info`.
|
||||
@@ -99,6 +99,13 @@ def convert_to_presentation_currency(gl_entries, currency_info):
|
||||
company_currency = currency_info["company_currency"]
|
||||
|
||||
account_currencies = list(set(entry["account_currency"] for entry in gl_entries))
|
||||
exchange_gain_or_loss = False
|
||||
|
||||
if filters and isinstance(filters.get("account"), list):
|
||||
account_filter = filters.get("account")
|
||||
gain_loss_account = frappe.db.get_value("Company", filters.company, "exchange_gain_loss_account")
|
||||
|
||||
exchange_gain_or_loss = len(account_filter) == 1 and account_filter[0] == gain_loss_account
|
||||
|
||||
for entry in gl_entries:
|
||||
debit = flt(entry["debit"])
|
||||
@@ -107,7 +114,11 @@ def convert_to_presentation_currency(gl_entries, currency_info):
|
||||
credit_in_account_currency = flt(entry["credit_in_account_currency"])
|
||||
account_currency = entry["account_currency"]
|
||||
|
||||
if len(account_currencies) == 1 and account_currency == presentation_currency:
|
||||
if (
|
||||
len(account_currencies) == 1
|
||||
and account_currency == presentation_currency
|
||||
and not exchange_gain_or_loss
|
||||
):
|
||||
entry["debit"] = debit_in_account_currency
|
||||
entry["credit"] = credit_in_account_currency
|
||||
else:
|
||||
|
||||
@@ -522,7 +522,8 @@ def reconcile_against_document(
|
||||
skip_ref_details_update_for_pe=skip_ref_details_update_for_pe,
|
||||
dimensions_dict=dimensions_dict,
|
||||
)
|
||||
|
||||
if referenced_row.get("outstanding_amount"):
|
||||
referenced_row.outstanding_amount -= flt(entry.allocated_amount)
|
||||
doc.save(ignore_permissions=True)
|
||||
# re-submit advance entry
|
||||
doc = frappe.get_doc(entry.voucher_type, entry.voucher_no)
|
||||
|
||||
@@ -659,7 +659,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
|
||||
self.assertEqual(schedules, expected_depreciation_before_repair)
|
||||
|
||||
def test_daily_prorata_based_depreciation_schedule_after_cancelling_asset_repair_for(self):
|
||||
def test_daily_prorata_based_depreciation_schedule_after_cancelling_asset_repair(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
gross_purchase_amount=500,
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
"fieldname": "completion_date",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Completion Date",
|
||||
"mandatory_depends_on": "eval:doc.repair_status==\"Completed\"",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
@@ -260,7 +261,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-29 22:30:00.589597",
|
||||
"modified": "2025-07-18 15:59:53.981224",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Repair",
|
||||
|
||||
@@ -75,7 +75,7 @@ class AssetRepair(AccountsController):
|
||||
)
|
||||
|
||||
def validate_dates(self):
|
||||
if self.completion_date and (self.failure_date > self.completion_date):
|
||||
if self.completion_date and (getdate(self.failure_date) > getdate(self.completion_date)):
|
||||
frappe.throw(
|
||||
_("Completion Date can not be before Failure Date. Please adjust the dates accordingly.")
|
||||
)
|
||||
@@ -292,7 +292,7 @@ class AssetRepair(AccountsController):
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": getdate(),
|
||||
"posting_date": self.completion_date,
|
||||
"company": self.company,
|
||||
},
|
||||
item=self,
|
||||
@@ -309,7 +309,7 @@ class AssetRepair(AccountsController):
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": getdate(),
|
||||
"posting_date": self.completion_date,
|
||||
"against_voucher_type": "Purchase Invoice",
|
||||
"company": self.company,
|
||||
},
|
||||
@@ -344,7 +344,7 @@ class AssetRepair(AccountsController):
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": getdate(),
|
||||
"posting_date": self.completion_date,
|
||||
"company": self.company,
|
||||
},
|
||||
item=self,
|
||||
@@ -361,7 +361,7 @@ class AssetRepair(AccountsController):
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": getdate(),
|
||||
"posting_date": self.completion_date,
|
||||
"against_voucher_type": "Stock Entry",
|
||||
"against_voucher": stock_entry.name,
|
||||
"company": self.company,
|
||||
|
||||
@@ -4,7 +4,7 @@ import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.utils import add_months, flt, get_first_day, nowdate, nowtime, today
|
||||
from frappe.utils import add_days, add_months, flt, get_first_day, nowdate, nowtime, today
|
||||
|
||||
from erpnext.assets.doctype.asset.asset import (
|
||||
get_asset_account,
|
||||
@@ -388,6 +388,7 @@ def create_asset_repair(**args):
|
||||
|
||||
if args.submit:
|
||||
asset_repair.repair_status = "Completed"
|
||||
asset_repair.completion_date = add_days(args.failure_date, 1)
|
||||
asset_repair.cost_center = frappe.db.get_value("Company", asset.company, "cost_center")
|
||||
|
||||
if args.stock_consumption:
|
||||
|
||||
@@ -348,7 +348,7 @@ class BuyingController(SubcontractingController):
|
||||
tax_accounts, total_valuation_amount, total_actual_tax_amount = self.get_tax_details()
|
||||
|
||||
for i, item in enumerate(self.get("items")):
|
||||
if item.item_code and item.qty:
|
||||
if item.item_code and (item.qty or item.get("rejected_qty")):
|
||||
item_tax_amount, actual_tax_amount = 0.0, 0.0
|
||||
if i == (last_item_idx - 1):
|
||||
item_tax_amount = total_valuation_amount
|
||||
@@ -387,7 +387,19 @@ class BuyingController(SubcontractingController):
|
||||
if item.sales_incoming_rate: # for internal transfer
|
||||
net_rate = item.qty * item.sales_incoming_rate
|
||||
|
||||
if (
|
||||
not net_rate
|
||||
and item.get("rejected_qty")
|
||||
and frappe.get_single_value(
|
||||
"Buying Settings", "set_valuation_rate_for_rejected_materials"
|
||||
)
|
||||
):
|
||||
net_rate = item.rejected_qty * item.net_rate
|
||||
|
||||
qty_in_stock_uom = flt(item.qty * item.conversion_factor)
|
||||
if not qty_in_stock_uom and item.get("rejected_qty"):
|
||||
qty_in_stock_uom = flt(item.rejected_qty * item.conversion_factor)
|
||||
|
||||
if self.get("is_old_subcontracting_flow"):
|
||||
item.rm_supp_cost = self.get_supplied_items_cost(item.name, reset_outgoing_rate)
|
||||
item.valuation_rate = (
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1132
erpnext/locale/fa.po
1132
erpnext/locale/fa.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
95902
erpnext/locale/id.po
95902
erpnext/locale/id.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
10353
erpnext/locale/zh.po
10353
erpnext/locale/zh.po
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,16 @@ frappe.ui.form.on("Job Card", {
|
||||
};
|
||||
});
|
||||
|
||||
frm.events.set_company_filters(frm, "source_warehouse");
|
||||
frm.events.set_company_filters(frm, "wip_warehouse");
|
||||
frm.set_query("source_warehouse", "items", () => {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_indicator_formatter("sub_operation", function (doc) {
|
||||
if (doc.status == "Pending") {
|
||||
return "red";
|
||||
@@ -32,6 +42,16 @@ frappe.ui.form.on("Job Card", {
|
||||
});
|
||||
},
|
||||
|
||||
set_company_filters(frm, fieldname) {
|
||||
frm.set_query(fieldname, () => {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
make_fields_read_only(frm) {
|
||||
if (frm.doc.docstatus === 1) {
|
||||
frm.set_df_property("employee", "read_only", 1);
|
||||
|
||||
@@ -47,6 +47,14 @@ frappe.ui.form.on("Production Plan", {
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("sub_assembly_warehouse", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("material_request", "material_requests", function () {
|
||||
return {
|
||||
filters: {
|
||||
|
||||
@@ -2965,56 +2965,45 @@ class TestWorkOrder(IntegrationTestCase):
|
||||
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.operations[3].sequence_id = None
|
||||
|
||||
# Test 1 : If any one operation does not have sequence ID then error will be thrown
|
||||
self.assertRaises(frappe.ValidationError, wo.submit)
|
||||
|
||||
for op in wo.operations:
|
||||
op.sequence_id = None
|
||||
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")
|
||||
# Test 2 : If none of the operations have sequence ID then they will be sequenced as per their idx
|
||||
for op in wo.operations:
|
||||
self.assertEqual(op.sequence_id, op.idx)
|
||||
|
||||
wo = frappe.copy_doc(wo)
|
||||
wo.operations[3].sequence_id = 1
|
||||
wo.submit()
|
||||
wo.operations[0].sequence_id = 2
|
||||
|
||||
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")
|
||||
# Test 3 : Sequence IDs should not miss the correct sequence of numbers
|
||||
self.assertRaises(frappe.ValidationError, wo.submit)
|
||||
|
||||
wo = frappe.copy_doc(wo)
|
||||
wo.operations[0].sequence_id = 3
|
||||
wo.submit()
|
||||
wo.operations[1].sequence_id = 1
|
||||
|
||||
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
|
||||
# Test 4 : Sequence IDs should be in the correct ascending order
|
||||
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[0].sequence_id = 1
|
||||
wo.operations[1].sequence_id = 2
|
||||
wo.operations[2].sequence_id = 2
|
||||
wo.operations[3].sequence_id = 3
|
||||
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)
|
||||
# Test 5 : If two operations have the same sequence ID then the next operation will start 10 mins after the longest previous operation ends
|
||||
self.assertEqual(
|
||||
wo.operations[3].planned_start_time, add_to_date(wo.operations[1].planned_end_time, minutes=10)
|
||||
)
|
||||
|
||||
|
||||
def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse):
|
||||
|
||||
@@ -349,13 +349,18 @@ frappe.ui.form.on("Work Order", {
|
||||
return operations_data;
|
||||
},
|
||||
},
|
||||
function (data) {
|
||||
function () {
|
||||
const selected_rows = dialog.fields_dict["operations"].grid.get_selected_children();
|
||||
if (selected_rows.length == 0) {
|
||||
frappe.msgprint(__("Please select atleast one operation to create Job Card"));
|
||||
return;
|
||||
}
|
||||
frappe.call({
|
||||
method: "erpnext.manufacturing.doctype.work_order.work_order.make_job_card",
|
||||
freeze: true,
|
||||
args: {
|
||||
work_order: frm.doc.name,
|
||||
operations: data.operations,
|
||||
operations: selected_rows,
|
||||
},
|
||||
callback: function () {
|
||||
frm.reload_doc();
|
||||
@@ -366,7 +371,7 @@ frappe.ui.form.on("Work Order", {
|
||||
__("Create")
|
||||
);
|
||||
|
||||
dialog.fields_dict["operations"].grid.wrapper.find(".grid-add-row").hide();
|
||||
dialog.fields_dict["operations"].grid.grid_buttons.hide();
|
||||
|
||||
var pending_qty = 0;
|
||||
frm.doc.operations.forEach((data) => {
|
||||
|
||||
@@ -199,6 +199,7 @@ class WorkOrder(Document):
|
||||
|
||||
self.set_required_items(reset_only_qty=len(self.get("required_items")))
|
||||
self.enable_auto_reserve_stock()
|
||||
self.validate_operations_sequence()
|
||||
|
||||
def validate_dates(self):
|
||||
if self.actual_start_date and self.actual_end_date:
|
||||
@@ -229,6 +230,30 @@ class WorkOrder(Document):
|
||||
if self.is_new() and frappe.db.get_single_value("Stock Settings", "auto_reserve_stock"):
|
||||
self.reserve_stock = 1
|
||||
|
||||
def validate_operations_sequence(self):
|
||||
if all([not op.sequence_id for op in self.operations]):
|
||||
for op in self.operations:
|
||||
op.sequence_id = op.idx
|
||||
else:
|
||||
sequence_id = 1
|
||||
for op in self.operations:
|
||||
if op.idx == 1 and op.sequence_id != 1:
|
||||
frappe.throw(
|
||||
_("Row #1: Sequence ID must be 1 for Operation {0}.").format(
|
||||
frappe.bold(op.operation)
|
||||
)
|
||||
)
|
||||
elif op.sequence_id != sequence_id and op.sequence_id != sequence_id + 1:
|
||||
frappe.throw(
|
||||
_("Row #{0}: Sequence ID must be {1} or {2} for Operation {3}.").format(
|
||||
op.idx,
|
||||
frappe.bold(sequence_id),
|
||||
frappe.bold(sequence_id + 1),
|
||||
frappe.bold(op.operation),
|
||||
)
|
||||
)
|
||||
sequence_id = op.sequence_id
|
||||
|
||||
def set_warehouses(self):
|
||||
for row in self.required_items:
|
||||
if not row.source_warehouse:
|
||||
@@ -726,17 +751,6 @@ 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
|
||||
|
||||
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:
|
||||
|
||||
@@ -202,6 +202,7 @@
|
||||
"fieldname": "sequence_id",
|
||||
"fieldtype": "Int",
|
||||
"label": "Sequence ID",
|
||||
"non_negative": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
@@ -299,7 +300,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-04-09 16:21:47.110564",
|
||||
"modified": "2025-05-15 15:10:06.885440",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Work Order Operation",
|
||||
|
||||
@@ -427,4 +427,5 @@ erpnext.patches.v15_0.set_status_cancelled_on_cancelled_pos_opening_entry_and_po
|
||||
erpnext.patches.v15_0.set_company_on_pos_inv_merge_log
|
||||
erpnext.patches.v15_0.rename_price_list_to_buying_price_list
|
||||
erpnext.patches.v15_0.repost_gl_entries_with_no_account_subcontracting
|
||||
erpnext.patches.v15_0.patch_missing_buying_price_list_in_material_request
|
||||
erpnext.patches.v15_0.remove_sales_partner_from_consolidated_sales_invoice
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import frappe
|
||||
import frappe.defaults
|
||||
|
||||
|
||||
def execute():
|
||||
if frappe.db.has_column("Material Request", "buying_price_list") and (
|
||||
default_buying_price_list := frappe.defaults.get_defaults().buying_price_list
|
||||
):
|
||||
docs = frappe.get_all(
|
||||
"Material Request", filters={"buying_price_list": ["is", "not set"], "docstatus": 1}, pluck="name"
|
||||
)
|
||||
for doc in docs:
|
||||
frappe.db.set_value("Material Request", doc, "buying_price_list", default_buying_price_list)
|
||||
@@ -787,7 +787,8 @@ class SalesOrder(SellingController):
|
||||
|
||||
if self.delivery_date:
|
||||
for item in self.items:
|
||||
item.delivery_date = self.delivery_date
|
||||
if not item.delivery_date:
|
||||
item.delivery_date = self.delivery_date
|
||||
|
||||
|
||||
def get_unreserved_qty(item: object, reserved_qty_details: dict) -> float:
|
||||
|
||||
@@ -713,6 +713,10 @@ class PurchaseReceipt(BuyingController):
|
||||
warehouse_with_no_account = []
|
||||
|
||||
for d in self.get("items"):
|
||||
remarks = self.get("remarks") or _("Accounting Entry for {0}").format(
|
||||
"Asset" if d.is_fixed_asset else "Stock"
|
||||
)
|
||||
|
||||
if (
|
||||
provisional_accounting_for_non_stock_items
|
||||
and d.item_code not in stock_items
|
||||
@@ -724,10 +728,6 @@ class PurchaseReceipt(BuyingController):
|
||||
d, gl_entries, self.posting_date, d.get("provisional_expense_account")
|
||||
)
|
||||
elif flt(d.qty) and (flt(d.valuation_rate) or self.is_return):
|
||||
remarks = self.get("remarks") or _("Accounting Entry for {0}").format(
|
||||
"Asset" if d.is_fixed_asset else "Stock"
|
||||
)
|
||||
|
||||
if not (
|
||||
(erpnext.is_perpetual_inventory_enabled(self.company) and d.item_code in stock_items)
|
||||
or (d.is_fixed_asset and not d.purchase_invoice)
|
||||
@@ -772,7 +772,7 @@ class PurchaseReceipt(BuyingController):
|
||||
make_amount_difference_entry(d)
|
||||
make_sub_contracting_gl_entries(d)
|
||||
make_divisional_loss_gl_entry(d, outgoing_amount)
|
||||
elif (d.warehouse and d.warehouse not in warehouse_with_no_account) or (
|
||||
elif (d.warehouse and d.qty and d.warehouse not in warehouse_with_no_account) or (
|
||||
not frappe.db.get_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials")
|
||||
and d.rejected_warehouse
|
||||
and d.rejected_warehouse not in warehouse_with_no_account
|
||||
@@ -785,10 +785,18 @@ class PurchaseReceipt(BuyingController):
|
||||
if d.rejected_qty and frappe.db.get_single_value(
|
||||
"Buying Settings", "set_valuation_rate_for_rejected_materials"
|
||||
):
|
||||
stock_asset_rbnb = (
|
||||
self.get_company_default("asset_received_but_not_billed")
|
||||
if d.is_fixed_asset
|
||||
else self.get_company_default("stock_received_but_not_billed")
|
||||
)
|
||||
|
||||
stock_value_diff = get_stock_value_difference(self.name, d.name, d.rejected_warehouse)
|
||||
stock_asset_account_name = warehouse_account[d.rejected_warehouse]["account"]
|
||||
|
||||
make_item_asset_inward_gl_entry(d, stock_value_diff, stock_asset_account_name)
|
||||
if not d.qty:
|
||||
make_stock_received_but_not_billed_entry(d)
|
||||
|
||||
if warehouse_with_no_account:
|
||||
frappe.msgprint(
|
||||
|
||||
@@ -3693,7 +3693,7 @@ class TestPurchaseReceipt(IntegrationTestCase):
|
||||
|
||||
columns, data = execute(
|
||||
filters=frappe._dict(
|
||||
{"item_code": item_code, "warehouse": pr.items[0].warehouse, "company": pr.company}
|
||||
{"item_code": [item_code], "warehouse": [pr.items[0].warehouse], "company": pr.company}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -4286,6 +4286,47 @@ class TestPurchaseReceipt(IntegrationTestCase):
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials", 0)
|
||||
|
||||
def test_valuation_rate_for_rejected_materials_withoout_accepted_materials(self):
|
||||
item = make_item("Test Item with Rej Material Valuation WO Accepted", {"is_stock_item": 1})
|
||||
company = "_Test Company with perpetual inventory"
|
||||
|
||||
warehouse = create_warehouse(
|
||||
"_Test In-ward Warehouse",
|
||||
company="_Test Company with perpetual inventory",
|
||||
)
|
||||
|
||||
rej_warehouse = create_warehouse(
|
||||
"_Test Warehouse - Rejected Material",
|
||||
company="_Test Company with perpetual inventory",
|
||||
)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", 1)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials", 1)
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
item_code=item.name,
|
||||
qty=0,
|
||||
rate=100,
|
||||
company=company,
|
||||
warehouse=warehouse,
|
||||
rejected_qty=5,
|
||||
rejected_warehouse=rej_warehouse,
|
||||
)
|
||||
|
||||
gl_entry = frappe.get_all(
|
||||
"GL Entry", filters={"debit": (">", 0), "voucher_no": pr.name}, pluck="name"
|
||||
)
|
||||
|
||||
stock_value_diff = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"warehouse": rej_warehouse, "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
|
||||
self.assertTrue(gl_entry)
|
||||
self.assertEqual(stock_value_diff, 500.00)
|
||||
|
||||
def test_no_valuation_rate_for_rejected_materials(self):
|
||||
item = make_item("Test Item with Rej Material No Valuation", {"is_stock_item": 1})
|
||||
company = "_Test Company with perpetual inventory"
|
||||
|
||||
@@ -253,19 +253,35 @@ def get_warehouses_based_on_account(account, company=None):
|
||||
|
||||
# Will be use for frappe.qb
|
||||
def apply_warehouse_filter(query, sle, filters):
|
||||
if warehouse := filters.get("warehouse"):
|
||||
warehouse_table = frappe.qb.DocType("Warehouse")
|
||||
if not (warehouses := filters.get("warehouse")):
|
||||
return query
|
||||
|
||||
lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"])
|
||||
chilren_subquery = (
|
||||
frappe.qb.from_(warehouse_table)
|
||||
.select(warehouse_table.name)
|
||||
.where(
|
||||
(warehouse_table.lft >= lft)
|
||||
& (warehouse_table.rgt <= rgt)
|
||||
& (warehouse_table.name == sle.warehouse)
|
||||
)
|
||||
)
|
||||
query = query.where(ExistsCriterion(chilren_subquery))
|
||||
warehouse_table = frappe.qb.DocType("Warehouse")
|
||||
|
||||
if isinstance(warehouses, str):
|
||||
warehouses = [warehouses]
|
||||
|
||||
warehouse_range = frappe.get_all(
|
||||
"Warehouse",
|
||||
filters={
|
||||
"name": ("in", warehouses),
|
||||
},
|
||||
fields=["lft", "rgt"],
|
||||
as_list=True,
|
||||
)
|
||||
|
||||
child_query = frappe.qb.from_(warehouse_table).select(warehouse_table.name)
|
||||
|
||||
range_conditions = [
|
||||
(warehouse_table.lft >= lft) & (warehouse_table.rgt <= rgt) for lft, rgt in warehouse_range
|
||||
]
|
||||
|
||||
combined_condition = range_conditions[0]
|
||||
for condition in range_conditions[1:]:
|
||||
combined_condition = combined_condition | condition
|
||||
|
||||
child_query = child_query.where(combined_condition).where(warehouse_table.name == sle.warehouse)
|
||||
|
||||
query = query.where(ExistsCriterion(child_query))
|
||||
|
||||
return query
|
||||
|
||||
@@ -36,38 +36,57 @@ frappe.query_reports["Stock Balance"] = {
|
||||
},
|
||||
{
|
||||
fieldname: "item_code",
|
||||
label: __("Item"),
|
||||
fieldtype: "Link",
|
||||
label: __("Items"),
|
||||
fieldtype: "MultiSelectList",
|
||||
width: "80",
|
||||
options: "Item",
|
||||
get_query: function () {
|
||||
get_data: async function (txt) {
|
||||
let item_group = frappe.query_report.get_filter_value("item_group");
|
||||
|
||||
return {
|
||||
query: "erpnext.controllers.queries.item_query",
|
||||
filters: {
|
||||
...(item_group && { item_group }),
|
||||
is_stock_item: 1,
|
||||
},
|
||||
let filters = {
|
||||
...(item_group && { item_group }),
|
||||
is_stock_item: 1,
|
||||
};
|
||||
|
||||
let { message: data } = await frappe.call({
|
||||
method: "erpnext.controllers.queries.item_query",
|
||||
args: {
|
||||
doctype: "Item",
|
||||
txt: txt,
|
||||
searchfield: "name",
|
||||
start: 0,
|
||||
page_len: 10,
|
||||
filters: filters,
|
||||
as_dict: 1,
|
||||
},
|
||||
});
|
||||
|
||||
data = data.map(({ name, description }) => {
|
||||
return {
|
||||
value: name,
|
||||
description: description,
|
||||
};
|
||||
});
|
||||
|
||||
return data || [];
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "warehouse",
|
||||
label: __("Warehouse"),
|
||||
fieldtype: "Link",
|
||||
label: __("Warehouses"),
|
||||
fieldtype: "MultiSelectList",
|
||||
width: "80",
|
||||
options: "Warehouse",
|
||||
get_query: () => {
|
||||
get_data: (txt) => {
|
||||
let warehouse_type = frappe.query_report.get_filter_value("warehouse_type");
|
||||
let company = frappe.query_report.get_filter_value("company");
|
||||
|
||||
return {
|
||||
filters: {
|
||||
...(warehouse_type && { warehouse_type }),
|
||||
...(company && { company }),
|
||||
},
|
||||
let filters = {
|
||||
...(warehouse_type && { warehouse_type }),
|
||||
...(company && { company }),
|
||||
};
|
||||
|
||||
return frappe.db.get_link_options("Warehouse", txt, filters);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -8,7 +8,6 @@ from typing import Any, TypedDict
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import Order
|
||||
from frappe.query_builder.functions import Coalesce
|
||||
from frappe.utils import add_days, cint, date_diff, flt, getdate
|
||||
from frappe.utils.nestedset import get_descendants_of
|
||||
@@ -26,8 +25,8 @@ class StockBalanceFilter(TypedDict):
|
||||
from_date: str
|
||||
to_date: str
|
||||
item_group: str | None
|
||||
item: str | None
|
||||
warehouse: str | None
|
||||
item: list[str] | None
|
||||
warehouse: list[str] | None
|
||||
warehouse_type: str | None
|
||||
include_uom: str | None # include extra info in converted UOM
|
||||
show_stock_ageing_data: bool
|
||||
@@ -361,6 +360,7 @@ class StockBalanceReport:
|
||||
|
||||
if self.filters.get("warehouse"):
|
||||
query = apply_warehouse_filter(query, sle, self.filters)
|
||||
|
||||
elif warehouse_type := self.filters.get("warehouse_type"):
|
||||
query = (
|
||||
query.join(warehouse_table)
|
||||
@@ -375,13 +375,11 @@ class StockBalanceReport:
|
||||
children = get_descendants_of("Item Group", item_group, ignore_permissions=True)
|
||||
query = query.where(item_table.item_group.isin([*children, item_group]))
|
||||
|
||||
for field in ["item_code", "brand"]:
|
||||
if not self.filters.get(field):
|
||||
continue
|
||||
elif field == "item_code":
|
||||
query = query.where(item_table.name == self.filters.get(field))
|
||||
else:
|
||||
query = query.where(item_table[field] == self.filters.get(field))
|
||||
if item_codes := self.filters.get("item_code"):
|
||||
query = query.where(item_table.name.isin(item_codes))
|
||||
|
||||
if brand := self.filters.get("brand"):
|
||||
query = query.where(item_table.brand == brand)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ class TestStockBalance(IntegrationTestCase):
|
||||
self.filters = _dict(
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"item_code": self.item.name,
|
||||
"item_code": [self.item.name],
|
||||
"from_date": "2020-01-01",
|
||||
"to_date": str(today()),
|
||||
}
|
||||
@@ -165,6 +165,6 @@ class TestStockBalance(IntegrationTestCase):
|
||||
variant.save()
|
||||
|
||||
self.generate_stock_ledger(variant.name, [_dict(qty=5, rate=10)])
|
||||
rows = stock_balance(self.filters.update({"show_variant_attributes": 1, "item_code": variant.name}))
|
||||
rows = stock_balance(self.filters.update({"show_variant_attributes": 1, "item_code": [variant.name]}))
|
||||
self.assertPartialDictEq(attributes, rows[0])
|
||||
self.assertInvariants(rows)
|
||||
|
||||
@@ -27,25 +27,44 @@ frappe.query_reports["Stock Ledger"] = {
|
||||
},
|
||||
{
|
||||
fieldname: "warehouse",
|
||||
label: __("Warehouse"),
|
||||
fieldtype: "Link",
|
||||
label: __("Warehouses"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Warehouse",
|
||||
get_query: function () {
|
||||
get_data: function (txt) {
|
||||
const company = frappe.query_report.get_filter_value("company");
|
||||
return {
|
||||
filters: { company: company },
|
||||
};
|
||||
|
||||
return frappe.db.get_link_options("Warehouse", txt, {
|
||||
company: company,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "item_code",
|
||||
label: __("Item"),
|
||||
fieldtype: "Link",
|
||||
label: __("Items"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Item",
|
||||
get_query: function () {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.item_query",
|
||||
};
|
||||
get_data: async function (txt) {
|
||||
let { message: data } = await frappe.call({
|
||||
method: "erpnext.controllers.queries.item_query",
|
||||
args: {
|
||||
doctype: "Item",
|
||||
txt: txt,
|
||||
searchfield: "name",
|
||||
start: 0,
|
||||
page_len: 10,
|
||||
filters: {},
|
||||
as_dict: 1,
|
||||
},
|
||||
});
|
||||
|
||||
data = data.map(({ name, description }) => {
|
||||
return {
|
||||
value: name,
|
||||
description: description,
|
||||
};
|
||||
});
|
||||
|
||||
return data || [];
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -456,19 +456,23 @@ def get_items(filters):
|
||||
query = frappe.qb.from_(item).select(item.name)
|
||||
conditions = []
|
||||
|
||||
if item_code := filters.get("item_code"):
|
||||
conditions.append(item.name == item_code)
|
||||
if item_codes := filters.get("item_code"):
|
||||
conditions.append(item.name.isin(item_codes))
|
||||
|
||||
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 filters.get("item_group") and (
|
||||
condition := get_item_group_condition(filters.get("item_group"), item)
|
||||
):
|
||||
conditions.append(condition)
|
||||
|
||||
items = []
|
||||
if conditions:
|
||||
for condition in conditions:
|
||||
query = query.where(condition)
|
||||
|
||||
items = [r[0] for r in query.run()]
|
||||
|
||||
return items
|
||||
@@ -505,6 +509,7 @@ def get_item_details(items, sl_entries, include_uom):
|
||||
return item_details
|
||||
|
||||
|
||||
# TODO: THIS IS NOT USED
|
||||
def get_sle_conditions(filters):
|
||||
conditions = []
|
||||
if filters.get("warehouse"):
|
||||
@@ -535,8 +540,8 @@ def get_opening_balance_from_batch(filters, columns, sl_entries):
|
||||
}
|
||||
|
||||
for fields in ["item_code", "warehouse"]:
|
||||
if filters.get(fields):
|
||||
query_filters[fields] = filters.get(fields)
|
||||
if value := filters.get(fields):
|
||||
query_filters[fields] = ("in", value)
|
||||
|
||||
opening_data = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
@@ -567,8 +572,16 @@ def get_opening_balance_from_batch(filters, columns, sl_entries):
|
||||
)
|
||||
|
||||
for field in ["item_code", "warehouse", "company"]:
|
||||
if filters.get(field):
|
||||
query = query.where(table[field] == filters.get(field))
|
||||
value = filters.get(field)
|
||||
|
||||
if not value:
|
||||
continue
|
||||
|
||||
if isinstance(value, list | tuple):
|
||||
query = query.where(table[field].isin(value))
|
||||
|
||||
else:
|
||||
query = query.where(table[field] == value)
|
||||
|
||||
bundle_data = query.run(as_dict=True)
|
||||
|
||||
@@ -623,13 +636,34 @@ def get_opening_balance(filters, columns, sl_entries):
|
||||
return row
|
||||
|
||||
|
||||
def get_warehouse_condition(warehouse):
|
||||
warehouse_details = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"], as_dict=1)
|
||||
if warehouse_details:
|
||||
return f" exists (select name from `tabWarehouse` wh \
|
||||
where wh.lft >= {warehouse_details.lft} and wh.rgt <= {warehouse_details.rgt} and warehouse = wh.name)"
|
||||
def get_warehouse_condition(warehouses):
|
||||
if not warehouses:
|
||||
return ""
|
||||
|
||||
return ""
|
||||
if isinstance(warehouses, str):
|
||||
warehouses = [warehouses]
|
||||
|
||||
warehouse_range = frappe.get_all(
|
||||
"Warehouse",
|
||||
filters={
|
||||
"name": ("in", warehouses),
|
||||
},
|
||||
fields=["lft", "rgt"],
|
||||
as_list=True,
|
||||
)
|
||||
|
||||
if not warehouse_range:
|
||||
return ""
|
||||
|
||||
alias = "wh"
|
||||
conditions = []
|
||||
for lft, rgt in warehouse_range:
|
||||
conditions.append(f"({alias}.lft >= {lft} and {alias}.rgt <= {rgt})")
|
||||
|
||||
conditions = " or ".join(conditions)
|
||||
|
||||
return f" exists (select name from `tabWarehouse` {alias} \
|
||||
where ({conditions}) and warehouse = {alias}.name)"
|
||||
|
||||
|
||||
def get_item_group_condition(item_group, item_table=None):
|
||||
|
||||
@@ -17,7 +17,7 @@ class TestStockLedgerReeport(IntegrationTestCase):
|
||||
company="_Test Company",
|
||||
from_date=today(),
|
||||
to_date=add_days(today(), 30),
|
||||
item_code="_Test Stock Report Serial Item",
|
||||
item_code=["_Test Stock Report Serial Item"],
|
||||
)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
|
||||
@@ -18,8 +18,15 @@ batch = get_random("Batch")
|
||||
REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [
|
||||
("Stock Ledger", {"_optional": True}),
|
||||
("Stock Ledger", {"batch_no": batch}),
|
||||
("Stock Ledger", {"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}),
|
||||
("Stock Balance", {"_optional": True}),
|
||||
("Stock Ledger", {"item_code": ["_Test Item"], "warehouse": ["_Test Warehouse - _TC"]}),
|
||||
(
|
||||
"Stock Balance",
|
||||
{
|
||||
"item_code": ["_Test Item"],
|
||||
"warehouse": ["_Test Warehouse - _TC"],
|
||||
"item_group": "_Test Item Group",
|
||||
},
|
||||
),
|
||||
("Stock Projected Qty", {"_optional": True}),
|
||||
("Batch-Wise Balance History", {}),
|
||||
("Itemwise Recommended Reorder Level", {"item_group": "All Item Groups"}),
|
||||
|
||||
@@ -1683,8 +1683,20 @@ def get_stock_ledger_entries(
|
||||
):
|
||||
"""get stock ledger entries filtered by specific posting datetime conditions"""
|
||||
conditions = f" and posting_datetime {operator} %(posting_datetime)s"
|
||||
if previous_sle.get("warehouse"):
|
||||
conditions += " and warehouse = %(warehouse)s"
|
||||
|
||||
if item_code := previous_sle.get("item_code"):
|
||||
if isinstance(item_code, list | tuple):
|
||||
conditions += " and item_code in %(item_code)s"
|
||||
else:
|
||||
conditions += " and item_code = %(item_code)s"
|
||||
|
||||
if warehouse := previous_sle.get("warehouse"):
|
||||
if isinstance(warehouse, list | tuple):
|
||||
conditions += " and warehouse in %(warehouse)s"
|
||||
|
||||
else:
|
||||
conditions += " and warehouse = %(warehouse)s"
|
||||
|
||||
elif previous_sle.get("warehouse_condition"):
|
||||
conditions += " and " + previous_sle.get("warehouse_condition")
|
||||
|
||||
@@ -1727,8 +1739,7 @@ def get_stock_ledger_entries(
|
||||
"""
|
||||
select *, posting_datetime as "timestamp"
|
||||
from `tabStock Ledger Entry`
|
||||
where item_code = %(item_code)s
|
||||
and is_cancelled = 0
|
||||
where is_cancelled = 0
|
||||
{conditions}
|
||||
order by posting_datetime {order}, creation {order}
|
||||
{limit} {for_update}""".format(
|
||||
|
||||
@@ -316,9 +316,12 @@ class TransactionBase(StatusUpdater):
|
||||
setattr(item_obj, k, v)
|
||||
|
||||
def handle_internal_parties(self, item_obj: object, item_details: dict) -> None:
|
||||
fetch_valuation_rate_for_internal_transaction = cint(
|
||||
frappe.get_single_value("Accounts Settings", "fetch_valuation_rate_for_internal_transaction")
|
||||
)
|
||||
if (
|
||||
self.get("is_internal_customer") or self.get("is_internal_supplier")
|
||||
) and self.represents_company == self.company:
|
||||
) and fetch_valuation_rate_for_internal_transaction:
|
||||
args = frappe._dict(
|
||||
{
|
||||
"item_code": item_obj.item_code,
|
||||
|
||||
Reference in New Issue
Block a user