Merge branch 'develop' of https://github.com/frappe/erpnext into payment_request_flow

This commit is contained in:
Deepesh Garg 2022-12-20 11:42:15 +05:30
commit ff48634cbb
63 changed files with 683 additions and 222 deletions

View File

@ -485,6 +485,10 @@ def set_default_accounts(company):
"default_payable_account": frappe.db.get_value(
"Account", {"company": company.name, "account_type": "Payable", "is_group": 0}
),
"default_provisional_account": frappe.db.get_value(
"Account",
{"company": company.name, "account_type": "Service Received But Not Billed", "is_group": 0},
),
}
)

View File

@ -34,9 +34,6 @@ class JournalEntry(AccountsController):
def __init__(self, *args, **kwargs):
super(JournalEntry, self).__init__(*args, **kwargs)
def get_feed(self):
return self.voucher_type
def validate(self):
if self.voucher_type == "Opening Entry":
self.is_opening = "Yes"

View File

@ -305,6 +305,7 @@
"fieldname": "source_exchange_rate",
"fieldtype": "Float",
"label": "Exchange Rate",
"precision": "9",
"print_hide": 1,
"reqd": 1
},
@ -334,6 +335,7 @@
"fieldname": "target_exchange_rate",
"fieldtype": "Float",
"label": "Exchange Rate",
"precision": "9",
"print_hide": 1,
"reqd": 1
},
@ -731,7 +733,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-02-23 20:08:39.559814",
"modified": "2022-12-08 16:25:43.824051",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",

View File

@ -684,35 +684,34 @@ class PaymentEntry(AccountsController):
)
def validate_payment_against_negative_invoice(self):
if (self.payment_type == "Pay" and self.party_type == "Customer") or (
self.payment_type == "Receive" and self.party_type == "Supplier"
if (self.payment_type != "Pay" or self.party_type != "Customer") and (
self.payment_type != "Receive" or self.party_type != "Supplier"
):
return
total_negative_outstanding = sum(
abs(flt(d.outstanding_amount)) for d in self.get("references") if flt(d.outstanding_amount) < 0
total_negative_outstanding = sum(
abs(flt(d.outstanding_amount)) for d in self.get("references") if flt(d.outstanding_amount) < 0
)
paid_amount = self.paid_amount if self.payment_type == "Receive" else self.received_amount
additional_charges = sum(flt(d.amount) for d in self.deductions)
if not total_negative_outstanding:
if self.party_type == "Customer":
msg = _("Cannot pay to Customer without any negative outstanding invoice")
else:
msg = _("Cannot receive from Supplier without any negative outstanding invoice")
frappe.throw(msg, InvalidPaymentEntry)
elif paid_amount - additional_charges > total_negative_outstanding:
frappe.throw(
_("Paid Amount cannot be greater than total negative outstanding amount {0}").format(
total_negative_outstanding
),
InvalidPaymentEntry,
)
paid_amount = self.paid_amount if self.payment_type == "Receive" else self.received_amount
additional_charges = sum([flt(d.amount) for d in self.deductions])
if not total_negative_outstanding:
frappe.throw(
_("Cannot {0} {1} {2} without any negative outstanding invoice").format(
_(self.payment_type),
(_("to") if self.party_type == "Customer" else _("from")),
self.party_type,
),
InvalidPaymentEntry,
)
elif paid_amount - additional_charges > total_negative_outstanding:
frappe.throw(
_("Paid Amount cannot be greater than total negative outstanding amount {0}").format(
total_negative_outstanding
),
InvalidPaymentEntry,
)
def set_title(self):
if frappe.flags.in_import and self.title:
# do not set title dynamically if title exists during data import.
@ -1188,6 +1187,7 @@ def get_outstanding_reference_documents(args):
ple = qb.DocType("Payment Ledger Entry")
common_filter = []
accounting_dimensions_filter = []
posting_and_due_date = []
# confirm that Supplier is not blocked
@ -1217,7 +1217,7 @@ def get_outstanding_reference_documents(args):
# Add cost center condition
if args.get("cost_center"):
condition += " and cost_center='%s'" % args.get("cost_center")
common_filter.append(ple.cost_center == args.get("cost_center"))
accounting_dimensions_filter.append(ple.cost_center == args.get("cost_center"))
date_fields_dict = {
"posting_date": ["from_posting_date", "to_posting_date"],
@ -1243,6 +1243,7 @@ def get_outstanding_reference_documents(args):
posting_date=posting_and_due_date,
min_outstanding=args.get("outstanding_amt_greater_than"),
max_outstanding=args.get("outstanding_amt_less_than"),
accounting_dimensions=accounting_dimensions_filter,
)
outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices)

View File

@ -25,7 +25,8 @@
"in_list_view": 1,
"label": "Type",
"options": "DocType",
"reqd": 1
"reqd": 1,
"search_index": 1
},
{
"columns": 2,
@ -35,7 +36,8 @@
"in_list_view": 1,
"label": "Name",
"options": "reference_doctype",
"reqd": 1
"reqd": 1,
"search_index": 1
},
{
"fieldname": "due_date",
@ -104,7 +106,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-09-26 17:06:55.597389",
"modified": "2022-12-12 12:31:44.919895",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Reference",
@ -113,5 +115,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -23,6 +23,7 @@ class PaymentReconciliation(Document):
def __init__(self, *args, **kwargs):
super(PaymentReconciliation, self).__init__(*args, **kwargs)
self.common_filter_conditions = []
self.accounting_dimension_filter_conditions = []
self.ple_posting_date_filter = []
@frappe.whitelist()
@ -193,6 +194,7 @@ class PaymentReconciliation(Document):
posting_date=self.ple_posting_date_filter,
min_outstanding=self.minimum_invoice_amount if self.minimum_invoice_amount else None,
max_outstanding=self.maximum_invoice_amount if self.maximum_invoice_amount else None,
accounting_dimensions=self.accounting_dimension_filter_conditions,
)
if self.invoice_limit:
@ -381,7 +383,7 @@ class PaymentReconciliation(Document):
self.common_filter_conditions.append(ple.company == self.company)
if self.get("cost_center") and (get_invoices or get_return_invoices):
self.common_filter_conditions.append(ple.cost_center == self.cost_center)
self.accounting_dimension_filter_conditions.append(ple.cost_center == self.cost_center)
if get_invoices:
if self.from_invoice_date:

