Merge pull request #42649 from frappe/version-15-hotfix

chore: release v15
This commit is contained in:
ruthra kumar
2024-08-07 14:59:36 +05:30
committed by GitHub
24 changed files with 3416 additions and 158 deletions

View File

@@ -165,8 +165,25 @@ frappe.ui.form.on("Payment Entry", {
filters: filters, filters: filters,
}; };
}); });
},
frm.set_query("sales_taxes_and_charges_template", function () {
return {
filters: {
company: frm.doc.company,
disabled: false,
},
};
});
frm.set_query("purchase_taxes_and_charges_template", function () {
return {
filters: {
company: frm.doc.company,
disabled: false,
},
};
});
},
refresh: function (frm) { refresh: function (frm) {
erpnext.hide_company(frm); erpnext.hide_company(frm);
frm.events.hide_unhide_fields(frm); frm.events.hide_unhide_fields(frm);

View File

@@ -37,7 +37,6 @@ from erpnext.accounts.utils import (
get_account_currency, get_account_currency,
get_balance_on, get_balance_on,
get_outstanding_invoices, get_outstanding_invoices,
get_party_types_from_account_type,
) )
from erpnext.controllers.accounts_controller import ( from erpnext.controllers.accounts_controller import (
AccountsController, AccountsController,
@@ -1119,90 +1118,82 @@ class PaymentEntry(AccountsController):
self.make_advance_gl_entries(cancel=cancel) self.make_advance_gl_entries(cancel=cancel)
def add_party_gl_entries(self, gl_entries): def add_party_gl_entries(self, gl_entries):
if self.party_account: if not self.party_account:
if self.payment_type == "Receive": return
against_account = self.paid_to
else:
against_account = self.paid_from
party_gl_dict = self.get_gl_dict( if self.payment_type == "Receive":
against_account = self.paid_to
else:
against_account = self.paid_from
party_account_type = frappe.db.get_value("Party Type", self.party_type, "account_type")
party_gl_dict = self.get_gl_dict(
{
"account": self.party_account,
"party_type": self.party_type,
"party": self.party,
"against": against_account,
"account_currency": self.party_account_currency,
"cost_center": self.cost_center,
},
item=self,
)
for d in self.get("references"):
# re-defining dr_or_cr for every reference in order to avoid the last value affecting calculation of reverse
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
cost_center = self.cost_center
if d.reference_doctype == "Sales Invoice" and not cost_center:
cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center")
gle = party_gl_dict.copy()
allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(d)
if (
d.reference_doctype in ["Sales Invoice", "Purchase Invoice"]
and d.allocated_amount < 0
and (
(party_account_type == "Receivable" and self.payment_type == "Pay")
or (party_account_type == "Payable" and self.payment_type == "Receive")
)
):
# reversing dr_cr because because it will get reversed in gl processing due to negative amount
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
gle.update(
{ {
"account": self.party_account, dr_or_cr: allocated_amount_in_company_currency,
"party_type": self.party_type, dr_or_cr + "_in_account_currency": d.allocated_amount,
"party": self.party, "against_voucher_type": d.reference_doctype,
"against": against_account, "against_voucher": d.reference_name,
"account_currency": self.party_account_currency, "cost_center": cost_center,
"cost_center": self.cost_center, }
}, )
item=self, gl_entries.append(gle)
if self.unallocated_amount:
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
exchange_rate = self.get_exchange_rate()
base_unallocated_amount = self.unallocated_amount * exchange_rate
gle = party_gl_dict.copy()
gle.update(
{
dr_or_cr + "_in_account_currency": self.unallocated_amount,
dr_or_cr: base_unallocated_amount,
}
) )
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit" if self.book_advance_payments_in_separate_party_account:
for d in self.get("references"):
# re-defining dr_or_cr for every reference in order to avoid the last value affecting calculation of reverse
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
cost_center = self.cost_center
if d.reference_doctype == "Sales Invoice" and not cost_center:
cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center")
gle = party_gl_dict.copy()
allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(d)
reverse_dr_or_cr = 0
if d.reference_doctype in ["Sales Invoice", "Purchase Invoice"]:
is_return = frappe.db.get_value(d.reference_doctype, d.reference_name, "is_return")
payable_party_types = get_party_types_from_account_type("Payable")
receivable_party_types = get_party_types_from_account_type("Receivable")
if (
is_return
and self.party_type in receivable_party_types
and (self.payment_type == "Pay")
):
reverse_dr_or_cr = 1
elif (
is_return
and self.party_type in payable_party_types
and (self.payment_type == "Receive")
):
reverse_dr_or_cr = 1
if is_return and not reverse_dr_or_cr:
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
gle.update( gle.update(
{ {
dr_or_cr: abs(allocated_amount_in_company_currency), "against_voucher_type": "Payment Entry",
dr_or_cr + "_in_account_currency": abs(d.allocated_amount), "against_voucher": self.name,
"against_voucher_type": d.reference_doctype,
"against_voucher": d.reference_name,
"cost_center": cost_center,
} }
) )
gl_entries.append(gle) gl_entries.append(gle)
if self.unallocated_amount:
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
exchange_rate = self.get_exchange_rate()
base_unallocated_amount = self.unallocated_amount * exchange_rate
gle = party_gl_dict.copy()
gle.update(
{
dr_or_cr + "_in_account_currency": self.unallocated_amount,
dr_or_cr: base_unallocated_amount,
}
)
if self.book_advance_payments_in_separate_party_account:
gle.update(
{
"against_voucher_type": "Payment Entry",
"against_voucher": self.name,
}
)
gl_entries.append(gle)
def make_advance_gl_entries( def make_advance_gl_entries(
self, entry: object | dict = None, cancel: bool = 0, update_outstanding: str = "Yes" self, entry: object | dict = None, cancel: bool = 0, update_outstanding: str = "Yes"

View File

@@ -6,7 +6,9 @@ import unittest
import frappe import frappe
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
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.controllers.sales_and_purchase_return import make_return_doc
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.get_item_details import get_item_details from erpnext.stock.get_item_details import get_item_details
@@ -1311,6 +1313,69 @@ class TestPricingRule(unittest.TestCase):
pricing_rule.is_recursive = True pricing_rule.is_recursive = True
self.assertRaises(frappe.ValidationError, pricing_rule.save) self.assertRaises(frappe.ValidationError, pricing_rule.save)
def test_ignore_pricing_rule_for_credit_note(self):
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
pricing_rule = make_pricing_rule(
discount_percentage=20,
selling=1,
buying=1,
priority=1,
title="_Test Pricing Rule",
)
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1)
item = si.items[0]
si.submit()
self.assertEqual(item.discount_percentage, 20)
self.assertEqual(item.rate, 80)
# change discount on pricing rule
pricing_rule.discount_percentage = 30
pricing_rule.save()
credit_note = make_return_doc(si.doctype, si.name)
credit_note.save()
self.assertEqual(credit_note.ignore_pricing_rule, 1)
self.assertEqual(credit_note.pricing_rules, [])
self.assertEqual(credit_note.items[0].discount_percentage, 20)
self.assertEqual(credit_note.items[0].rate, 80)
self.assertEqual(credit_note.items[0].pricing_rules, None)
credit_note.delete()
si.cancel()
def test_ignore_pricing_rule_for_debit_note(self):
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
pricing_rule = make_pricing_rule(
discount_percentage=20,
buying=1,
priority=1,
title="_Test Pricing Rule",
)
pi = make_purchase_invoice(do_not_submit=True, supplier="_Test Supplier 1", qty=1)
item = pi.items[0]
pi.submit()
self.assertEqual(item.discount_percentage, 20)
self.assertEqual(item.rate, 40)
# change discount on pricing rule
pricing_rule.discount_percentage = 30
pricing_rule.save()
# create debit note from purchase invoice
debit_note = make_return_doc(pi.doctype, pi.name)
debit_note.save()
self.assertEqual(debit_note.ignore_pricing_rule, 1)
self.assertEqual(debit_note.pricing_rules, [])
self.assertEqual(debit_note.items[0].discount_percentage, 20)
self.assertEqual(debit_note.items[0].rate, 40)
self.assertEqual(debit_note.items[0].pricing_rules, None)
debit_note.delete()
pi.cancel()
test_dependencies = ["Campaign"] test_dependencies = ["Campaign"]

View File

@@ -375,12 +375,14 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
AND ja.party in %s AND ja.party in %s
AND j.apply_tds = 1 AND j.apply_tds = 1
AND j.tax_withholding_category = %s AND j.tax_withholding_category = %s
AND j.company = %s
""", """,
( (
tax_details.from_date, tax_details.from_date,
tax_details.to_date, tax_details.to_date,
tuple(parties), tuple(parties),
tax_details.get("tax_withholding_category"), tax_details.get("tax_withholding_category"),
company,
), ),
as_dict=1, as_dict=1,
) )
@@ -497,6 +499,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
"unallocated_amount": (">", 0), "unallocated_amount": (">", 0),
"posting_date": ["between", (tax_details.from_date, tax_details.to_date)], "posting_date": ["between", (tax_details.from_date, tax_details.to_date)],
"tax_withholding_category": tax_details.get("tax_withholding_category"), "tax_withholding_category": tax_details.get("tax_withholding_category"),
"company": inv.company,
} }
field = "sum(tax_withholding_net_total)" field = "sum(tax_withholding_net_total)"

