diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index d67d59b5d45..a4f6a74a5ab 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -211,8 +211,7 @@ class ExchangeRateRevaluation(Document): # Handle Accounts with '0' balance in Account/Base Currency for d in [x for x in account_details if x.zero_balance]: - # TODO: Set new balance in Base/Account currency - if d.balance > 0: + if d.balance != 0: current_exchange_rate = new_exchange_rate = 0 new_balance_in_account_currency = 0 # this will be '0' @@ -399,6 +398,9 @@ class ExchangeRateRevaluation(Document): journal_entry_accounts = [] for d in accounts: + if not flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")): + continue + dr_or_cr = ( "debit_in_account_currency" if d.get("balance_in_account_currency") > 0 @@ -448,7 +450,13 @@ class ExchangeRateRevaluation(Document): } ) - journal_entry_accounts.append( + journal_entry.set("accounts", journal_entry_accounts) + journal_entry.set_amounts_in_company_currency() + journal_entry.set_total_debit_credit() + + self.gain_loss_unbooked += journal_entry.difference - self.gain_loss_unbooked + journal_entry.append( + "accounts", { "account": unrealized_exchange_gain_loss_account, "balance": get_balance_on(unrealized_exchange_gain_loss_account), @@ -460,10 +468,9 @@ class ExchangeRateRevaluation(Document): "exchange_rate": 1, "reference_type": "Exchange Rate Revaluation", "reference_name": self.name, - } + }, ) - journal_entry.set("accounts", journal_entry_accounts) journal_entry.set_amounts_in_company_currency() journal_entry.set_total_debit_credit() journal_entry.save() diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html index 3920d4cf096..b9680dfb3bf 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html @@ -15,7 +15,7 @@

{{ _("STATEMENTS OF ACCOUNTS") }}