View File

@ -8,6 +8,8 @@ from frappe import qb
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, nowdate
from erpnext import get_default_cost_center
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.party import get_party_account
@ -20,6 +22,7 @@ class TestPaymentReconciliation(FrappeTestCase):
self.create_item()
self.create_customer()
self.create_account()
self.create_cost_center()
self.clear_old_entries()
def tearDown(self):
@ -216,6 +219,22 @@ class TestPaymentReconciliation(FrappeTestCase):
)
return je
def create_cost_center(self):
# Setup cost center
cc_name = "Sub"
self.main_cc = frappe.get_doc("Cost Center", get_default_cost_center(self.company))
cc_exists = frappe.db.get_list("Cost Center", filters={"cost_center_name": cc_name})
if cc_exists:
self.sub_cc = frappe.get_doc("Cost Center", cc_exists[0].name)
else:
sub_cc = frappe.new_doc("Cost Center")
sub_cc.cost_center_name = "Sub"
sub_cc.parent_cost_center = self.main_cc.parent_cost_center
sub_cc.company = self.main_cc.company
self.sub_cc = sub_cc.save()
def test_filter_min_max(self):
# check filter condition minimum and maximum amount
self.create_sales_invoice(qty=1, rate=300)
@ -578,3 +597,24 @@ class TestPaymentReconciliation(FrappeTestCase):
self.assertEqual(len(pr.payments), 1)
self.assertEqual(pr.payments[0].amount, amount)
self.assertEqual(pr.payments[0].currency, "EUR")
def test_differing_cost_center_on_invoice_and_payment(self):
"""
Cost Center filter should not affect outstanding amount calculation
"""
si = self.create_sales_invoice(qty=1, rate=100, do_not_submit=True)
si.cost_center = self.main_cc.name
si.submit()
pr = get_payment_entry(si.doctype, si.name)
pr.cost_center = self.sub_cc.name
pr = pr.save().submit()
pr = self.create_payment_reconciliation()
pr.cost_center = self.main_cc.name
pr.get_unreconciled_entries()
# check PR tool output
self.assertEqual(len(pr.get("invoices")), 0)
self.assertEqual(len(pr.get("payments")), 0)

View File

@ -255,7 +255,7 @@ def apply_pricing_rule(args, doc=None):
for item in item_list:
args_copy = copy.deepcopy(args)
args_copy.update(item)
data = get_pricing_rule_for_item(args_copy, item.get("price_list_rate"), doc=doc)
data = get_pricing_rule_for_item(args_copy, doc=doc)
out.append(data)
if (
@ -292,7 +292,7 @@ def update_pricing_rule_uom(pricing_rule, args):
pricing_rule.uom = row.uom
def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=False):
def get_pricing_rule_for_item(args, doc=None, for_validate=False):
from erpnext.accounts.doctype.pricing_rule.utils import (
get_applied_pricing_rules,
get_pricing_rule_items,

View File

@ -1123,7 +1123,7 @@ def make_pricing_rule(**args):
"apply_on": args.apply_on or "Item Code",
"applicable_for": args.applicable_for,
"selling": args.selling or 0,
"currency": "USD",
"currency": "INR",
"apply_discount_on_rate": args.apply_discount_on_rate or 0,
"buying": args.buying or 0,
"min_qty": args.min_qty or 0.0,

View File

@ -250,6 +250,17 @@ def get_other_conditions(conditions, values, args):
and ifnull(`tabPricing Rule`.valid_upto, '2500-12-31')"""
values["transaction_date"] = args.get("transaction_date")
if args.get("doctype") in [
"Quotation",
"Sales Order",
"Delivery Note",
"Sales Invoice",
"POS Invoice",
]:
conditions += """ and ifnull(`tabPricing Rule`.selling, 0) = 1"""
else:
conditions += """ and ifnull(`tabPricing Rule`.buying, 0) = 1"""
return conditions
@ -669,7 +680,7 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
item_details.free_item_data.append(free_item_data_args)
def apply_pricing_rule_for_free_items(doc, pricing_rule_args, set_missing_values=False):
def apply_pricing_rule_for_free_items(doc, pricing_rule_args):
if pricing_rule_args:
items = tuple((d.item_code, d.pricing_rules) for d in doc.items if d.is_free_item)

View File

@ -64,12 +64,13 @@
"tax_withholding_net_total",
"base_tax_withholding_net_total",
"taxes_section",
"tax_category",
"taxes_and_charges",
"column_break_58",
"tax_category",
"column_break_49",
"shipping_rule",
"column_break_49",
"incoterm",
"named_place",
"section_break_51",
"taxes",
"totals",
@ -1541,13 +1542,19 @@
"fieldtype": "Link",
"label": "Incoterm",
"options": "Incoterm"
},
{
"depends_on": "incoterm",
"fieldname": "named_place",
"fieldtype": "Data",
"label": "Named Place"
}
],
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2022-11-25 12:44:29.935567",
"modified": "2022-12-12 18:37:38.142688",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@ -61,12 +61,13 @@
"total",
"net_total",
"taxes_section",
"tax_category",
"taxes_and_charges",
"column_break_38",
"shipping_rule",
"incoterm",
"column_break_55",
"tax_category",
"incoterm",
"named_place",
"section_break_40",
"taxes",
"section_break_43",
@ -2122,6 +2123,12 @@
"fieldtype": "Link",
"label": "Incoterm",
"options": "Incoterm"
},
{
"depends_on": "incoterm",
"fieldname": "named_place",
"fieldtype": "Data",
"label": "Named Place"
}
],
"icon": "fa fa-file-text",
@ -2134,7 +2141,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2022-12-05 16:18:14.532114",
"modified": "2022-12-12 18:34:33.409895",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@ -280,7 +280,8 @@ class Subscription(Document):
self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval())
self.validate_end_date()
self.validate_to_follow_calendar_months()
self.cost_center = erpnext.get_default_cost_center(self.get("company"))
if not self.cost_center:
self.cost_center = erpnext.get_default_cost_center(self.get("company"))
def validate_trial_period(self):
"""

View File

@ -121,12 +121,24 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
else:
tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted)
cost_center = get_cost_center(inv)
tax_row.update({"cost_center": cost_center})
if inv.doctype == "Purchase Invoice":
return tax_row, tax_deducted_on_advances, voucher_wise_amount
else:
return tax_row
def get_cost_center(inv):
cost_center = frappe.get_cached_value("Company", inv.company, "cost_center")
if len(inv.get("taxes", [])) > 0:
cost_center = inv.get("taxes")[0].cost_center
return cost_center
def get_tax_withholding_details(tax_withholding_category, posting_date, company):
tax_withholding = frappe.get_doc("Tax Withholding Category", tax_withholding_category)