View File

@@ -104,7 +104,7 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters):
if total_credit: if total_credit:
data.append(total_credit) data.append(total_credit)
report_summary = get_bs_summary( report_summary, primitive_summary = get_bs_summary(
companies, companies,
asset, asset,
liability, liability,
@@ -175,7 +175,7 @@ def get_profit_loss_data(fiscal_year, companies, columns, filters):
chart = get_pl_chart_data(filters, columns, income, expense, net_profit_loss) chart = get_pl_chart_data(filters, columns, income, expense, net_profit_loss)
report_summary = get_pl_summary( report_summary, primitive_summary = get_pl_summary(
companies, "", income, expense, net_profit_loss, company_currency, filters, True companies, "", income, expense, net_profit_loss, company_currency, filters, True
) )

View File

@@ -209,6 +209,11 @@ frappe.query_reports["General Ledger"] = {
label: __("Ignore Exchange Rate Revaluation Journals"), label: __("Ignore Exchange Rate Revaluation Journals"),
fieldtype: "Check", fieldtype: "Check",
}, },
{
fieldname: "ignore_cr_dr_notes",
label: __("Ignore System Generated Credit / Debit Notes"),
fieldtype: "Check",
},
], ],
}; };

View File

@@ -236,6 +236,20 @@ def get_conditions(filters):
if err_journals: if err_journals:
filters.update({"voucher_no_not_in": [x[0] for x in err_journals]}) filters.update({"voucher_no_not_in": [x[0] for x in err_journals]})
if filters.get("ignore_cr_dr_notes"):
system_generated_cr_dr_journals = frappe.db.get_all(
"Journal Entry",
filters={
"company": filters.get("company"),
"docstatus": 1,
"voucher_type": ("in", ["Credit Note", "Debit Note"]),
"is_system_generated": 1,
},
as_list=True,
)
if system_generated_cr_dr_journals:
filters.update({"voucher_no_not_in": [x[0] for x in system_generated_cr_dr_journals]})
if filters.get("voucher_no_not_in"): if filters.get("voucher_no_not_in"):
conditions.append("voucher_no not in %(voucher_no_not_in)s") conditions.append("voucher_no not in %(voucher_no_not_in)s")

