Merge pull request #34442 from frappe/version-14-hotfix

chore: release v14
This commit is contained in:
Deepesh Garg
2023-03-14 23:05:05 +05:30
committed by GitHub
24 changed files with 320 additions and 270 deletions

View File

@@ -211,8 +211,7 @@ class ExchangeRateRevaluation(Document):
# Handle Accounts with '0' balance in Account/Base Currency # Handle Accounts with '0' balance in Account/Base Currency
for d in [x for x in account_details if x.zero_balance]: 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 current_exchange_rate = new_exchange_rate = 0
new_balance_in_account_currency = 0 # this will be '0' new_balance_in_account_currency = 0 # this will be '0'
@@ -399,6 +398,9 @@ class ExchangeRateRevaluation(Document):
journal_entry_accounts = [] journal_entry_accounts = []
for d in accounts: for d in accounts:
if not flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")):
continue
dr_or_cr = ( dr_or_cr = (
"debit_in_account_currency" "debit_in_account_currency"
if d.get("balance_in_account_currency") > 0 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, "account": unrealized_exchange_gain_loss_account,
"balance": get_balance_on(unrealized_exchange_gain_loss_account), "balance": get_balance_on(unrealized_exchange_gain_loss_account),
@@ -460,10 +468,9 @@ class ExchangeRateRevaluation(Document):
"exchange_rate": 1, "exchange_rate": 1,
"reference_type": "Exchange Rate Revaluation", "reference_type": "Exchange Rate Revaluation",
"reference_name": self.name, "reference_name": self.name,
} },
) )
journal_entry.set("accounts", journal_entry_accounts)
journal_entry.set_amounts_in_company_currency() journal_entry.set_amounts_in_company_currency()
journal_entry.set_total_debit_credit() journal_entry.set_total_debit_credit()
journal_entry.save() journal_entry.save()

View File

@@ -15,7 +15,7 @@
</div> </div>
<h2 class="text-center">{{ _("STATEMENTS OF ACCOUNTS") }}</h2> <h2 class="text-center">{{ _("STATEMENTS OF ACCOUNTS") }}</h2>
<div> <div>
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{filters.party[0] }}</b></h5> <h5 style="float: left;">{{ _("Customer: ") }} <b>{{filters.party_name[0] }}</b></h5>
<h5 style="float: right;"> <h5 style="float: right;">
{{ _("Date: ") }} {{ _("Date: ") }}
<b>{{ frappe.format(filters.from_date, 'Date')}} <b>{{ frappe.format(filters.from_date, 'Date')}}

View File

