Merge pull request #33403 from frappe/version-13-hotfix

chore: release v13
This commit is contained in:
Deepesh Garg
2022-12-20 19:30:24 +05:30
committed by GitHub
12 changed files with 251 additions and 100 deletions

View File

@@ -3,8 +3,10 @@
import frappe
from frappe import _, msgprint
from frappe import _, msgprint, qb
from frappe.model.document import Document
from frappe.query_builder import Criterion
from frappe.query_builder.functions import Sum
from frappe.utils import flt, getdate, nowdate, today
import erpnext
@@ -120,58 +122,77 @@ class PaymentReconciliation(Document):
return list(journal_entries)
def get_dr_or_cr_notes(self):
condition = self.get_conditions(get_return_invoices=True)
gl = qb.DocType("GL Entry")
voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
doc = qb.DocType(voucher_type)
# build conditions
sub_query_conditions = []
conditions = []
sub_query_conditions.append(doc.company == self.company)
if self.get("from_payment_date"):
sub_query_conditions.append(doc.posting_date.gte(self.from_payment_date))
if self.get("to_payment_date"):
sub_query_conditions.append(doc.posting_date.lte(self.to_payment_date))
if self.get("cost_center"):
condition += " and doc.cost_center = '{0}' ".format(self.cost_center)
sub_query_conditions.append(doc.cost_center == self.cost_center)
dr_or_cr = (
"credit_in_account_currency"
gl["credit_in_account_currency"]
if erpnext.get_party_account_type(self.party_type) == "Receivable"
else "debit_in_account_currency"
else gl["debit_in_account_currency"]
)
reconciled_dr_or_cr = (
"debit_in_account_currency"
if dr_or_cr == "credit_in_account_currency"
else "credit_in_account_currency"
gl["debit_in_account_currency"]
if dr_or_cr == gl["credit_in_account_currency"]
else gl["credit_in_account_currency"]
)
voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
if self.minimum_payment_amount:
conditions.append(dr_or_cr.gte(self.minimum_payment_amount))
if self.maximum_payment_amount:
conditions.append(dr_or_cr.lte(self.maximum_payment_amount))
return frappe.db.sql(
""" SELECT doc.name as reference_name, %(voucher_type)s as reference_type,
(sum(gl.{dr_or_cr}) - sum(gl.{reconciled_dr_or_cr})) as amount, doc.posting_date,
account_currency as currency
FROM `tab{doc}` doc, `tabGL Entry` gl
WHERE
(doc.name = gl.against_voucher or doc.name = gl.voucher_no)
and doc.{party_type_field} = %(party)s
and doc.is_return = 1 and ifnull(doc.return_against, "") = ""
and gl.against_voucher_type = %(voucher_type)s
and doc.docstatus = 1 and gl.party = %(party)s
and gl.party_type = %(party_type)s and gl.account = %(account)s
and gl.is_cancelled = 0 {condition}
GROUP BY doc.name
Having
amount > 0
ORDER BY doc.posting_date
""".format(
doc=voucher_type,
dr_or_cr=dr_or_cr,
reconciled_dr_or_cr=reconciled_dr_or_cr,
party_type_field=frappe.scrub(self.party_type),
condition=condition or "",
),
{
"party": self.party,
"party_type": self.party_type,
"voucher_type": voucher_type,
"account": self.receivable_payable_account,
},
as_dict=1,
sub_query = (
qb.from_(doc)
.select(doc.name)
.where(Criterion.all(sub_query_conditions))
.where(
(doc.docstatus == 1)
& (doc.is_return == 1)
& ((doc.return_against == "") | (doc.return_against.isnull()))
)
)
query = (
qb.from_(gl)
.select(
gl.voucher_type.as_("reference_type"),
gl.voucher_no.as_("reference_name"),
(Sum(dr_or_cr) - Sum(reconciled_dr_or_cr)).as_("amount"),
gl.posting_date,
gl.account_currency.as_("currency"),
)
.where(
(gl.voucher_type == voucher_type)
& (gl.voucher_no.isin(sub_query))
& (gl.is_cancelled == 0)
& (gl.account == self.receivable_payable_account)
& (gl.party_type == self.party_type)
& (gl.party == self.party)
)
.where(Criterion.all(conditions))
.groupby(gl.voucher_no)
.having(qb.Field("amount") > 0)
)
dr_cr_notes = query.run(as_dict=True)
return dr_cr_notes
def add_payment_entries(self, non_reconciled_payments):
self.set("payments", [])
@@ -369,7 +390,7 @@ class PaymentReconciliation(Document):
if not invoices_to_reconcile:
frappe.throw(_("No records found in Allocation table"))
def get_conditions(self, get_invoices=False, get_payments=False, get_return_invoices=False):
def get_conditions(self, get_invoices=False, get_payments=False):
condition = " and company = '{0}' ".format(self.company)
if get_invoices:
@@ -397,35 +418,7 @@ class PaymentReconciliation(Document):
condition += " and {dr_or_cr} <= {amount}".format(
dr_or_cr=dr_or_cr, amount=flt(self.maximum_invoice_amount)
)
elif get_return_invoices:
condition = " and doc.company = '{0}' ".format(self.company)
condition += (
" and doc.posting_date >= {0}".format(frappe.db.escape(self.from_payment_date))
if self.from_payment_date
else ""
)
condition += (
" and doc.posting_date <= {0}".format(frappe.db.escape(self.to_payment_date))
if self.to_payment_date
else ""
)
dr_or_cr = (
"debit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable"
else "credit_in_account_currency"
)
if self.minimum_invoice_amount:
condition += " and gl.{dr_or_cr} >= {amount}".format(
dr_or_cr=dr_or_cr, amount=flt(self.minimum_payment_amount)
)
if self.maximum_invoice_amount:
condition += " and gl.{dr_or_cr} <= {amount}".format(
dr_or_cr=dr_or_cr, amount=flt(self.maximum_payment_amount)
)
else:
elif get_payments:
condition += (
" and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date))
if self.from_payment_date

View File

@@ -9,6 +9,7 @@ from six import iteritems
from erpnext.accounts.report.financial_statements import (
get_columns,
get_cost_centers_with_children,
get_data,
get_filtered_list_for_consolidated_report,
get_period_list,
@@ -161,10 +162,11 @@ def get_account_type_based_data(company, account_type, period_list, accumulated_
total = 0
for period in period_list:
start_date = get_start_date(period, accumulated_values, company)
filters.start_date = start_date
filters.end_date = period["to_date"]
filters.account_type = account_type
amount = get_account_type_based_gl_data(
company, start_date, period["to_date"], account_type, filters
)
amount = get_account_type_based_gl_data(company, filters)
if amount and account_type == "Depreciation":
amount *= -1
@@ -176,7 +178,7 @@ def get_account_type_based_data(company, account_type, period_list, accumulated_
return data
def get_account_type_based_gl_data(company, start_date, end_date, account_type, filters=None):
def get_account_type_based_gl_data(company, filters=None):
cond = ""
filters = frappe._dict(filters or {})
@@ -192,17 +194,21 @@ def get_account_type_based_gl_data(company, start_date, end_date, account_type,
frappe.db.escape(cstr(filters.finance_book))
)
if filters.get("cost_center"):
filters.cost_center = get_cost_centers_with_children(filters.cost_center)
cond += " and cost_center in %(cost_center)s"
gl_sum = frappe.db.sql_list(
"""
select sum(credit) - sum(debit)
from `tabGL Entry`
where company=%s and posting_date >= %s and posting_date <= %s
where company=%(company)s and posting_date >= %(start_date)s and posting_date <= %(end_date)s
and voucher_type != 'Period Closing Voucher'
and account in ( SELECT name FROM tabAccount WHERE account_type = %s) {cond}
and account in ( SELECT name FROM tabAccount WHERE account_type = %(account_type)s) {cond}
""".format(
cond=cond
),
(company, start_date, end_date, account_type),
filters,
)
return gl_sum[0] if gl_sum and gl_sum[0] else 0

View File

@@ -268,10 +268,12 @@ def get_cash_flow_data(fiscal_year, companies, filters):
def get_account_type_based_data(account_type, companies, fiscal_year, filters):
data = {}
total = 0
filters.account_type = account_type
filters.start_date = fiscal_year.year_start_date
filters.end_date = fiscal_year.year_end_date
for company in companies:
amount = get_account_type_based_gl_data(
company, fiscal_year.year_start_date, fiscal_year.year_end_date, account_type, filters
)
amount = get_account_type_based_gl_data(company, filters)
if amount and account_type == "Depreciation":
amount *= -1

View File

@@ -333,16 +333,21 @@ class StatusUpdater(Document):
)
def warn_about_bypassing_with_role(self, item, qty_or_amount, role):
action = _("Over Receipt/Delivery") if qty_or_amount == "qty" else _("Overbilling")
if qty_or_amount == "qty":
msg = _("Over Receipt/Delivery of {0} {1} ignored for item {2} because you have {3} role.")
else:
msg = _("Overbilling of {0} {1} ignored for item {2} because you have {3} role.")
msg = _("{} of {} {} ignored for item {} because you have {} role.").format(
action,
_(item["target_ref_field"].title()),
frappe.bold(item["reduce_by"]),
frappe.bold(item.get("item_code")),
role,
frappe.msgprint(
msg.format(
_(item["target_ref_field"].title()),
frappe.bold(item["reduce_by"]),
frappe.bold(item.get("item_code")),
role,
),
indicator="orange",
alert=True,
)
frappe.msgprint(msg, indicator="orange", alert=True)
def update_qty(self, update_modified=True):
"""Updates qty or amount at row level

View File

@@ -526,6 +526,7 @@ scheduler_events = {
"erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall.create_process_loan_security_shortfall",
"erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans",
"erpnext.crm.doctype.lead.lead.daily_open_lead",
"erpnext.stock.doctype.stock_entry.stock_entry.audit_incorrect_valuation_entries",
],
"weekly": ["erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_weekly"],
"monthly": ["erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_monthly"],

View File

@@ -1145,6 +1145,37 @@ class TestWorkOrder(FrappeTestCase):
except frappe.MandatoryError:
self.fail("Batch generation causing failing in Work Order")
@change_settings("Manufacturing Settings", {"make_serial_no_batch_from_work_order": 1})
def test_auto_serial_no_creation(self):
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
fg_item = frappe.generate_hash(length=20)
child_item = frappe.generate_hash(length=20)
bom_tree = {fg_item: {child_item: {}}}
create_nested_bom(bom_tree, prefix="")
item = frappe.get_doc("Item", fg_item)
item.has_serial_no = 1
item.serial_no_series = f"{item.name}.#####"
item.save()
try:
wo_order = make_wo_order_test_record(item=fg_item, qty=2, skip_transfer=True)
serial_nos = wo_order.serial_no
stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10))
stock_entry.set_work_order_details()
stock_entry.set_serial_no_batch_for_finished_good()
for row in stock_entry.items:
if row.item_code == fg_item:
self.assertTrue(row.serial_no)
self.assertEqual(sorted(get_serial_nos(row.serial_no)), sorted(get_serial_nos(serial_nos)))
except frappe.MandatoryError:
self.fail("Batch generation causing failing in Work Order")
@change_settings(
"Manufacturing Settings",
{"backflush_raw_materials_based_on": "Material Transferred for Manufacture"},

View File

@@ -181,13 +181,13 @@ class PickList(Document):
if item_map.get(key):
item_map[key].qty += item.qty
item_map[key].stock_qty += item.stock_qty
item_map[key].stock_qty += flt(item.stock_qty, item.precision("stock_qty"))
else:
item_map[key] = item
# maintain count of each item (useful to limit get query)
self.item_count_map.setdefault(item_code, 0)
self.item_count_map[item_code] += item.stock_qty
self.item_count_map[item_code] += flt(item.stock_qty, item.precision("stock_qty"))
return item_map.values()

View File

@@ -4,13 +4,25 @@
import json
from collections import defaultdict
from typing import Dict
import frappe
from frappe import _
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder.functions import Sum
from frappe.utils import cint, comma_or, cstr, flt, format_time, formatdate, getdate, nowdate
from six import iteritems, itervalues, string_types
from frappe.utils import (
add_days,
cint,
comma_or,
cstr,
flt,
format_time,
formatdate,
getdate,
nowdate,
today,
)
import erpnext
from erpnext.accounts.general_ledger import process_gl_map
@@ -2192,16 +2204,16 @@ class StockEntry(StockController):
d.qty -= process_loss_dict[d.item_code][1]
def set_serial_no_batch_for_finished_good(self):
serial_nos = ""
serial_nos = []
if self.pro_doc.serial_no:
serial_nos = self.get_serial_nos_for_fg()
serial_nos = self.get_serial_nos_for_fg() or []
for row in self.items:
if row.is_finished_item and row.item_code == self.pro_doc.production_item:
if serial_nos:
row.serial_no = "\n".join(serial_nos[0 : cint(row.qty)])
def get_serial_nos_for_fg(self, args):
def get_serial_nos_for_fg(self):
fields = [
"`tabStock Entry`.`name`",
"`tabStock Entry Detail`.`qty`",
@@ -2217,9 +2229,7 @@ class StockEntry(StockController):
]
stock_entries = frappe.get_all("Stock Entry", fields=fields, filters=filters)
if self.pro_doc.serial_no:
return self.get_available_serial_nos(stock_entries)
return self.get_available_serial_nos(stock_entries)
def get_available_serial_nos(self, stock_entries):
used_serial_nos = []
@@ -2556,3 +2566,63 @@ def get_supplied_items(purchase_order):
)
return supplied_item_details
def audit_incorrect_valuation_entries():
# Audit of stock transfer entries having incorrect valuation
from erpnext.controllers.stock_controller import create_repost_item_valuation_entry
stock_entries = get_incorrect_stock_entries()
for stock_entry, values in stock_entries.items():
reposting_data = frappe._dict(
{
"posting_date": values.posting_date,
"posting_time": values.posting_time,
"voucher_type": "Stock Entry",
"voucher_no": stock_entry,
"company": values.company,
}
)
create_repost_item_valuation_entry(reposting_data)
def get_incorrect_stock_entries() -> Dict:
stock_entry = frappe.qb.DocType("Stock Entry")
stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
transfer_purposes = [
"Material Transfer",
"Material Transfer for Manufacture",
"Send to Subcontractor",
]
query = (
frappe.qb.from_(stock_entry)
.inner_join(stock_ledger_entry)
.on(stock_entry.name == stock_ledger_entry.voucher_no)
.select(
stock_entry.name,
stock_entry.company,
stock_entry.posting_date,
stock_entry.posting_time,
Sum(stock_ledger_entry.stock_value_difference).as_("stock_value"),
)
.where(
(stock_entry.docstatus == 1)
& (stock_entry.purpose.isin(transfer_purposes))
& (stock_ledger_entry.modified > add_days(today(), -2))
)
.groupby(stock_ledger_entry.voucher_detail_no)
.having(Sum(stock_ledger_entry.stock_value_difference) != 0)
)
data = query.run(as_dict=True)
stock_entries = {}
for row in data:
if abs(row.stock_value) > 0.1 and row.name not in stock_entries:
stock_entries.setdefault(row.name, row)
return stock_entries

View File

@@ -5,7 +5,7 @@
import frappe
from frappe.permissions import add_user_permission, remove_user_permission
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, flt, nowdate, nowtime, today
from frappe.utils import add_days, flt, now, nowdate, nowtime, today
from six import iteritems
from erpnext.accounts.doctype.account.test_account import get_inventory_account
@@ -18,6 +18,8 @@ from erpnext.stock.doctype.item.test_item import (
from erpnext.stock.doctype.serial_no.serial_no import * # noqa
from erpnext.stock.doctype.stock_entry.stock_entry import (
FinishedGoodError,
audit_incorrect_valuation_entries,
get_incorrect_stock_entries,
move_sample_to_retention_warehouse,
)
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
@@ -1571,6 +1573,44 @@ class TestStockEntry(FrappeTestCase):
self.assertRaises(BatchExpiredError, se.save)
def test_audit_incorrect_stock_entries(self):
item_code = "Test Incorrect Valuation Rate Item - 001"
create_item(item_code=item_code, is_stock_item=1)
make_stock_entry(
item_code=item_code,
purpose="Material Receipt",
posting_date=add_days(nowdate(), -10),
qty=2,
rate=500,
to_warehouse="_Test Warehouse - _TC",
)
transfer_entry = make_stock_entry(
item_code=item_code,
purpose="Material Transfer",
qty=2,
rate=500,
from_warehouse="_Test Warehouse - _TC",
to_warehouse="_Test Warehouse 1 - _TC",
)
sle_name = frappe.db.get_value(
"Stock Ledger Entry", {"voucher_no": transfer_entry.name, "actual_qty": (">", 0)}, "name"
)
frappe.db.set_value(
"Stock Ledger Entry", sle_name, {"modified": add_days(now(), -1), "stock_value_difference": 10}
)
stock_entries = get_incorrect_stock_entries()
self.assertTrue(transfer_entry.name in stock_entries)
audit_incorrect_valuation_entries()
stock_entries = get_incorrect_stock_entries()
self.assertFalse(transfer_entry.name in stock_entries)
def make_serialized_item(**args):
args = frappe._dict(args)

View File

@@ -813,9 +813,9 @@ def insert_item_price(args):
):
if frappe.has_permission("Item Price", "write"):
price_list_rate = (
(args.rate + args.discount_amount) / args.get("conversion_factor")
(flt(args.rate) + flt(args.discount_amount)) / args.get("conversion_factor")
if args.get("conversion_factor")
else (args.rate + args.discount_amount)
else (flt(args.rate) + flt(args.discount_amount))
)
item_price = frappe.db.get_value(

View File

@@ -75,6 +75,7 @@ def get_item_info(filters):
if filters.get("brand"):
conditions.append("item.brand=%(brand)s")
conditions.append("is_stock_item = 1")
conditions.append("disabled = 0")
return frappe.db.sql(
"""select name, item_name, description, brand, item_group,

View File

@@ -1844,6 +1844,8 @@ Outstanding Amt,Offener Betrag,
Outstanding Cheques and Deposits to clear,Ausstehende Schecks und Anzahlungen zum verbuchen,
Outstanding for {0} cannot be less than zero ({1}),Ausstände für {0} können nicht kleiner als Null sein ({1}),
Outward taxable supplies(zero rated),Steuerpflichtige Lieferungen aus dem Ausland (null bewertet),
Over Receipt/Delivery of {0} {1} ignored for item {2} because you have {3} role.,"Überhöhte Annahme bzw. Lieferung von Artikel {2} mit {0} {1} wurde ignoriert, weil Sie die Rolle {3} haben."
Overbilling of {0} {1} ignored for item {2} because you have {3} role.,"Überhöhte Abrechnung von Artikel {2} mit {0} {1} wurde ignoriert, weil Sie die Rolle {3} haben."
Overdue,Überfällig,
Overlap in scoring between {0} and {1},Überlappung beim Scoring zwischen {0} und {1},
Overlapping conditions found between:,Überlagernde Bedingungen gefunden zwischen:,
Can't render this file because it is too large.