View File

@@ -2,13 +2,32 @@
# MIT License. See license.txt # MIT License. See license.txt
import frappe import frappe
from frappe import qb
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase
from frappe.utils import flt, today from frappe.utils import flt, today
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.general_ledger.general_ledger import execute from erpnext.accounts.report.general_ledger.general_ledger import execute
from erpnext.controllers.sales_and_purchase_return import make_return_doc
class TestGeneralLedger(FrappeTestCase): class TestGeneralLedger(FrappeTestCase):
def setUp(self):
self.company = "_Test Company"
self.clear_old_entries()
def clear_old_entries(self):
doctype_list = [
"GL Entry",
"Payment Ledger Entry",
"Sales Invoice",
"Purchase Invoice",
"Payment Entry",
"Journal Entry",
]
for doctype in doctype_list:
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
def test_foreign_account_balance_after_exchange_rate_revaluation(self): def test_foreign_account_balance_after_exchange_rate_revaluation(self):
""" """
Checks the correctness of balance after exchange rate revaluation Checks the correctness of balance after exchange rate revaluation
@@ -248,3 +267,68 @@ class TestGeneralLedger(FrappeTestCase):
) )
) )
self.assertIn(revaluation_jv.name, set([x.voucher_no for x in data])) self.assertIn(revaluation_jv.name, set([x.voucher_no for x in data]))
def test_ignore_cr_dr_notes_filter(self):
si = create_sales_invoice()
cr_note = make_return_doc(si.doctype, si.name)
cr_note.submit()
pr = frappe.get_doc("Payment Reconciliation")
pr.company = si.company
pr.party_type = "Customer"
pr.party = si.customer
pr.receivable_payable_account = si.debit_to
pr.get_unreconciled_entries()
invoices = [invoice.as_dict() for invoice in pr.invoices if invoice.invoice_number == si.name]
payments = [payment.as_dict() for payment in pr.payments if payment.reference_name == cr_note.name]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.reconcile()
system_generated_journal = frappe.db.get_all(
"Journal Entry",
filters={
"docstatus": 1,
"reference_type": si.doctype,
"reference_name": si.name,
"voucher_type": "Credit Note",
"is_system_generated": True,
},
fields=["name"],
)
self.assertEqual(len(system_generated_journal), 1)
expected = set([si.name, cr_note.name, system_generated_journal[0].name])
# Without ignore_cr_dr_notes
columns, data = execute(
frappe._dict(
{
"company": si.company,
"from_date": si.posting_date,
"to_date": si.posting_date,
"account": [si.debit_to],
"group_by": "Group by Voucher (Consolidated)",
"ignore_cr_dr_notes": False,
}
)
)
actual = set([x.voucher_no for x in data if x.voucher_no])
self.assertEqual(expected, actual)
# Without ignore_cr_dr_notes
expected = set([si.name, cr_note.name])
columns, data = execute(
frappe._dict(
{
"company": si.company,
"from_date": si.posting_date,
"to_date": si.posting_date,
"account": [si.debit_to],
"group_by": "Group by Voucher (Consolidated)",
"ignore_cr_dr_notes": True,
}
)
)
actual = set([x.voucher_no for x in data if x.voucher_no])
self.assertEqual(expected, actual)

View File

@@ -188,11 +188,21 @@ frappe.ui.form.on("Asset", {
frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation); frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation);
if (frm.doc.is_composite_asset) { if (frm.doc.is_composite_asset) {
$(".primary-action").prop("hidden", true); frappe.call({
$(".form-message").text("Capitalize this asset to confirm"); method: "erpnext.assets.doctype.asset.asset.has_active_capitalization",
args: {
asset: frm.doc.name,
},
callback: function (r) {
if (!r.message) {
$(".primary-action").prop("hidden", true);
$(".form-message").text("Capitalize this asset to confirm");
frm.add_custom_button(__("Capitalize Asset"), function () { frm.add_custom_button(__("Capitalize Asset"), function () {
frm.trigger("create_asset_capitalization"); frm.trigger("create_asset_capitalization");
});
}
},
}); });
} }
} }