@@ -23,7 +23,7 @@ from erpnext.accounts.report.general_ledger.general_ledger import execute as get
class ProcessStatementOfAccounts(Document): class ProcessStatementOfAccounts(Document):
def validate(self): def validate(self):
if not self.subject: 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: if not self.body:
self.body = "Hello {{ customer.name }},<br>PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}." self.body = "Hello {{ customer.name }},<br>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, "account": [doc.account] if doc.account else None,
"party_type": "Customer", "party_type": "Customer",
"party": [entry.customer], "party": [entry.customer],
"party_name": [entry.customer_name] if entry.customer_name else None,
"presentation_currency": presentation_currency, "presentation_currency": presentation_currency,
"group_by": doc.group_by, "group_by": doc.group_by,
"currency": doc.currency, "currency": doc.currency,
@@ -153,7 +154,7 @@ def get_customers_based_on_territory_or_customer_group(customer_collection, coll
] ]
return frappe.get_list( return frappe.get_list(
"Customer", "Customer",
fields=["name", "email_id"], fields=["name", "customer_name", "email_id"],
filters=[[fields_dict[customer_collection], "IN", selected]], 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"): if sales_person_records.get("Customer"):
return frappe.get_list( return frappe.get_list(
"Customer", "Customer",
fields=["name", "email_id"], fields=["name", "customer_name", "email_id"],
filters=[["name", "in", list(sales_person_records["Customer"])]], filters=[["name", "in", list(sales_person_records["Customer"])]],
) )
else: else:
@@ -225,7 +226,7 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
if customer_collection == "Sales Partner": if customer_collection == "Sales Partner":
customers = frappe.get_list( customers = frappe.get_list(
"Customer", "Customer",
fields=["name", "email_id"], fields=["name", "customer_name", "email_id"],
filters=[["default_sales_partner", "=", collection_name]], filters=[["default_sales_partner", "=", collection_name]],
) )
else: else:
@@ -244,7 +245,12 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
continue continue
customer_list.append( 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 return customer_list

View File

@@ -1,12 +1,12 @@
{ {
"actions": [], "actions": [],
"allow_workflow": 1,
"creation": "2020-08-03 16:35:21.852178", "creation": "2020-08-03 16:35:21.852178",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"customer", "customer",
"customer_name",
"billing_email", "billing_email",
"primary_email" "primary_email"
], ],
@@ -30,11 +30,18 @@
"fieldtype": "Read Only", "fieldtype": "Read Only",
"in_list_view": 1, "in_list_view": 1,
"label": "Billing Email" "label": "Billing Email"
},
{
"fetch_from": "customer.customer_name",
"fieldname": "customer_name",
"fieldtype": "Data",
"label": "Customer Name",
"read_only": 1
} }
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-08-03 22:55:38.875601", "modified": "2023-03-13 00:12:34.508086",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Process Statement Of Accounts Customer", "name": "Process Statement Of Accounts Customer",
@@ -43,5 +50,6 @@
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -32,9 +32,6 @@
"cost_center", "cost_center",
"dimension_col_break", "dimension_col_break",
"project", "project",
"column_break_27",
"campaign",
"source",
"currency_and_price_list", "currency_and_price_list",
"currency", "currency",
"conversion_rate", "conversion_rate",
@@ -203,7 +200,9 @@
"more_information", "more_information",
"status", "status",
"inter_company_invoice_reference", "inter_company_invoice_reference",
"campaign",
"represents_company", "represents_company",
"source",
"customer_group", "customer_group",
"col_break23", "col_break23",
"is_internal_customer", "is_internal_customer",
@@ -2083,10 +2082,6 @@
"fieldname": "company_addr_col_break", "fieldname": "company_addr_col_break",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"fieldname": "column_break_27",
"fieldtype": "Column Break"
},
{ {
"fieldname": "column_break_52", "fieldname": "column_break_52",
"fieldtype": "Column Break" "fieldtype": "Column Break"
@@ -2143,11 +2138,10 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2022-11-07 16:02:07.972258", "modified": "2023-03-13 11:43:15.883055",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",
"name_case": "Title Case",
"naming_rule": "By \"Naming Series\" field", "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [

View File

@@ -32,6 +32,16 @@ from erpnext import get_company_currency
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen 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): class DuplicatePartyAccountError(frappe.ValidationError):
pass pass
@@ -124,12 +134,6 @@ def _get_party_details(
set_other_values(party_details, party, party_type) set_other_values(party_details, party, party_type)
set_price_list(party_details, party, party_type, price_list, pos_profile) 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( tax_template = set_taxes(
party.name, party.name,
party_type, party_type,
@@ -211,20 +215,10 @@ def set_address_details(
else: else:
party_details.update(get_company_address(company)) party_details.update(get_company_address(company))
if doctype and doctype in [ if doctype in SALES_TRANSACTION_TYPES and party_details.company_address:
"Delivery Note", party_details.update(get_fetch_values(doctype, "company_address", party_details.company_address))
"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)
elif doctype and doctype in ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]: if doctype in PURCHASE_TRANSACTION_TYPES:
if shipping_address: if shipping_address:
party_details.update( party_details.update(
shipping_address=shipping_address, shipping_address=shipping_address,
@@ -250,9 +244,21 @@ def set_address_details(
**get_fetch_values(doctype, "shipping_address", party_details.billing_address) **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) 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 @erpnext.allow_regional

View File

@@ -138,7 +138,8 @@ def prepare_companywise_opening_balance(asset_data, liability_data, equity_data,
for data in [asset_data, liability_data, equity_data]: for data in [asset_data, liability_data, equity_data]:
if data: if data:
account_name = get_root_account_name(data[0].root_type, company) 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 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): def get_root_account_name(root_type, company):
return frappe.get_all( root_account = frappe.get_all(
"Account", "Account",
fields=["account_name"], fields=["account_name"],
filters={ filters={
@@ -165,7 +166,10 @@ def get_root_account_name(root_type, company):
"parent_account": ("is", "not set"), "parent_account": ("is", "not set"),
}, },
as_list=1, as_list=1,
)[0][0] )
if root_account:
return root_account[0][0]
def get_profit_loss_data(fiscal_year, companies, columns, filters): def get_profit_loss_data(fiscal_year, companies, columns, filters):

View File

@@ -78,7 +78,6 @@ def validate_filters(filters):
def get_data(filters): def get_data(filters):
accounts = frappe.db.sql( accounts = frappe.db.sql(
"""select name, account_number, parent_account, account_name, root_type, report_type, lft, rgt """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), ignore_closing_entries=not flt(filters.with_period_closing_entry),
) )
total_row = calculate_values( calculate_values(accounts, gl_entries_by_account, opening_balances)
accounts, gl_entries_by_account, opening_balances, filters, company_currency
)
accumulate_values_into_parents(accounts, accounts_by_name) 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 = filter_out_zero_value_rows(
data, parent_children_map, show_zero_values=filters.get("show_zero_values") 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 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 = { init = {
"opening_debit": 0.0, "opening_debit": 0.0,
"opening_credit": 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, "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: for d in accounts:
d.update(init.copy()) d.update(init.copy())
@@ -261,8 +242,28 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, filters,
prepare_opening_closing(d) 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 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] 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 = [] data = []
for d in accounts: 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 row["has_value"] = has_value
data.append(row) data.append(row)
total_row = calculate_total_row(accounts, company_currency)
data.extend([{}, total_row]) data.extend([{}, total_row])
return data return data

View File

@@ -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)] 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 # 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, doctype,
fields=fields, fields=fields,
filters=[ filters=[

View File

@@ -76,12 +76,9 @@ def get_transaction_list(
ignore_permissions = False ignore_permissions = False
if not filters: if not filters:
filters = [] filters = {}
if doctype in ["Supplier Quotation", "Purchase Invoice"]: filters["docstatus"] = ["<", "2"] if doctype in ["Supplier Quotation", "Purchase Invoice"] else 1
filters.append((doctype, "docstatus", "<", 2))
else:
filters.append((doctype, "docstatus", "=", 1))
if (user != "Guest" and is_website_user()) or doctype == "Request for Quotation": if (user != "Guest" and is_website_user()) or doctype == "Request for Quotation":
parties_doctype = ( parties_doctype = (
@@ -92,12 +89,12 @@ def get_transaction_list(
if customers: if customers:
if doctype == "Quotation": if doctype == "Quotation":
filters.append(("quotation_to", "=", "Customer")) filters["quotation_to"] = "Customer"
filters.append(("party_name", "in", customers)) filters["party_name"] = ["in", customers]
else: else:
filters.append(("customer", "in", customers)) filters["customer"] = ["in", customers]
elif suppliers: elif suppliers:
filters.append(("supplier", "in", suppliers)) filters["supplier"] = ["in", suppliers]
elif not custom: elif not custom:
return [] return []
@@ -110,7 +107,7 @@ def get_transaction_list(
if not customers and not suppliers and custom: if not customers and not suppliers and custom:
ignore_permissions = False ignore_permissions = False
filters = [] filters = {}
transactions = get_list_for_transactions( transactions = get_list_for_transactions(
doctype, doctype,

View File

@@ -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) { validate: function(frm) {
@@ -130,6 +126,10 @@ frappe.ui.form.on("Opportunity", {
} else { } else {
frappe.contacts.clear_address_and_contact(frm); 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) { 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'} frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Customer'}
} else if(frm.doc.opportunity_from == "Lead" && frm.doc.party_name) { } else if(frm.doc.opportunity_from == "Lead" && frm.doc.party_name) {
frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Lead'} 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'}
} }
}, },

View File

@@ -31,7 +31,7 @@ class BOMTree:
# specifying the attributes to save resources # specifying the attributes to save resources
# ref: https://docs.python.org/3/reference/datamodel.html#slots # 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__( def __init__(
self, name: str, is_bom: bool = True, exploded_qty: float = 1.0, qty: float = 1 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): def __create_tree(self):
bom = frappe.get_cached_doc("BOM", self.name) bom = frappe.get_cached_doc("BOM", self.name)
self.item_code = bom.item self.item_code = bom.item
self.bom_qty = bom.quantity
for item in bom.get("items", []): 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 exploded_qty = self.exploded_qty * qty
if item.bom_no: if item.bom_no:
child = BOMTree(item.bom_no, exploded_qty=exploded_qty, qty=qty) child = BOMTree(item.bom_no, exploded_qty=exploded_qty, qty=qty)

View File

@@ -506,7 +506,7 @@ frappe.ui.form.on("Work Order Item", {
callback: function(r) { callback: function(r) {
if (r.message) { if (r.message) {
frappe.model.set_value(cdt, cdn, { frappe.model.set_value(cdt, cdn, {
"required_qty": 1, "required_qty": row.required_qty || 1,
"item_name": r.message.item_name, "item_name": r.message.item_name,
"description": r.message.description, "description": r.message.description,
"source_warehouse": r.message.default_warehouse, "source_warehouse": r.message.default_warehouse,

View File

@@ -682,7 +682,7 @@ class WorkOrder(Document):
for node in bom_traversal: for node in bom_traversal:
if node.is_bom: 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") bom_qty = frappe.get_cached_value("BOM", self.bom_no, "quantity")
operations.extend(_get_operations(self.bom_no, qty=1.0 / bom_qty)) operations.extend(_get_operations(self.bom_no, qty=1.0 / bom_qty))

View File

@@ -4,7 +4,8 @@
import frappe import frappe
from frappe import _ 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 from pypika.terms import ExistsCriterion
@@ -34,57 +35,55 @@ def get_columns():
def get_bom_stock(filters): def get_bom_stock(filters):
qty_to_produce = filters.get("qty_to_produce") or 1 qty_to_produce = filters.get("qty_to_produce")
if int(qty_to_produce) < 0: if cint(qty_to_produce) <= 0:
frappe.throw(_("Quantity to Produce can not be less than Zero")) frappe.throw(_("Quantity to Produce should be greater than zero."))
if filters.get("show_exploded_view"): if filters.get("show_exploded_view"):
bom_item_table = "BOM Explosion Item" bom_item_table = "BOM Explosion Item"
else: else:
bom_item_table = "BOM Item" bom_item_table = "BOM Item"
bin = frappe.qb.DocType("Bin") warehouse_details = frappe.db.get_value(
bom = frappe.qb.DocType("BOM") "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
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)
) )
if filters.get("warehouse"): BOM = frappe.qb.DocType("BOM")
warehouse_details = frappe.db.get_value( BOM_ITEM = frappe.qb.DocType(bom_item_table)
"Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 BIN = frappe.qb.DocType("Bin")
) WH = frappe.qb.DocType("Warehouse")
CONDITIONS = ()
if warehouse_details: if warehouse_details:
wh = frappe.qb.DocType("Warehouse") CONDITIONS = ExistsCriterion(
query = query.where( frappe.qb.from_(WH)
ExistsCriterion( .select(WH.name)
frappe.qb.from_(wh) .where(
.select(wh.name) (WH.lft >= warehouse_details.lft)
.where( & (WH.rgt <= warehouse_details.rgt)
(wh.lft >= warehouse_details.lft) & (BIN.warehouse == WH.name)
& (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()

View File

@@ -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

View File

@@ -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.change_autoname_for_tax_withheld_vouchers
erpnext.patches.v14_0.update_asset_value_for_manual_depr_entries erpnext.patches.v14_0.update_asset_value_for_manual_depr_entries
erpnext.patches.v14_0.set_pick_list_status 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_gl_to_payment_ledger
erpnext.patches.v14_0.migrate_remarks_from_gl_to_payment_ledger

View File

@@ -1,6 +1,6 @@
import frappe import frappe
from frappe import qb 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.custom import ConstantColumn
from frappe.query_builder.functions import Count, IfNull from frappe.query_builder.functions import Count, IfNull
from frappe.utils import flt from frappe.utils import flt
@@ -18,9 +18,21 @@ def create_accounting_dimension_fields():
make_dimension_in_accounting_doctypes(dimension, ["Payment Ledger Entry"]) 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): for index, entry in enumerate(gl_entries, 0):
entry.name = start + index 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(): def get_columns():
@@ -49,6 +61,9 @@ def get_columns():
"finance_book", "finance_book",
] ]
if frappe.db.has_column("Payment Ledger Entry", "remarks"):
columns.append("remarks")
dimensions_and_defaults = get_dimensions() dimensions_and_defaults = get_dimensions()
if dimensions_and_defaults: if dimensions_and_defaults:
for dimension in dimensions_and_defaults[0]: for dimension in dimensions_and_defaults[0]:
@@ -99,12 +114,17 @@ def execute():
ifelse = CustomFunction("IF", ["condition", "then", "else"]) ifelse = CustomFunction("IF", ["condition", "then", "else"])
# Get Records Count # Get Records Count
accounts = ( relavant_accounts = (
qb.from_(account) qb.from_(account)
.select(account.name) .select(account.name, account.account_type)
.where((account.account_type == "Receivable") | (account.account_type == "Payable")) .where((account.account_type == "Receivable") | (account.account_type == "Payable"))
.orderby(account.name) .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 = ( un_processed = (
qb.from_(gl) qb.from_(gl)
.select(Count(gl.name)) .select(Count(gl.name))
@@ -122,37 +142,21 @@ def execute():
while True: while True:
if last_name: 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: else:
where_clause = gl.is_cancelled == 0 where_clause = gl.account.isin(accounts) & gl.is_cancelled == 0
gl_entries = ( gl_entries = (
qb.from_(gl) qb.from_(gl)
.inner_join(account)
.on((gl.account == account.name) & (account.account_type.isin(["Receivable", "Payable"])))
.select( .select(
gl.star, gl.star,
ConstantColumn(1).as_("docstatus"), ConstantColumn(1).as_("docstatus"),
account.account_type.as_("account_type"),
IfNull( IfNull(
ifelse(gl.against_voucher_type == "", None, gl.against_voucher_type), gl.voucher_type ifelse(gl.against_voucher_type == "", None, gl.against_voucher_type), gl.voucher_type
).as_("against_voucher_type"), ).as_("against_voucher_type"),
IfNull(ifelse(gl.against_voucher == "", None, gl.against_voucher), gl.voucher_no).as_( IfNull(ifelse(gl.against_voucher == "", None, gl.against_voucher), gl.voucher_no).as_(
"against_voucher_no" "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) .where(where_clause)
.orderby(gl.name) .orderby(gl.name)
@@ -163,8 +167,8 @@ def execute():
if gl_entries: if gl_entries:
last_name = gl_entries[-1].name last_name = gl_entries[-1].name
# primary key(name) for payment ledger records # add primary key(name) and calculate based on debit and credit
generate_name_for_payment_ledger_entries(gl_entries, processed) generate_name_and_calculate_amount(gl_entries, processed, receivable_accounts)
try: try:
insert_query = build_insert_query() insert_query = build_insert_query()

View File

@@ -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")

View File

@@ -5,6 +5,8 @@ frappe.ui.form.on("Timesheet", {
setup: function(frm) { setup: function(frm) {
frappe.require("/assets/erpnext/js/projects/timer.js"); frappe.require("/assets/erpnext/js/projects/timer.js");
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice'];
frm.fields_dict.employee.get_query = function() { frm.fields_dict.employee.get_query = function() {
return { return {
filters:{ filters:{

View File

@@ -2,7 +2,7 @@ import datetime
import frappe import frappe
from frappe.tests.utils import FrappeTestCase 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.sales_order import make_sales_invoice
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order 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): class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
def setUp(self):
self.cleanup_old_entries()
def tearDown(self): def tearDown(self):
frappe.db.rollback() 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): def create_payment_terms_template(self):
# create template for 50-50 payments # create template for 50-50 payments
template = None template = None
@@ -348,7 +355,7 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
item = create_item(item_code="_Test Excavator 1", is_stock_item=0) item = create_item(item_code="_Test Excavator 1", is_stock_item=0)
transaction_date = nowdate() transaction_date = nowdate()
so = make_sales_order( 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), delivery_date=add_days(transaction_date, -15),
item=item.item_code, item=item.item_code,
qty=10, qty=10,
@@ -369,13 +376,15 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
sinv.items[0].qty = 6 sinv.items[0].qty = 6
sinv.insert() sinv.insert()
sinv.submit() sinv.submit()
first_due_date = add_days(add_months(transaction_date, -1), 15)
columns, data, message, chart = execute( columns, data, message, chart = execute(
frappe._dict( frappe._dict(
{ {
"company": "_Test Company", "company": "_Test Company",
"item": item.item_code, "item": item.item_code,
"from_due_date": add_days(transaction_date, -30), "from_due_date": add_months(transaction_date, -1),
"to_due_date": add_days(transaction_date, -15), "to_due_date": first_due_date,
} }
) )
) )
@@ -384,11 +393,11 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
{ {
"name": so.name, "name": so.name,
"customer": so.customer, "customer": so.customer,
"submitted": datetime.date.fromisoformat(add_days(transaction_date, -30)), "submitted": datetime.date.fromisoformat(add_months(transaction_date, -1)),
"status": "Completed", "status": "Completed",
"payment_term": None, "payment_term": None,
"description": "_Test 50-50", "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, "invoice_portion": 50.0,
"currency": "INR", "currency": "INR",
"base_payment_amount": 500000.0, "base_payment_amount": 500000.0,

View File

@@ -54,7 +54,7 @@ class ItemAlternative(Document):
if not item_data.allow_alternative_item: if not item_data.allow_alternative_item:
frappe.throw(alternate_item_check_msg.format(self.item_code)) frappe.throw(alternate_item_check_msg.format(self.item_code))
if self.two_way and not alternative_item_data.allow_alternative_item: 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): def validate_duplicate(self):
if frappe.db.get_value( if frappe.db.get_value(

View File

@@ -132,7 +132,7 @@ class TestFIFOValuation(unittest.TestCase):
total_qty = 0 total_qty = 0
for qty, rate in stock_queue: for qty, rate in stock_queue:
if qty == 0: if round_off_if_near_zero(qty) == 0:
continue continue
if qty > 0: if qty > 0:
self.queue.add_stock(qty, rate) self.queue.add_stock(qty, rate)
@@ -154,7 +154,7 @@ class TestFIFOValuation(unittest.TestCase):
for qty, rate in stock_queue: for qty, rate in stock_queue:
# don't allow negative stock # 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 continue
if qty > 0: if qty > 0:
self.queue.add_stock(qty, rate) self.queue.add_stock(qty, rate)
@@ -179,7 +179,7 @@ class TestFIFOValuation(unittest.TestCase):
for qty, rate in stock_queue: for qty, rate in stock_queue:
# don't allow negative stock # 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 continue
if qty > 0: if qty > 0:
self.queue.add_stock(qty, rate) self.queue.add_stock(qty, rate)
@@ -282,7 +282,7 @@ class TestLIFOValuation(unittest.TestCase):
total_qty = 0 total_qty = 0
for qty, rate in stock_stack: for qty, rate in stock_stack:
if qty == 0: if round_off_if_near_zero(qty) == 0:
continue continue
if qty > 0: if qty > 0:
self.stack.add_stock(qty, rate) self.stack.add_stock(qty, rate)
@@ -304,7 +304,7 @@ class TestLIFOValuation(unittest.TestCase):
for qty, rate in stock_stack: for qty, rate in stock_stack:
# don't allow negative stock # 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 continue
if qty > 0: if qty > 0:
self.stack.add_stock(qty, rate) self.stack.add_stock(qty, rate)

View File

@@ -2801,7 +2801,7 @@ Stock Ledger Entries and GL Entries are reposted for the selected Purchase Recei
Stock Levels,Niveaux du Stocks, Stock Levels,Niveaux du Stocks,
Stock Liabilities,Passif du Stock, Stock Liabilities,Passif du Stock,
Stock Options,Options 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 Received But Not Billed,Stock Reçus Mais Non Facturés,
Stock Reports,Rapports de stock, Stock Reports,Rapports de stock,
Stock Summary,Résumé du Stock, Stock Summary,Résumé du Stock,
Can't render this file because it is too large.