View File

@ -99,6 +99,9 @@ class ReceivablePayableReport(object):
# Get return entries
self.get_return_entries()
# Get Exchange Rate Revaluations
self.get_exchange_rate_revaluations()
self.data = []
for ple in self.ple_entries:
@ -251,7 +254,8 @@ class ReceivablePayableReport(object):
row.invoice_grand_total = row.invoiced
if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and (
abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision
(abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision)
or (row.voucher_no in self.err_journals)
):
# non-zero oustanding, we must consider this row
@ -1028,3 +1032,17 @@ class ReceivablePayableReport(object):
"data": {"labels": self.ageing_column_labels, "datasets": rows},
"type": "percentage",
}
def get_exchange_rate_revaluations(self):
je = qb.DocType("Journal Entry")
results = (
qb.from_(je)
.select(je.name)
.where(
(je.company == self.filters.company)
& (je.posting_date.lte(self.filters.report_date))
& (je.voucher_type == "Exchange Rate Revaluation")
)
.run()
)
self.err_journals = [x[0] for x in results] if results else []

View File

@ -1,9 +1,10 @@
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, getdate, today
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, flt, getdate, today
from erpnext import get_default_cost_center
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute
@ -17,10 +18,37 @@ class TestAccountsReceivable(FrappeTestCase):
frappe.db.sql("delete from `tabPayment Entry` where company='_Test Company 2'")
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'")
frappe.db.sql("delete from `tabPayment Ledger Entry` where company='_Test Company 2'")
frappe.db.sql("delete from `tabJournal Entry` where company='_Test Company 2'")
frappe.db.sql("delete from `tabExchange Rate Revaluation` where company='_Test Company 2'")
self.create_usd_account()
def tearDown(self):
frappe.db.rollback()
def create_usd_account(self):
name = "Debtors USD"
exists = frappe.db.get_list(
"Account", filters={"company": "_Test Company 2", "account_name": "Debtors USD"}
)
if exists:
self.debtors_usd = exists[0].name
else:
debtors = frappe.get_doc(
"Account",
frappe.db.get_list(
"Account", filters={"company": "_Test Company 2", "account_name": "Debtors"}
)[0].name,
)
debtors_usd = frappe.new_doc("Account")
debtors_usd.company = debtors.company
debtors_usd.account_name = "Debtors USD"
debtors_usd.account_currency = "USD"
debtors_usd.parent_account = debtors.parent_account
debtors_usd.account_type = debtors.account_type
self.debtors_usd = debtors_usd.save().name
def test_accounts_receivable(self):
filters = {
"company": "_Test Company 2",
@ -33,7 +61,7 @@ class TestAccountsReceivable(FrappeTestCase):
}
# check invoice grand total and invoiced column's value for 3 payment terms
name = make_sales_invoice()
name = make_sales_invoice().name
report = execute(filters)
expected_data = [[100, 30], [100, 50], [100, 20]]
@ -118,8 +146,74 @@ class TestAccountsReceivable(FrappeTestCase):
],
)
@change_settings(
"Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1}
)
def test_exchange_revaluation_for_party(self):
"""
Exchange Revaluation for party on Receivable/Payable shoule be included
"""
def make_sales_invoice():
company = "_Test Company 2"
customer = "_Test Customer 2"
# Using Exchange Gain/Loss account for unrealized as well.
company_doc = frappe.get_doc("Company", company)
company_doc.unrealized_exchange_gain_loss_account = company_doc.exchange_gain_loss_account
company_doc.save()
si = make_sales_invoice(no_payment_schedule=True, do_not_submit=True)
si.currency = "USD"
si.conversion_rate = 0.90
si.debit_to = self.debtors_usd
si = si.save().submit()
# Exchange Revaluation
err = frappe.new_doc("Exchange Rate Revaluation")
err.company = company
err.posting_date = today()
accounts = err.get_accounts_data()
err.extend("accounts", accounts)
err.accounts[0].new_exchange_rate = 0.95
row = err.accounts[0]
row.new_balance_in_base_currency = flt(
row.new_exchange_rate * flt(row.balance_in_account_currency)
)
row.gain_loss = row.new_balance_in_base_currency - flt(row.balance_in_base_currency)
err.set_total_gain_loss()
err = err.save().submit()
# Submit JV for ERR
jv = frappe.get_doc(err.make_jv_entry())
jv = jv.save()
for x in jv.accounts:
x.cost_center = get_default_cost_center(jv.company)
jv.submit()
filters = {
"company": company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
}
report = execute(filters)
expected_data_for_err = [0, -5, 0, 5]
row = [x for x in report[1] if x.voucher_type == jv.doctype and x.voucher_no == jv.name][0]
self.assertEqual(
expected_data_for_err,
[
row.invoiced,
row.paid,
row.credit_note,
row.outstanding,
],
)
def make_sales_invoice(no_payment_schedule=False, do_not_submit=False):
frappe.set_user("Administrator")
si = create_sales_invoice(
@ -134,22 +228,26 @@ def make_sales_invoice():
do_not_save=1,
)
si.append(
"payment_schedule",
dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30),
)
si.append(
"payment_schedule",
dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50),
)
si.append(
"payment_schedule",
dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20),
)
if not no_payment_schedule:
si.append(
"payment_schedule",
dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30),
)
si.append(
"payment_schedule",
dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50),
)
si.append(
"payment_schedule",
dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20),
)
si.submit()
si = si.save()
return si.name
if not do_not_submit:
si = si.submit()
return si
def make_payment(docname):

