mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-23 16:48:30 +00:00
Merge pull request #43254 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -22,8 +22,10 @@ class TestCostCenterAllocation(unittest.TestCase):
|
||||
cost_centers = [
|
||||
"Main Cost Center 1",
|
||||
"Main Cost Center 2",
|
||||
"Main Cost Center 3",
|
||||
"Sub Cost Center 1",
|
||||
"Sub Cost Center 2",
|
||||
"Sub Cost Center 3",
|
||||
]
|
||||
for cc in cost_centers:
|
||||
create_cost_center(cost_center_name=cc, company="_Test Company")
|
||||
@@ -36,7 +38,7 @@ class TestCostCenterAllocation(unittest.TestCase):
|
||||
)
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Cash - _TC", "Sales - _TC", 100, cost_center="Main Cost Center 1 - _TC", submit=True
|
||||
"Cash - _TC", "Sales - _TC", 100, cost_center="Main Cost Center 1 - _TC", submit=True
|
||||
)
|
||||
|
||||
expected_values = [["Sub Cost Center 1 - _TC", 0.0, 60], ["Sub Cost Center 2 - _TC", 0.0, 40]]
|
||||
@@ -120,7 +122,7 @@ class TestCostCenterAllocation(unittest.TestCase):
|
||||
def test_valid_from_based_on_existing_gle(self):
|
||||
# GLE posted against Sub Cost Center 1 on today
|
||||
jv = make_journal_entry(
|
||||
"_Test Cash - _TC",
|
||||
"Cash - _TC",
|
||||
"Sales - _TC",
|
||||
100,
|
||||
cost_center="Main Cost Center 1 - _TC",
|
||||
@@ -141,6 +143,53 @@ class TestCostCenterAllocation(unittest.TestCase):
|
||||
|
||||
jv.cancel()
|
||||
|
||||
def test_multiple_cost_center_allocation_on_same_main_cost_center(self):
|
||||
coa1 = create_cost_center_allocation(
|
||||
"_Test Company",
|
||||
"Main Cost Center 3 - _TC",
|
||||
{"Sub Cost Center 1 - _TC": 30, "Sub Cost Center 2 - _TC": 30, "Sub Cost Center 3 - _TC": 40},
|
||||
valid_from=add_days(today(), -5),
|
||||
)
|
||||
|
||||
coa2 = create_cost_center_allocation(
|
||||
"_Test Company",
|
||||
"Main Cost Center 3 - _TC",
|
||||
{"Sub Cost Center 1 - _TC": 50, "Sub Cost Center 2 - _TC": 50},
|
||||
valid_from=add_days(today(), -1),
|
||||
)
|
||||
|
||||
jv = make_journal_entry(
|
||||
"Cash - _TC",
|
||||
"Sales - _TC",
|
||||
100,
|
||||
cost_center="Main Cost Center 3 - _TC",
|
||||
posting_date=today(),
|
||||
submit=True,
|
||||
)
|
||||
|
||||
expected_values = {"Sub Cost Center 1 - _TC": 50, "Sub Cost Center 2 - _TC": 50}
|
||||
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
gl_entries = (
|
||||
frappe.qb.from_(gle)
|
||||
.select(gle.cost_center, gle.debit, gle.credit)
|
||||
.where(gle.voucher_type == "Journal Entry")
|
||||
.where(gle.voucher_no == jv.name)
|
||||
.where(gle.account == "Sales - _TC")
|
||||
.orderby(gle.cost_center)
|
||||
).run(as_dict=1)
|
||||
|
||||
self.assertTrue(gl_entries)
|
||||
|
||||
for gle in gl_entries:
|
||||
self.assertTrue(gle.cost_center in expected_values)
|
||||
self.assertEqual(gle.debit, 0)
|
||||
self.assertEqual(gle.credit, expected_values[gle.cost_center])
|
||||
|
||||
coa1.cancel()
|
||||
coa2.cancel()
|
||||
jv.cancel()
|
||||
|
||||
|
||||
def create_cost_center_allocation(
|
||||
company,
|
||||
|
||||
@@ -360,21 +360,23 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
|
||||
|
||||
accounts_add(doc, cdt, cdn) {
|
||||
var row = frappe.get_doc(cdt, cdn);
|
||||
row.exchange_rate = 1;
|
||||
$.each(doc.accounts, function (i, d) {
|
||||
if (d.account && d.party && d.party_type) {
|
||||
row.account = d.account;
|
||||
row.party = d.party;
|
||||
row.party_type = d.party_type;
|
||||
row.exchange_rate = d.exchange_rate;
|
||||
}
|
||||
});
|
||||
|
||||
// set difference
|
||||
if (doc.difference) {
|
||||
if (doc.difference > 0) {
|
||||
row.credit_in_account_currency = doc.difference;
|
||||
row.credit_in_account_currency = doc.difference / row.exchange_rate;
|
||||
row.credit = doc.difference;
|
||||
} else {
|
||||
row.debit_in_account_currency = -doc.difference;
|
||||
row.debit_in_account_currency = -doc.difference / row.exchange_rate;
|
||||
row.debit = -doc.difference;
|
||||
}
|
||||
}
|
||||
@@ -680,6 +682,7 @@ $.extend(erpnext.journal_entry, {
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
$.extend(d, r.message);
|
||||
erpnext.journal_entry.set_amount_on_last_row(frm, dt, dn);
|
||||
erpnext.journal_entry.set_debit_credit_in_company_currency(frm, dt, dn);
|
||||
refresh_field("accounts");
|
||||
}
|
||||
@@ -687,4 +690,26 @@ $.extend(erpnext.journal_entry, {
|
||||
});
|
||||
}
|
||||
},
|
||||
set_amount_on_last_row: function (frm, dt, dn) {
|
||||
let row = locals[dt][dn];
|
||||
let length = frm.doc.accounts.length;
|
||||
if (row.idx != length) return;
|
||||
|
||||
let difference = frm.doc.accounts.reduce((total, row) => {
|
||||
if (row.idx == length) return total;
|
||||
|
||||
return total + row.debit - row.credit;
|
||||
}, 0);
|
||||
|
||||
if (difference) {
|
||||
if (difference > 0) {
|
||||
row.credit_in_account_currency = difference / row.exchange_rate;
|
||||
row.credit = difference;
|
||||
} else {
|
||||
row.debit_in_account_currency = -difference / row.exchange_rate;
|
||||
row.debit = -difference;
|
||||
}
|
||||
}
|
||||
refresh_field("accounts");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -195,6 +195,11 @@ class JournalEntry(AccountsController):
|
||||
self.update_booked_depreciation()
|
||||
|
||||
def on_update_after_submit(self):
|
||||
# Flag will be set on Reconciliation
|
||||
# Reconciliation tool will anyways repost ledger entries. So, no need to check and do implicit repost.
|
||||
if self.flags.get("ignore_reposting_on_reconciliation"):
|
||||
return
|
||||
|
||||
self.needs_repost = self.check_if_fields_updated(fields_to_check=[], child_tables={"accounts": []})
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
|
||||
@@ -385,7 +385,15 @@ frappe.ui.form.on("Payment Entry", {
|
||||
payment_type: function (frm) {
|
||||
if (frm.doc.payment_type == "Internal Transfer") {
|
||||
$.each(
|
||||
["party", "party_balance", "paid_from", "paid_to", "references", "total_allocated_amount"],
|
||||
[
|
||||
"party",
|
||||
"party_type",
|
||||
"party_balance",
|
||||
"paid_from",
|
||||
"paid_to",
|
||||
"references",
|
||||
"total_allocated_amount",
|
||||
],
|
||||
function (i, field) {
|
||||
frm.set_value(field, null);
|
||||
}
|
||||
|
||||
@@ -1791,6 +1791,79 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
# 'Is Opening' should always be 'No' for normal advance payments
|
||||
self.assertEqual(gl_with_opening_set, [])
|
||||
|
||||
@change_settings("Accounts Settings", {"delete_linked_ledger_entries": 1})
|
||||
def test_delete_linked_exchange_gain_loss_journal(self):
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||
make_customer,
|
||||
)
|
||||
|
||||
debtors = create_account(
|
||||
account_name="Debtors USD",
|
||||
parent_account="Accounts Receivable - _TC",
|
||||
company="_Test Company",
|
||||
account_currency="USD",
|
||||
account_type="Receivable",
|
||||
)
|
||||
|
||||
# create a customer
|
||||
customer = make_customer(customer="_Test Party USD")
|
||||
cust_doc = frappe.get_doc("Customer", customer)
|
||||
cust_doc.default_currency = "USD"
|
||||
test_account_details = {
|
||||
"company": "_Test Company",
|
||||
"account": debtors,
|
||||
}
|
||||
cust_doc.append("accounts", test_account_details)
|
||||
cust_doc.save()
|
||||
|
||||
# create a sales invoice
|
||||
si = create_sales_invoice(
|
||||
customer=customer,
|
||||
currency="USD",
|
||||
conversion_rate=83.970000000,
|
||||
debit_to=debtors,
|
||||
do_not_save=1,
|
||||
)
|
||||
si.party_account_currency = "USD"
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
# create a payment entry for the invoice
|
||||
pe = get_payment_entry("Sales Invoice", si.name)
|
||||
pe.reference_no = "1"
|
||||
pe.reference_date = frappe.utils.nowdate()
|
||||
pe.paid_amount = 100
|
||||
pe.source_exchange_rate = 90
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": "_Test Exchange Gain/Loss - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"amount": 2710,
|
||||
},
|
||||
)
|
||||
pe.save()
|
||||
pe.submit()
|
||||
|
||||
# check creation of journal entry
|
||||
jv = frappe.get_all(
|
||||
"Journal Entry Account",
|
||||
{"reference_type": pe.doctype, "reference_name": pe.name, "docstatus": 1},
|
||||
pluck="parent",
|
||||
)
|
||||
self.assertTrue(jv)
|
||||
|
||||
# check cancellation of payment entry and journal entry
|
||||
pe.cancel()
|
||||
self.assertTrue(pe.docstatus == 2)
|
||||
self.assertTrue(frappe.db.get_value("Journal Entry", {"name": jv[0]}, "docstatus") == 2)
|
||||
|
||||
# check deletion of payment entry and journal entry
|
||||
pe.delete()
|
||||
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, pe.doctype, pe.name)
|
||||
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, "Journal Entry", jv[0])
|
||||
|
||||
|
||||
def create_payment_entry(**args):
|
||||
payment_entry = frappe.new_doc("Payment Entry")
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, flt, nowdate
|
||||
from frappe.utils import add_days, add_years, flt, getdate, nowdate, today
|
||||
|
||||
from erpnext import get_default_cost_center
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
@@ -13,6 +13,7 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_pay
|
||||
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.party import get_party_account
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
|
||||
@@ -1845,6 +1846,78 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
|
||||
def test_reconciliation_on_closed_period_payment(self):
|
||||
# create backdated fiscal year
|
||||
first_fy_start_date = frappe.db.get_value("Fiscal Year", {"disabled": 0}, "min(year_start_date)")
|
||||
prev_fy_start_date = add_years(first_fy_start_date, -1)
|
||||
prev_fy_end_date = add_days(first_fy_start_date, -1)
|
||||
create_fiscal_year(
|
||||
company=self.company, year_start_date=prev_fy_start_date, year_end_date=prev_fy_end_date
|
||||
)
|
||||
|
||||
# make journal entry for previous year
|
||||
je_1 = frappe.new_doc("Journal Entry")
|
||||
je_1.posting_date = add_days(prev_fy_start_date, 20)
|
||||
je_1.company = self.company
|
||||
je_1.user_remark = "test"
|
||||
je_1.set(
|
||||
"accounts",
|
||||
[
|
||||
{
|
||||
"account": self.debit_to,
|
||||
"cost_center": self.cost_center,
|
||||
"party_type": "Customer",
|
||||
"party": self.customer,
|
||||
"debit_in_account_currency": 0,
|
||||
"credit_in_account_currency": 1000,
|
||||
},
|
||||
{
|
||||
"account": self.bank,
|
||||
"cost_center": self.sub_cc.name,
|
||||
"credit_in_account_currency": 0,
|
||||
"debit_in_account_currency": 500,
|
||||
},
|
||||
{
|
||||
"account": self.cash,
|
||||
"cost_center": self.sub_cc.name,
|
||||
"credit_in_account_currency": 0,
|
||||
"debit_in_account_currency": 500,
|
||||
},
|
||||
],
|
||||
)
|
||||
je_1.submit()
|
||||
|
||||
# make period closing voucher
|
||||
pcv = make_period_closing_voucher(
|
||||
company=self.company, cost_center=self.cost_center, posting_date=prev_fy_end_date
|
||||
)
|
||||
pcv.reload()
|
||||
# check if period closing voucher is completed
|
||||
self.assertEqual(pcv.gle_processing_status, "Completed")
|
||||
|
||||
# make journal entry for active year
|
||||
je_2 = self.create_journal_entry(
|
||||
acc1=self.debit_to, acc2=self.income_account, amount=1000, posting_date=today()
|
||||
)
|
||||
je_2.accounts[0].party_type = "Customer"
|
||||
je_2.accounts[0].party = self.customer
|
||||
je_2.submit()
|
||||
|
||||
# process reconciliation on closed period payment
|
||||
pr = self.create_payment_reconciliation(party_is_customer=True)
|
||||
pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = None
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [invoice.as_dict() for invoice in pr.invoices]
|
||||
payments = [payment.as_dict() for payment in pr.payments]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
je_1.reload()
|
||||
je_2.reload()
|
||||
|
||||
# check whether the payment reconciliation is done on the closed period
|
||||
self.assertEqual(pr.get("invoices"), [])
|
||||
self.assertEqual(pr.get("payments"), [])
|
||||
|
||||
|
||||
def make_customer(customer_name, currency=None):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
@@ -1872,3 +1945,61 @@ def make_supplier(supplier_name, currency=None):
|
||||
return supplier.name
|
||||
else:
|
||||
return supplier_name
|
||||
|
||||
|
||||
def create_fiscal_year(company, year_start_date, year_end_date):
|
||||
fy_docname = frappe.db.exists(
|
||||
"Fiscal Year", {"year_start_date": year_start_date, "year_end_date": year_end_date}
|
||||
)
|
||||
if not fy_docname:
|
||||
fy_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Fiscal Year",
|
||||
"year": f"{getdate(year_start_date).year}-{getdate(year_end_date).year}",
|
||||
"year_start_date": year_start_date,
|
||||
"year_end_date": year_end_date,
|
||||
"companies": [{"company": company}],
|
||||
}
|
||||
).save()
|
||||
return fy_doc
|
||||
else:
|
||||
fy_doc = frappe.get_doc("Fiscal Year", fy_docname)
|
||||
if not frappe.db.exists("Fiscal Year Company", {"parent": fy_docname, "company": company}):
|
||||
fy_doc.append("companies", {"company": company})
|
||||
fy_doc.save()
|
||||
return fy_doc
|
||||
|
||||
|
||||
def make_period_closing_voucher(company, cost_center, posting_date=None, submit=True):
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
|
||||
parent_account = frappe.db.get_value(
|
||||
"Account", {"company": company, "account_name": "Current Liabilities", "is_group": 1}, "name"
|
||||
)
|
||||
surplus_account = create_account(
|
||||
account_name="Reserve and Surplus",
|
||||
is_group=0,
|
||||
company=company,
|
||||
root_type="Liability",
|
||||
report_type="Balance Sheet",
|
||||
account_currency="INR",
|
||||
parent_account=parent_account,
|
||||
doctype="Account",
|
||||
)
|
||||
pcv = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Period Closing Voucher",
|
||||
"transaction_date": posting_date or today(),
|
||||
"posting_date": posting_date or today(),
|
||||
"company": company,
|
||||
"fiscal_year": get_fiscal_year(posting_date or today(), company=company)[0],
|
||||
"cost_center": cost_center,
|
||||
"closing_account_head": surplus_account,
|
||||
"remarks": "test",
|
||||
}
|
||||
)
|
||||
pcv.insert()
|
||||
if submit:
|
||||
pcv.submit()
|
||||
|
||||
return pcv
|
||||
|
||||
@@ -419,7 +419,8 @@
|
||||
"depends_on": "eval:doc.rate_or_discount==\"Rate\"",
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Rate"
|
||||
"label": "Rate",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -647,7 +648,7 @@
|
||||
"icon": "fa fa-gift",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2024-05-17 13:16:34.496704",
|
||||
"modified": "2024-09-16 18:14:51.314765",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pricing Rule",
|
||||
@@ -709,4 +710,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title"
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
|
||||
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
|
||||
@@ -14,7 +15,7 @@ from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.get_item_details import get_item_details
|
||||
|
||||
|
||||
class TestPricingRule(unittest.TestCase):
|
||||
class TestPricingRule(FrappeTestCase):
|
||||
def setUp(self):
|
||||
delete_existing_pricing_rules()
|
||||
setup_pricing_rule_data()
|
||||
|
||||
@@ -648,11 +648,11 @@ frappe.ui.form.on("Purchase Invoice", {
|
||||
},
|
||||
|
||||
onload: function (frm) {
|
||||
if (frm.doc.__onload && frm.is_new()) {
|
||||
if (frm.doc.supplier) {
|
||||
if (frm.doc.__onload && frm.doc.supplier) {
|
||||
if (frm.is_new()) {
|
||||
frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0;
|
||||
}
|
||||
if (!frm.doc.__onload.enable_apply_tds) {
|
||||
if (!frm.doc.__onload.supplier_tds) {
|
||||
frm.set_df_property("apply_tds", "read_only", 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1271,6 +1271,7 @@
|
||||
"fieldtype": "Select",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"no_copy": 1,
|
||||
"options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nPartly Paid\nUnpaid\nOverdue\nCancelled\nInternal Transfer",
|
||||
"print_hide": 1
|
||||
},
|
||||
@@ -1630,7 +1631,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-07-25 19:42:36.931278",
|
||||
"modified": "2024-09-11 12:59:19.130593",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -346,22 +346,6 @@ class PurchaseInvoice(BuyingController):
|
||||
self.tax_withholding_category = tds_category
|
||||
self.set_onload("supplier_tds", tds_category)
|
||||
|
||||
# If Linked Purchase Order has TDS applied, enable 'apply_tds' checkbox
|
||||
if purchase_orders := [x.purchase_order for x in self.items if x.purchase_order]:
|
||||
po = qb.DocType("Purchase Order")
|
||||
po_with_tds = (
|
||||
qb.from_(po)
|
||||
.select(po.name)
|
||||
.where(
|
||||
po.docstatus.eq(1)
|
||||
& (po.name.isin(purchase_orders))
|
||||
& (po.apply_tds.eq(1))
|
||||
& (po.tax_withholding_category.notnull())
|
||||
)
|
||||
.run()
|
||||
)
|
||||
self.set_onload("enable_apply_tds", True if po_with_tds else False)
|
||||
|
||||
super().set_missing_values(for_validate)
|
||||
|
||||
def validate_credit_to_acc(self):
|
||||
|
||||
@@ -179,50 +179,53 @@ def process_gl_map(gl_map, merge_entries=True, precision=None):
|
||||
|
||||
|
||||
def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None):
|
||||
cost_center_allocation = get_cost_center_allocation_data(gl_map[0]["company"], gl_map[0]["posting_date"])
|
||||
if not cost_center_allocation:
|
||||
return gl_map
|
||||
|
||||
new_gl_map = []
|
||||
for d in gl_map:
|
||||
cost_center = d.get("cost_center")
|
||||
|
||||
# Validate budget against main cost center
|
||||
validate_expense_against_budget(d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision))
|
||||
|
||||
if cost_center and cost_center_allocation.get(cost_center):
|
||||
for sub_cost_center, percentage in cost_center_allocation.get(cost_center, {}).items():
|
||||
gle = copy.deepcopy(d)
|
||||
gle.cost_center = sub_cost_center
|
||||
for field in ("debit", "credit", "debit_in_account_currency", "credit_in_account_currency"):
|
||||
gle[field] = flt(flt(d.get(field)) * percentage / 100, precision)
|
||||
new_gl_map.append(gle)
|
||||
else:
|
||||
cost_center_allocation = get_cost_center_allocation_data(
|
||||
gl_map[0]["company"], gl_map[0]["posting_date"], cost_center
|
||||
)
|
||||
if not cost_center_allocation:
|
||||
new_gl_map.append(d)
|
||||
continue
|
||||
|
||||
for sub_cost_center, percentage in cost_center_allocation:
|
||||
gle = copy.deepcopy(d)
|
||||
gle.cost_center = sub_cost_center
|
||||
for field in ("debit", "credit", "debit_in_account_currency", "credit_in_account_currency"):
|
||||
gle[field] = flt(flt(d.get(field)) * percentage / 100, precision)
|
||||
new_gl_map.append(gle)
|
||||
|
||||
return new_gl_map
|
||||
|
||||
|
||||
def get_cost_center_allocation_data(company, posting_date):
|
||||
par = frappe.qb.DocType("Cost Center Allocation")
|
||||
child = frappe.qb.DocType("Cost Center Allocation Percentage")
|
||||
def get_cost_center_allocation_data(company, posting_date, cost_center):
|
||||
cost_center_allocation = frappe.db.get_value(
|
||||
"Cost Center Allocation",
|
||||
{
|
||||
"docstatus": 1,
|
||||
"company": company,
|
||||
"valid_from": ("<=", posting_date),
|
||||
"main_cost_center": cost_center,
|
||||
},
|
||||
pluck="name",
|
||||
order_by="valid_from desc",
|
||||
)
|
||||
|
||||
records = (
|
||||
frappe.qb.from_(par)
|
||||
.inner_join(child)
|
||||
.on(par.name == child.parent)
|
||||
.select(par.main_cost_center, child.cost_center, child.percentage)
|
||||
.where(par.docstatus == 1)
|
||||
.where(par.company == company)
|
||||
.where(par.valid_from <= posting_date)
|
||||
.orderby(par.valid_from, order=frappe.qb.desc)
|
||||
).run(as_dict=True)
|
||||
if not cost_center_allocation:
|
||||
return []
|
||||
|
||||
cc_allocation = frappe._dict()
|
||||
for d in records:
|
||||
cc_allocation.setdefault(d.main_cost_center, frappe._dict()).setdefault(d.cost_center, d.percentage)
|
||||
records = frappe.db.get_all(
|
||||
"Cost Center Allocation Percentage",
|
||||
{"parent": cost_center_allocation},
|
||||
["cost_center", "percentage"],
|
||||
as_list=True,
|
||||
)
|
||||
|
||||
return cc_allocation
|
||||
return records
|
||||
|
||||
|
||||
def merge_similar_entries(gl_map, precision=None):
|
||||
|
||||
@@ -232,9 +232,15 @@ def get_group_by_asset_data(filters):
|
||||
def get_assets_for_grouped_by_category(filters):
|
||||
condition = ""
|
||||
if filters.get("asset_category"):
|
||||
condition = " and a.asset_category = '{}'".format(filters.get("asset_category"))
|
||||
condition = f" and a.asset_category = '{filters.get('asset_category')}'"
|
||||
finance_book_filter = ""
|
||||
if filters.get("finance_book"):
|
||||
finance_book_filter += " and ifnull(gle.finance_book, '')=%(finance_book)s"
|
||||
condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = a.name and ads.finance_book = %(finance_book)s)"
|
||||
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
f"""
|
||||
SELECT results.asset_category,
|
||||
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
|
||||
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
|
||||
@@ -264,7 +270,14 @@ def get_assets_for_grouped_by_category(filters):
|
||||
aca.parent = a.asset_category and aca.company_name = %(company)s
|
||||
join `tabCompany` company on
|
||||
company.name = %(company)s
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) {0}
|
||||
where
|
||||
a.docstatus=1
|
||||
and a.company=%(company)s
|
||||
and a.purchase_date <= %(to_date)s
|
||||
and gle.debit != 0
|
||||
and gle.is_cancelled = 0
|
||||
and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
|
||||
{condition} {finance_book_filter}
|
||||
group by a.asset_category
|
||||
union
|
||||
SELECT a.asset_category,
|
||||
@@ -280,11 +293,16 @@ def get_assets_for_grouped_by_category(filters):
|
||||
end), 0) as depreciation_eliminated_during_the_period,
|
||||
0 as depreciation_amount_during_the_period
|
||||
from `tabAsset` a
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {0}
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
|
||||
group by a.asset_category) as results
|
||||
group by results.asset_category
|
||||
""".format(condition),
|
||||
{"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company},
|
||||
""",
|
||||
{
|
||||
"to_date": filters.to_date,
|
||||
"from_date": filters.from_date,
|
||||
"company": filters.company,
|
||||
"finance_book": filters.get("finance_book", ""),
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
@@ -292,9 +310,15 @@ def get_assets_for_grouped_by_category(filters):
|
||||
def get_assets_for_grouped_by_asset(filters):
|
||||
condition = ""
|
||||
if filters.get("asset"):
|
||||
condition = " and a.name = '{}'".format(filters.get("asset"))
|
||||
condition = f" and a.name = '{filters.get('asset')}'"
|
||||
finance_book_filter = ""
|
||||
if filters.get("finance_book"):
|
||||
finance_book_filter += " and ifnull(gle.finance_book, '')=%(finance_book)s"
|
||||
condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = a.name and ads.finance_book = %(finance_book)s)"
|
||||
|
||||
# nosemgrep
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
f"""
|
||||
SELECT results.name as asset,
|
||||
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
|
||||
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
|
||||
@@ -324,7 +348,14 @@ def get_assets_for_grouped_by_asset(filters):
|
||||
aca.parent = a.asset_category and aca.company_name = %(company)s
|
||||
join `tabCompany` company on
|
||||
company.name = %(company)s
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) {0}
|
||||
where
|
||||
a.docstatus=1
|
||||
and a.company=%(company)s
|
||||
and a.purchase_date <= %(to_date)s
|
||||
and gle.debit != 0
|
||||
and gle.is_cancelled = 0
|
||||
and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
|
||||
{finance_book_filter} {condition}
|
||||
group by a.name
|
||||
union
|
||||
SELECT a.name as name,
|
||||
@@ -340,11 +371,16 @@ def get_assets_for_grouped_by_asset(filters):
|
||||
end), 0) as depreciation_eliminated_during_the_period,
|
||||
0 as depreciation_amount_during_the_period
|
||||
from `tabAsset` a
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {0}
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
|
||||
group by a.name) as results
|
||||
group by results.name
|
||||
""".format(condition),
|
||||
{"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company},
|
||||
""",
|
||||
{
|
||||
"to_date": filters.to_date,
|
||||
"from_date": filters.from_date,
|
||||
"company": filters.company,
|
||||
"finance_book": filters.get("finance_book", ""),
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
@@ -665,6 +665,8 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
|
||||
|
||||
# will work as update after submit
|
||||
journal_entry.flags.ignore_validate_update_after_submit = True
|
||||
# Ledgers will be reposted by Reconciliation tool
|
||||
journal_entry.flags.ignore_reposting_on_reconciliation = True
|
||||
if not do_not_save:
|
||||
journal_entry.save(ignore_permissions=True)
|
||||
|
||||
@@ -745,40 +747,74 @@ def cancel_exchange_gain_loss_journal(
|
||||
Cancel Exchange Gain/Loss for Sales/Purchase Invoice, if they have any.
|
||||
"""
|
||||
if parent_doc.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]:
|
||||
journals = frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
filters={
|
||||
"reference_type": parent_doc.doctype,
|
||||
"reference_name": parent_doc.name,
|
||||
"docstatus": 1,
|
||||
},
|
||||
fields=["parent"],
|
||||
as_list=1,
|
||||
gain_loss_journals = get_linked_exchange_gain_loss_journal(
|
||||
referenced_dt=parent_doc.doctype, referenced_dn=parent_doc.name, je_docstatus=1
|
||||
)
|
||||
|
||||
if journals:
|
||||
gain_loss_journals = frappe.db.get_all(
|
||||
"Journal Entry",
|
||||
filters={
|
||||
"name": ["in", [x[0] for x in journals]],
|
||||
"voucher_type": "Exchange Gain Or Loss",
|
||||
"docstatus": 1,
|
||||
},
|
||||
as_list=1,
|
||||
)
|
||||
for doc in gain_loss_journals:
|
||||
gain_loss_je = frappe.get_doc("Journal Entry", doc[0])
|
||||
if referenced_dt and referenced_dn:
|
||||
references = [(x.reference_type, x.reference_name) for x in gain_loss_je.accounts]
|
||||
if (
|
||||
len(references) == 2
|
||||
and (referenced_dt, referenced_dn) in references
|
||||
and (parent_doc.doctype, parent_doc.name) in references
|
||||
):
|
||||
# only cancel JE generated against parent_doc and referenced_dn
|
||||
gain_loss_je.cancel()
|
||||
else:
|
||||
for doc in gain_loss_journals:
|
||||
gain_loss_je = frappe.get_doc("Journal Entry", doc)
|
||||
if referenced_dt and referenced_dn:
|
||||
references = [(x.reference_type, x.reference_name) for x in gain_loss_je.accounts]
|
||||
if (
|
||||
len(references) == 2
|
||||
and (referenced_dt, referenced_dn) in references
|
||||
and (parent_doc.doctype, parent_doc.name) in references
|
||||
):
|
||||
# only cancel JE generated against parent_doc and referenced_dn
|
||||
gain_loss_je.cancel()
|
||||
else:
|
||||
gain_loss_je.cancel()
|
||||
|
||||
|
||||
def delete_exchange_gain_loss_journal(
|
||||
parent_doc: dict | object, referenced_dt: str | None = None, referenced_dn: str | None = None
|
||||
) -> None:
|
||||
"""
|
||||
Delete Exchange Gain/Loss for Sales/Purchase Invoice, if they have any.
|
||||
"""
|
||||
if parent_doc.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]:
|
||||
gain_loss_journals = get_linked_exchange_gain_loss_journal(
|
||||
referenced_dt=parent_doc.doctype, referenced_dn=parent_doc.name, je_docstatus=2
|
||||
)
|
||||
for doc in gain_loss_journals:
|
||||
gain_loss_je = frappe.get_doc("Journal Entry", doc)
|
||||
if referenced_dt and referenced_dn:
|
||||
references = [(x.reference_type, x.reference_name) for x in gain_loss_je.accounts]
|
||||
if (
|
||||
len(references) == 2
|
||||
and (referenced_dt, referenced_dn) in references
|
||||
and (parent_doc.doctype, parent_doc.name) in references
|
||||
):
|
||||
# only delete JE generated against parent_doc and referenced_dn
|
||||
gain_loss_je.delete()
|
||||
else:
|
||||
gain_loss_je.delete()
|
||||
|
||||
|
||||
def get_linked_exchange_gain_loss_journal(referenced_dt: str, referenced_dn: str, je_docstatus: int) -> list:
|
||||
"""
|
||||
Get all the linked exchange gain/loss journal entries for a given document.
|
||||
"""
|
||||
gain_loss_journals = []
|
||||
if journals := frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
{
|
||||
"reference_type": referenced_dt,
|
||||
"reference_name": referenced_dn,
|
||||
"docstatus": je_docstatus,
|
||||
},
|
||||
pluck="parent",
|
||||
):
|
||||
gain_loss_journals = frappe.db.get_all(
|
||||
"Journal Entry",
|
||||
{
|
||||
"name": ["in", journals],
|
||||
"voucher_type": "Exchange Gain Or Loss",
|
||||
"is_system_generated": 1,
|
||||
"docstatus": je_docstatus,
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
return gain_loss_journals
|
||||
|
||||
|
||||
def cancel_common_party_journal(self):
|
||||
|
||||
@@ -623,6 +623,9 @@ class Asset(AccountsController):
|
||||
return records
|
||||
|
||||
def validate_make_gl_entry(self):
|
||||
if self.is_composite_asset:
|
||||
return True
|
||||
|
||||
purchase_document = self.get_purchase_document()
|
||||
if not purchase_document:
|
||||
return False
|
||||
|
||||
@@ -65,6 +65,31 @@ frappe.ui.form.on("Purchase Order", {
|
||||
}
|
||||
},
|
||||
|
||||
supplier: function (frm) {
|
||||
// Do not update if inter company reference is there as the details will already be updated
|
||||
if (frm.updating_party_details || frm.doc.inter_company_invoice_reference) return;
|
||||
|
||||
if (frm.doc.__onload && frm.doc.__onload.load_after_mapping) return;
|
||||
|
||||
erpnext.utils.get_party_details(
|
||||
frm,
|
||||
"erpnext.accounts.party.get_party_details",
|
||||
{
|
||||
posting_date: frm.doc.transaction_date,
|
||||
bill_date: frm.doc.bill_date,
|
||||
party: frm.doc.supplier,
|
||||
party_type: "Supplier",
|
||||
account: frm.doc.credit_to,
|
||||
price_list: frm.doc.buying_price_list,
|
||||
fetch_payment_terms_template: cint(!frm.doc.ignore_default_payment_terms_template),
|
||||
},
|
||||
function () {
|
||||
frm.set_df_property("apply_tds", "read_only", frm.supplier_tds ? 0 : 1);
|
||||
frm.set_df_property("tax_withholding_category", "hidden", frm.supplier_tds ? 0 : 1);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
get_materials_from_supplier: function (frm) {
|
||||
let po_details = [];
|
||||
|
||||
@@ -108,6 +133,15 @@ frappe.ui.form.on("Purchase Order", {
|
||||
frm.set_value("transaction_date", frappe.datetime.get_today());
|
||||
}
|
||||
|
||||
if (frm.doc.__onload && frm.doc.supplier) {
|
||||
if (frm.is_new()) {
|
||||
frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0;
|
||||
}
|
||||
if (!frm.doc.__onload.supplier_tds) {
|
||||
frm.set_df_property("apply_tds", "read_only", 1);
|
||||
}
|
||||
}
|
||||
|
||||
erpnext.queries.setup_queries(frm, "Warehouse", function () {
|
||||
return erpnext.queries.warehouse(frm.doc);
|
||||
});
|
||||
|
||||
@@ -648,6 +648,13 @@ class PurchaseOrder(BuyingController):
|
||||
if sco:
|
||||
update_sco_status(sco, "Closed" if self.status == "Closed" else None)
|
||||
|
||||
def set_missing_values(self, for_validate=False):
|
||||
tds_category = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category")
|
||||
if tds_category and not for_validate:
|
||||
self.set_onload("supplier_tds", tds_category)
|
||||
|
||||
super().set_missing_values(for_validate)
|
||||
|
||||
|
||||
@frappe.request_cache
|
||||
def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0):
|
||||
@@ -760,6 +767,11 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
|
||||
def postprocess(source, target):
|
||||
target.flags.ignore_permissions = ignore_permissions
|
||||
set_missing_values(source, target)
|
||||
|
||||
# set tax_withholding_category from Purchase Order
|
||||
if source.apply_tds and source.tax_withholding_category and target.apply_tds:
|
||||
target.tax_withholding_category = source.tax_withholding_category
|
||||
|
||||
# Get the advance paid Journal Entries in Purchase Invoice Advance
|
||||
if target.get("allocate_advances_automatically"):
|
||||
target.set_advances()
|
||||
|
||||
@@ -346,12 +346,17 @@ class AccountsController(TransactionBase):
|
||||
repost_doc.save(ignore_permissions=True)
|
||||
|
||||
def on_trash(self):
|
||||
from erpnext.accounts.utils import delete_exchange_gain_loss_journal
|
||||
|
||||
self._remove_references_in_repost_doctypes()
|
||||
self._remove_references_in_unreconcile()
|
||||
self.remove_serial_and_batch_bundle()
|
||||
|
||||
# delete sl and gl entries on deletion of transaction
|
||||
if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"):
|
||||
# delete linked exchange gain/loss journal
|
||||
delete_exchange_gain_loss_journal(self)
|
||||
|
||||
ple = frappe.qb.DocType("Payment Ledger Entry")
|
||||
frappe.qb.from_(ple).delete().where(
|
||||
(ple.voucher_type == self.doctype) & (ple.voucher_no == self.name)
|
||||
|
||||
@@ -640,7 +640,7 @@ class SellingController(StockController):
|
||||
if self.doctype in ["Sales Order", "Quotation"]:
|
||||
for item in self.items:
|
||||
item.gross_profit = flt(
|
||||
((item.base_rate - flt(item.valuation_rate)) * item.stock_qty),
|
||||
((flt(item.stock_uom_rate) - flt(item.valuation_rate)) * item.stock_qty),
|
||||
self.precision("amount", item),
|
||||
)
|
||||
|
||||
|
||||
170
erpnext/crm/frappe_crm_api.py
Normal file
170
erpnext/crm/frappe_crm_api.py
Normal file
@@ -0,0 +1,170 @@
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_custom_fields_for_frappe_crm():
|
||||
frappe.only_for("System Manager")
|
||||
custom_fields = {
|
||||
"Quotation": [
|
||||
{
|
||||
"fieldname": "crm_deal",
|
||||
"fieldtype": "Data",
|
||||
"label": "Frappe CRM Deal",
|
||||
"insert_after": "party_name",
|
||||
}
|
||||
],
|
||||
"Customer": [
|
||||
{
|
||||
"fieldname": "crm_deal",
|
||||
"fieldtype": "Data",
|
||||
"label": "Frappe CRM Deal",
|
||||
"insert_after": "prospect_name",
|
||||
}
|
||||
],
|
||||
}
|
||||
create_custom_fields(custom_fields, ignore_validate=True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_prospect_against_crm_deal():
|
||||
frappe.only_for("System Manager")
|
||||
doc = frappe.form_dict
|
||||
prospect = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Prospect",
|
||||
"company_name": doc.organization or doc.lead_name,
|
||||
"no_of_employees": doc.no_of_employees,
|
||||
"prospect_owner": doc.deal_owner,
|
||||
"company": doc.erpnext_company,
|
||||
"crm_deal": doc.crm_deal,
|
||||
"territory": doc.territory,
|
||||
"industry": doc.industry,
|
||||
"website": doc.website,
|
||||
"annual_revenue": doc.annual_revenue,
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
prospect_name = frappe.db.get_value("Prospect", {"company_name": prospect.company_name})
|
||||
if not prospect_name:
|
||||
prospect.insert()
|
||||
prospect_name = prospect.name
|
||||
except Exception:
|
||||
frappe.log_error(
|
||||
frappe.get_traceback(),
|
||||
f"Error while creating prospect against CRM Deal: {frappe.form_dict.get('crm_deal_id')}",
|
||||
)
|
||||
pass
|
||||
|
||||
create_contacts(json.loads(doc.contacts), prospect.company_name, "Prospect", prospect_name)
|
||||
create_address("Prospect", prospect_name, doc.address)
|
||||
frappe.response["message"] = prospect_name
|
||||
|
||||
|
||||
def create_contacts(contacts, organization=None, link_doctype=None, link_docname=None):
|
||||
for c in contacts:
|
||||
c = frappe._dict(c)
|
||||
existing_contact = contact_exists(c.email, c.mobile_no)
|
||||
if existing_contact:
|
||||
contact = frappe.get_doc("Contact", existing_contact)
|
||||
else:
|
||||
contact = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Contact",
|
||||
"first_name": c.get("full_name"),
|
||||
"gender": c.get("gender"),
|
||||
"company_name": organization,
|
||||
}
|
||||
)
|
||||
|
||||
if c.get("email"):
|
||||
contact.append("email_ids", {"email_id": c.get("email"), "is_primary": 1})
|
||||
|
||||
if c.get("mobile_no"):
|
||||
contact.append("phone_nos", {"phone": c.get("mobile_no"), "is_primary_mobile_no": 1})
|
||||
|
||||
link_doc(contact, link_doctype, link_docname)
|
||||
|
||||
contact.save(ignore_permissions=True)
|
||||
|
||||
|
||||
def create_address(doctype, docname, address):
|
||||
if not address:
|
||||
return
|
||||
try:
|
||||
_address = frappe.db.exists("Address", address.get("name"))
|
||||
if not _address:
|
||||
new_address_doc = frappe.new_doc("Address")
|
||||
for field in [
|
||||
"address_title",
|
||||
"address_type",
|
||||
"address_line1",
|
||||
"address_line2",
|
||||
"city",
|
||||
"state",
|
||||
"pincode",
|
||||
"country",
|
||||
]:
|
||||
if address.get(field):
|
||||
new_address_doc.set(field, address.get(field))
|
||||
|
||||
new_address_doc.append("links", {"link_doctype": doctype, "link_name": docname})
|
||||
new_address_doc.insert(ignore_mandatory=True)
|
||||
return new_address_doc.name
|
||||
else:
|
||||
address = frappe.get_doc("Address", _address)
|
||||
link_doc(address, doctype, docname)
|
||||
address.save(ignore_permissions=True)
|
||||
return address.name
|
||||
except Exception:
|
||||
frappe.log_error(frappe.get_traceback(), f"Error while creating address for {docname}")
|
||||
|
||||
|
||||
def link_doc(doc, link_doctype, link_docname):
|
||||
already_linked = any(
|
||||
[(link.link_doctype == link_doctype and link.link_name == link_docname) for link in doc.links]
|
||||
)
|
||||
if not already_linked:
|
||||
doc.append(
|
||||
"links", {"link_doctype": link_doctype, "link_name": link_docname, "link_title": link_docname}
|
||||
)
|
||||
|
||||
|
||||
def contact_exists(email, mobile_no):
|
||||
email_exist = frappe.db.exists("Contact Email", {"email_id": email})
|
||||
mobile_exist = frappe.db.exists("Contact Phone", {"phone": mobile_no})
|
||||
|
||||
doctype = "Contact Email" if email_exist else "Contact Phone"
|
||||
name = email_exist or mobile_exist
|
||||
|
||||
if name:
|
||||
return frappe.db.get_value(doctype, name, "parent")
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_customer(customer_data=None):
|
||||
frappe.only_for("System Manager")
|
||||
if not customer_data:
|
||||
customer_data = frappe.form_dict
|
||||
|
||||
try:
|
||||
customer_name = frappe.db.exists("Customer", {"customer_name": customer_data.get("customer_name")})
|
||||
if not customer_name:
|
||||
customer = frappe.get_doc({"doctype": "Customer", **customer_data}).insert(
|
||||
ignore_permissions=True
|
||||
)
|
||||
customer_name = customer.name
|
||||
|
||||
contacts = json.loads(customer_data.get("contacts"))
|
||||
create_contacts(contacts, customer_name, "Customer", customer_name)
|
||||
create_address("Customer", customer_name, customer_data.get("address"))
|
||||
return customer_name
|
||||
except Exception:
|
||||
frappe.log_error(frappe.get_traceback(), "Error while creating customer against Frappe CRM Deal")
|
||||
pass
|
||||
@@ -30,6 +30,9 @@ def update_report_json(report):
|
||||
report_json = json.loads(report.json)
|
||||
report_filter = report_json.get("filters")
|
||||
|
||||
if not report_filter:
|
||||
return
|
||||
|
||||
keys_to_pop = [key for key in report_filter if key.startswith("range")]
|
||||
report_filter["range"] = ", ".join(str(report_filter.pop(key)) for key in keys_to_pop)
|
||||
|
||||
|
||||
@@ -213,6 +213,13 @@ class Project(Document):
|
||||
frappe.db.set_value("Sales Order", {"project": self.name}, "project", "")
|
||||
|
||||
def update_percent_complete(self):
|
||||
if self.status == "Completed":
|
||||
if (
|
||||
len(frappe.get_all("Task", dict(project=self.name))) == 0
|
||||
): # A project without tasks should be able to complete
|
||||
self.percent_complete_method = "Manual"
|
||||
self.percent_complete = 100
|
||||
|
||||
if self.percent_complete_method == "Manual":
|
||||
if self.status == "Completed":
|
||||
self.percent_complete = 100
|
||||
|
||||
@@ -199,6 +199,34 @@ class TestProject(FrappeTestCase):
|
||||
if not pt.is_group:
|
||||
self.assertIsNotNone(pt.parent_task)
|
||||
|
||||
def test_project_having_no_tasks_complete(self):
|
||||
project_name = "Test Project - No Tasks Completion"
|
||||
frappe.db.sql(""" delete from tabTask where project = %s """, project_name)
|
||||
frappe.delete_doc("Project", project_name)
|
||||
|
||||
project = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Project",
|
||||
"project_name": project_name,
|
||||
"status": "Open",
|
||||
"expected_start_date": nowdate(),
|
||||
"company": "_Test Company",
|
||||
}
|
||||
).insert()
|
||||
|
||||
tasks = frappe.get_all(
|
||||
"Task",
|
||||
["subject", "exp_end_date", "depends_on_tasks", "name", "parent_task"],
|
||||
dict(project=project.name),
|
||||
order_by="creation asc",
|
||||
)
|
||||
|
||||
self.assertEqual(project.status, "Open")
|
||||
self.assertEqual(len(tasks), 0)
|
||||
project.status = "Completed"
|
||||
project.save()
|
||||
self.assertEqual(project.status, "Completed")
|
||||
|
||||
|
||||
def get_project(name, template):
|
||||
project = frappe.get_doc(
|
||||
|
||||
@@ -1502,6 +1502,31 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
}
|
||||
|
||||
batch_no(frm, cdt, cdn) {
|
||||
let row = locals[cdt][cdn];
|
||||
if (row.use_serial_batch_fields && row.batch_no) {
|
||||
var params = this._get_args(row);
|
||||
params.batch_no = row.batch_no;
|
||||
params.uom = row.uom;
|
||||
|
||||
frappe.call({
|
||||
method: "erpnext.stock.get_item_details.get_batch_based_item_price",
|
||||
args: {
|
||||
params: params,
|
||||
item_code: row.item_code,
|
||||
},
|
||||
callback: function(r) {
|
||||
if (!r.exc && r.message) {
|
||||
row.price_list_rate = r.message;
|
||||
row.rate = r.message;
|
||||
refresh_field("rate", row.name, row.parentfield);
|
||||
refresh_field("price_list_rate", row.name, row.parentfield);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
toggle_item_grid_columns(company_currency) {
|
||||
const me = this;
|
||||
// toggle columns
|
||||
|
||||
@@ -71,7 +71,7 @@ frappe.ui.form.on("Quotation", {
|
||||
frm.trigger("set_label");
|
||||
frm.trigger("toggle_reqd_lead_customer");
|
||||
frm.trigger("set_dynamic_field_label");
|
||||
frm.set_value("party_name", "");
|
||||
// frm.set_value("party_name", ""); // removed to set party_name from url for crm integration
|
||||
frm.set_value("customer_name", "");
|
||||
},
|
||||
|
||||
|
||||
@@ -220,6 +220,10 @@ class Quotation(SellingController):
|
||||
"Lead", self.party_name, ["lead_name", "company_name"]
|
||||
)
|
||||
self.customer_name = company_name or lead_name
|
||||
elif self.party_name and self.quotation_to == "Prospect":
|
||||
self.customer_name = self.party_name
|
||||
elif self.party_name and self.quotation_to == "CRM Deal":
|
||||
self.customer_name = frappe.db.get_value("CRM Deal", self.party_name, "organization")
|
||||
|
||||
def update_opportunity(self, status):
|
||||
for opportunity in set(d.prevdoc_docname for d in self.get("items")):
|
||||
|
||||
@@ -389,28 +389,14 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
placeholder: discount ? discount + "%" : __("Enter discount percentage."),
|
||||
input_class: "input-xs",
|
||||
onchange: function () {
|
||||
if (flt(this.value) != 0) {
|
||||
frappe.model.set_value(
|
||||
frm.doc.doctype,
|
||||
frm.doc.name,
|
||||
"additional_discount_percentage",
|
||||
flt(this.value)
|
||||
);
|
||||
me.hide_discount_control(this.value);
|
||||
} else {
|
||||
frappe.model.set_value(
|
||||
frm.doc.doctype,
|
||||
frm.doc.name,
|
||||
"additional_discount_percentage",
|
||||
0
|
||||
);
|
||||
me.$add_discount_elem.css({
|
||||
border: "1px dashed var(--gray-500)",
|
||||
padding: "var(--padding-sm) var(--padding-md)",
|
||||
});
|
||||
me.$add_discount_elem.html(`${me.get_discount_icon()} ${__("Add Discount")}`);
|
||||
me.discount_field = undefined;
|
||||
}
|
||||
this.value = flt(this.value);
|
||||
frappe.model.set_value(
|
||||
frm.doc.doctype,
|
||||
frm.doc.name,
|
||||
"additional_discount_percentage",
|
||||
flt(this.value)
|
||||
);
|
||||
me.hide_discount_control(this.value);
|
||||
},
|
||||
},
|
||||
parent: this.$add_discount_elem.find(".add-discount-field"),
|
||||
@@ -421,9 +407,13 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
}
|
||||
|
||||
hide_discount_control(discount) {
|
||||
if (!discount) {
|
||||
this.$add_discount_elem.css({ padding: "0px", border: "none" });
|
||||
this.$add_discount_elem.html(`<div class="add-discount-field"></div>`);
|
||||
if (!flt(discount)) {
|
||||
this.$add_discount_elem.css({
|
||||
border: "1px dashed var(--gray-500)",
|
||||
padding: "var(--padding-sm) var(--padding-md)",
|
||||
});
|
||||
this.$add_discount_elem.html(`${this.get_discount_icon()} ${__("Add Discount")}`);
|
||||
this.discount_field = undefined;
|
||||
} else {
|
||||
this.$add_discount_elem.css({
|
||||
border: "1px dashed var(--dark-green-500)",
|
||||
@@ -1051,6 +1041,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
this.highlight_checkout_btn(false);
|
||||
}
|
||||
|
||||
this.hide_discount_control(frm.doc.additional_discount_percentage);
|
||||
this.update_totals_section(frm);
|
||||
|
||||
if (frm.doc.docstatus === 1) {
|
||||
|
||||
@@ -149,7 +149,11 @@ class HolidayList(Document):
|
||||
unique_dates = []
|
||||
for row in self.holidays:
|
||||
if row.holiday_date in unique_dates:
|
||||
frappe.throw(_("Holiday Date {0} added multiple times").format(frappe.bold(row.holiday_date)))
|
||||
frappe.throw(
|
||||
_("Holiday Date {0} added multiple times").format(
|
||||
frappe.bold(formatdate(row.holiday_date))
|
||||
)
|
||||
)
|
||||
|
||||
unique_dates.append(row.holiday_date)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div>
|
||||
<a href={{ route }}>{{ title }}</a>
|
||||
<a href={{ route }}>{{ title or name }}</a>
|
||||
</div>
|
||||
<!-- this is a sample default list template -->
|
||||
|
||||
@@ -10,7 +10,7 @@ from frappe.model import child_table_fields, default_fields
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.model.utils import get_fetch_values
|
||||
from frappe.query_builder.functions import IfNull, Sum
|
||||
from frappe.utils import add_days, add_months, cint, cstr, flt, getdate
|
||||
from frappe.utils import add_days, add_months, cint, cstr, flt, getdate, parse_json
|
||||
|
||||
from erpnext import get_company_currency
|
||||
from erpnext.accounts.doctype.pricing_rule.pricing_rule import (
|
||||
@@ -889,7 +889,7 @@ def insert_item_price(args):
|
||||
)
|
||||
|
||||
|
||||
def get_item_price(args, item_code, ignore_party=False):
|
||||
def get_item_price(args, item_code, ignore_party=False, force_batch_no=False) -> list[dict]:
|
||||
"""
|
||||
Get name, price_list_rate from Item Price based on conditions
|
||||
Check if the desired qty is within the increment of the packing list.
|
||||
@@ -906,13 +906,17 @@ def get_item_price(args, item_code, ignore_party=False):
|
||||
(ip.item_code == item_code)
|
||||
& (ip.price_list == args.get("price_list"))
|
||||
& (IfNull(ip.uom, "").isin(["", args.get("uom")]))
|
||||
& (IfNull(ip.batch_no, "").isin(["", args.get("batch_no")]))
|
||||
)
|
||||
.orderby(ip.valid_from, order=frappe.qb.desc)
|
||||
.orderby(IfNull(ip.batch_no, ""), order=frappe.qb.desc)
|
||||
.orderby(ip.uom, order=frappe.qb.desc)
|
||||
)
|
||||
|
||||
if force_batch_no:
|
||||
query = query.where(ip.batch_no == args.get("batch_no"))
|
||||
else:
|
||||
query = query.where(IfNull(ip.batch_no, "").isin(["", args.get("batch_no")]))
|
||||
|
||||
if not ignore_party:
|
||||
if args.get("customer"):
|
||||
query = query.where(ip.customer == args.get("customer"))
|
||||
@@ -930,6 +934,21 @@ def get_item_price(args, item_code, ignore_party=False):
|
||||
return query.run()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_batch_based_item_price(params, item_code) -> float:
|
||||
if isinstance(params, str):
|
||||
params = parse_json(params)
|
||||
|
||||
item_price = get_item_price(params, item_code, force_batch_no=True)
|
||||
if not item_price:
|
||||
item_price = get_item_price(params, item_code, ignore_party=True, force_batch_no=True)
|
||||
|
||||
if item_price and item_price[0].uom == params.get("uom"):
|
||||
return item_price[0].price_list_rate
|
||||
|
||||
return 0.0
|
||||
|
||||
|
||||
def get_price_list_rate_for(args, item_code):
|
||||
"""
|
||||
:param customer: link to Customer DocType
|
||||
|
||||
Reference in New Issue
Block a user