View File

@@ -221,7 +221,6 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval:!doc.is_composite_asset",
"fieldname": "gross_purchase_amount", "fieldname": "gross_purchase_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Gross Purchase Amount", "label": "Gross Purchase Amount",
@@ -580,7 +579,7 @@
"link_fieldname": "target_asset" "link_fieldname": "target_asset"
} }
], ],
"modified": "2024-07-07 22:27:14.733839", "modified": "2024-08-01 16:39:09.340973",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset", "name": "Asset",

View File

@@ -1035,6 +1035,14 @@ def get_asset_value_after_depreciation(asset_name, finance_book=None):
return asset.get_value_after_depreciation(finance_book) return asset.get_value_after_depreciation(finance_book)
@frappe.whitelist()
def has_active_capitalization(asset):
active_capitalizations = frappe.db.count(
"Asset Capitalization", filters={"target_asset": asset, "docstatus": 1}
)
return active_capitalizations > 0
@frappe.whitelist() @frappe.whitelist()
def split_asset(asset_name, split_qty): def split_asset(asset_name, split_qty):
asset = frappe.get_doc("Asset", asset_name) asset = frappe.get_doc("Asset", asset_name)

View File

@@ -14,6 +14,9 @@ from erpnext.stock.doctype.item.item import get_last_purchase_details, validate_
def update_last_purchase_rate(doc, is_submit) -> None: def update_last_purchase_rate(doc, is_submit) -> None:
"""updates last_purchase_rate in item table for each item""" """updates last_purchase_rate in item table for each item"""
if doc.get("is_internal_supplier"):
return
this_purchase_date = getdate(doc.get("posting_date") or doc.get("transaction_date")) this_purchase_date = getdate(doc.get("posting_date") or doc.get("transaction_date"))
for d in doc.get("items"): for d in doc.get("items"):

View File

@@ -85,7 +85,6 @@ force_item_fields = (
"brand", "brand",
"stock_uom", "stock_uom",
"is_fixed_asset", "is_fixed_asset",
"item_tax_rate",
"pricing_rules", "pricing_rules",
"weight_per_unit", "weight_per_unit",
"weight_uom", "weight_uom",
@@ -743,7 +742,6 @@ class AccountsController(TransactionBase):
args["is_subcontracted"] = self.is_subcontracted args["is_subcontracted"] = self.is_subcontracted
ret = get_item_details(args, self, for_validate=for_validate, overwrite_warehouse=False) ret = get_item_details(args, self, for_validate=for_validate, overwrite_warehouse=False)
for fieldname, value in ret.items(): for fieldname, value in ret.items():
if item.meta.get_field(fieldname) and value is not None: if item.meta.get_field(fieldname) and value is not None:
if item.get(fieldname) is None or fieldname in force_item_fields: if item.get(fieldname) is None or fieldname in force_item_fields:
@@ -753,7 +751,10 @@ class AccountsController(TransactionBase):
fieldname fieldname
): ):
item.set(fieldname, value) item.set(fieldname, value)
elif fieldname == "item_tax_rate" and not (
self.get("is_return") and self.get("return_against")
):
item.set(fieldname, value)
elif fieldname == "serial_no": elif fieldname == "serial_no":
# Ensure that serial numbers are matched against Stock UOM # Ensure that serial numbers are matched against Stock UOM
item_conversion_factor = item.get("conversion_factor") or 1.0 item_conversion_factor = item.get("conversion_factor") or 1.0

View File

@@ -319,6 +319,8 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
def set_missing_values(source, target): def set_missing_values(source, target):
doc = frappe.get_doc(target) doc = frappe.get_doc(target)
doc.is_return = 1 doc.is_return = 1
doc.ignore_pricing_rule = 1
doc.pricing_rules = []
doc.return_against = source.name doc.return_against = source.name
doc.set_warehouse = "" doc.set_warehouse = ""
if doctype == "Sales Invoice" or doctype == "POS Invoice": if doctype == "Sales Invoice" or doctype == "POS Invoice":
@@ -478,6 +480,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
def update_item(source_doc, target_doc, source_parent): def update_item(source_doc, target_doc, source_parent):
target_doc.qty = -1 * source_doc.qty target_doc.qty = -1 * source_doc.qty
target_doc.pricing_rules = None
if doctype in ["Purchase Receipt", "Subcontracting Receipt"]: if doctype in ["Purchase Receipt", "Subcontracting Receipt"]:
returned_qty_map = get_returned_qty_map_for_row( returned_qty_map = get_returned_qty_map_for_row(
source_parent.name, source_parent.supplier, source_doc.name, doctype source_parent.name, source_parent.supplier, source_doc.name, doctype

View File

@@ -92,6 +92,9 @@ class calculate_taxes_and_totals:
self.doc.base_tax_withholding_net_total = sum_base_net_amount self.doc.base_tax_withholding_net_total = sum_base_net_amount
def validate_item_tax_template(self): def validate_item_tax_template(self):
if self.doc.get("is_return") and self.doc.get("return_against"):
return
for item in self._items: for item in self._items:
if item.item_code and item.get("item_tax_template"): if item.item_code and item.get("item_tax_template"):
item_doc = frappe.get_cached_doc("Item", item.item_code) item_doc = frappe.get_cached_doc("Item", item.item_code)
@@ -241,7 +244,6 @@ class calculate_taxes_and_totals:
"tax_fraction_for_current_item", "tax_fraction_for_current_item",
"grand_total_fraction_for_current_item", "grand_total_fraction_for_current_item",
] ]
if tax.charge_type != "Actual" and not ( if tax.charge_type != "Actual" and not (
self.discount_amount_applied and self.doc.apply_discount_on == "Grand Total" self.discount_amount_applied and self.doc.apply_discount_on == "Grand Total"
): ):