View File

@ -533,12 +533,13 @@ def get_accounts(root_type, companies):
],
filters={"company": company, "root_type": root_type},
):
if account.account_name not in added_accounts:
if account.account_number:
account_key = account.account_number + "-" + account.account_name
else:
account_key = account.account_name
if account_key not in added_accounts:
accounts.append(account)
if account.account_number:
account_key = account.account_number + "-" + account.account_name
else:
account_key = account.account_name
added_accounts.append(account_key)
return accounts

View File

@ -503,7 +503,7 @@ class GrossProfitGenerator(object):
invoice_portion = 100
elif row.invoice_portion:
invoice_portion = row.invoice_portion
else:
elif row.payment_amount:
invoice_portion = row.payment_amount * 100 / row.base_net_amount
if i == 0:

View File

@ -234,8 +234,11 @@ def modify_report_columns(doctype, field, column):
if field in ["item_tax_rate", "base_net_amount"]:
return None
if doctype == "GL Entry" and field in ["debit", "credit"]:
column.update({"label": _("Amount"), "fieldname": "amount"})
if doctype == "GL Entry":
if field in ["debit", "credit"]:
column.update({"label": _("Amount"), "fieldname": "amount"})
elif field == "voucher_type":
column.update({"fieldtype": "Data", "options": ""})
if field == "taxes_and_charges":
column.update({"label": _("Taxes and Charges Template")})

View File

