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,