View File

@@ -8,7 +8,7 @@ from itertools import groupby
import frappe import frappe
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from frappe import _ from frappe import _
from frappe.utils import cint, flt from frappe.utils import cint, flt, getdate
from erpnext.setup.utils import get_exchange_rate from erpnext.setup.utils import get_exchange_rate
@@ -21,7 +21,15 @@ class SalesPipelineAnalytics:
def __init__(self, filters=None): def __init__(self, filters=None):
self.filters = frappe._dict(filters or {}) self.filters = frappe._dict(filters or {})
def validate_filters(self):
if not self.filters.from_date:
frappe.throw(_("From Date is mandatory"))
if not self.filters.to_date:
frappe.throw(_("To Date is mandatory"))
def run(self): def run(self):
self.validate_filters()
self.get_columns() self.get_columns()
self.get_data() self.get_data()
self.get_chart_data() self.get_chart_data()
@@ -185,7 +193,7 @@ class SalesPipelineAnalytics:
count_or_amount = info.get(based_on) count_or_amount = info.get(based_on)
if self.filters.get("pipeline_by") == "Owner": if self.filters.get("pipeline_by") == "Owner":
if value == "Not Assigned" or value == "[]" or value is None: if value == "Not Assigned" or value == "[]" or value is None or not value:
assigned_to = ["Not Assigned"] assigned_to = ["Not Assigned"]
else: else:
assigned_to = json.loads(value) assigned_to = json.loads(value)
@@ -227,10 +235,9 @@ class SalesPipelineAnalytics:
def get_month_list(self): def get_month_list(self):
month_list = [] month_list = []
current_date = date.today() current_date = getdate(self.filters.get("from_date"))
month_number = date.today().month
for _month in range(month_number, 13): while current_date < getdate(self.filters.get("to_date")):
month_list.append(current_date.strftime("%B")) month_list.append(current_date.strftime("%B"))
current_date = current_date + relativedelta(months=1) current_date = current_date + relativedelta(months=1)

View File

@@ -1,19 +1,21 @@
import unittest import unittest
import frappe import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.crm.report.sales_pipeline_analytics.sales_pipeline_analytics import execute from erpnext.crm.report.sales_pipeline_analytics.sales_pipeline_analytics import execute
class TestSalesPipelineAnalytics(unittest.TestCase): class TestSalesPipelineAnalytics(FrappeTestCase):
@classmethod def setUp(self):
def setUpClass(self):
frappe.db.delete("Opportunity") frappe.db.delete("Opportunity")
create_company() create_company()
create_customer() create_customer()
create_opportunity() create_opportunity()
def test_sales_pipeline_analytics(self): def test_sales_pipeline_analytics(self):
self.from_date = "2021-01-01"
self.to_date = "2021-12-31"
self.check_for_monthly_and_number() self.check_for_monthly_and_number()
self.check_for_monthly_and_amount() self.check_for_monthly_and_amount()
self.check_for_quarterly_and_number() self.check_for_quarterly_and_number()
@@ -28,6 +30,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase):
"status": "Open", "status": "Open",
"opportunity_type": "Sales", "opportunity_type": "Sales",
"company": "Best Test", "company": "Best Test",
"from_date": self.from_date,
"to_date": self.to_date,
} }
report = execute(filters) report = execute(filters)
@@ -43,6 +47,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase):
"status": "Open", "status": "Open",
"opportunity_type": "Sales", "opportunity_type": "Sales",
"company": "Best Test", "company": "Best Test",
"from_date": self.from_date,
"to_date": self.to_date,
} }
report = execute(filters) report = execute(filters)
@@ -59,6 +65,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase):
"status": "Open", "status": "Open",
"opportunity_type": "Sales", "opportunity_type": "Sales",
"company": "Best Test", "company": "Best Test",
"from_date": self.from_date,
"to_date": self.to_date,
} }
report = execute(filters) report = execute(filters)
@@ -74,6 +82,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase):
"status": "Open", "status": "Open",
"opportunity_type": "Sales", "opportunity_type": "Sales",
"company": "Best Test", "company": "Best Test",
"from_date": self.from_date,
"to_date": self.to_date,
} }
report = execute(filters) report = execute(filters)
@@ -90,6 +100,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase):
"status": "Open", "status": "Open",
"opportunity_type": "Sales", "opportunity_type": "Sales",
"company": "Best Test", "company": "Best Test",
"from_date": self.from_date,
"to_date": self.to_date,
} }
report = execute(filters) report = execute(filters)
@@ -105,6 +117,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase):
"status": "Open", "status": "Open",
"opportunity_type": "Sales", "opportunity_type": "Sales",
"company": "Best Test", "company": "Best Test",
"from_date": self.from_date,
"to_date": self.to_date,
} }
report = execute(filters) report = execute(filters)
@@ -121,6 +135,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase):
"status": "Open", "status": "Open",
"opportunity_type": "Sales", "opportunity_type": "Sales",
"company": "Best Test", "company": "Best Test",
"from_date": self.from_date,
"to_date": self.to_date,
} }
report = execute(filters) report = execute(filters)
@@ -136,6 +152,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase):
"status": "Open", "status": "Open",
"opportunity_type": "Sales", "opportunity_type": "Sales",
"company": "Best Test", "company": "Best Test",
"from_date": self.from_date,
"to_date": self.to_date,
} }
report = execute(filters) report = execute(filters)
@@ -153,8 +171,8 @@ class TestSalesPipelineAnalytics(unittest.TestCase):
"opportunity_type": "Sales", "opportunity_type": "Sales",
"company": "Best Test", "company": "Best Test",
"opportunity_source": "Cold Calling", "opportunity_source": "Cold Calling",
"from_date": "2021-08-01", "from_date": self.from_date,
"to_date": "2021-08-31", "to_date": self.to_date,
} }
report = execute(filters) report = execute(filters)