@ -836,6 +836,7 @@ def get_outstanding_invoices(
posting_date=None,
min_outstanding=None,
max_outstanding=None,
accounting_dimensions=None,
):
ple = qb.DocType("Payment Ledger Entry")
@ -866,6 +867,7 @@ def get_outstanding_invoices(
min_outstanding=min_outstanding,
max_outstanding=max_outstanding,
get_invoices=True,
accounting_dimensions=accounting_dimensions or [],
)
for d in invoice_list:
@ -1615,6 +1617,7 @@ class QueryPaymentLedger(object):
.where(ple.delinked == 0)
.where(Criterion.all(filter_on_voucher_no))
.where(Criterion.all(self.common_filter))
.where(Criterion.all(self.dimensions_filter))
.where(Criterion.all(self.voucher_posting_date))
.groupby(ple.voucher_type, ple.voucher_no, ple.party_type, ple.party)
)
@ -1702,6 +1705,7 @@ class QueryPaymentLedger(object):
max_outstanding=None,
get_payments=False,
get_invoices=False,
accounting_dimensions=None,
):
"""
Fetch voucher amount and outstanding amount from Payment Ledger using Database CTE
@ -1717,6 +1721,7 @@ class QueryPaymentLedger(object):
self.reset()
self.vouchers = vouchers
self.common_filter = common_filter or []
self.dimensions_filter = accounting_dimensions or []
self.voucher_posting_date = posting_date or []
self.min_outstanding = min_outstanding
self.max_outstanding = max_outstanding

View File

@ -62,12 +62,13 @@
"set_reserve_warehouse",
"supplied_items",
"taxes_section",
"tax_category",
"taxes_and_charges",
"column_break_53",
"tax_category",
"column_break_50",
"shipping_rule",
"column_break_50",
"incoterm",
"named_place",
"section_break_52",
"taxes",
"totals",
@ -1256,13 +1257,19 @@
"fieldtype": "Link",
"label": "Incoterm",
"options": "Incoterm"
},
{
"depends_on": "incoterm",
"fieldname": "named_place",
"fieldtype": "Data",
"label": "Named Place"
}
],
"icon": "fa fa-file-text",
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2022-11-17 17:28:07.729943",
"modified": "2022-12-12 18:36:37.455134",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",

View File

@ -57,44 +57,96 @@ frappe.ui.form.on("Request for Quotation",{
});
}, __("Tools"));
frm.add_custom_button(__('Download PDF'), () => {
var suppliers = [];
const fields = [{
fieldtype: 'Link',
label: __('Select a Supplier'),
fieldname: 'supplier',
options: 'Supplier',
reqd: 1,
get_query: () => {
return {
filters: [
["Supplier", "name", "in", frm.doc.suppliers.map((row) => {return row.supplier;})]
]
}
}
}];
frappe.prompt(fields, data => {
var child = locals[cdt][cdn]
var w = window.open(
frappe.urllib.get_full_url("/api/method/erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_pdf?"
+"doctype="+encodeURIComponent(frm.doc.doctype)
+"&name="+encodeURIComponent(frm.doc.name)
+"&supplier="+encodeURIComponent(data.supplier)
+"&no_letterhead=0"));
if(!w) {
frappe.msgprint(__("Please enable pop-ups")); return;
}
frm.add_custom_button(
__("Download PDF"),
() => {
frappe.prompt(
[
{
fieldtype: "Link",
label: "Select a Supplier",
fieldname: "supplier",
options: "Supplier",
reqd: 1,
default: frm.doc.suppliers?.length == 1 ? frm.doc.suppliers[0].supplier : "",
get_query: () => {
return {
filters: [
[
"Supplier",
"name",
"in",
frm.doc.suppliers.map((row) => {
return row.supplier;
}),
],
],
};
},
},
{
fieldtype: "Section Break",
label: "Print Settings",
fieldname: "print_settings",
collapsible: 1,
},
{
fieldtype: "Link",
label: "Print Format",
fieldname: "print_format",
options: "Print Format",
placeholder: "Standard",
get_query: () => {
return {
filters: {
doc_type: "Request for Quotation",
},
};
},
},
{
fieldtype: "Link",
label: "Language",
fieldname: "language",
options: "Language",
default: frappe.boot.lang,
},
{
fieldtype: "Link",
label: "Letter Head",
fieldname: "letter_head",
options: "Letter Head",
default: frm.doc.letter_head,
},
],
(data) => {
var w = window.open(
frappe.urllib.get_full_url(
"/api/method/erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_pdf?" +
new URLSearchParams({
doctype: frm.doc.doctype,
name: frm.doc.name,
supplier: data.supplier,
print_format: data.print_format || "Standard",
language: data.language || frappe.boot.lang,
letter_head: data.letter_head || frm.doc.letter_head || "",
}).toString()
)
);
if (!w) {
frappe.msgprint(__("Please enable pop-ups"));
return;
}
},
"Download PDF for Supplier",
"Download"
);
},
'Download PDF for Supplier',
'Download');
},
__("Tools"));
__("Tools")
);
frm.page.set_inner_btn_group_as_primary(__('Create'));
frm.page.set_inner_btn_group_as_primary(__("Create"));
}
},
make_supplier_quotation: function(frm) {

View File

@ -389,10 +389,17 @@ def create_rfq_items(sq_doc, supplier, data):
@frappe.whitelist()
def get_pdf(doctype, name, supplier):
doc = get_rfq_doc(doctype, name, supplier)
if doc:
download_pdf(doctype, name, doc=doc)
def get_pdf(doctype, name, supplier, print_format=None, language=None, letter_head=None):
# permissions get checked in `download_pdf`
if doc := get_rfq_doc(doctype, name, supplier):
download_pdf(
doctype,
name,
print_format,
doc=doc,
language=language,
letter_head=letter_head or None,
)
def get_rfq_doc(doctype, name, supplier):

View File

@ -20,9 +20,6 @@ from erpnext.utilities.transaction_base import TransactionBase
class Supplier(TransactionBase):
def get_feed(self):
return self.supplier_name
def onload(self):
"""Load address and contacts in `__onload`"""
load_address_and_contact(self)

View File

@ -40,12 +40,13 @@
"total",
"net_total",
"taxes_section",
"tax_category",
"taxes_and_charges",
"column_break_34",
"tax_category",
"column_break_36",
"shipping_rule",
"column_break_36",
"incoterm",
"named_place",
"section_break_38",
"taxes",
"totals",
@ -830,6 +831,12 @@
"fieldtype": "Link",
"label": "Incoterm",
"options": "Incoterm"
},
{
"depends_on": "incoterm",
"fieldname": "named_place",
"fieldtype": "Data",
"label": "Named Place"
}
],
"icon": "fa fa-shopping-cart",
@ -837,7 +844,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-11-17 17:27:32.179686",
"modified": "2022-12-12 18:35:39.740974",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Quotation",

View File

@ -197,7 +197,7 @@ class AccountsController(TransactionBase):
validate_einvoice_fields(self)
if self.doctype != "Material Request":
if self.doctype != "Material Request" and not self.ignore_pricing_rule:
apply_pricing_rule_on_transaction(self)
def before_cancel(self):

View File

@ -25,10 +25,6 @@ class BuyingController(SubcontractingController):
def __setup__(self):
self.flags.ignore_permlevel_for_fields = ["buying_price_list", "price_list_currency"]
def get_feed(self):
if self.get("supplier_name"):
return _("From {0} | {1} {2}").format(self.supplier_name, self.currency, self.grand_total)
def validate(self):
super(BuyingController, self).validate()
if getattr(self, "supplier", None) and not self.supplier_name:

View File

@ -19,9 +19,6 @@ class SellingController(StockController):
def __setup__(self):
self.flags.ignore_permlevel_for_fields = ["selling_price_list", "price_list_currency"]
def get_feed(self):
return _("To {0} | {1} {2}").format(self.customer_name, self.currency, self.grand_total)
def onload(self):
super(SellingController, self).onload()
if self.doctype in ("Sales Order", "Delivery Note", "Sales Invoice"):

View File

@ -347,16 +347,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 = _("{0} of {1} {2} ignored for item {3} because you have {4} 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

@ -14,9 +14,6 @@ from erpnext.crm.utils import CRMNote, copy_comments, link_communications, link_
class Lead(SellingController, CRMNote):
def get_feed(self):
return "{0}: {1}".format(_(self.status), self.lead_name)
def onload(self):
customer = frappe.db.get_value("Customer", {"lead_name": self.name})
self.get("__onload").is_customer = customer

View File

@ -420,6 +420,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.utils.open_leads_opportunities_based_on_todays_event",
"erpnext.stock.doctype.stock_entry.stock_entry.audit_incorrect_valuation_entries",
],
"monthly_long": [
"erpnext.accounts.deferred_revenue.process_deferred_accounting",

View File

@ -10,9 +10,6 @@ from erpnext.utilities.transaction_base import TransactionBase
class MaintenanceVisit(TransactionBase):
def get_feed(self):
return _("To {0}").format(self.customer_name)
def validate_serial_no(self):
for d in self.get("purposes"):
if d.serial_no and not frappe.db.exists("Serial No", d.serial_no):

View File

@ -1154,6 +1154,36 @@ 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
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

@ -4,7 +4,7 @@
import frappe
from frappe import _
from frappe.query_builder.functions import Floor, Sum
from frappe.query_builder.functions import Sum
from pypika.terms import ExistsCriterion
@ -58,9 +58,9 @@ def get_bom_stock(filters):
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))),
(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)

View File

@ -102,7 +102,7 @@ def create_party_contact(doctype, fullname, user, party_name):
contact = frappe.new_doc("Contact")
contact.update({"first_name": fullname, "email_id": user})
contact.append("links", dict(link_doctype=doctype, link_name=party_name))
contact.append("email_ids", dict(email_id=user))
contact.append("email_ids", dict(email_id=user, is_primary=True))
contact.flags.ignore_mandatory = True
contact.insert(ignore_permissions=True)

View File

@ -15,9 +15,6 @@ from erpnext.setup.doctype.holiday_list.holiday_list import is_holiday
class Project(Document):
def get_feed(self):
return "{0}: {1}".format(_(self.status), frappe.safe_decode(self.project_name))
def onload(self):
self.set_onload(
"activity_summary",

View File

@ -20,9 +20,6 @@ class CircularReferenceError(frappe.ValidationError):
class Task(NestedSet):
nsm_parent_field = "parent_task"
def get_feed(self):
return "{0}: {1}".format(_(self.status), self.subject)
def get_customer_details(self):
cust = frappe.db.sql("select customer_name from `tabCustomer` where name=%s", self.customer)
if cust:

View File

@ -58,7 +58,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
if (
in_list(["Sales Invoice", "POS Invoice"], this.frm.doc.doctype)
&& this.frm.doc.s_pos
&& this.frm.doc.is_pos
&& this.frm.doc.is_return
) {
this.set_total_amount_to_default_mop();

View File

@ -1130,10 +1130,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
qty(doc, cdt, cdn) {
let item = frappe.get_doc(cdt, cdn);
item.pricing_rules = ''
this.conversion_factor(doc, cdt, cdn, true);
this.calculate_stock_uom_rate(doc, cdt, cdn);
this.apply_pricing_rule(item, true);
// item.pricing_rules = ''
frappe.run_serially([
() => this.remove_pricing_rule(item),
() => this.conversion_factor(doc, cdt, cdn, true),
() => this.calculate_stock_uom_rate(doc, cdt, cdn),
() => this.apply_pricing_rule(item, true)
]);
}
calculate_stock_uom_rate(doc, cdt, cdn) {
@ -1357,16 +1360,21 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
var item_list = [];
$.each(this.frm.doc["items"] || [], function(i, d) {
if (d.item_code && !d.is_free_item) {
item_list.push({
"doctype": d.doctype,
"name": d.name,
"item_code": d.item_code,
"pricing_rules": d.pricing_rules,
"parenttype": d.parenttype,
"parent": d.parent,
"price_list_rate": d.price_list_rate
})
if (d.item_code) {
if (d.is_free_item) {
// Simply remove free items
me.frm.get_field("items").grid.grid_rows[i].remove();
} else {
item_list.push({
"doctype": d.doctype,
"name": d.name,
"item_code": d.item_code,
"pricing_rules": d.pricing_rules,
"parenttype": d.parenttype,
"parent": d.parent,
"price_list_rate": d.price_list_rate
})
}
}
});
return this.frm.call({

View File

@ -27,9 +27,6 @@ from erpnext.utilities.transaction_base import TransactionBase
class Customer(TransactionBase):
def get_feed(self):
return self.customer_name
def onload(self):
"""Load address and contacts in `__onload`"""
load_address_and_contact(self)

View File

@ -43,12 +43,13 @@
"total",
"net_total",
"taxes_section",
"tax_category",
"taxes_and_charges",
"column_break_36",
"tax_category",
"column_break_34",
"shipping_rule",
"column_break_34",
"incoterm",
"named_place",
"section_break_36",
"taxes",
"section_break_39",
@ -1059,13 +1060,19 @@
"fieldtype": "Link",
"label": "Incoterm",
"options": "Incoterm"
},
{
"depends_on": "incoterm",
"fieldname": "named_place",
"fieldtype": "Data",
"label": "Named Place"
}
],
"icon": "fa fa-shopping-cart",
"idx": 82,
"is_submittable": 1,
"links": [],
"modified": "2022-11-17 17:20:54.984348",
"modified": "2022-12-12 18:32:28.671332",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation",

View File

@ -30,6 +30,24 @@ class TestQuotation(FrappeTestCase):
self.assertTrue(sales_order.get("payment_schedule"))
def test_maintain_rate_in_sales_cycle_is_enforced(self):
from erpnext.selling.doctype.quotation.quotation import make_sales_order
maintain_rate = frappe.db.get_single_value("Selling Settings", "maintain_same_sales_rate")
frappe.db.set_single_value("Selling Settings", "maintain_same_sales_rate", 1)
quotation = frappe.copy_doc(test_records[0])
quotation.transaction_date = nowdate()
quotation.valid_till = add_months(quotation.transaction_date, 1)
quotation.insert()
quotation.submit()
sales_order = make_sales_order(quotation.name)
sales_order.items[0].rate = 1
self.assertRaises(frappe.ValidationError, sales_order.save)
frappe.db.set_single_value("Selling Settings", "maintain_same_sales_rate", maintain_rate)
def test_make_sales_order_with_different_currency(self):
from erpnext.selling.doctype.quotation.quotation import make_sales_order

View File

@ -58,12 +58,13 @@
"total",
"net_total",
"taxes_section",
"tax_category",
"taxes_and_charges",
"column_break_38",
"tax_category",
"column_break_49",
"shipping_rule",
"column_break_49",
"incoterm",
"named_place",
"section_break_40",
"taxes",
"section_break_43",
@ -1630,13 +1631,19 @@
"fieldtype": "Link",
"label": "Incoterm",
"options": "Incoterm"
},
{
"depends_on": "incoterm",
"fieldname": "named_place",
"fieldtype": "Data",
"label": "Named Place"
}
],
"icon": "fa fa-file-text",
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2022-11-17 17:22:00.413878",
"modified": "2022-12-12 18:34:00.681780",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",

View File

@ -194,7 +194,7 @@ class SalesOrder(SellingController):
)
if cint(frappe.db.get_single_value("Selling Settings", "maintain_same_sales_rate")):
self.validate_rate_with_reference_doc([["Quotation", "prev_docname", "quotation_item"]])
self.validate_rate_with_reference_doc([["Quotation", "prevdoc_docname", "quotation_item"]])
def update_enquiry_status(self, prevdoc, flag):
enq = frappe.db.sql(

View File

@ -12,7 +12,10 @@ def get_data():
"Auto Repeat": "reference_document",
"Maintenance Visit": "prevdoc_docname",
},
"internal_links": {"Quotation": ["items", "prevdoc_docname"]},
"internal_links": {
"Quotation": ["items", "prevdoc_docname"],
"Material Request": ["items", "material_request"],
},
"transactions": [
{
"label": _("Fulfillment"),

View File

@ -44,6 +44,12 @@ frappe.query_reports["Sales Order Analysis"] = {
}
}
},
{
"fieldname": "warehouse",
"label": __("Warehouse"),
"fieldtype": "Link",
"options": "Warehouse"
},
{
"fieldname": "status",
"label": __("Status"),

View File

@ -53,6 +53,9 @@ def get_conditions(filters):
if filters.get("status"):
conditions += " and so.status in %(status)s"
if filters.get("warehouse"):
conditions += " and soi.warehouse = %(warehouse)s"
return conditions

View File

@ -70,9 +70,6 @@ class Company(NestedSet):
self.abbr = self.abbr.strip()
# if self.get('__islocal') and len(self.abbr) > 5:
# frappe.throw(_("Abbreviation cannot have more than 5 characters"))
if not self.abbr.strip():
frappe.throw(_("Abbreviation is mandatory"))

View File

@ -204,7 +204,7 @@ class TransactionDeletionRecord(Document):
@frappe.whitelist()
def get_doctypes_to_be_ignored():
doctypes_to_be_ignored_list = [
doctypes_to_be_ignored = [
"Account",
"Cost Center",
"Warehouse",
@ -223,4 +223,7 @@ def get_doctypes_to_be_ignored():
"Customer",
"Supplier",
]
return doctypes_to_be_ignored_list
doctypes_to_be_ignored.extend(frappe.get_hooks("company_data_to_be_ignored") or [])
return doctypes_to_be_ignored

View File

@ -57,12 +57,13 @@
"total",
"net_total",
"taxes_section",
"tax_category",
"taxes_and_charges",
"column_break_43",
"tax_category",
"column_break_39",
"shipping_rule",
"column_break_39",
"incoterm",
"named_place",
"section_break_41",
"taxes",
"section_break_44",
@ -1388,13 +1389,19 @@
"fieldtype": "Link",
"label": "Incoterm",
"options": "Incoterm"
},
{
"depends_on": "incoterm",
"fieldname": "named_place",
"fieldtype": "Data",
"label": "Named Place"
}
],
"icon": "fa fa-truck",
"idx": 146,
"is_submittable": 1,
"links": [],
"modified": "2022-11-17 17:22:42.860790",
"modified": "2022-12-12 18:38:53.067799",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",

View File

@ -22,9 +22,6 @@ form_grid_templates = {"items": "templates/form_grid/material_request_grid.html"
class MaterialRequest(BuyingController):
def get_feed(self):
return
def check_if_already_pulled(self):
pass

View File

@ -192,13 +192,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

@ -58,12 +58,13 @@
"total",
"net_total",
"taxes_charges_section",
"tax_category",
"taxes_and_charges",
"shipping_col",
"tax_category",
"column_break_53",
"shipping_rule",
"column_break_53",
"incoterm",
"named_place",
"taxes_section",
"taxes",
"totals",
@ -1225,13 +1226,19 @@
"fieldtype": "Link",
"label": "Incoterm",
"options": "Incoterm"
},
{
"depends_on": "incoterm",
"fieldname": "named_place",
"fieldtype": "Data",
"label": "Named Place"
}
],
"icon": "fa fa-truck",
"idx": 261,
"is_submittable": 1,
"links": [],
"modified": "2022-11-17 17:29:30.067536",
"modified": "2022-12-12 18:40:32.447752",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt",

View File

@ -766,13 +766,13 @@ def get_delivery_note_serial_no(item_code, qty, delivery_note):
@frappe.whitelist()
def auto_fetch_serial_number(
qty: float,
qty: int,
item_code: str,
warehouse: str,
posting_date: Optional[str] = None,
batch_nos: Optional[Union[str, List[str]]] = None,
for_doctype: Optional[str] = None,
exclude_sr_nos: Optional[List[str]] = None,
exclude_sr_nos=None,
) -> List[str]:
filters = frappe._dict({"item_code": item_code, "warehouse": warehouse})

View File

@ -4,12 +4,24 @@
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 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
@ -83,9 +95,6 @@ class StockEntry(StockController):
}
)
def get_feed(self):
return self.stock_entry_type
def onload(self):
for item in self.get("items"):
item.update(get_bin_details(item.item_code, item.s_warehouse))
@ -2239,16 +2248,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`",
@ -2264,9 +2273,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 = []
@ -2705,3 +2712,62 @@ def get_stock_entry_data(work_order):
)
.orderby(stock_entry.creation, stock_entry_detail.item_code, stock_entry_detail.idx)
).run(as_dict=1)
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 erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.stock.doctype.item.test_item import (
@ -17,6 +17,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
@ -1614,6 +1616,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

@ -715,8 +715,8 @@ def get_itemwise_batch(warehouse, posting_date, company, item_code=None):
def get_stock_balance_for(
item_code: str,
warehouse: str,
posting_date: str,
posting_time: str,
posting_date,
posting_time,
batch_no: Optional[str] = None,
with_valuation_rate: bool = True,
):

View File

@ -113,7 +113,7 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
if args.get(key) is None:
args[key] = value
data = get_pricing_rule_for_item(args, out.price_list_rate, doc, for_validate=for_validate)
data = get_pricing_rule_for_item(args, doc=doc, for_validate=for_validate)
out.update(data)
@ -828,9 +828,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(
@ -1305,7 +1305,7 @@ def apply_price_list_on_item(args):
item_doc = frappe.db.get_value("Item", args.item_code, ["name", "variant_of"], as_dict=1)
item_details = get_price_list_rate(args, item_doc)
item_details.update(get_pricing_rule_for_item(args, item_details.price_list_rate))
item_details.update(get_pricing_rule_for_item(args))
return item_details

View File

@ -82,7 +82,7 @@ def get_item_info(filters):
item.safety_stock,
item.lead_time_days,
)
.where(item.is_stock_item == 1)
.where((item.is_stock_item == 1) & (item.disabled == 0))
)
if brand := filters.get("brand"):

View File

@ -18,9 +18,6 @@ from frappe.utils.user import is_website_user
class Issue(Document):
def get_feed(self):
return "{0}: {1}".format(_(self.status), self.subject)
def validate(self):
if self.is_new() and self.via_customer_portal:
self.flags.create_communication = True

View File

@ -10,9 +10,6 @@ from erpnext.utilities.transaction_base import TransactionBase
class WarrantyClaim(TransactionBase):
def get_feed(self):
return _("{0}: From {1}").format(self.status, self.customer_name)
def validate(self):
if session["user"] != "Guest" and not self.customer:
frappe.throw(_("Customer is required"))

View File

@ -1849,6 +1849,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:,
@ -9914,4 +9916,3 @@ Cost and Freight,Kosten und Fracht,
Delivered at Place,Geliefert benannter Ort,
Delivered at Place Unloaded,Geliefert benannter Ort entladen,
Delivered Duty Paid,Geliefert verzollt,
{0} of {1} {2} ignored for item {3} because you have {4} role,"{0} von Artikel {3} mit {1} {2} wurde ignoriert, weil Sie die Rolle {4} haben."

Can't render this file because it is too large.

View File

@ -110,6 +110,7 @@ def get_price(item_code, price_list, customer_group, company, qty=1):
"conversion_rate": 1,
"for_shopping_cart": True,
"currency": frappe.db.get_value("Price List", price_list, "currency"),
"doctype": "Quotation",
}
)