Merge branch 'develop' into asset_depreciation_schedule

This commit is contained in:
Anand Baburajan
2022-12-23 11:02:17 +05:30
committed by GitHub
41 changed files with 609 additions and 151 deletions

View File

@@ -485,6 +485,10 @@ def set_default_accounts(company):
"default_payable_account": frappe.db.get_value( "default_payable_account": frappe.db.get_value(
"Account", {"company": company.name, "account_type": "Payable", "is_group": 0} "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

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

View File

@@ -1187,6 +1187,7 @@ def get_outstanding_reference_documents(args):
ple = qb.DocType("Payment Ledger Entry") ple = qb.DocType("Payment Ledger Entry")
common_filter = [] common_filter = []
accounting_dimensions_filter = []
posting_and_due_date = [] posting_and_due_date = []
# confirm that Supplier is not blocked # confirm that Supplier is not blocked
@@ -1216,7 +1217,7 @@ def get_outstanding_reference_documents(args):
# Add cost center condition # Add cost center condition
if args.get("cost_center"): if args.get("cost_center"):
condition += " and cost_center='%s'" % 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 = { date_fields_dict = {
"posting_date": ["from_posting_date", "to_posting_date"], "posting_date": ["from_posting_date", "to_posting_date"],
@@ -1242,6 +1243,7 @@ def get_outstanding_reference_documents(args):
posting_date=posting_and_due_date, posting_date=posting_and_due_date,
min_outstanding=args.get("outstanding_amt_greater_than"), min_outstanding=args.get("outstanding_amt_greater_than"),
max_outstanding=args.get("outstanding_amt_less_than"), max_outstanding=args.get("outstanding_amt_less_than"),
accounting_dimensions=accounting_dimensions_filter,
) )
outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices) outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices)
@@ -1638,7 +1640,7 @@ def get_payment_entry(
): ):
reference_doc = None reference_doc = None
doc = frappe.get_doc(dt, dn) doc = frappe.get_doc(dt, dn)
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) > 0: if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= 99.99:
frappe.throw(_("Can only make payment against unbilled {0}").format(dt)) frappe.throw(_("Can only make payment against unbilled {0}").format(dt))
if not party_type: if not party_type:

View File

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

View File

@@ -8,6 +8,8 @@ from frappe import qb
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, nowdate from frappe.utils import add_days, 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.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.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.party import get_party_account from erpnext.accounts.party import get_party_account
@@ -20,6 +22,7 @@ class TestPaymentReconciliation(FrappeTestCase):
self.create_item() self.create_item()
self.create_customer() self.create_customer()
self.create_account() self.create_account()
self.create_cost_center()
self.clear_old_entries() self.clear_old_entries()
def tearDown(self): def tearDown(self):
@@ -216,6 +219,22 @@ class TestPaymentReconciliation(FrappeTestCase):
) )
return je 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): def test_filter_min_max(self):
# check filter condition minimum and maximum amount # check filter condition minimum and maximum amount
self.create_sales_invoice(qty=1, rate=300) self.create_sales_invoice(qty=1, rate=300)
@@ -578,3 +597,24 @@ class TestPaymentReconciliation(FrappeTestCase):
self.assertEqual(len(pr.payments), 1) self.assertEqual(len(pr.payments), 1)
self.assertEqual(pr.payments[0].amount, amount) self.assertEqual(pr.payments[0].amount, amount)
self.assertEqual(pr.payments[0].currency, "EUR") 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