View File

@@ -534,6 +534,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
quotation_to: me.frm.doc.quotation_to, quotation_to: me.frm.doc.quotation_to,
supplier: me.frm.doc.supplier, supplier: me.frm.doc.supplier,
currency: me.frm.doc.currency, currency: me.frm.doc.currency,
is_internal_supplier: me.frm.doc.is_internal_supplier,
is_internal_customer: me.frm.doc.is_internal_customer,
update_stock: update_stock, update_stock: update_stock,
conversion_rate: me.frm.doc.conversion_rate, conversion_rate: me.frm.doc.conversion_rate,
price_list: me.frm.doc.selling_price_list || me.frm.doc.buying_price_list, price_list: me.frm.doc.selling_price_list || me.frm.doc.buying_price_list,
@@ -826,47 +828,76 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
var me = this; var me = this;
var set_pricing = function() { var set_pricing = function() {
if(me.frm.doc.company && me.frm.fields_dict.currency) { if(me.frm.doc.company && me.frm.fields_dict.currency) {
var company_currency = me.get_company_currency();
var company_doc = frappe.get_doc(":Company", me.frm.doc.company);
if (!me.frm.doc.currency) {
me.frm.set_value("currency", company_currency);
}
if (me.frm.doc.currency == company_currency) {
me.frm.set_value("conversion_rate", 1.0);
}
if (me.frm.doc.price_list_currency == company_currency) {
me.frm.set_value('plc_conversion_rate', 1.0);
}
if (company_doc){
if (company_doc.default_letter_head) {
if(me.frm.fields_dict.letter_head) {
me.frm.set_value("letter_head", company_doc.default_letter_head);
}
}
let selling_doctypes_for_tc = ["Sales Invoice", "Quotation", "Sales Order", "Delivery Note"];
if (company_doc.default_selling_terms && frappe.meta.has_field(me.frm.doc.doctype, "tc_name") &&
selling_doctypes_for_tc.includes(me.frm.doc.doctype) && !me.frm.doc.tc_name) {
me.frm.set_value("tc_name", company_doc.default_selling_terms);
}
let buying_doctypes_for_tc = ["Request for Quotation", "Supplier Quotation", "Purchase Order",
"Material Request", "Purchase Receipt"];
// Purchase Invoice is excluded as per issue #3345
if (company_doc.default_buying_terms && frappe.meta.has_field(me.frm.doc.doctype, "tc_name") &&
buying_doctypes_for_tc.includes(me.frm.doc.doctype) && !me.frm.doc.tc_name) {
me.frm.set_value("tc_name", company_doc.default_buying_terms);
}
}
frappe.run_serially([ frappe.run_serially([
() => me.frm.script_manager.trigger("currency"), () => get_party_currency(),
() => me.update_item_tax_map(), () => me.update_item_tax_map(),
() => me.apply_default_taxes(), () => me.apply_default_taxes(),
() => me.apply_pricing_rule() () => me.apply_pricing_rule(),
() => set_terms(),
() => set_letter_head(),
]); ]);
} }
} }
var get_party_currency = function() {
var party_type = frappe.meta.has_field(me.frm.doc.doctype, "customer") ? "Customer" : "Supplier";
var party_name = me.frm.doc[party_type.toLowerCase()];
if (party_name) {
frappe.call({
method: "frappe.client.get_value",
args: {
doctype: party_type,
filters: { name: party_name },
fieldname: "default_currency",
},
callback: function (r) {
if (r.message) {
set_currency(r.message.default_currency);
}
}
})
} else {
set_currency();
}
}
var set_currency = function(party_default_currency) {
var company_currency = me.get_company_currency();
var currency = party_default_currency || company_currency;
if (me.frm.doc.currency != currency) {
me.frm.set_value("currency", currency);
}
if (me.frm.doc.currency == company_currency) {
me.frm.set_value("conversion_rate", 1.0);
}
if (me.frm.doc.price_list_currency == company_currency) {
me.frm.set_value('plc_conversion_rate', 1.0);
}
me.frm.script_manager.trigger("currency");
}
var set_terms = function() {
if (frappe.meta.has_field(me.frm.doc.doctype, "tc_name") && !me.frm.doc.tc_name) {
var company_doc = frappe.get_doc(":Company", me.frm.doc.company);
var selling_doctypes = ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"];
var company_terms_fieldname = selling_doctypes.includes(me.frm.doc.doctype) ? "default_selling_terms" : "default_buying_terms";
if (company_doc && company_doc[company_terms_fieldname]) {
me.frm.set_value("tc_name", company_doc[company_terms_fieldname]);
}
}
}
var set_letter_head = function() {
if(me.frm.fields_dict.letter_head) {
var company_doc = frappe.get_doc(":Company", me.frm.doc.company);
if (company_doc && company_doc.default_letter_head) {
me.frm.set_value("letter_head", company_doc.default_letter_head);
}
}
}
var set_party_account = function(set_pricing) { var set_party_account = function(set_pricing) {
if (["Sales Invoice", "Purchase Invoice"].includes(me.frm.doc.doctype)) { if (["Sales Invoice", "Purchase Invoice"].includes(me.frm.doc.doctype)) {
if(me.frm.doc.doctype=="Sales Invoice") { if(me.frm.doc.doctype=="Sales Invoice") {
@@ -1621,7 +1652,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
"update_stock": ['Sales Invoice', 'Purchase Invoice'].includes(me.frm.doc.doctype) ? cint(me.frm.doc.update_stock) : 0, "update_stock": ['Sales Invoice', 'Purchase Invoice'].includes(me.frm.doc.doctype) ? cint(me.frm.doc.update_stock) : 0,
"conversion_factor": me.frm.doc.conversion_factor, "conversion_factor": me.frm.doc.conversion_factor,
"pos_profile": me.frm.doc.doctype == 'Sales Invoice' ? me.frm.doc.pos_profile : '', "pos_profile": me.frm.doc.doctype == 'Sales Invoice' ? me.frm.doc.pos_profile : '',
"coupon_code": me.frm.doc.coupon_code "coupon_code": me.frm.doc.coupon_code,
"is_internal_supplier": me.frm.doc.is_internal_supplier,
"is_internal_customer": me.frm.doc.is_internal_customer,
}; };
} }
@@ -1949,6 +1982,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
let item_rates = {}; let item_rates = {};
let item_tax_templates = {}; let item_tax_templates = {};
if (me.frm.doc.is_return && me.frm.doc.return_against) return;
$.each(this.frm.doc.items || [], function(i, item) { $.each(this.frm.doc.items || [], function(i, item) {
if (item.item_code) { if (item.item_code) {
// Use combination of name and item code in case same item is added multiple times // Use combination of name and item code in case same item is added multiple times

View File

@@ -193,6 +193,9 @@ erpnext.SalesFunnel = class SalesFunnel {
this.options.width = ($(this.elements.funnel_wrapper).width() * 2.0) / 3.0; this.options.width = ($(this.elements.funnel_wrapper).width() * 2.0) / 3.0;
this.options.height = (Math.sqrt(3) * this.options.width) / 2.0; this.options.height = (Math.sqrt(3) * this.options.width) / 2.0;
const min_height = (this.options.height * 0.1) / this.options.data.length;
const height = this.options.height * 0.9;
// calculate total weightage // calculate total weightage
// as height decreases, area decreases by the square of the reduction // as height decreases, area decreases by the square of the reduction
// hence, compensating by squaring the index value // hence, compensating by squaring the index value
@@ -202,7 +205,7 @@ erpnext.SalesFunnel = class SalesFunnel {
// calculate height for each data // calculate height for each data
$.each(this.options.data, function (i, d) { $.each(this.options.data, function (i, d) {
d.height = (me.options.height * d.value * Math.pow(i + 1, 2)) / me.options.total_weightage; d.height = (height * d.value * Math.pow(i + 1, 2)) / me.options.total_weightage + min_height;
}); });
this.elements.canvas = $("<canvas></canvas>") this.elements.canvas = $("<canvas></canvas>")

View File

@@ -807,6 +807,9 @@ def get_price_list_rate(args, item_doc, out=None):
if price_list_rate is None or frappe.db.get_single_value( if price_list_rate is None or frappe.db.get_single_value(
"Stock Settings", "update_existing_price_list_rate" "Stock Settings", "update_existing_price_list_rate"
): ):
if args.get("is_internal_supplier") or args.get("is_internal_customer"):
return out
if args.price_list and args.rate: if args.price_list and args.rate:
insert_item_price(args) insert_item_price(args)
@@ -818,7 +821,11 @@ def get_price_list_rate(args, item_doc, out=None):
if frappe.db.get_single_value("Buying Settings", "disable_last_purchase_rate"): if frappe.db.get_single_value("Buying Settings", "disable_last_purchase_rate"):
return out return out
if not out.price_list_rate and args.transaction_type == "buying": if (
not args.get("is_internal_supplier")
and not out.price_list_rate
and args.transaction_type == "buying"
):
from erpnext.stock.doctype.item.item import get_last_purchase_details from erpnext.stock.doctype.item.item import get_last_purchase_details
out.update(get_last_purchase_details(item_doc.name, args.name, args.conversion_rate)) out.update(get_last_purchase_details(item_doc.name, args.name, args.conversion_rate))

View File

@@ -54,6 +54,12 @@ def get_columns(filters):
"width": 150, "width": 150,
"options": "Batch", "options": "Batch",
}, },
{
"label": _("Expiry Date"),
"fieldname": "expiry_date",
"fieldtype": "Date",
"width": 120,
},
{"label": _("Balance Qty"), "fieldname": "balance_qty", "fieldtype": "Float", "width": 150}, {"label": _("Balance Qty"), "fieldname": "balance_qty", "fieldtype": "Float", "width": 150},
] ]
) )
@@ -97,6 +103,7 @@ def get_batchwise_data_from_stock_ledger(filters):
table.item_code, table.item_code,
table.batch_no, table.batch_no,
table.warehouse, table.warehouse,
batch.expiry_date,
Sum(table.actual_qty).as_("balance_qty"), Sum(table.actual_qty).as_("balance_qty"),
) )
.where(table.is_cancelled == 0) .where(table.is_cancelled == 0)
@@ -127,6 +134,7 @@ def get_batchwise_data_from_serial_batch_bundle(batchwise_data, filters):
table.item_code, table.item_code,
ch_table.batch_no, ch_table.batch_no,
table.warehouse, table.warehouse,
batch.expiry_date,
Sum(ch_table.qty).as_("balance_qty"), Sum(ch_table.qty).as_("balance_qty"),
) )
.where((table.is_cancelled == 0) & (table.docstatus == 1)) .where((table.is_cancelled == 0) & (table.docstatus == 1))
@@ -152,10 +160,14 @@ def get_query_based_on_filters(query, batch, table, filters):
if filters.batch_no: if filters.batch_no:
query = query.where(batch.name == filters.batch_no) query = query.where(batch.name == filters.batch_no)
if not filters.include_expired_batches: if filters.to_date == today():
query = query.where((batch.expiry_date >= today()) | (batch.expiry_date.isnull())) if not filters.include_expired_batches:
if filters.to_date == today(): query = query.where((batch.expiry_date >= today()) | (batch.expiry_date.isnull()))
query = query.where(batch.batch_qty > 0)
query = query.where(batch.batch_qty > 0)
else:
query = query.where(table.posting_date <= filters.to_date)
if filters.warehouse: if filters.warehouse:
lft, rgt = frappe.db.get_value("Warehouse", filters.warehouse, ["lft", "rgt"]) lft, rgt = frappe.db.get_value("Warehouse", filters.warehouse, ["lft", "rgt"])