-
{{ _("Customer: ") }} {{filters.party[0] }}
+
{{ _("Customer: ") }} {{filters.party_name[0] }}
{{ _("Date: ") }} {{ frappe.format(filters.from_date, 'Date')}} diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 01f716daa21..39b80143208 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -23,7 +23,7 @@ from erpnext.accounts.report.general_ledger.general_ledger import execute as get class ProcessStatementOfAccounts(Document): def validate(self): if not self.subject: - self.subject = "Statement Of Accounts for {{ customer.name }}" + self.subject = "Statement Of Accounts for {{ customer.customer_name }}" if not self.body: self.body = "Hello {{ customer.name }},
PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}." @@ -86,6 +86,7 @@ def get_report_pdf(doc, consolidated=True): "account": [doc.account] if doc.account else None, "party_type": "Customer", "party": [entry.customer], + "party_name": [entry.customer_name] if entry.customer_name else None, "presentation_currency": presentation_currency, "group_by": doc.group_by, "currency": doc.currency, @@ -153,7 +154,7 @@ def get_customers_based_on_territory_or_customer_group(customer_collection, coll ] return frappe.get_list( "Customer", - fields=["name", "email_id"], + fields=["name", "customer_name", "email_id"], filters=[[fields_dict[customer_collection], "IN", selected]], ) @@ -176,7 +177,7 @@ def get_customers_based_on_sales_person(sales_person): if sales_person_records.get("Customer"): return frappe.get_list( "Customer", - fields=["name", "email_id"], + fields=["name", "customer_name", "email_id"], filters=[["name", "in", list(sales_person_records["Customer"])]], ) else: @@ -225,7 +226,7 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory): if customer_collection == "Sales Partner": customers = frappe.get_list( "Customer", - fields=["name", "email_id"], + fields=["name", "customer_name", "email_id"], filters=[["default_sales_partner", "=", collection_name]], ) else: @@ -244,7 +245,12 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory): continue customer_list.append( - {"name": customer.name, "primary_email": primary_email, "billing_email": billing_email} + { + "name": customer.name, + "customer_name": customer.customer_name, + "primary_email": primary_email, + "billing_email": billing_email, + } ) return customer_list diff --git a/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.json b/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.json index dd04dc1b3c6..8bffd6a93b9 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.json +++ b/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.json @@ -1,12 +1,12 @@ { "actions": [], - "allow_workflow": 1, "creation": "2020-08-03 16:35:21.852178", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ "customer", + "customer_name", "billing_email", "primary_email" ], @@ -30,11 +30,18 @@ "fieldtype": "Read Only", "in_list_view": 1, "label": "Billing Email" + }, + { + "fetch_from": "customer.customer_name", + "fieldname": "customer_name", + "fieldtype": "Data", + "label": "Customer Name", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2020-08-03 22:55:38.875601", + "modified": "2023-03-13 00:12:34.508086", "modified_by": "Administrator", "module": "Accounts", "name": "Process Statement Of Accounts Customer", @@ -43,5 +50,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index f18fef5a148..9a5b42be4bb 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -32,9 +32,6 @@ "cost_center", "dimension_col_break", "project", - "column_break_27", - "campaign", - "source", "currency_and_price_list", "currency", "conversion_rate", @@ -203,7 +200,9 @@ "more_information", "status", "inter_company_invoice_reference", + "campaign", "represents_company", + "source", "customer_group", "col_break23", "is_internal_customer", @@ -2083,10 +2082,6 @@ "fieldname": "company_addr_col_break", "fieldtype": "Column Break" }, - { - "fieldname": "column_break_27", - "fieldtype": "Column Break" - }, { "fieldname": "column_break_52", "fieldtype": "Column Break" @@ -2143,11 +2138,10 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2022-11-07 16:02:07.972258", + "modified": "2023-03-13 11:43:15.883055", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", - "name_case": "Title Case", "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 078e51c905f..4e2e2eb12c8 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -32,6 +32,16 @@ from erpnext import get_company_currency from erpnext.accounts.utils import get_fiscal_year from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen +PURCHASE_TRANSACTION_TYPES = {"Purchase Order", "Purchase Receipt", "Purchase Invoice"} +SALES_TRANSACTION_TYPES = { + "Quotation", + "Sales Order", + "Delivery Note", + "Sales Invoice", + "POS Invoice", +} +TRANSACTION_TYPES = PURCHASE_TRANSACTION_TYPES | SALES_TRANSACTION_TYPES + class DuplicatePartyAccountError(frappe.ValidationError): pass @@ -124,12 +134,6 @@ def _get_party_details( set_other_values(party_details, party, party_type) set_price_list(party_details, party, party_type, price_list, pos_profile) - party_details["tax_category"] = get_address_tax_category( - party.get("tax_category"), - party_address, - shipping_address if party_type != "Supplier" else party_address, - ) - tax_template = set_taxes( party.name, party_type, @@ -211,20 +215,10 @@ def set_address_details( else: party_details.update(get_company_address(company)) - if doctype and doctype in [ - "Delivery Note", - "Sales Invoice", - "Sales Order", - "Quotation", - "POS Invoice", - ]: - if party_details.company_address: - party_details.update( - get_fetch_values(doctype, "company_address", party_details.company_address) - ) - get_regional_address_details(party_details, doctype, company) + if doctype in SALES_TRANSACTION_TYPES and party_details.company_address: + party_details.update(get_fetch_values(doctype, "company_address", party_details.company_address)) - elif doctype and doctype in ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]: + if doctype in PURCHASE_TRANSACTION_TYPES: if shipping_address: party_details.update( shipping_address=shipping_address, @@ -250,9 +244,21 @@ def set_address_details( **get_fetch_values(doctype, "shipping_address", party_details.billing_address) ) + party_address, shipping_address = ( + party_details.get(billing_address_field), + party_details.shipping_address_name, + ) + + party_details["tax_category"] = get_address_tax_category( + party.get("tax_category"), + party_address, + shipping_address if party_type != "Supplier" else party_address, + ) + + if doctype in TRANSACTION_TYPES: get_regional_address_details(party_details, doctype, company) - return party_details.get(billing_address_field), party_details.shipping_address_name + return party_address, shipping_address @erpnext.allow_regional diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index 560b79243d7..9a3e82486af 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -138,7 +138,8 @@ def prepare_companywise_opening_balance(asset_data, liability_data, equity_data, for data in [asset_data, liability_data, equity_data]: if data: account_name = get_root_account_name(data[0].root_type, company) - opening_value += get_opening_balance(account_name, data, company) or 0.0 + if account_name: + opening_value += get_opening_balance(account_name, data, company) or 0.0 opening_balance[company] = opening_value @@ -155,7 +156,7 @@ def get_opening_balance(account_name, data, company): def get_root_account_name(root_type, company): - return frappe.get_all( + root_account = frappe.get_all( "Account", fields=["account_name"], filters={ @@ -165,7 +166,10 @@ def get_root_account_name(root_type, company): "parent_account": ("is", "not set"), }, as_list=1, - )[0][0] + ) + + if root_account: + return root_account[0][0] def get_profit_loss_data(fiscal_year, companies, columns, filters): diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index 6d2cd8ed411..61bc58009a6 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -78,7 +78,6 @@ def validate_filters(filters): def get_data(filters): - accounts = frappe.db.sql( """select name, account_number, parent_account, account_name, root_type, report_type, lft, rgt @@ -118,12 +117,10 @@ def get_data(filters): ignore_closing_entries=not flt(filters.with_period_closing_entry), ) - total_row = calculate_values( - accounts, gl_entries_by_account, opening_balances, filters, company_currency - ) + calculate_values(accounts, gl_entries_by_account, opening_balances) accumulate_values_into_parents(accounts, accounts_by_name) - data = prepare_data(accounts, filters, total_row, parent_children_map, company_currency) + data = prepare_data(accounts, filters, parent_children_map, company_currency) data = filter_out_zero_value_rows( data, parent_children_map, show_zero_values=filters.get("show_zero_values") ) @@ -218,7 +215,7 @@ def get_rootwise_opening_balances(filters, report_type): return opening -def calculate_values(accounts, gl_entries_by_account, opening_balances, filters, company_currency): +def calculate_values(accounts, gl_entries_by_account, opening_balances): init = { "opening_debit": 0.0, "opening_credit": 0.0, @@ -228,22 +225,6 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, filters, "closing_credit": 0.0, } - total_row = { - "account": "'" + _("Total") + "'", - "account_name": "'" + _("Total") + "'", - "warn_if_negative": True, - "opening_debit": 0.0, - "opening_credit": 0.0, - "debit": 0.0, - "credit": 0.0, - "closing_debit": 0.0, - "closing_credit": 0.0, - "parent_account": None, - "indent": 0, - "has_value": True, - "currency": company_currency, - } - for d in accounts: d.update(init.copy()) @@ -261,8 +242,28 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, filters, prepare_opening_closing(d) - for field in value_fields: - total_row[field] += d[field] + +def calculate_total_row(accounts, company_currency): + total_row = { + "account": "'" + _("Total") + "'", + "account_name": "'" + _("Total") + "'", + "warn_if_negative": True, + "opening_debit": 0.0, + "opening_credit": 0.0, + "debit": 0.0, + "credit": 0.0, + "closing_debit": 0.0, + "closing_credit": 0.0, + "parent_account": None, + "indent": 0, + "has_value": True, + "currency": company_currency, + } + + for d in accounts: + if not d.parent_account: + for field in value_fields: + total_row[field] += d[field] return total_row @@ -274,7 +275,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name): accounts_by_name[d.parent_account][key] += d[key] -def prepare_data(accounts, filters, total_row, parent_children_map, company_currency): +def prepare_data(accounts, filters, parent_children_map, company_currency): data = [] for d in accounts: @@ -305,6 +306,7 @@ def prepare_data(accounts, filters, total_row, parent_children_map, company_curr row["has_value"] = has_value data.append(row) + total_row = calculate_total_row(accounts, company_currency) data.extend([{}, total_row]) return data diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 341d51b8a5f..15c270e58ad 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -305,7 +305,7 @@ def get_returned_qty_map_for_row(return_against, party, row_name, doctype): fields += ["sum(abs(`tab{0}`.received_stock_qty)) as received_stock_qty".format(child_doctype)] # Used retrun against and supplier and is_retrun because there is an index added for it - data = frappe.db.get_list( + data = frappe.get_all( doctype, fields=fields, filters=[ diff --git a/erpnext/controllers/website_list_for_contact.py b/erpnext/controllers/website_list_for_contact.py index 467323035ea..7c3c38706dc 100644 --- a/erpnext/controllers/website_list_for_contact.py +++ b/erpnext/controllers/website_list_for_contact.py @@ -76,12 +76,9 @@ def get_transaction_list( ignore_permissions = False if not filters: - filters = [] + filters = {} - if doctype in ["Supplier Quotation", "Purchase Invoice"]: - filters.append((doctype, "docstatus", "<", 2)) - else: - filters.append((doctype, "docstatus", "=", 1)) + filters["docstatus"] = ["<", "2"] if doctype in ["Supplier Quotation", "Purchase Invoice"] else 1 if (user != "Guest" and is_website_user()) or doctype == "Request for Quotation": parties_doctype = ( @@ -92,12 +89,12 @@ def get_transaction_list( if customers: if doctype == "Quotation": - filters.append(("quotation_to", "=", "Customer")) - filters.append(("party_name", "in", customers)) + filters["quotation_to"] = "Customer" + filters["party_name"] = ["in", customers] else: - filters.append(("customer", "in", customers)) + filters["customer"] = ["in", customers] elif suppliers: - filters.append(("supplier", "in", suppliers)) + filters["supplier"] = ["in", suppliers] elif not custom: return [] @@ -110,7 +107,7 @@ def get_transaction_list( if not customers and not suppliers and custom: ignore_permissions = False - filters = [] + filters = {} transactions = get_list_for_transactions( doctype, diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js index 1f76a1ae2eb..b2617955a36 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.js +++ b/erpnext/crm/doctype/opportunity/opportunity.js @@ -19,10 +19,6 @@ frappe.ui.form.on("Opportunity", { } } }); - - if (frm.doc.opportunity_from && frm.doc.party_name){ - frm.trigger('set_contact_link'); - } }, validate: function(frm) { @@ -130,6 +126,10 @@ frappe.ui.form.on("Opportunity", { } else { frappe.contacts.clear_address_and_contact(frm); } + + if (frm.doc.opportunity_from && frm.doc.party_name) { + frm.trigger('set_contact_link'); + } }, set_contact_link: function(frm) { @@ -137,6 +137,8 @@ frappe.ui.form.on("Opportunity", { frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Customer'} } else if(frm.doc.opportunity_from == "Lead" && frm.doc.party_name) { frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Lead'} + } else if (frm.doc.opportunity_from == "Prospect" && frm.doc.party_name) { + frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Prospect'} } }, diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 8ab79e68be9..619a415c8bc 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -31,7 +31,7 @@ class BOMTree: # specifying the attributes to save resources # ref: https://docs.python.org/3/reference/datamodel.html#slots - __slots__ = ["name", "child_items", "is_bom", "item_code", "exploded_qty", "qty"] + __slots__ = ["name", "child_items", "is_bom", "item_code", "qty", "exploded_qty", "bom_qty"] def __init__( self, name: str, is_bom: bool = True, exploded_qty: float = 1.0, qty: float = 1 @@ -50,9 +50,10 @@ class BOMTree: def __create_tree(self): bom = frappe.get_cached_doc("BOM", self.name) self.item_code = bom.item + self.bom_qty = bom.quantity for item in bom.get("items", []): - qty = item.qty / bom.quantity # quantity per unit + qty = item.stock_qty / bom.quantity # quantity per unit exploded_qty = self.exploded_qty * qty if item.bom_no: child = BOMTree(item.bom_no, exploded_qty=exploded_qty, qty=qty) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 4aff42cb73d..97480b29454 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -506,7 +506,7 @@ frappe.ui.form.on("Work Order Item", { callback: function(r) { if (r.message) { frappe.model.set_value(cdt, cdn, { - "required_qty": 1, + "required_qty": row.required_qty || 1, "item_name": r.message.item_name, "description": r.message.description, "source_warehouse": r.message.default_warehouse, diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index ae9e9c69628..66b871c746f 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -682,7 +682,7 @@ class WorkOrder(Document): for node in bom_traversal: if node.is_bom: - operations.extend(_get_operations(node.name, qty=node.exploded_qty)) + operations.extend(_get_operations(node.name, qty=node.exploded_qty / node.bom_qty)) bom_qty = frappe.get_cached_value("BOM", self.bom_no, "quantity") operations.extend(_get_operations(self.bom_no, qty=1.0 / bom_qty)) diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py index cdf1541f888..3573a3a93d8 100644 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py +++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py @@ -4,7 +4,8 @@ import frappe from frappe import _ -from frappe.query_builder.functions import Sum +from frappe.query_builder.functions import Floor, Sum +from frappe.utils import cint from pypika.terms import ExistsCriterion @@ -34,57 +35,55 @@ def get_columns(): def get_bom_stock(filters): - qty_to_produce = filters.get("qty_to_produce") or 1 - if int(qty_to_produce) < 0: - frappe.throw(_("Quantity to Produce can not be less than Zero")) + qty_to_produce = filters.get("qty_to_produce") + if cint(qty_to_produce) <= 0: + frappe.throw(_("Quantity to Produce should be greater than zero.")) if filters.get("show_exploded_view"): bom_item_table = "BOM Explosion Item" else: bom_item_table = "BOM Item" - bin = frappe.qb.DocType("Bin") - bom = frappe.qb.DocType("BOM") - bom_item = frappe.qb.DocType(bom_item_table) - - query = ( - frappe.qb.from_(bom) - .inner_join(bom_item) - .on(bom.name == bom_item.parent) - .left_join(bin) - .on(bom_item.item_code == bin.item_code) - .select( - bom_item.item_code, - bom_item.description, - bom_item.stock_qty, - bom_item.stock_uom, - (bom_item.stock_qty / bom.quantity) * qty_to_produce, - Sum(bin.actual_qty), - Sum(bin.actual_qty) / (bom_item.stock_qty / bom.quantity), - ) - .where((bom_item.parent == filters.get("bom")) & (bom_item.parenttype == "BOM")) - .groupby(bom_item.item_code) + warehouse_details = frappe.db.get_value( + "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 ) - if filters.get("warehouse"): - warehouse_details = frappe.db.get_value( - "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 - ) + BOM = frappe.qb.DocType("BOM") + BOM_ITEM = frappe.qb.DocType(bom_item_table) + BIN = frappe.qb.DocType("Bin") + WH = frappe.qb.DocType("Warehouse") + CONDITIONS = () - if warehouse_details: - wh = frappe.qb.DocType("Warehouse") - query = query.where( - ExistsCriterion( - frappe.qb.from_(wh) - .select(wh.name) - .where( - (wh.lft >= warehouse_details.lft) - & (wh.rgt <= warehouse_details.rgt) - & (bin.warehouse == wh.name) - ) - ) + if warehouse_details: + CONDITIONS = ExistsCriterion( + frappe.qb.from_(WH) + .select(WH.name) + .where( + (WH.lft >= warehouse_details.lft) + & (WH.rgt <= warehouse_details.rgt) + & (BIN.warehouse == WH.name) ) - else: - query = query.where(bin.warehouse == filters.get("warehouse")) + ) + else: + CONDITIONS = BIN.warehouse == filters.get("warehouse") - return query.run() + QUERY = ( + frappe.qb.from_(BOM) + .inner_join(BOM_ITEM) + .on(BOM.name == BOM_ITEM.parent) + .left_join(BIN) + .on((BOM_ITEM.item_code == BIN.item_code) & (CONDITIONS)) + .select( + BOM_ITEM.item_code, + BOM_ITEM.description, + BOM_ITEM.stock_qty, + BOM_ITEM.stock_uom, + BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity, + Sum(BIN.actual_qty).as_("actual_qty"), + Sum(Floor(BIN.actual_qty / (BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity))), + ) + .where((BOM_ITEM.parent == filters.get("bom")) & (BOM_ITEM.parenttype == "BOM")) + .groupby(BOM_ITEM.item_code) + ) + + return QUERY.run() diff --git a/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py new file mode 100644 index 00000000000..1c56ebe24d4 --- /dev/null +++ b/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py @@ -0,0 +1,108 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + + +import frappe +from frappe.exceptions import ValidationError +from frappe.tests.utils import FrappeTestCase +from frappe.utils import floor + +from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom +from erpnext.manufacturing.report.bom_stock_report.bom_stock_report import ( + get_bom_stock as bom_stock_report, +) +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + +class TestBomStockReport(FrappeTestCase): + def setUp(self): + self.warehouse = "_Test Warehouse - _TC" + self.fg_item, self.rm_items = create_items() + make_stock_entry(target=self.warehouse, item_code=self.rm_items[0], qty=20, basic_rate=100) + make_stock_entry(target=self.warehouse, item_code=self.rm_items[1], qty=40, basic_rate=200) + self.bom = make_bom(item=self.fg_item, quantity=1, raw_materials=self.rm_items, rm_qty=10) + + def test_bom_stock_report(self): + # Test 1: When `qty_to_produce` is 0. + filters = frappe._dict( + { + "bom": self.bom.name, + "warehouse": "Stores - _TC", + "qty_to_produce": 0, + } + ) + self.assertRaises(ValidationError, bom_stock_report, filters) + + # Test 2: When stock is not available. + data = bom_stock_report( + frappe._dict( + { + "bom": self.bom.name, + "warehouse": "Stores - _TC", + "qty_to_produce": 1, + } + ) + ) + expected_data = get_expected_data(self.bom, "Stores - _TC", 1) + self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data)) + + # Test 3: When stock is available. + data = bom_stock_report( + frappe._dict( + { + "bom": self.bom.name, + "warehouse": self.warehouse, + "qty_to_produce": 1, + } + ) + ) + expected_data = get_expected_data(self.bom, self.warehouse, 1) + self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data)) + + +def create_items(): + fg_item = make_item(properties={"is_stock_item": 1}).name + rm_item1 = make_item( + properties={ + "is_stock_item": 1, + "standard_rate": 100, + "opening_stock": 100, + "last_purchase_rate": 100, + } + ).name + rm_item2 = make_item( + properties={ + "is_stock_item": 1, + "standard_rate": 200, + "opening_stock": 200, + "last_purchase_rate": 200, + } + ).name + + return fg_item, [rm_item1, rm_item2] + + +def get_expected_data(bom, warehouse, qty_to_produce, show_exploded_view=False): + expected_data = [] + + for item in bom.get("exploded_items") if show_exploded_view else bom.get("items"): + in_stock_qty = frappe.get_cached_value( + "Bin", {"item_code": item.item_code, "warehouse": warehouse}, "actual_qty" + ) + + expected_data.append( + [ + item.item_code, + item.description, + item.stock_qty, + item.stock_uom, + item.stock_qty * qty_to_produce / bom.quantity, + in_stock_qty, + floor(in_stock_qty / (item.stock_qty * qty_to_produce / bom.quantity)) + if in_stock_qty + else None, + ] + ) + + return expected_data diff --git a/erpnext/patches.txt b/erpnext/patches.txt index adebe2b8ee9..3c0eb5107ce 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -325,6 +325,5 @@ erpnext.patches.v14_0.update_entry_type_for_journal_entry erpnext.patches.v14_0.change_autoname_for_tax_withheld_vouchers erpnext.patches.v14_0.update_asset_value_for_manual_depr_entries erpnext.patches.v14_0.set_pick_list_status -# below 2 migration patches should always run last +# below migration patches should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger -erpnext.patches.v14_0.migrate_remarks_from_gl_to_payment_ledger diff --git a/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py b/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py index 853a99a4895..72c8c074d2c 100644 --- a/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py +++ b/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py @@ -1,6 +1,6 @@ import frappe from frappe import qb -from frappe.query_builder import Case, CustomFunction +from frappe.query_builder import CustomFunction from frappe.query_builder.custom import ConstantColumn from frappe.query_builder.functions import Count, IfNull from frappe.utils import flt @@ -18,9 +18,21 @@ def create_accounting_dimension_fields(): make_dimension_in_accounting_doctypes(dimension, ["Payment Ledger Entry"]) -def generate_name_for_payment_ledger_entries(gl_entries, start): +def generate_name_and_calculate_amount(gl_entries, start, receivable_accounts): for index, entry in enumerate(gl_entries, 0): entry.name = start + index + if entry.account in receivable_accounts: + entry.account_type = "Receivable" + entry.amount = entry.debit - entry.credit + entry.amount_in_account_currency = ( + entry.debit_in_account_currency - entry.credit_in_account_currency + ) + else: + entry.account_type = "Payable" + entry.amount = entry.credit - entry.debit + entry.amount_in_account_currency = ( + entry.credit_in_account_currency - entry.debit_in_account_currency + ) def get_columns(): @@ -49,6 +61,9 @@ def get_columns(): "finance_book", ] + if frappe.db.has_column("Payment Ledger Entry", "remarks"): + columns.append("remarks") + dimensions_and_defaults = get_dimensions() if dimensions_and_defaults: for dimension in dimensions_and_defaults[0]: @@ -99,12 +114,17 @@ def execute(): ifelse = CustomFunction("IF", ["condition", "then", "else"]) # Get Records Count - accounts = ( + relavant_accounts = ( qb.from_(account) - .select(account.name) + .select(account.name, account.account_type) .where((account.account_type == "Receivable") | (account.account_type == "Payable")) .orderby(account.name) + .run(as_dict=True) ) + + receivable_accounts = [x.name for x in relavant_accounts if x.account_type == "Receivable"] + accounts = [x.name for x in relavant_accounts] + un_processed = ( qb.from_(gl) .select(Count(gl.name)) @@ -122,37 +142,21 @@ def execute(): while True: if last_name: - where_clause = gl.name.gt(last_name) & (gl.is_cancelled == 0) + where_clause = gl.name.gt(last_name) & gl.account.isin(accounts) & gl.is_cancelled == 0 else: - where_clause = gl.is_cancelled == 0 + where_clause = gl.account.isin(accounts) & gl.is_cancelled == 0 gl_entries = ( qb.from_(gl) - .inner_join(account) - .on((gl.account == account.name) & (account.account_type.isin(["Receivable", "Payable"]))) .select( gl.star, ConstantColumn(1).as_("docstatus"), - account.account_type.as_("account_type"), IfNull( ifelse(gl.against_voucher_type == "", None, gl.against_voucher_type), gl.voucher_type ).as_("against_voucher_type"), IfNull(ifelse(gl.against_voucher == "", None, gl.against_voucher), gl.voucher_no).as_( "against_voucher_no" ), - # convert debit/credit to amount - Case() - .when(account.account_type == "Receivable", gl.debit - gl.credit) - .else_(gl.credit - gl.debit) - .as_("amount"), - # convert debit/credit in account currency to amount in account currency - Case() - .when( - account.account_type == "Receivable", - gl.debit_in_account_currency - gl.credit_in_account_currency, - ) - .else_(gl.credit_in_account_currency - gl.debit_in_account_currency) - .as_("amount_in_account_currency"), ) .where(where_clause) .orderby(gl.name) @@ -163,8 +167,8 @@ def execute(): if gl_entries: last_name = gl_entries[-1].name - # primary key(name) for payment ledger records - generate_name_for_payment_ledger_entries(gl_entries, processed) + # add primary key(name) and calculate based on debit and credit + generate_name_and_calculate_amount(gl_entries, processed, receivable_accounts) try: insert_query = build_insert_query() diff --git a/erpnext/patches/v14_0/migrate_remarks_from_gl_to_payment_ledger.py b/erpnext/patches/v14_0/migrate_remarks_from_gl_to_payment_ledger.py deleted file mode 100644 index 9d216c4028c..00000000000 --- a/erpnext/patches/v14_0/migrate_remarks_from_gl_to_payment_ledger.py +++ /dev/null @@ -1,98 +0,0 @@ -import frappe -from frappe import qb -from frappe.query_builder import CustomFunction -from frappe.query_builder.functions import Count, IfNull -from frappe.utils import flt - - -def execute(): - """ - Migrate 'remarks' field from 'tabGL Entry' to 'tabPayment Ledger Entry' - """ - - if frappe.reload_doc("accounts", "doctype", "payment_ledger_entry"): - - gle = qb.DocType("GL Entry") - ple = qb.DocType("Payment Ledger Entry") - - # Get empty PLE records - un_processed = ( - qb.from_(ple).select(Count(ple.name)).where((ple.remarks.isnull()) & (ple.delinked == 0)).run() - )[0][0] - - if un_processed: - print(f"Remarks for {un_processed} Payment Ledger records will be updated from GL Entry") - - ifelse = CustomFunction("IF", ["condition", "then", "else"]) - - processed = 0 - last_percent_update = 0 - batch_size = 1000 - last_name = None - - while True: - if last_name: - where_clause = (ple.name.gt(last_name)) & (ple.remarks.isnull()) & (ple.delinked == 0) - else: - where_clause = (ple.remarks.isnull()) & (ple.delinked == 0) - - # results are deterministic - names = ( - qb.from_(ple).select(ple.name).where(where_clause).orderby(ple.name).limit(batch_size).run() - ) - - if names: - last_name = names[-1][0] - - pl_entries = ( - qb.from_(ple) - .left_join(gle) - .on( - (ple.account == gle.account) - & (ple.party_type == gle.party_type) - & (ple.party == gle.party) - & (ple.voucher_type == gle.voucher_type) - & (ple.voucher_no == gle.voucher_no) - & ( - ple.against_voucher_type - == IfNull( - ifelse(gle.against_voucher_type == "", None, gle.against_voucher_type), gle.voucher_type - ) - ) - & ( - ple.against_voucher_no - == IfNull(ifelse(gle.against_voucher == "", None, gle.against_voucher), gle.voucher_no) - ) - & (ple.company == gle.company) - & ( - ((ple.account_type == "Receivable") & (ple.amount == (gle.debit - gle.credit))) - | (ple.account_type == "Payable") & (ple.amount == (gle.credit - gle.debit)) - ) - & (gle.remarks.notnull()) - & (gle.is_cancelled == 0) - ) - .select(ple.name) - .distinct() - .select( - gle.remarks.as_("gle_remarks"), - ) - .where(ple.name.isin(names)) - .run(as_dict=True) - ) - - if pl_entries: - for entry in pl_entries: - query = qb.update(ple).set(ple.remarks, entry.gle_remarks).where((ple.name == entry.name)) - query.run() - - frappe.db.commit() - - processed += len(pl_entries) - percentage = flt((processed / un_processed) * 100, 2) - if percentage - last_percent_update > 1: - print(f"{percentage}% ({processed}) PLE records updated") - last_percent_update = percentage - - else: - break - print("Remarks succesfully migrated") diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js index a376bf46a5b..d1d07a79d67 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.js +++ b/erpnext/projects/doctype/timesheet/timesheet.js @@ -5,6 +5,8 @@ frappe.ui.form.on("Timesheet", { setup: function(frm) { frappe.require("/assets/erpnext/js/projects/timer.js"); + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice']; + frm.fields_dict.employee.get_query = function() { return { filters:{ diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py index 525ae8e7ea7..58516f6f625 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py @@ -2,7 +2,7 @@ import datetime import frappe from frappe.tests.utils import FrappeTestCase -from frappe.utils import add_days, nowdate +from frappe.utils import add_days, add_months, nowdate from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order @@ -15,9 +15,16 @@ test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Temp class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): + def setUp(self): + self.cleanup_old_entries() + def tearDown(self): frappe.db.rollback() + def cleanup_old_entries(self): + frappe.db.delete("Sales Invoice", filters={"company": "_Test Company"}) + frappe.db.delete("Sales Order", filters={"company": "_Test Company"}) + def create_payment_terms_template(self): # create template for 50-50 payments template = None @@ -348,7 +355,7 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): item = create_item(item_code="_Test Excavator 1", is_stock_item=0) transaction_date = nowdate() so = make_sales_order( - transaction_date=add_days(transaction_date, -30), + transaction_date=add_months(transaction_date, -1), delivery_date=add_days(transaction_date, -15), item=item.item_code, qty=10, @@ -369,13 +376,15 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): sinv.items[0].qty = 6 sinv.insert() sinv.submit() + + first_due_date = add_days(add_months(transaction_date, -1), 15) columns, data, message, chart = execute( frappe._dict( { "company": "_Test Company", "item": item.item_code, - "from_due_date": add_days(transaction_date, -30), - "to_due_date": add_days(transaction_date, -15), + "from_due_date": add_months(transaction_date, -1), + "to_due_date": first_due_date, } ) ) @@ -384,11 +393,11 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): { "name": so.name, "customer": so.customer, - "submitted": datetime.date.fromisoformat(add_days(transaction_date, -30)), + "submitted": datetime.date.fromisoformat(add_months(transaction_date, -1)), "status": "Completed", "payment_term": None, "description": "_Test 50-50", - "due_date": datetime.date.fromisoformat(add_days(transaction_date, -15)), + "due_date": datetime.date.fromisoformat(first_due_date), "invoice_portion": 50.0, "currency": "INR", "base_payment_amount": 500000.0, diff --git a/erpnext/stock/doctype/item_alternative/item_alternative.py b/erpnext/stock/doctype/item_alternative/item_alternative.py index fb1a28d846b..0c24d3c780f 100644 --- a/erpnext/stock/doctype/item_alternative/item_alternative.py +++ b/erpnext/stock/doctype/item_alternative/item_alternative.py @@ -54,7 +54,7 @@ class ItemAlternative(Document): if not item_data.allow_alternative_item: frappe.throw(alternate_item_check_msg.format(self.item_code)) if self.two_way and not alternative_item_data.allow_alternative_item: - frappe.throw(alternate_item_check_msg.format(self.item_code)) + frappe.throw(alternate_item_check_msg.format(self.alternative_item_code)) def validate_duplicate(self): if frappe.db.get_value( diff --git a/erpnext/stock/tests/test_valuation.py b/erpnext/stock/tests/test_valuation.py index e60c1caac34..05f153b4a0c 100644 --- a/erpnext/stock/tests/test_valuation.py +++ b/erpnext/stock/tests/test_valuation.py @@ -132,7 +132,7 @@ class TestFIFOValuation(unittest.TestCase): total_qty = 0 for qty, rate in stock_queue: - if qty == 0: + if round_off_if_near_zero(qty) == 0: continue if qty > 0: self.queue.add_stock(qty, rate) @@ -154,7 +154,7 @@ class TestFIFOValuation(unittest.TestCase): for qty, rate in stock_queue: # don't allow negative stock - if qty == 0 or total_qty + qty < 0 or abs(qty) < 0.1: + if round_off_if_near_zero(qty) == 0 or total_qty + qty < 0 or abs(qty) < 0.1: continue if qty > 0: self.queue.add_stock(qty, rate) @@ -179,7 +179,7 @@ class TestFIFOValuation(unittest.TestCase): for qty, rate in stock_queue: # don't allow negative stock - if qty == 0 or total_qty + qty < 0 or abs(qty) < 0.1: + if round_off_if_near_zero(qty) == 0 or total_qty + qty < 0 or abs(qty) < 0.1: continue if qty > 0: self.queue.add_stock(qty, rate) @@ -282,7 +282,7 @@ class TestLIFOValuation(unittest.TestCase): total_qty = 0 for qty, rate in stock_stack: - if qty == 0: + if round_off_if_near_zero(qty) == 0: continue if qty > 0: self.stack.add_stock(qty, rate) @@ -304,7 +304,7 @@ class TestLIFOValuation(unittest.TestCase): for qty, rate in stock_stack: # don't allow negative stock - if qty == 0 or total_qty + qty < 0 or abs(qty) < 0.1: + if round_off_if_near_zero(qty) == 0 or total_qty + qty < 0 or abs(qty) < 0.1: continue if qty > 0: self.stack.add_stock(qty, rate) diff --git a/erpnext/translations/fr.csv b/erpnext/translations/fr.csv index 8367afd3312..bace129213d 100644 --- a/erpnext/translations/fr.csv +++ b/erpnext/translations/fr.csv @@ -2801,7 +2801,7 @@ Stock Ledger Entries and GL Entries are reposted for the selected Purchase Recei Stock Levels,Niveaux du Stocks, Stock Liabilities,Passif du Stock, Stock Options,Options du Stock, -Stock Qty,Qté en Stock, +Stock Qty,Qté en unité de stock, Stock Received But Not Billed,Stock Reçus Mais Non Facturés, Stock Reports,Rapports de stock, Stock Summary,Résumé du Stock,