@@ -42,7 +42,7 @@ frappe.ui.form.on("Payment Request", "refresh", function(frm) {
}); });
} }
if(!frm.doc.payment_gateway_account && frm.doc.status == "Initiated") { if((!frm.doc.payment_gateway_account || frm.doc.payment_request_type == "Outward") && frm.doc.status == "Initiated") {
frm.add_custom_button(__('Create Payment Entry'), function(){ frm.add_custom_button(__('Create Payment Entry'), function(){
frappe.call({ frappe.call({
method: "erpnext.accounts.doctype.payment_request.payment_request.make_payment_entry", method: "erpnext.accounts.doctype.payment_request.payment_request.make_payment_entry",

View File

@@ -261,6 +261,7 @@ class PaymentRequest(Document):
payment_entry.update( payment_entry.update(
{ {
"mode_of_payment": self.mode_of_payment,
"reference_no": self.name, "reference_no": self.name,
"reference_date": nowdate(), "reference_date": nowdate(),
"remarks": "Payment Entry against {0} {1} via Payment Request {2}".format( "remarks": "Payment Entry against {0} {1} via Payment Request {2}".format(
@@ -410,25 +411,22 @@ def make_payment_request(**args):
else "" else ""
) )
existing_payment_request = None draft_payment_request = frappe.db.get_value(
if args.order_type == "Shopping Cart": "Payment Request",
existing_payment_request = frappe.db.get_value( {"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": 0},
"Payment Request", )
{"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": ("!=", 2)},
)
if existing_payment_request: existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn)
if existing_payment_request_amount:
grand_total -= existing_payment_request_amount
if draft_payment_request:
frappe.db.set_value( frappe.db.set_value(
"Payment Request", existing_payment_request, "grand_total", grand_total, update_modified=False "Payment Request", draft_payment_request, "grand_total", grand_total, update_modified=False
) )
pr = frappe.get_doc("Payment Request", existing_payment_request) pr = frappe.get_doc("Payment Request", draft_payment_request)
else: else:
if args.order_type != "Shopping Cart":
existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn)
if existing_payment_request_amount:
grand_total -= existing_payment_request_amount
pr = frappe.new_doc("Payment Request") pr = frappe.new_doc("Payment Request")
pr.update( pr.update(
{ {

View File

@@ -280,7 +280,8 @@ class Subscription(Document):
self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval()) self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval())
self.validate_end_date() self.validate_end_date()
self.validate_to_follow_calendar_months() 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): def validate_trial_period(self):
""" """

View File

@@ -121,12 +121,24 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
else: else:
tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted) 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": if inv.doctype == "Purchase Invoice":
return tax_row, tax_deducted_on_advances, voucher_wise_amount return tax_row, tax_deducted_on_advances, voucher_wise_amount
else: else:
return tax_row 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): def get_tax_withholding_details(tax_withholding_category, posting_date, company):
tax_withholding = frappe.get_doc("Tax Withholding Category", tax_withholding_category) tax_withholding = frappe.get_doc("Tax Withholding Category", tax_withholding_category)

View File

@@ -99,6 +99,9 @@ class ReceivablePayableReport(object):
# Get return entries # Get return entries
self.get_return_entries() self.get_return_entries()
# Get Exchange Rate Revaluations
self.get_exchange_rate_revaluations()
self.data = [] self.data = []
for ple in self.ple_entries: for ple in self.ple_entries:
@@ -251,7 +254,8 @@ class ReceivablePayableReport(object):
row.invoice_grand_total = row.invoiced row.invoice_grand_total = row.invoiced
if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and ( 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 # non-zero oustanding, we must consider this row
@@ -794,19 +798,19 @@ class ReceivablePayableReport(object):
if self.filters.get("payment_terms_template"): if self.filters.get("payment_terms_template"):
self.qb_selection_filter.append( self.qb_selection_filter.append(
self.ple.party_isin( self.ple.party.isin(
qb.from_(self.customer).where( qb.from_(self.customer)
self.customer.payment_terms == self.filters.get("payment_terms_template") .select(self.customer.name)
) .where(self.customer.payment_terms == self.filters.get("payment_terms_template"))
) )
) )
if self.filters.get("sales_partner"): if self.filters.get("sales_partner"):
self.qb_selection_filter.append( self.qb_selection_filter.append(
self.ple.party_isin( self.ple.party.isin(
qb.from_(self.customer).where( qb.from_(self.customer)
self.customer.default_sales_partner == self.filters.get("payment_terms_template") .select(self.customer.name)
) .where(self.customer.default_sales_partner == self.filters.get("payment_terms_template"))
) )
) )
@@ -1028,3 +1032,17 @@ class ReceivablePayableReport(object):
"data": {"labels": self.ageing_column_labels, "datasets": rows}, "data": {"labels": self.ageing_column_labels, "datasets": rows},
"type": "percentage", "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 unittest
import frappe import frappe
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, getdate, today 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.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute 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 `tabPayment Entry` where company='_Test Company 2'")
frappe.db.sql("delete from `tabGL 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 `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): def tearDown(self):
frappe.db.rollback() 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): def test_accounts_receivable(self):
filters = { filters = {
"company": "_Test Company 2", "company": "_Test Company 2",
@@ -33,7 +61,7 @@ class TestAccountsReceivable(FrappeTestCase):
} }
# check invoice grand total and invoiced column's value for 3 payment terms # 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) report = execute(filters)
expected_data = [[100, 30], [100, 50], [100, 20]] 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") frappe.set_user("Administrator")
si = create_sales_invoice( si = create_sales_invoice(
@@ -134,22 +228,26 @@ def make_sales_invoice():
do_not_save=1, do_not_save=1,
) )
si.append( if not no_payment_schedule:
"payment_schedule", si.append(
dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30), "payment_schedule",
) dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30),
si.append( )
"payment_schedule", si.append(
dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50), "payment_schedule",
) dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50),
si.append( )
"payment_schedule", si.append(
dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20), "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): def make_payment(docname):

View File

@@ -8,6 +8,7 @@ from frappe.utils import cint, cstr
from erpnext.accounts.report.financial_statements import ( from erpnext.accounts.report.financial_statements import (
get_columns, get_columns,
get_cost_centers_with_children,
get_data, get_data,
get_filtered_list_for_consolidated_report, get_filtered_list_for_consolidated_report,
get_period_list, get_period_list,
@@ -160,10 +161,11 @@ def get_account_type_based_data(company, account_type, period_list, accumulated_
total = 0 total = 0
for period in period_list: for period in period_list:
start_date = get_start_date(period, accumulated_values, company) 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( amount = get_account_type_based_gl_data(company, filters)
company, start_date, period["to_date"], account_type, filters
)
if amount and account_type == "Depreciation": if amount and account_type == "Depreciation":
amount *= -1 amount *= -1
@@ -175,7 +177,7 @@ def get_account_type_based_data(company, account_type, period_list, accumulated_
return data 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 = "" cond = ""
filters = frappe._dict(filters or {}) filters = frappe._dict(filters or {})
@@ -191,17 +193,21 @@ def get_account_type_based_gl_data(company, start_date, end_date, account_type,
frappe.db.escape(cstr(filters.finance_book)) 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( gl_sum = frappe.db.sql_list(
""" """
select sum(credit) - sum(debit) select sum(credit) - sum(debit)
from `tabGL Entry` 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 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( """.format(
cond=cond cond=cond
), ),
(company, start_date, end_date, account_type), filters,
) )
return gl_sum[0] if gl_sum and gl_sum[0] else 0 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): def get_account_type_based_data(account_type, companies, fiscal_year, filters):
data = {} data = {}
total = 0 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: for company in companies:
amount = get_account_type_based_gl_data( amount = get_account_type_based_gl_data(company, filters)
company, fiscal_year.year_start_date, fiscal_year.year_end_date, account_type, filters
)
if amount and account_type == "Depreciation": if amount and account_type == "Depreciation":
amount *= -1 amount *= -1

View File

@@ -607,6 +607,7 @@ class GrossProfitGenerator(object):
return abs(previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty)) return abs(previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty))
else: else:
return flt(row.qty) * self.get_average_buying_rate(row, item_code) return flt(row.qty) * self.get_average_buying_rate(row, item_code)
return 0.0
def get_buying_amount(self, row, item_code): def get_buying_amount(self, row, item_code):
# IMP NOTE # IMP NOTE

View File

@@ -6,6 +6,8 @@ from frappe.utils import add_days, flt, nowdate
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.gross_profit.gross_profit import execute from erpnext.accounts.report.gross_profit.gross_profit import execute
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
@@ -14,6 +16,7 @@ class TestGrossProfit(FrappeTestCase):
def setUp(self): def setUp(self):
self.create_company() self.create_company()
self.create_item() self.create_item()
self.create_bundle()
self.create_customer() self.create_customer()
self.create_sales_invoice() self.create_sales_invoice()
self.clear_old_entries() self.clear_old_entries()
@@ -42,6 +45,7 @@ class TestGrossProfit(FrappeTestCase):
self.company = company.name self.company = company.name
self.cost_center = company.cost_center self.cost_center = company.cost_center
self.warehouse = "Stores - " + abbr self.warehouse = "Stores - " + abbr
self.finished_warehouse = "Finished Goods - " + abbr
self.income_account = "Sales - " + abbr self.income_account = "Sales - " + abbr
self.expense_account = "Cost of Goods Sold - " + abbr self.expense_account = "Cost of Goods Sold - " + abbr
self.debit_to = "Debtors - " + abbr self.debit_to = "Debtors - " + abbr
@@ -53,6 +57,23 @@ class TestGrossProfit(FrappeTestCase):
) )
self.item = item if isinstance(item, str) else item.item_code self.item = item if isinstance(item, str) else item.item_code
def create_bundle(self):
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
item2 = create_item(
item_code="_Test GP Item 2", is_stock_item=1, company=self.company, warehouse=self.warehouse
)
self.item2 = item2 if isinstance(item2, str) else item2.item_code
# This will be parent item
bundle = create_item(
item_code="_Test GP bundle", is_stock_item=0, company=self.company, warehouse=self.warehouse
)
self.bundle = bundle if isinstance(bundle, str) else bundle.item_code
# Create Product Bundle
self.product_bundle = make_product_bundle(parent=self.bundle, items=[self.item, self.item2])
def create_customer(self): def create_customer(self):
name = "_Test GP Customer" name = "_Test GP Customer"
if frappe.db.exists("Customer", name): if frappe.db.exists("Customer", name):
@@ -93,6 +114,28 @@ class TestGrossProfit(FrappeTestCase):
) )
return sinv return sinv
def create_delivery_note(
self, item=None, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
):
"""
Helper function to populate default values in Delivery Note
"""
dnote = create_delivery_note(
company=self.company,
customer=self.customer,
currency="INR",
item=item or self.item,
qty=qty,
rate=rate,
cost_center=self.cost_center,
warehouse=self.warehouse,
return_against=None,
expense_account=self.expense_account,
do_not_save=do_not_save,
do_not_submit=do_not_submit,
)
return dnote
def clear_old_entries(self): def clear_old_entries(self):
doctype_list = [ doctype_list = [
"Sales Invoice", "Sales Invoice",
@@ -207,3 +250,55 @@ class TestGrossProfit(FrappeTestCase):
} }
gp_entry = [x for x in data if x.parent_invoice == sinv.name] gp_entry = [x for x in data if x.parent_invoice == sinv.name]
self.assertDictContainsSubset(expected_entry_with_dn, gp_entry[0]) self.assertDictContainsSubset(expected_entry_with_dn, gp_entry[0])
def test_bundled_delivery_note_with_different_warehouses(self):
"""
Test Delivery Note with bundled item. Packed Item from the bundle having different warehouses
"""
se = make_stock_entry(
company=self.company,
item_code=self.item,
target=self.warehouse,
qty=1,
basic_rate=100,
do_not_submit=True,
)
item = se.items[0]
se.append(
"items",
{
"item_code": self.item2,
"s_warehouse": "",
"t_warehouse": self.finished_warehouse,
"qty": 1,
"basic_rate": 100,
"conversion_factor": item.conversion_factor or 1.0,
"transfer_qty": flt(item.qty) * (flt(item.conversion_factor) or 1.0),
"serial_no": item.serial_no,
"batch_no": item.batch_no,
"cost_center": item.cost_center,
"expense_account": item.expense_account,
},
)
se = se.save().submit()
# Make a Delivery note with Product bundle
# Packed Items will have different warehouses
dnote = self.create_delivery_note(item=self.bundle, qty=1, rate=200, do_not_submit=True)
dnote.packed_items[1].warehouse = self.finished_warehouse
dnote = dnote.submit()
# make Sales Invoice for above delivery note
sinv = make_sales_invoice(dnote.name)
sinv = sinv.save().submit()
filters = frappe._dict(
company=self.company,
from_date=nowdate(),
to_date=nowdate(),
group_by="Invoice",
sales_invoice=sinv.name,
)
columns, data = execute(filters=filters)
self.assertGreater(len(data), 0)

View File

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

View File

@@ -235,11 +235,11 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
cur_frm.add_custom_button(__('Purchase Invoice'), cur_frm.add_custom_button(__('Purchase Invoice'),
this.make_purchase_invoice, __('Create')); this.make_purchase_invoice, __('Create'));
if(flt(doc.per_billed)==0 && doc.status != "Delivered") { if(flt(doc.per_billed) < 100 && doc.status != "Delivered") {
cur_frm.add_custom_button(__('Payment'), cur_frm.cscript.make_payment_entry, __('Create')); cur_frm.add_custom_button(__('Payment'), cur_frm.cscript.make_payment_entry, __('Create'));
} }
if(flt(doc.per_billed)==0) { if(flt(doc.per_billed) < 100) {
this.frm.add_custom_button(__('Payment Request'), this.frm.add_custom_button(__('Payment Request'),
function() { me.make_payment_request() }, __('Create')); function() { me.make_payment_request() }, __('Create'));
} }

View File

@@ -57,44 +57,96 @@ frappe.ui.form.on("Request for Quotation",{
}); });
}, __("Tools")); }, __("Tools"));
frm.add_custom_button(__('Download PDF'), () => { frm.add_custom_button(
var suppliers = []; __("Download PDF"),
const fields = [{ () => {
fieldtype: 'Link', frappe.prompt(
label: __('Select a Supplier'), [
fieldname: 'supplier', {
options: 'Supplier', fieldtype: "Link",
reqd: 1, label: "Select a Supplier",
get_query: () => { fieldname: "supplier",
return { options: "Supplier",
filters: [ reqd: 1,
["Supplier", "name", "in", frm.doc.suppliers.map((row) => {return row.supplier;})] default: frm.doc.suppliers?.length == 1 ? frm.doc.suppliers[0].supplier : "",
] get_query: () => {
} return {
} filters: [
}]; [
"Supplier",
frappe.prompt(fields, data => { "name",
var child = locals[cdt][cdn] "in",
frm.doc.suppliers.map((row) => {
var w = window.open( return row.supplier;
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; {
} 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', __("Tools")
'Download'); );
},
__("Tools"));
frm.page.set_inner_btn_group_as_primary(__('Create')); frm.page.set_inner_btn_group_as_primary(__("Create"));
} }
}, },
make_supplier_quotation: function(frm) { make_supplier_quotation: function(frm) {

View File

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

View File

@@ -102,7 +102,7 @@
} }
], ],
"links": [], "links": [],
"modified": "2021-06-30 13:09:14.228756", "modified": "2022-12-15 11:11:02.131986",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Appointment", "name": "Appointment",
@@ -121,16 +121,6 @@
"share": 1, "share": 1,
"write": 1 "write": 1
}, },
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Guest",
"share": 1
},
{ {
"create": 1, "create": 1,
"delete": 1, "delete": 1,
@@ -170,5 +160,6 @@
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -6,7 +6,9 @@ from collections import Counter
import frappe import frappe
from frappe import _ from frappe import _
from frappe.desk.form.assign_to import add as add_assignment
from frappe.model.document import Document from frappe.model.document import Document
from frappe.share import add_docshare
from frappe.utils import get_url, getdate, now from frappe.utils import get_url, getdate, now
from frappe.utils.verified_command import get_signed_params from frappe.utils.verified_command import get_signed_params
@@ -130,21 +132,18 @@ class Appointment(Document):
self.party = lead.name self.party = lead.name
def auto_assign(self): def auto_assign(self):
from frappe.desk.form.assign_to import add as add_assignemnt
existing_assignee = self.get_assignee_from_latest_opportunity() existing_assignee = self.get_assignee_from_latest_opportunity()
if existing_assignee: if existing_assignee:
# If the latest opportunity is assigned to someone # If the latest opportunity is assigned to someone
# Assign the appointment to the same # Assign the appointment to the same
add_assignemnt({"doctype": self.doctype, "name": self.name, "assign_to": [existing_assignee]}) self.assign_agent(existing_assignee)
return return
if self._assign: if self._assign:
return return
available_agents = _get_agents_sorted_by_asc_workload(getdate(self.scheduled_time)) available_agents = _get_agents_sorted_by_asc_workload(getdate(self.scheduled_time))
for agent in available_agents: for agent in available_agents:
if _check_agent_availability(agent, self.scheduled_time): if _check_agent_availability(agent, self.scheduled_time):
agent = agent[0] self.assign_agent(agent[0])
add_assignemnt({"doctype": self.doctype, "name": self.name, "assign_to": [agent]})
break break
def get_assignee_from_latest_opportunity(self): def get_assignee_from_latest_opportunity(self):
@@ -199,9 +198,15 @@ class Appointment(Document):
params = {"email": self.customer_email, "appointment": self.name} params = {"email": self.customer_email, "appointment": self.name}
return get_url(verify_route + "?" + get_signed_params(params)) return get_url(verify_route + "?" + get_signed_params(params))
def assign_agent(self, agent):
if not frappe.has_permission(doc=self, user=agent):
add_docshare(self.doctype, self.name, agent, flags={"ignore_share_permission": True})
add_assignment({"doctype": self.doctype, "name": self.name, "assign_to": [agent]})
def _get_agents_sorted_by_asc_workload(date): def _get_agents_sorted_by_asc_workload(date):
appointments = frappe.db.get_list("Appointment", fields="*") appointments = frappe.get_all("Appointment", fields="*")
agent_list = _get_agent_list_as_strings() agent_list = _get_agent_list_as_strings()
if not appointments: if not appointments:
return agent_list return agent_list
@@ -226,7 +231,7 @@ def _get_agent_list_as_strings():
def _check_agent_availability(agent_email, scheduled_time): def _check_agent_availability(agent_email, scheduled_time):
appointemnts_at_scheduled_time = frappe.get_list( appointemnts_at_scheduled_time = frappe.get_all(
"Appointment", filters={"scheduled_time": scheduled_time} "Appointment", filters={"scheduled_time": scheduled_time}
) )
for appointment in appointemnts_at_scheduled_time: for appointment in appointemnts_at_scheduled_time:

View File

@@ -1,4 +1,5 @@
{ {
"actions": [],
"creation": "2019-08-27 10:56:48.309824", "creation": "2019-08-27 10:56:48.309824",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
@@ -101,7 +102,8 @@
} }
], ],
"issingle": 1, "issingle": 1,
"modified": "2019-11-26 12:14:17.669366", "links": [],
"modified": "2022-12-15 11:10:13.517742",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Appointment Booking Settings", "name": "Appointment Booking Settings",
@@ -117,13 +119,6 @@
"share": 1, "share": 1,
"write": 1 "write": 1
}, },
{
"email": 1,
"print": 1,
"read": 1,
"role": "Guest",
"share": 1
},
{ {
"create": 1, "create": 1,
"email": 1, "email": 1,
@@ -147,5 +142,6 @@
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

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

View File

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

View File

@@ -298,7 +298,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
} }
make_payment_request() { make_payment_request() {
var me = this; let me = this;
const payment_request_type = (in_list(['Sales Order', 'Sales Invoice'], this.frm.doc.doctype)) const payment_request_type = (in_list(['Sales Order', 'Sales Invoice'], this.frm.doc.doctype))
? "Inward" : "Outward"; ? "Inward" : "Outward";
@@ -314,7 +314,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}, },
callback: function(r) { callback: function(r) {
if(!r.exc){ if(!r.exc){
var doc = frappe.model.sync(r.message); frappe.model.sync(r.message);
frappe.set_route("Form", r.message.doctype, r.message.name); frappe.set_route("Form", r.message.doctype, r.message.name);
} }
} }

View File

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

View File

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

View File

@@ -70,9 +70,6 @@ class Company(NestedSet):
self.abbr = self.abbr.strip() 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(): if not self.abbr.strip():
frappe.throw(_("Abbreviation is mandatory")) frappe.throw(_("Abbreviation is mandatory"))

View File

@@ -204,7 +204,7 @@ class TransactionDeletionRecord(Document):
@frappe.whitelist() @frappe.whitelist()
def get_doctypes_to_be_ignored(): def get_doctypes_to_be_ignored():
doctypes_to_be_ignored_list = [ doctypes_to_be_ignored = [
"Account", "Account",
"Cost Center", "Cost Center",
"Warehouse", "Warehouse",
@@ -223,4 +223,7 @@ def get_doctypes_to_be_ignored():
"Customer", "Customer",
"Supplier", "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

@@ -192,13 +192,13 @@ class PickList(Document):
if item_map.get(key): if item_map.get(key):
item_map[key].qty += item.qty 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: else:
item_map[key] = item item_map[key] = item
# maintain count of each item (useful to limit get query) # maintain count of each item (useful to limit get query)
self.item_count_map.setdefault(item_code, 0) 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() return item_map.values()

View File

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

View File

@@ -4,12 +4,24 @@
import json import json
from collections import defaultdict from collections import defaultdict
from typing import Dict
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.query_builder.functions import Sum 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 import erpnext
from erpnext.accounts.general_ledger import process_gl_map from erpnext.accounts.general_ledger import process_gl_map
@@ -2700,3 +2712,62 @@ def get_stock_entry_data(work_order):
) )
.orderby(stock_entry.creation, stock_entry_detail.item_code, stock_entry_detail.idx) .orderby(stock_entry.creation, stock_entry_detail.item_code, stock_entry_detail.idx)
).run(as_dict=1) ).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 import frappe
from frappe.permissions import add_user_permission, remove_user_permission from frappe.permissions import add_user_permission, remove_user_permission
from frappe.tests.utils import FrappeTestCase, change_settings 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.accounts.doctype.account.test_account import get_inventory_account
from erpnext.stock.doctype.item.test_item import ( 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.serial_no.serial_no import * # noqa
from erpnext.stock.doctype.stock_entry.stock_entry import ( from erpnext.stock.doctype.stock_entry.stock_entry import (
FinishedGoodError, FinishedGoodError,
audit_incorrect_valuation_entries,
get_incorrect_stock_entries,
move_sample_to_retention_warehouse, move_sample_to_retention_warehouse,
) )
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry 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) 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): def make_serialized_item(**args):
args = frappe._dict(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( def get_stock_balance_for(
item_code: str, item_code: str,
warehouse: str, warehouse: str,
posting_date: str, posting_date,
posting_time: str, posting_time,
batch_no: Optional[str] = None, batch_no: Optional[str] = None,
with_valuation_rate: bool = True, with_valuation_rate: bool = True,
): ):

View File

@@ -828,9 +828,9 @@ def insert_item_price(args):
): ):
if frappe.has_permission("Item Price", "write"): if frappe.has_permission("Item Price", "write"):
price_list_rate = ( 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") 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( item_price = frappe.db.get_value(

View File

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

View File

@@ -2,8 +2,6 @@ frappe.ready(async () => {
initialise_select_date(); initialise_select_date();
}) })
window.holiday_list = [];
async function initialise_select_date() { async function initialise_select_date() {
navigate_to_page(1); navigate_to_page(1);
await get_global_variables(); await get_global_variables();
@@ -20,7 +18,6 @@ async function get_global_variables() {
window.timezones = (await frappe.call({ window.timezones = (await frappe.call({
method:'erpnext.www.book_appointment.index.get_timezones' method:'erpnext.www.book_appointment.index.get_timezones'
})).message; })).message;
window.holiday_list = window.appointment_settings.holiday_list;
} }
function setup_timezone_selector() { function setup_timezone_selector() {

View File

@@ -26,8 +26,12 @@ def get_context(context):
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
def get_appointment_settings(): def get_appointment_settings():
settings = frappe.get_doc("Appointment Booking Settings") settings = frappe.get_cached_value(
settings.holiday_list = frappe.get_doc("Holiday List", settings.holiday_list) "Appointment Booking Settings",
None,
["advance_booking_days", "appointment_duration", "success_redirect_url"],
as_dict=True,
)
return settings return settings
@@ -106,7 +110,7 @@ def create_appointment(date, time, tz, contact):
appointment.customer_details = contact.get("notes", None) appointment.customer_details = contact.get("notes", None)
appointment.customer_email = contact.get("email", None) appointment.customer_email = contact.get("email", None)
appointment.status = "Open" appointment.status = "Open"
appointment.insert() appointment.insert(ignore_permissions=True)
return appointment return appointment

View File

@@ -2,7 +2,6 @@ import frappe
from frappe.utils.verified_command import verify_request from frappe.utils.verified_command import verify_request
@frappe.whitelist(allow_guest=True)
def get_context(context): def get_context(context):
if not verify_request(): if not verify_request():
context.success = False context.success = False