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
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()

View File

@@ -15,7 +15,7 @@
</div>
<h2 class="text-center">{{ _("STATEMENTS OF ACCOUNTS") }}</h2>
<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;">
{{ _("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):
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 }},<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,
"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

View File

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

View File

@@ -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": [

View File

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

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]:
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):

View File

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

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)]
# 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=[

View File

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

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) {
@@ -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'}
}
},

View File

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

View File

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

View File

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

View File

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

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

View File

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

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) {
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:{

View File

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

View File

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

View File

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

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 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,
Can't render this file because it is too large.