View File

@@ -465,10 +465,13 @@ class FIFOSlots:
) )
) )
for field in ["item_code", "warehouse"]: for field in ["item_code"]:
if self.filters.get(field): if self.filters.get(field):
query = query.where(bundle[field] == self.filters.get(field)) query = query.where(bundle[field] == self.filters.get(field))
if self.filters.get("warehouse"):
query = self.__get_warehouse_conditions(bundle, query)
bundle_wise_serial_nos = frappe._dict({}) bundle_wise_serial_nos = frappe._dict({})
for bundle_name, serial_no in query.run(): for bundle_name, serial_no in query.run():
bundle_wise_serial_nos.setdefault(bundle_name, []).append(serial_no) bundle_wise_serial_nos.setdefault(bundle_name, []).append(serial_no)

View File

@@ -114,18 +114,23 @@ def validate_filters(filters):
def get_warehouse_list(filters): def get_warehouse_list(filters):
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents if not filters.get("warehouse"):
return frappe.get_all(
"Warehouse",
filters={"company": filters.get("company"), "is_group": 0},
fields=["name"],
order_by="name",
)
wh = frappe.qb.DocType("Warehouse") warehouse = frappe.qb.DocType("Warehouse")
query = frappe.qb.from_(wh).select(wh.name).where(wh.is_group == 0) lft, rgt = frappe.db.get_value("Warehouse", filters.get("warehouse"), ["lft", "rgt"])
user_permitted_warehouse = get_permitted_documents("Warehouse") return (
if user_permitted_warehouse: frappe.qb.from_(warehouse)
query = query.where(wh.name.isin(set(user_permitted_warehouse))) .select("name")
elif filters.get("warehouse"): .where((warehouse.lft >= lft) & (warehouse.rgt <= rgt))
query = query.where(wh.name == filters.get("warehouse")) .run(as_dict=True)
)
return query.run(as_dict=True)
def add_warehouse_column(columns, warehouse_list): def add_warehouse_column(columns, warehouse_list):

File diff suppressed because it is too large Load Diff