Merge branch 'develop' into subcontracting
This commit is contained in:
commit
78ff1783b1
16
.github/workflows/linters.yml
vendored
16
.github/workflows/linters.yml
vendored
@ -11,10 +11,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python 3.8
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Install and Run Pre-commit
|
||||
uses: pre-commit/action@v2.0.3
|
||||
@ -22,10 +22,8 @@ jobs:
|
||||
- name: Download Semgrep rules
|
||||
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
|
||||
|
||||
- uses: returntocorp/semgrep-action@v1
|
||||
env:
|
||||
SEMGREP_TIMEOUT: 120
|
||||
with:
|
||||
config: >-
|
||||
r/python.lang.correctness
|
||||
./frappe-semgrep-rules/rules
|
||||
- name: Download semgrep
|
||||
run: pip install semgrep==0.97.0
|
||||
|
||||
- name: Run Semgrep rules
|
||||
run: semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness
|
||||
|
@ -149,22 +149,6 @@ frappe.ui.form.on("Journal Entry", {
|
||||
}
|
||||
});
|
||||
}
|
||||
else if(frm.doc.voucher_type=="Opening Entry") {
|
||||
return frappe.call({
|
||||
type:"GET",
|
||||
method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_opening_accounts",
|
||||
args: {
|
||||
"company": frm.doc.company
|
||||
},
|
||||
callback: function(r) {
|
||||
frappe.model.clear_table(frm.doc, "accounts");
|
||||
if(r.message) {
|
||||
update_jv_details(frm.doc, r.message);
|
||||
}
|
||||
cur_frm.set_value("is_opening", "Yes");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -137,7 +137,8 @@
|
||||
"fieldname": "finance_book",
|
||||
"fieldtype": "Link",
|
||||
"label": "Finance Book",
|
||||
"options": "Finance Book"
|
||||
"options": "Finance Book",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "2_add_edit_gl_entries",
|
||||
@ -538,7 +539,7 @@
|
||||
"idx": 176,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-04-06 17:18:46.865259",
|
||||
"modified": "2022-06-23 22:01:32.348337",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
|
@ -1204,24 +1204,6 @@ def get_payment_entry(ref_doc, args):
|
||||
return je if args.get("journal_entry") else je.as_dict()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_opening_accounts(company):
|
||||
"""get all balance sheet accounts for opening entry"""
|
||||
accounts = frappe.db.sql_list(
|
||||
"""select
|
||||
name from tabAccount
|
||||
where
|
||||
is_group=0 and report_type='Balance Sheet' and company={0} and
|
||||
name not in (select distinct account from tabWarehouse where
|
||||
account is not null and account != '')
|
||||
order by name asc""".format(
|
||||
frappe.db.escape(company)
|
||||
)
|
||||
)
|
||||
|
||||
return [{"account": a, "balance": get_balance_on(a)} for a in accounts]
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_against_jv(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
@ -165,17 +165,6 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
super(PurchaseInvoice, self).set_missing_values(for_validate)
|
||||
|
||||
def check_conversion_rate(self):
|
||||
default_currency = erpnext.get_company_currency(self.company)
|
||||
if not default_currency:
|
||||
throw(_("Please enter default currency in Company Master"))
|
||||
if (
|
||||
(self.currency == default_currency and flt(self.conversion_rate) != 1.00)
|
||||
or not self.conversion_rate
|
||||
or (self.currency != default_currency and flt(self.conversion_rate) == 1.00)
|
||||
):
|
||||
throw(_("Conversion rate cannot be 0 or 1"))
|
||||
|
||||
def validate_credit_to_acc(self):
|
||||
if not self.credit_to:
|
||||
self.credit_to = get_party_account("Supplier", self.supplier, self.company)
|
||||
|
@ -114,6 +114,7 @@ class SalesInvoice(SellingController):
|
||||
self.set_income_account_for_fixed_assets()
|
||||
self.validate_item_cost_centers()
|
||||
self.validate_income_account()
|
||||
self.check_conversion_rate()
|
||||
|
||||
validate_inter_company_party(
|
||||
self.doctype, self.customer, self.company, self.inter_company_invoice_reference
|
||||
|
@ -792,6 +792,54 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
jv.cancel()
|
||||
self.assertEqual(frappe.db.get_value("Sales Invoice", w.name, "outstanding_amount"), 562.0)
|
||||
|
||||
def test_outstanding_on_cost_center_allocation(self):
|
||||
# setup cost centers
|
||||
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||
from erpnext.accounts.doctype.cost_center_allocation.test_cost_center_allocation import (
|
||||
create_cost_center_allocation,
|
||||
)
|
||||
|
||||
cost_centers = [
|
||||
"Main Cost Center 1",
|
||||
"Sub Cost Center 1",
|
||||
"Sub Cost Center 2",
|
||||
]
|
||||
for cc in cost_centers:
|
||||
create_cost_center(cost_center_name=cc, company="_Test Company")
|
||||
|
||||
cca = create_cost_center_allocation(
|
||||
"_Test Company",
|
||||
"Main Cost Center 1 - _TC",
|
||||
{"Sub Cost Center 1 - _TC": 60, "Sub Cost Center 2 - _TC": 40},
|
||||
)
|
||||
|
||||
# make invoice
|
||||
si = frappe.copy_doc(test_records[0])
|
||||
si.is_pos = 0
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
|
||||
# make payment - fully paid
|
||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
|
||||
pe.reference_no = "1"
|
||||
pe.reference_date = nowdate()
|
||||
pe.paid_from_account_currency = si.currency
|
||||
pe.paid_to_account_currency = si.currency
|
||||
pe.source_exchange_rate = 1
|
||||
pe.target_exchange_rate = 1
|
||||
pe.paid_amount = si.outstanding_amount
|
||||
pe.cost_center = cca.main_cost_center
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
# cancel cost center allocation
|
||||
cca.cancel()
|
||||
|
||||
si.reload()
|
||||
self.assertEqual(si.outstanding_amount, 0)
|
||||
|
||||
def test_sales_invoice_gl_entry_without_perpetual_inventory(self):
|
||||
si = frappe.copy_doc(test_records[1])
|
||||
si.insert()
|
||||
@ -1583,6 +1631,17 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
self.assertTrue(gle)
|
||||
|
||||
def test_invoice_exchange_rate(self):
|
||||
si = create_sales_invoice(
|
||||
customer="_Test Customer USD",
|
||||
debit_to="_Test Receivable USD - _TC",
|
||||
currency="USD",
|
||||
conversion_rate=1,
|
||||
do_not_save=1,
|
||||
)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, si.save)
|
||||
|
||||
def test_invalid_currency(self):
|
||||
# Customer currency = USD
|
||||
|
||||
|
@ -132,7 +132,7 @@ def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None):
|
||||
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_company_currency"):
|
||||
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:
|
||||
|
@ -43,7 +43,7 @@ def get_columns():
|
||||
"options": "Account",
|
||||
"width": 170,
|
||||
},
|
||||
{"label": _("Amount"), "fieldname": "amount", "width": 120},
|
||||
{"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 120},
|
||||
]
|
||||
|
||||
return columns
|
||||
|
@ -425,7 +425,7 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
update_value_in_dict(totals, "opening", gle)
|
||||
update_value_in_dict(totals, "closing", gle)
|
||||
|
||||
elif gle.posting_date <= to_date:
|
||||
elif gle.posting_date <= to_date or (cstr(gle.is_opening) == "Yes" and show_opening_entries):
|
||||
if not group_by_voucher_consolidated:
|
||||
update_value_in_dict(gle_map[group_by_value].totals, "total", gle)
|
||||
update_value_in_dict(gle_map[group_by_value].totals, "closing", gle)
|
||||
|
@ -252,6 +252,7 @@ class Asset(AccountsController):
|
||||
number_of_pending_depreciations += 1
|
||||
|
||||
skip_row = False
|
||||
should_get_last_day = is_last_day_of_the_month(finance_book.depreciation_start_date)
|
||||
|
||||
for n in range(start[finance_book.idx - 1], number_of_pending_depreciations):
|
||||
# If depreciation is already completed (for double declining balance)
|
||||
@ -265,6 +266,9 @@ class Asset(AccountsController):
|
||||
finance_book.depreciation_start_date, n * cint(finance_book.frequency_of_depreciation)
|
||||
)
|
||||
|
||||
if should_get_last_day:
|
||||
schedule_date = get_last_day(schedule_date)
|
||||
|
||||
# schedule date will be a year later from start date
|
||||
# so monthly schedule date is calculated by removing 11 months from it
|
||||
monthly_schedule_date = add_months(schedule_date, -finance_book.frequency_of_depreciation + 1)
|
||||
@ -849,14 +853,9 @@ class Asset(AccountsController):
|
||||
if args.get("rate_of_depreciation") and on_validate:
|
||||
return args.get("rate_of_depreciation")
|
||||
|
||||
no_of_years = (
|
||||
flt(args.get("total_number_of_depreciations") * flt(args.get("frequency_of_depreciation")))
|
||||
/ 12
|
||||
)
|
||||
value = flt(args.get("expected_value_after_useful_life")) / flt(self.gross_purchase_amount)
|
||||
|
||||
# square root of flt(salvage_value) / flt(asset_cost)
|
||||
depreciation_rate = math.pow(value, 1.0 / flt(no_of_years, 2))
|
||||
depreciation_rate = math.pow(value, 1.0 / flt(args.get("total_number_of_depreciations"), 2))
|
||||
|
||||
return 100 * (1 - flt(depreciation_rate, float_precision))
|
||||
|
||||
@ -1105,9 +1104,18 @@ def is_cwip_accounting_enabled(asset_category):
|
||||
def get_total_days(date, frequency):
|
||||
period_start_date = add_months(date, cint(frequency) * -1)
|
||||
|
||||
if is_last_day_of_the_month(date):
|
||||
period_start_date = get_last_day(period_start_date)
|
||||
|
||||
return date_diff(date, period_start_date)
|
||||
|
||||
|
||||
def is_last_day_of_the_month(date):
|
||||
last_day_of_the_month = get_last_day(date)
|
||||
|
||||
return getdate(last_day_of_the_month) == getdate(date)
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
def get_depreciation_amount(asset, depreciable_value, row):
|
||||
if row.depreciation_method in ("Straight Line", "Manual"):
|
||||
|
@ -707,6 +707,39 @@ class TestDepreciationMethods(AssetSetup):
|
||||
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
def test_monthly_depreciation_by_wdv_method(self):
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date="2022-02-15",
|
||||
purchase_date="2022-02-15",
|
||||
depreciation_method="Written Down Value",
|
||||
gross_purchase_amount=10000,
|
||||
expected_value_after_useful_life=5000,
|
||||
depreciation_start_date="2022-02-28",
|
||||
total_number_of_depreciations=5,
|
||||
frequency_of_depreciation=1,
|
||||
)
|
||||
|
||||
expected_schedules = [
|
||||
["2022-02-28", 645.0, 645.0],
|
||||
["2022-03-31", 1206.8, 1851.8],
|
||||
["2022-04-30", 1051.12, 2902.92],
|
||||
["2022-05-31", 915.52, 3818.44],
|
||||
["2022-06-30", 797.42, 4615.86],
|
||||
["2022-07-15", 384.14, 5000.0],
|
||||
]
|
||||
|
||||
schedules = [
|
||||
[
|
||||
cstr(d.schedule_date),
|
||||
flt(d.depreciation_amount, 2),
|
||||
flt(d.accumulated_depreciation_amount, 2),
|
||||
]
|
||||
for d in asset.get("schedules")
|
||||
]
|
||||
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
def test_discounted_wdv_depreciation_rate_for_indian_region(self):
|
||||
# set indian company
|
||||
company_flag = frappe.flags.company
|
||||
@ -838,7 +871,7 @@ class TestDepreciationBasics(AssetSetup):
|
||||
expected_values = [["2020-12-31", 30000.0], ["2021-12-31", 30000.0], ["2022-12-31", 30000.0]]
|
||||
|
||||
for i, schedule in enumerate(asset.schedules):
|
||||
self.assertEqual(expected_values[i][0], schedule.schedule_date)
|
||||
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
|
||||
self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
|
||||
|
||||
def test_set_accumulated_depreciation(self):
|
||||
@ -1333,6 +1366,32 @@ class TestDepreciationBasics(AssetSetup):
|
||||
asset.cost_center = "Main - _TC"
|
||||
asset.submit()
|
||||
|
||||
def test_depreciation_on_final_day_of_the_month(self):
|
||||
"""Tests if final day of the month is picked each time, if the depreciation start date is the last day of the month."""
|
||||
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
calculate_depreciation=1,
|
||||
purchase_date="2020-01-30",
|
||||
available_for_use_date="2020-02-15",
|
||||
depreciation_start_date="2020-02-29",
|
||||
frequency_of_depreciation=1,
|
||||
total_number_of_depreciations=5,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
expected_dates = [
|
||||
"2020-02-29",
|
||||
"2020-03-31",
|
||||
"2020-04-30",
|
||||
"2020-05-31",
|
||||
"2020-06-30",
|
||||
"2020-07-15",
|
||||
]
|
||||
|
||||
for i, schedule in enumerate(asset.schedules):
|
||||
self.assertEqual(getdate(expected_dates[i]), getdate(schedule.schedule_date))
|
||||
|
||||
|
||||
def create_asset_data():
|
||||
if not frappe.db.exists("Asset Category", "Computers"):
|
||||
|
@ -136,6 +136,43 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
# ordered qty decreases as ordered qty is 0 (deleted row)
|
||||
self.assertEqual(get_ordered_qty(), existing_ordered_qty - 10) # 0
|
||||
|
||||
def test_supplied_items_validations_on_po_update_after_submit(self):
|
||||
po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1, qty=5, rate=100)
|
||||
item = po.items[0]
|
||||
|
||||
original_supplied_items = {po.name: po.required_qty for po in po.supplied_items}
|
||||
|
||||
# Just update rate
|
||||
trans_item = [
|
||||
{
|
||||
"item_code": "_Test FG Item",
|
||||
"rate": 20,
|
||||
"qty": 5,
|
||||
"conversion_factor": 1.0,
|
||||
"docname": item.name,
|
||||
}
|
||||
]
|
||||
update_child_qty_rate("Purchase Order", json.dumps(trans_item), po.name)
|
||||
po.reload()
|
||||
|
||||
new_supplied_items = {po.name: po.required_qty for po in po.supplied_items}
|
||||
self.assertEqual(set(original_supplied_items.keys()), set(new_supplied_items.keys()))
|
||||
|
||||
# Update qty to 2x
|
||||
trans_item[0]["qty"] *= 2
|
||||
update_child_qty_rate("Purchase Order", json.dumps(trans_item), po.name)
|
||||
po.reload()
|
||||
|
||||
new_supplied_items = {po.name: po.required_qty for po in po.supplied_items}
|
||||
self.assertEqual(2 * sum(original_supplied_items.values()), sum(new_supplied_items.values()))
|
||||
|
||||
# Set transfer qty and attempt to update qty, shouldn't be allowed
|
||||
po.supplied_items[0].supplied_qty = 2
|
||||
po.supplied_items[0].db_update()
|
||||
trans_item[0]["qty"] *= 2
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
update_child_qty_rate("Purchase Order", json.dumps(trans_item), po.name)
|
||||
|
||||
def test_update_child(self):
|
||||
mr = make_material_request(qty=10)
|
||||
po = make_purchase_order(mr.name)
|
||||
|
@ -59,6 +59,7 @@ frappe.query_reports["Purchase Order Analysis"] = {
|
||||
for (let option of status){
|
||||
options.push({
|
||||
"value": option,
|
||||
"label": __(option),
|
||||
"description": ""
|
||||
})
|
||||
}
|
||||
|
@ -1848,6 +1848,17 @@ class AccountsController(TransactionBase):
|
||||
jv.save()
|
||||
jv.submit()
|
||||
|
||||
def check_conversion_rate(self):
|
||||
default_currency = erpnext.get_company_currency(self.company)
|
||||
if not default_currency:
|
||||
throw(_("Please enter default currency in Company Master"))
|
||||
if (
|
||||
(self.currency == default_currency and flt(self.conversion_rate) != 1.00)
|
||||
or not self.conversion_rate
|
||||
or (self.currency != default_currency and flt(self.conversion_rate) == 1.00)
|
||||
):
|
||||
throw(_("Conversion rate cannot be 0 or 1"))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_tax_rate(account_head):
|
||||
@ -2429,7 +2440,7 @@ def update_bin_on_delete(row, doctype):
|
||||
update_bin_qty(row.item_code, row.warehouse, qty_dict)
|
||||
|
||||
|
||||
def validate_and_delete_children(parent, data):
|
||||
def validate_and_delete_children(parent, data) -> bool:
|
||||
deleted_children = []
|
||||
updated_item_names = [d.get("docname") for d in data]
|
||||
for item in parent.items:
|
||||
@ -2448,6 +2459,8 @@ def validate_and_delete_children(parent, data):
|
||||
for d in deleted_children:
|
||||
update_bin_on_delete(d, parent.doctype)
|
||||
|
||||
return bool(deleted_children)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
|
||||
@ -2511,13 +2524,38 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
):
|
||||
frappe.throw(_("Cannot set quantity less than received quantity"))
|
||||
|
||||
def should_update_supplied_items(doc) -> bool:
|
||||
"""Subcontracted PO can allow following changes *after submit*:
|
||||
|
||||
1. Change rate of subcontracting - regardless of other changes.
|
||||
2. Change qty and/or add new items and/or remove items
|
||||
Exception: Transfer/Consumption is already made, qty change not allowed.
|
||||
"""
|
||||
|
||||
supplied_items_processed = any(
|
||||
item.supplied_qty or item.consumed_qty or item.returned_qty for item in doc.supplied_items
|
||||
)
|
||||
|
||||
update_supplied_items = (
|
||||
any_qty_changed or items_added_or_removed or any_conversion_factor_changed
|
||||
)
|
||||
if update_supplied_items and supplied_items_processed:
|
||||
frappe.throw(_("Item qty can not be updated as raw materials are already processed."))
|
||||
|
||||
return update_supplied_items
|
||||
|
||||
data = json.loads(trans_items)
|
||||
|
||||
any_qty_changed = False # updated to true if any item's qty changes
|
||||
items_added_or_removed = False # updated to true if any new item is added or removed
|
||||
any_conversion_factor_changed = False
|
||||
|
||||
sales_doctypes = ["Sales Order", "Sales Invoice", "Delivery Note", "Quotation"]
|
||||
parent = frappe.get_doc(parent_doctype, parent_doctype_name)
|
||||
|
||||
check_doc_permissions(parent, "write")
|
||||
validate_and_delete_children(parent, data)
|
||||
_removed_items = validate_and_delete_children(parent, data)
|
||||
items_added_or_removed |= _removed_items
|
||||
|
||||
for d in data:
|
||||
new_child_flag = False
|
||||
@ -2528,6 +2566,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
|
||||
if not d.get("docname"):
|
||||
new_child_flag = True
|
||||
items_added_or_removed = True
|
||||
check_doc_permissions(parent, "create")
|
||||
child_item = get_new_child_item(d)
|
||||
else:
|
||||
@ -2550,6 +2589,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
qty_unchanged = prev_qty == new_qty
|
||||
uom_unchanged = prev_uom == new_uom
|
||||
conversion_factor_unchanged = prev_con_fac == new_con_fac
|
||||
any_conversion_factor_changed |= not conversion_factor_unchanged
|
||||
date_unchanged = (
|
||||
prev_date == getdate(new_date) if prev_date and new_date else False
|
||||
) # in case of delivery note etc
|
||||
@ -2563,6 +2603,8 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
continue
|
||||
|
||||
validate_quantity(child_item, d)
|
||||
if flt(child_item.get("qty")) != flt(d.get("qty")):
|
||||
any_qty_changed = True
|
||||
|
||||
child_item.qty = flt(d.get("qty"))
|
||||
rate_precision = child_item.precision("rate") or 2
|
||||
@ -2668,6 +2710,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
parent.update_ordered_and_reserved_qty()
|
||||
parent.update_receiving_percentage()
|
||||
if parent.is_old_subcontracting_flow:
|
||||
if should_update_supplied_items(parent):
|
||||
parent.update_reserved_qty_for_subcontract()
|
||||
parent.create_raw_materials_supplied()
|
||||
parent.save()
|
||||
|
@ -166,7 +166,7 @@ class StockController(AccountsController):
|
||||
"against": warehouse_account[sle.warehouse]["account"],
|
||||
"cost_center": item_row.cost_center,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"credit": flt(sle.stock_value_difference, precision),
|
||||
"debit": -1 * flt(sle.stock_value_difference, precision),
|
||||
"project": item_row.get("project") or self.get("project"),
|
||||
"is_opening": item_row.get("is_opening") or self.get("is_opening") or "No",
|
||||
},
|
||||
|
0
erpnext/crm/doctype/crm_note/__init__.py
Normal file
0
erpnext/crm/doctype/crm_note/__init__.py
Normal file
48
erpnext/crm/doctype/crm_note/crm_note.json
Normal file
48
erpnext/crm/doctype/crm_note/crm_note.json
Normal file
@ -0,0 +1,48 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "autoincrement",
|
||||
"creation": "2022-06-04 15:49:23.416644",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"note",
|
||||
"added_by",
|
||||
"added_on"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"columns": 5,
|
||||
"fieldname": "note",
|
||||
"fieldtype": "Text Editor",
|
||||
"in_list_view": 1,
|
||||
"label": "Note"
|
||||
},
|
||||
{
|
||||
"fieldname": "added_by",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Added By",
|
||||
"options": "User"
|
||||
},
|
||||
{
|
||||
"fieldname": "added_on",
|
||||
"fieldtype": "Datetime",
|
||||
"in_list_view": 1,
|
||||
"label": "Added On"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-06-04 16:29:07.807252",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "CRM Note",
|
||||
"naming_rule": "Autoincrement",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
9
erpnext/crm/doctype/crm_note/crm_note.py
Normal file
9
erpnext/crm/doctype/crm_note/crm_note.py
Normal file
@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class CRMNote(Document):
|
||||
pass
|
@ -10,12 +10,10 @@
|
||||
"campaign_naming_by",
|
||||
"allow_lead_duplication_based_on_emails",
|
||||
"column_break_4",
|
||||
"create_event_on_next_contact_date",
|
||||
"auto_creation_of_contact",
|
||||
"opportunity_section",
|
||||
"close_opportunity_after_days",
|
||||
"column_break_9",
|
||||
"create_event_on_next_contact_date_opportunity",
|
||||
"quotation_section",
|
||||
"default_valid_till",
|
||||
"section_break_13",
|
||||
@ -55,12 +53,6 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Auto Creation of Contact"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "create_event_on_next_contact_date",
|
||||
"fieldtype": "Check",
|
||||
"label": "Create Event on Next Contact Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "opportunity_section",
|
||||
"fieldtype": "Section Break",
|
||||
@ -73,12 +65,6 @@
|
||||
"fieldtype": "Int",
|
||||
"label": "Close Replied Opportunity After Days"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "create_event_on_next_contact_date_opportunity",
|
||||
"fieldtype": "Check",
|
||||
"label": "Create Event on Next Contact Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
@ -105,7 +91,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-12-20 12:51:38.894252",
|
||||
"modified": "2022-06-06 11:22:08.464253",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "CRM Settings",
|
||||
@ -143,5 +129,6 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -24,31 +24,39 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
|
||||
this.frm.set_query("lead_owner", function (doc, cdt, cdn) {
|
||||
return { query: "frappe.core.doctype.user.user.user_query" }
|
||||
});
|
||||
|
||||
this.frm.set_query("contact_by", function (doc, cdt, cdn) {
|
||||
return { query: "frappe.core.doctype.user.user.user_query" }
|
||||
});
|
||||
}
|
||||
|
||||
refresh () {
|
||||
var me = this;
|
||||
let doc = this.frm.doc;
|
||||
erpnext.toggle_naming_series();
|
||||
frappe.dynamic_link = { doc: doc, fieldname: 'name', doctype: 'Lead' }
|
||||
frappe.dynamic_link = {
|
||||
doc: doc,
|
||||
fieldname: 'name',
|
||||
doctype: 'Lead'
|
||||
};
|
||||
|
||||
if (!this.frm.is_new() && doc.__onload && !doc.__onload.is_customer) {
|
||||
this.frm.add_custom_button(__("Customer"), this.make_customer, __("Create"));
|
||||
this.frm.add_custom_button(__("Opportunity"), this.make_opportunity, __("Create"));
|
||||
this.frm.add_custom_button(__("Opportunity"), function() {
|
||||
me.frm.trigger("make_opportunity");
|
||||
}, __("Create"));
|
||||
this.frm.add_custom_button(__("Quotation"), this.make_quotation, __("Create"));
|
||||
if (!doc.__onload.linked_prospects.length) {
|
||||
this.frm.add_custom_button(__("Prospect"), this.make_prospect, __("Create"));
|
||||
this.frm.add_custom_button(__('Add to Prospect'), this.add_lead_to_prospect, __('Action'));
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.frm.is_new()) {
|
||||
frappe.contacts.render_address_and_contact(this.frm);
|
||||
cur_frm.trigger('render_contact_day_html');
|
||||
} else {
|
||||
frappe.contacts.clear_address_and_contact(this.frm);
|
||||
}
|
||||
|
||||
this.frm.dashboard.links_area.hide();
|
||||
this.show_notes();
|
||||
this.show_activities();
|
||||
}
|
||||
|
||||
add_lead_to_prospect () {
|
||||
@ -74,7 +82,7 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
|
||||
}
|
||||
},
|
||||
freeze: true,
|
||||
freeze_message: __('...Adding Lead to Prospect')
|
||||
freeze_message: __('Adding Lead to Prospect...')
|
||||
});
|
||||
}, __('Add Lead to Prospect'), __('Add'));
|
||||
}
|
||||
@ -86,13 +94,6 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
|
||||
})
|
||||
}
|
||||
|
||||
make_opportunity () {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.crm.doctype.lead.lead.make_opportunity",
|
||||
frm: cur_frm
|
||||
})
|
||||
}
|
||||
|
||||
make_quotation () {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.crm.doctype.lead.lead.make_quotation",
|
||||
@ -111,9 +112,10 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
|
||||
prospect.fax = cur_frm.doc.fax;
|
||||
prospect.website = cur_frm.doc.website;
|
||||
prospect.prospect_owner = cur_frm.doc.lead_owner;
|
||||
prospect.notes = cur_frm.doc.notes;
|
||||
|
||||
let lead_prospect_row = frappe.model.add_child(prospect, 'prospect_lead');
|
||||
lead_prospect_row.lead = cur_frm.doc.name;
|
||||
let leads_row = frappe.model.add_child(prospect, 'leads');
|
||||
leads_row.lead = cur_frm.doc.name;
|
||||
|
||||
frappe.set_route("Form", "Prospect", prospect.name);
|
||||
});
|
||||
@ -125,26 +127,109 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
|
||||
}
|
||||
}
|
||||
|
||||
contact_date () {
|
||||
if (this.frm.doc.contact_date) {
|
||||
let d = moment(this.frm.doc.contact_date);
|
||||
d.add(1, "day");
|
||||
this.frm.set_value("ends_on", d.format(frappe.defaultDatetimeFormat));
|
||||
}
|
||||
show_notes() {
|
||||
if (this.frm.doc.docstatus == 1) return;
|
||||
|
||||
const crm_notes = new erpnext.utils.CRMNotes({
|
||||
frm: this.frm,
|
||||
notes_wrapper: $(this.frm.fields_dict.notes_html.wrapper),
|
||||
});
|
||||
crm_notes.refresh();
|
||||
}
|
||||
|
||||
render_contact_day_html() {
|
||||
if (cur_frm.doc.contact_date) {
|
||||
let contact_date = frappe.datetime.obj_to_str(cur_frm.doc.contact_date);
|
||||
let diff_days = frappe.datetime.get_day_diff(contact_date, frappe.datetime.get_today());
|
||||
let color = diff_days > 0 ? "orange" : "green";
|
||||
let message = diff_days > 0 ? __("Next Contact Date") : __("Last Contact Date");
|
||||
let html = `<div class="col-xs-12">
|
||||
<span class="indicator whitespace-nowrap ${color}"><span> ${message} : ${frappe.datetime.global_date_format(contact_date)}</span></span>
|
||||
</div>` ;
|
||||
cur_frm.dashboard.set_headline_alert(html);
|
||||
}
|
||||
show_activities() {
|
||||
if (this.frm.doc.docstatus == 1) return;
|
||||
|
||||
const crm_activities = new erpnext.utils.CRMActivities({
|
||||
frm: this.frm,
|
||||
open_activities_wrapper: $(this.frm.fields_dict.open_activities_html.wrapper),
|
||||
all_activities_wrapper: $(this.frm.fields_dict.all_activities_html.wrapper),
|
||||
form_wrapper: $(this.frm.wrapper),
|
||||
});
|
||||
crm_activities.refresh();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
extend_cscript(cur_frm.cscript, new erpnext.LeadController({ frm: cur_frm }));
|
||||
|
||||
frappe.ui.form.on("Lead", {
|
||||
make_opportunity: async function(frm) {
|
||||
let existing_prospect = (await frappe.db.get_value("Prospect Lead",
|
||||
{
|
||||
"lead": frm.doc.name
|
||||
},
|
||||
"name", null, "Prospect"
|
||||
)).message.name;
|
||||
|
||||
if (!existing_prospect) {
|
||||
var fields = [
|
||||
{
|
||||
"label": "Create Prospect",
|
||||
"fieldname": "create_prospect",
|
||||
"fieldtype": "Check",
|
||||
"default": 1
|
||||
},
|
||||
{
|
||||
"label": "Prospect Name",
|
||||
"fieldname": "prospect_name",
|
||||
"fieldtype": "Data",
|
||||
"default": frm.doc.company_name,
|
||||
"depends_on": "create_prospect"
|
||||
}
|
||||
];
|
||||
}
|
||||
let existing_contact = (await frappe.db.get_value("Contact",
|
||||
{
|
||||
"first_name": frm.doc.first_name || frm.doc.lead_name,
|
||||
"last_name": frm.doc.last_name
|
||||
},
|
||||
"name"
|
||||
)).message.name;
|
||||
|
||||
if (!existing_contact) {
|
||||
fields.push(
|
||||
{
|
||||
"label": "Create Contact",
|
||||
"fieldname": "create_contact",
|
||||
"fieldtype": "Check",
|
||||
"default": "1"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (fields) {
|
||||
var d = new frappe.ui.Dialog({
|
||||
title: __('Create Opportunity'),
|
||||
fields: fields,
|
||||
primary_action: function() {
|
||||
var data = d.get_values();
|
||||
frappe.call({
|
||||
method: 'create_prospect_and_contact',
|
||||
doc: frm.doc,
|
||||
args: {
|
||||
data: data,
|
||||
},
|
||||
freeze: true,
|
||||
callback: function(r) {
|
||||
if (!r.exc) {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.crm.doctype.lead.lead.make_opportunity",
|
||||
frm: frm
|
||||
});
|
||||
}
|
||||
d.hide();
|
||||
}
|
||||
});
|
||||
},
|
||||
primary_action_label: __('Create')
|
||||
});
|
||||
d.show();
|
||||
} else {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.crm.doctype.lead.lead.make_opportunity",
|
||||
frm: frm
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
@ -3,78 +3,80 @@
|
||||
"allow_events_in_timeline": 1,
|
||||
"allow_import": 1,
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2013-04-10 11:45:37",
|
||||
"creation": "2022-02-08 13:14:41.083327",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"email_append_to": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"lead_details",
|
||||
"naming_series",
|
||||
"salutation",
|
||||
"first_name",
|
||||
"middle_name",
|
||||
"last_name",
|
||||
"column_break_1",
|
||||
"lead_name",
|
||||
"col_break123",
|
||||
"status",
|
||||
"company_name",
|
||||
"designation",
|
||||
"job_title",
|
||||
"gender",
|
||||
"contact_details_section",
|
||||
"source",
|
||||
"col_break123",
|
||||
"lead_owner",
|
||||
"status",
|
||||
"customer",
|
||||
"type",
|
||||
"request_type",
|
||||
"contact_info_tab",
|
||||
"email_id",
|
||||
"website",
|
||||
"column_break_20",
|
||||
"mobile_no",
|
||||
"whatsapp_no",
|
||||
"column_break_16",
|
||||
"phone",
|
||||
"phone_ext",
|
||||
"additional_information_section",
|
||||
"organization_section",
|
||||
"company_name",
|
||||
"no_of_employees",
|
||||
"column_break_28",
|
||||
"annual_revenue",
|
||||
"industry",
|
||||
"market_segment",
|
||||
"column_break_22",
|
||||
"column_break_31",
|
||||
"territory",
|
||||
"fax",
|
||||
"website",
|
||||
"type",
|
||||
"request_type",
|
||||
"address_section",
|
||||
"address_html",
|
||||
"column_break_38",
|
||||
"city",
|
||||
"pincode",
|
||||
"county",
|
||||
"column_break2",
|
||||
"contact_html",
|
||||
"state",
|
||||
"country",
|
||||
"section_break_12",
|
||||
"lead_owner",
|
||||
"ends_on",
|
||||
"column_break_14",
|
||||
"contact_by",
|
||||
"contact_date",
|
||||
"lead_source_details_section",
|
||||
"company",
|
||||
"territory",
|
||||
"language",
|
||||
"column_break_50",
|
||||
"source",
|
||||
"column_break2",
|
||||
"contact_html",
|
||||
"qualification_tab",
|
||||
"qualification_status",
|
||||
"column_break_64",
|
||||
"qualified_by",
|
||||
"qualified_on",
|
||||
"other_info_tab",
|
||||
"campaign_name",
|
||||
"company",
|
||||
"column_break_22",
|
||||
"language",
|
||||
"image",
|
||||
"title",
|
||||
"column_break_50",
|
||||
"disabled",
|
||||
"unsubscribed",
|
||||
"blog_subscriber",
|
||||
"notes_section",
|
||||
"notes",
|
||||
"other_information_section",
|
||||
"customer",
|
||||
"image",
|
||||
"title"
|
||||
"activities_tab",
|
||||
"open_activities_html",
|
||||
"all_activities_section",
|
||||
"all_activities_html",
|
||||
"notes_tab",
|
||||
"notes_html",
|
||||
"notes"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "lead_details",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Lead Details",
|
||||
"options": "fa fa-user"
|
||||
},
|
||||
{
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Select",
|
||||
@ -86,6 +88,7 @@
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "lead_name",
|
||||
"fieldtype": "Data",
|
||||
"in_global_search": 1,
|
||||
@ -108,7 +111,7 @@
|
||||
{
|
||||
"fieldname": "email_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Email Address",
|
||||
"label": "Email",
|
||||
"oldfieldname": "email_id",
|
||||
"oldfieldtype": "Data",
|
||||
"options": "Email",
|
||||
@ -189,50 +192,9 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_12",
|
||||
"fieldname": "contact_info_tab",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Follow Up"
|
||||
},
|
||||
{
|
||||
"fieldname": "contact_by",
|
||||
"fieldtype": "Link",
|
||||
"label": "Next Contact By",
|
||||
"oldfieldname": "contact_by",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "User",
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_14",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"fieldname": "contact_date",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Next Contact Date",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "contact_date",
|
||||
"oldfieldtype": "Date",
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"fieldname": "ends_on",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Ends On",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "notes_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Notes"
|
||||
},
|
||||
{
|
||||
"fieldname": "notes",
|
||||
"fieldtype": "Text Editor",
|
||||
"label": "Notes"
|
||||
"label": "Contact Info"
|
||||
},
|
||||
{
|
||||
"fieldname": "address_html",
|
||||
@ -240,34 +202,6 @@
|
||||
"label": "Address HTML",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "city",
|
||||
"fieldtype": "Data",
|
||||
"label": "City/Town",
|
||||
"mandatory_depends_on": "eval: doc.address_title && doc.address_type"
|
||||
},
|
||||
{
|
||||
"fieldname": "county",
|
||||
"fieldtype": "Data",
|
||||
"label": "County"
|
||||
},
|
||||
{
|
||||
"fieldname": "state",
|
||||
"fieldtype": "Data",
|
||||
"label": "State"
|
||||
},
|
||||
{
|
||||
"fieldname": "country",
|
||||
"fieldtype": "Link",
|
||||
"label": "Country",
|
||||
"mandatory_depends_on": "eval: doc.address_title && doc.address_type",
|
||||
"options": "Country"
|
||||
},
|
||||
{
|
||||
"fieldname": "pincode",
|
||||
"fieldtype": "Data",
|
||||
"label": "Postal Code"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break2",
|
||||
"fieldtype": "Column Break"
|
||||
@ -289,7 +223,7 @@
|
||||
{
|
||||
"fieldname": "mobile_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "Mobile No.",
|
||||
"label": "Mobile No",
|
||||
"oldfieldname": "mobile_no",
|
||||
"oldfieldtype": "Data",
|
||||
"options": "Phone"
|
||||
@ -347,8 +281,7 @@
|
||||
"fieldtype": "Data",
|
||||
"label": "Website",
|
||||
"oldfieldname": "website",
|
||||
"oldfieldtype": "Data",
|
||||
"options": "URL"
|
||||
"oldfieldtype": "Data"
|
||||
},
|
||||
{
|
||||
"fieldname": "territory",
|
||||
@ -380,14 +313,6 @@
|
||||
"label": "Title",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "designation",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Designation",
|
||||
"options": "Designation"
|
||||
},
|
||||
{
|
||||
"fieldname": "language",
|
||||
"fieldtype": "Link",
|
||||
@ -410,12 +335,6 @@
|
||||
"fieldtype": "Data",
|
||||
"label": "Last Name"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "additional_information_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Additional Information"
|
||||
},
|
||||
{
|
||||
"fieldname": "no_of_employees",
|
||||
"fieldtype": "Int",
|
||||
@ -428,35 +347,13 @@
|
||||
{
|
||||
"fieldname": "whatsapp_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "WhatsApp No.",
|
||||
"label": "WhatsApp",
|
||||
"options": "Phone"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"depends_on": "eval: !doc.__islocal",
|
||||
"fieldname": "address_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Address"
|
||||
},
|
||||
{
|
||||
"fieldname": "lead_source_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Lead Source Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_50",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "other_information_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Other Information"
|
||||
},
|
||||
{
|
||||
"fieldname": "contact_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Contact Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_16",
|
||||
"fieldtype": "Column Break"
|
||||
@ -465,17 +362,156 @@
|
||||
"fieldname": "phone_ext",
|
||||
"fieldtype": "Data",
|
||||
"label": "Phone Ext."
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "qualification_tab",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Qualification"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "notes_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Notes"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "other_info_tab",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Additional Information"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_1",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "qualified_by",
|
||||
"fieldtype": "Link",
|
||||
"label": "Qualified By",
|
||||
"options": "User"
|
||||
},
|
||||
{
|
||||
"fieldname": "qualified_on",
|
||||
"fieldtype": "Date",
|
||||
"label": "Qualified on"
|
||||
},
|
||||
{
|
||||
"fieldname": "qualification_status",
|
||||
"fieldtype": "Select",
|
||||
"label": "Qualification Status",
|
||||
"options": "Unqualified\nIn Process\nQualified"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "address_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Address & Contacts"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_64",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_20",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "job_title",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Job Title"
|
||||
},
|
||||
{
|
||||
"fieldname": "annual_revenue",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Annual Revenue"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "activities_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Activities"
|
||||
},
|
||||
{
|
||||
"fieldname": "organization_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Organization"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_28",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_31",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "notes_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Notes HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "open_activities_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Open Activities HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "all_activities_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "All Activities"
|
||||
},
|
||||
{
|
||||
"fieldname": "all_activities_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "All Activities HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "notes",
|
||||
"fieldtype": "Table",
|
||||
"hidden": 1,
|
||||
"label": "Notes",
|
||||
"no_copy": 1,
|
||||
"options": "CRM Note"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disabled"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_38",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "city",
|
||||
"fieldtype": "Data",
|
||||
"label": "City"
|
||||
},
|
||||
{
|
||||
"fieldname": "state",
|
||||
"fieldtype": "Data",
|
||||
"label": "State"
|
||||
},
|
||||
{
|
||||
"fieldname": "country",
|
||||
"fieldtype": "Link",
|
||||
"label": "Country",
|
||||
"options": "Country"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-user",
|
||||
"idx": 5,
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"modified": "2021-08-04 00:24:57.208590",
|
||||
"modified": "2022-06-27 21:56:17.392756",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Lead",
|
||||
"name_case": "Title Case",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@ -535,6 +571,7 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"subject_field": "title",
|
||||
"title_field": "title"
|
||||
}
|
@ -1,27 +1,19 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.contacts.address_and_contact import load_address_and_contact
|
||||
from frappe.email.inbox import link_communication_to_document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils import (
|
||||
comma_and,
|
||||
cstr,
|
||||
get_link_to_form,
|
||||
getdate,
|
||||
has_gravatar,
|
||||
nowdate,
|
||||
validate_email_address,
|
||||
)
|
||||
from frappe.utils import comma_and, get_link_to_form, has_gravatar, validate_email_address
|
||||
|
||||
from erpnext.accounts.party import set_taxes
|
||||
from erpnext.controllers.selling_controller import SellingController
|
||||
from erpnext.crm.utils import CRMNote, copy_comments, link_communications, link_open_events
|
||||
|
||||
|
||||
class Lead(SellingController):
|
||||
class Lead(SellingController, CRMNote):
|
||||
def get_feed(self):
|
||||
return "{0}: {1}".format(_(self.status), self.lead_name)
|
||||
|
||||
@ -29,6 +21,7 @@ class Lead(SellingController):
|
||||
customer = frappe.db.get_value("Customer", {"lead_name": self.name})
|
||||
self.get("__onload").is_customer = customer
|
||||
load_address_and_contact(self)
|
||||
self.set_onload("linked_prospects", self.get_linked_prospects())
|
||||
|
||||
def validate(self):
|
||||
self.set_full_name()
|
||||
@ -37,79 +30,42 @@ class Lead(SellingController):
|
||||
self.set_status()
|
||||
self.check_email_id_is_unique()
|
||||
self.validate_email_id()
|
||||
self.validate_contact_date()
|
||||
self.set_prev()
|
||||
|
||||
def set_full_name(self):
|
||||
if self.first_name:
|
||||
self.lead_name = " ".join(filter(None, [self.first_name, self.middle_name, self.last_name]))
|
||||
|
||||
def validate_email_id(self):
|
||||
if self.email_id:
|
||||
if not self.flags.ignore_email_validation:
|
||||
validate_email_address(self.email_id, throw=True)
|
||||
|
||||
if self.email_id == self.lead_owner:
|
||||
frappe.throw(_("Lead Owner cannot be same as the Lead"))
|
||||
|
||||
if self.email_id == self.contact_by:
|
||||
frappe.throw(_("Next Contact By cannot be same as the Lead Email Address"))
|
||||
|
||||
if self.is_new() or not self.image:
|
||||
self.image = has_gravatar(self.email_id)
|
||||
|
||||
def validate_contact_date(self):
|
||||
if self.contact_date and getdate(self.contact_date) < getdate(nowdate()):
|
||||
frappe.throw(_("Next Contact Date cannot be in the past"))
|
||||
|
||||
if self.ends_on and self.contact_date and (getdate(self.ends_on) < getdate(self.contact_date)):
|
||||
frappe.throw(_("Ends On date cannot be before Next Contact Date."))
|
||||
|
||||
def on_update(self):
|
||||
self.add_calendar_event()
|
||||
self.update_prospects()
|
||||
|
||||
def set_prev(self):
|
||||
if self.is_new():
|
||||
self._prev = frappe._dict({"contact_date": None, "ends_on": None, "contact_by": None})
|
||||
else:
|
||||
self._prev = frappe.db.get_value(
|
||||
"Lead", self.name, ["contact_date", "ends_on", "contact_by"], as_dict=1
|
||||
)
|
||||
|
||||
def before_insert(self):
|
||||
self.contact_doc = None
|
||||
if frappe.db.get_single_value("CRM Settings", "auto_creation_of_contact"):
|
||||
self.contact_doc = self.create_contact()
|
||||
|
||||
def after_insert(self):
|
||||
self.update_links()
|
||||
self.link_to_contact()
|
||||
|
||||
def update_links(self):
|
||||
# update contact links
|
||||
if self.contact_doc:
|
||||
self.contact_doc.append(
|
||||
"links", {"link_doctype": "Lead", "link_name": self.name, "link_title": self.lead_name}
|
||||
)
|
||||
self.contact_doc.save()
|
||||
def on_update(self):
|
||||
self.update_prospect()
|
||||
|
||||
def add_calendar_event(self, opts=None, force=False):
|
||||
if frappe.db.get_single_value("CRM Settings", "create_event_on_next_contact_date"):
|
||||
super(Lead, self).add_calendar_event(
|
||||
{
|
||||
"owner": self.lead_owner,
|
||||
"starts_on": self.contact_date,
|
||||
"ends_on": self.ends_on or "",
|
||||
"subject": ("Contact " + cstr(self.lead_name)),
|
||||
"description": ("Contact " + cstr(self.lead_name))
|
||||
+ (self.contact_by and (". By : " + cstr(self.contact_by)) or ""),
|
||||
},
|
||||
force,
|
||||
def on_trash(self):
|
||||
frappe.db.sql("""update `tabIssue` set lead='' where lead=%s""", self.name)
|
||||
|
||||
self.unlink_dynamic_links()
|
||||
self.remove_link_from_prospect()
|
||||
|
||||
def set_full_name(self):
|
||||
if self.first_name:
|
||||
self.lead_name = " ".join(
|
||||
filter(None, [self.salutation, self.first_name, self.middle_name, self.last_name])
|
||||
)
|
||||
|
||||
def update_prospects(self):
|
||||
prospects = frappe.get_all("Prospect Lead", filters={"lead": self.name}, fields=["parent"])
|
||||
for row in prospects:
|
||||
prospect = frappe.get_doc("Prospect", row.parent)
|
||||
prospect.save(ignore_permissions=True)
|
||||
def set_lead_name(self):
|
||||
if not self.lead_name:
|
||||
# Check for leads being created through data import
|
||||
if not self.company_name and not self.email_id and not self.flags.ignore_mandatory:
|
||||
frappe.throw(_("A Lead requires either a person's name or an organization's name"))
|
||||
elif self.company_name:
|
||||
self.lead_name = self.company_name
|
||||
else:
|
||||
self.lead_name = self.email_id.split("@")[0]
|
||||
|
||||
def set_title(self):
|
||||
self.title = self.company_name or self.lead_name
|
||||
|
||||
def check_email_id_is_unique(self):
|
||||
if self.email_id:
|
||||
@ -124,15 +80,47 @@ class Lead(SellingController):
|
||||
|
||||
if duplicate_leads:
|
||||
frappe.throw(
|
||||
_("Email Address must be unique, already exists for {0}").format(comma_and(duplicate_leads)),
|
||||
_("Email Address must be unique, it is already used in {0}").format(
|
||||
comma_and(duplicate_leads)
|
||||
),
|
||||
frappe.DuplicateEntryError,
|
||||
)
|
||||
|
||||
def on_trash(self):
|
||||
frappe.db.sql("""update `tabIssue` set lead='' where lead=%s""", self.name)
|
||||
def validate_email_id(self):
|
||||
if self.email_id:
|
||||
if not self.flags.ignore_email_validation:
|
||||
validate_email_address(self.email_id, throw=True)
|
||||
|
||||
self.unlink_dynamic_links()
|
||||
self.delete_events()
|
||||
if self.email_id == self.lead_owner:
|
||||
frappe.throw(_("Lead Owner cannot be same as the Lead Email Address"))
|
||||
|
||||
if self.is_new() or not self.image:
|
||||
self.image = has_gravatar(self.email_id)
|
||||
|
||||
def link_to_contact(self):
|
||||
# update contact links
|
||||
if self.contact_doc:
|
||||
self.contact_doc.append(
|
||||
"links", {"link_doctype": "Lead", "link_name": self.name, "link_title": self.lead_name}
|
||||
)
|
||||
self.contact_doc.save()
|
||||
|
||||
def update_prospect(self):
|
||||
lead_row_name = frappe.db.get_value(
|
||||
"Prospect Lead", filters={"lead": self.name}, fieldname="name"
|
||||
)
|
||||
if lead_row_name:
|
||||
lead_row = frappe.get_doc("Prospect Lead", lead_row_name)
|
||||
lead_row.update(
|
||||
{
|
||||
"lead_name": self.lead_name,
|
||||
"email": self.email_id,
|
||||
"mobile_no": self.mobile_no,
|
||||
"lead_owner": self.lead_owner,
|
||||
"status": self.status,
|
||||
}
|
||||
)
|
||||
lead_row.db_update()
|
||||
|
||||
def unlink_dynamic_links(self):
|
||||
links = frappe.get_all(
|
||||
@ -155,6 +143,30 @@ class Lead(SellingController):
|
||||
linked_doc.remove(to_remove)
|
||||
linked_doc.save(ignore_permissions=True)
|
||||
|
||||
def remove_link_from_prospect(self):
|
||||
prospects = self.get_linked_prospects()
|
||||
|
||||
for d in prospects:
|
||||
prospect = frappe.get_doc("Prospect", d.parent)
|
||||
if len(prospect.get("leads")) == 1:
|
||||
prospect.delete(ignore_permissions=True)
|
||||
else:
|
||||
to_remove = None
|
||||
for d in prospect.get("leads"):
|
||||
if d.lead == self.name:
|
||||
to_remove = d
|
||||
|
||||
if to_remove:
|
||||
prospect.remove(to_remove)
|
||||
prospect.save(ignore_permissions=True)
|
||||
|
||||
def get_linked_prospects(self):
|
||||
return frappe.get_all(
|
||||
"Prospect Lead",
|
||||
filters={"lead": self.name},
|
||||
fields=["parent"],
|
||||
)
|
||||
|
||||
def has_customer(self):
|
||||
return frappe.db.get_value("Customer", {"lead_name": self.name})
|
||||
|
||||
@ -171,21 +183,16 @@ class Lead(SellingController):
|
||||
"Quotation", {"party_name": self.name, "docstatus": 1, "status": "Lost"}
|
||||
)
|
||||
|
||||
def set_lead_name(self):
|
||||
if not self.lead_name:
|
||||
# Check for leads being created through data import
|
||||
if not self.company_name and not self.email_id and not self.flags.ignore_mandatory:
|
||||
frappe.throw(_("A Lead requires either a person's name or an organization's name"))
|
||||
elif self.company_name:
|
||||
self.lead_name = self.company_name
|
||||
else:
|
||||
self.lead_name = self.email_id.split("@")[0]
|
||||
@frappe.whitelist()
|
||||
def create_prospect_and_contact(self, data):
|
||||
data = frappe._dict(data)
|
||||
if data.create_contact:
|
||||
self.create_contact()
|
||||
|
||||
def set_title(self):
|
||||
self.title = self.company_name or self.lead_name
|
||||
if data.create_prospect:
|
||||
self.create_prospect(data.prospect_name)
|
||||
|
||||
def create_contact(self):
|
||||
if frappe.db.get_single_value("CRM Settings", "auto_creation_of_contact"):
|
||||
if not self.lead_name:
|
||||
self.set_full_name()
|
||||
self.set_lead_name()
|
||||
@ -197,7 +204,7 @@ class Lead(SellingController):
|
||||
"last_name": self.last_name,
|
||||
"salutation": self.salutation,
|
||||
"gender": self.gender,
|
||||
"designation": self.designation,
|
||||
"job_title": self.job_title,
|
||||
"company_name": self.company_name,
|
||||
}
|
||||
)
|
||||
@ -216,6 +223,39 @@ class Lead(SellingController):
|
||||
|
||||
return contact
|
||||
|
||||
def create_prospect(self, company_name):
|
||||
try:
|
||||
prospect = frappe.new_doc("Prospect")
|
||||
|
||||
prospect.company_name = company_name or self.company_name
|
||||
prospect.no_of_employees = self.no_of_employees
|
||||
prospect.industry = self.industry
|
||||
prospect.market_segment = self.market_segment
|
||||
prospect.annual_revenue = self.annual_revenue
|
||||
prospect.territory = self.territory
|
||||
prospect.fax = self.fax
|
||||
prospect.website = self.website
|
||||
prospect.prospect_owner = self.lead_owner
|
||||
prospect.company = self.company
|
||||
prospect.notes = self.notes
|
||||
|
||||
prospect.append(
|
||||
"leads",
|
||||
{
|
||||
"lead": self.name,
|
||||
"lead_name": self.lead_name,
|
||||
"email": self.email_id,
|
||||
"mobile_no": self.mobile_no,
|
||||
"lead_owner": self.lead_owner,
|
||||
"status": self.status,
|
||||
},
|
||||
)
|
||||
prospect.flags.ignore_permissions = True
|
||||
prospect.flags.ignore_mandatory = True
|
||||
prospect.save()
|
||||
except frappe.DuplicateEntryError:
|
||||
frappe.throw(_("Prospect {0} already exists").format(company_name or self.company_name))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_customer(source_name, target_doc=None):
|
||||
@ -274,6 +314,8 @@ def make_opportunity(source_name, target_doc=None):
|
||||
"company_name": "customer_name",
|
||||
"email_id": "contact_email",
|
||||
"mobile_no": "contact_mobile",
|
||||
"lead_owner": "opportunity_owner",
|
||||
"notes": "notes",
|
||||
},
|
||||
}
|
||||
},
|
||||
@ -422,21 +464,25 @@ def get_lead_with_phone_number(number):
|
||||
return lead
|
||||
|
||||
|
||||
def daily_open_lead():
|
||||
leads = frappe.get_all("Lead", filters=[["contact_date", "Between", [nowdate(), nowdate()]]])
|
||||
for lead in leads:
|
||||
frappe.db.set_value("Lead", lead.name, "status", "Open")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_lead_to_prospect(lead, prospect):
|
||||
prospect = frappe.get_doc("Prospect", prospect)
|
||||
prospect.append("prospect_lead", {"lead": lead})
|
||||
prospect.append("leads", {"lead": lead})
|
||||
prospect.save(ignore_permissions=True)
|
||||
|
||||
carry_forward_communication_and_comments = frappe.db.get_single_value(
|
||||
"CRM Settings", "carry_forward_communication_and_comments"
|
||||
)
|
||||
|
||||
if carry_forward_communication_and_comments:
|
||||
copy_comments("Lead", lead, prospect)
|
||||
link_communications("Lead", lead, prospect)
|
||||
link_open_events("Lead", lead, prospect)
|
||||
|
||||
frappe.msgprint(
|
||||
_("Lead {0} has been added to prospect {1}.").format(
|
||||
frappe.bold(lead), frappe.bold(prospect.name)
|
||||
),
|
||||
title=_("Lead Added"),
|
||||
title=_("Lead -> Prospect"),
|
||||
indicator="green",
|
||||
)
|
||||
|
@ -16,7 +16,7 @@ frappe.listview_settings['Lead'] = {
|
||||
prospect.prospect_owner = r.lead_owner;
|
||||
|
||||
leads.forEach(function(lead) {
|
||||
let lead_prospect_row = frappe.model.add_child(prospect, 'prospect_lead');
|
||||
let lead_prospect_row = frappe.model.add_child(prospect, 'leads');
|
||||
lead_prospect_row.lead = lead.name;
|
||||
});
|
||||
frappe.set_route("Form", "Prospect", prospect.name);
|
||||
|
@ -5,7 +5,10 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import random_string
|
||||
from frappe.utils import random_string, today
|
||||
|
||||
from erpnext.crm.doctype.lead.lead import make_opportunity
|
||||
from erpnext.crm.utils import get_linked_prospect
|
||||
|
||||
test_records = frappe.get_test_records("Lead")
|
||||
|
||||
@ -83,6 +86,105 @@ class TestLead(unittest.TestCase):
|
||||
self.assertEqual(frappe.db.exists("Lead", lead_doc.name), None)
|
||||
self.assertEqual(len(address_1.get("links")), 1)
|
||||
|
||||
def test_prospect_creation_from_lead(self):
|
||||
frappe.db.sql("delete from `tabLead` where lead_name='Rahul Tripathi'")
|
||||
frappe.db.sql("delete from `tabProspect` where name='Prospect Company'")
|
||||
|
||||
lead = make_lead(
|
||||
first_name="Rahul",
|
||||
last_name="Tripathi",
|
||||
email_id="rahul@gmail.com",
|
||||
company_name="Prospect Company",
|
||||
)
|
||||
|
||||
event = create_event("Meeting 1", today(), "Lead", lead.name)
|
||||
|
||||
lead.create_prospect(lead.company_name)
|
||||
|
||||
prospect = get_linked_prospect("Lead", lead.name)
|
||||
self.assertEqual(prospect, "Prospect Company")
|
||||
|
||||
event.reload()
|
||||
self.assertEqual(event.event_participants[1].reference_doctype, "Prospect")
|
||||
self.assertEqual(event.event_participants[1].reference_docname, prospect)
|
||||
|
||||
def test_opportunity_from_lead(self):
|
||||
frappe.db.sql("delete from `tabLead` where lead_name='Rahul Tripathi'")
|
||||
frappe.db.sql("delete from `tabOpportunity` where party_name='Rahul Tripathi'")
|
||||
|
||||
lead = make_lead(
|
||||
first_name="Rahul",
|
||||
last_name="Tripathi",
|
||||
email_id="rahul@gmail.com",
|
||||
company_name="Prospect Company",
|
||||
)
|
||||
|
||||
lead.add_note("test note")
|
||||
event = create_event("Meeting 1", today(), "Lead", lead.name)
|
||||
create_todo("followup", "Lead", lead.name)
|
||||
|
||||
opportunity = make_opportunity(lead.name)
|
||||
opportunity.save()
|
||||
|
||||
self.assertEqual(opportunity.get("party_name"), lead.name)
|
||||
self.assertEqual(opportunity.notes[0].note, "test note")
|
||||
|
||||
event.reload()
|
||||
self.assertEqual(event.event_participants[1].reference_doctype, "Opportunity")
|
||||
self.assertEqual(event.event_participants[1].reference_docname, opportunity.name)
|
||||
|
||||
self.assertTrue(
|
||||
frappe.db.get_value(
|
||||
"ToDo", {"reference_type": "Opportunity", "reference_name": opportunity.name}
|
||||
)
|
||||
)
|
||||
|
||||
def test_copy_events_from_lead_to_prospect(self):
|
||||
frappe.db.sql("delete from `tabLead` where lead_name='Rahul Tripathi'")
|
||||
frappe.db.sql("delete from `tabProspect` where name='Prospect Company'")
|
||||
|
||||
lead = make_lead(
|
||||
first_name="Rahul",
|
||||
last_name="Tripathi",
|
||||
email_id="rahul@gmail.com",
|
||||
company_name="Prospect Company",
|
||||
)
|
||||
|
||||
lead.create_prospect(lead.company_name)
|
||||
prospect = get_linked_prospect("Lead", lead.name)
|
||||
|
||||
event = create_event("Meeting", today(), "Lead", lead.name)
|
||||
|
||||
self.assertEqual(len(event.event_participants), 2)
|
||||
self.assertEqual(event.event_participants[1].reference_doctype, "Prospect")
|
||||
self.assertEqual(event.event_participants[1].reference_docname, prospect)
|
||||
|
||||
|
||||
def create_event(subject, starts_on, reference_type, reference_name):
|
||||
event = frappe.new_doc("Event")
|
||||
event.subject = subject
|
||||
event.starts_on = starts_on
|
||||
event.event_type = "Private"
|
||||
event.all_day = 1
|
||||
event.owner = "Administrator"
|
||||
event.append(
|
||||
"event_participants", {"reference_doctype": reference_type, "reference_docname": reference_name}
|
||||
)
|
||||
event.reference_type = reference_type
|
||||
event.reference_name = reference_name
|
||||
event.insert()
|
||||
return event
|
||||
|
||||
|
||||
def create_todo(description, reference_type, reference_name):
|
||||
todo = frappe.new_doc("ToDo")
|
||||
todo.description = description
|
||||
todo.owner = "Administrator"
|
||||
todo.reference_type = reference_type
|
||||
todo.reference_name = reference_name
|
||||
todo.insert()
|
||||
return todo
|
||||
|
||||
|
||||
def make_lead(**args):
|
||||
args = frappe._dict(args)
|
||||
@ -93,6 +195,7 @@ def make_lead(**args):
|
||||
"first_name": args.first_name or "_Test",
|
||||
"last_name": args.last_name or "Lead",
|
||||
"email_id": args.email_id or "new_lead_{}@example.com".format(random_string(5)),
|
||||
"company_name": args.company_name or "_Test Company",
|
||||
}
|
||||
).insert()
|
||||
|
||||
|
@ -32,13 +32,6 @@ frappe.ui.form.on("Opportunity", {
|
||||
}
|
||||
},
|
||||
|
||||
contact_date: function(frm) {
|
||||
if(frm.doc.contact_date < frappe.datetime.now_datetime()){
|
||||
frm.set_value("contact_date", "");
|
||||
frappe.throw(__("Next follow up date should be greater than now."))
|
||||
}
|
||||
},
|
||||
|
||||
onload_post_render: function(frm) {
|
||||
frm.get_field("items").grid.set_multiple_add("item_code", "qty");
|
||||
},
|
||||
@ -130,6 +123,13 @@ frappe.ui.form.on("Opportunity", {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!frm.is_new()) {
|
||||
frappe.contacts.render_address_and_contact(frm);
|
||||
// frm.trigger('render_contact_day_html');
|
||||
} else {
|
||||
frappe.contacts.clear_address_and_contact(frm);
|
||||
}
|
||||
},
|
||||
|
||||
set_contact_link: function(frm) {
|
||||
@ -227,8 +227,7 @@ frappe.ui.form.on("Opportunity", {
|
||||
'total': flt(total),
|
||||
'base_total': flt(base_total)
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
});
|
||||
frappe.ui.form.on("Opportunity Item", {
|
||||
calculate: function(frm, cdt, cdn) {
|
||||
@ -264,13 +263,14 @@ erpnext.crm.Opportunity = class Opportunity extends frappe.ui.form.Controller {
|
||||
this.frm.trigger('currency');
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.show_notes();
|
||||
this.show_activities();
|
||||
}
|
||||
|
||||
setup_queries() {
|
||||
var me = this;
|
||||
|
||||
if(this.frm.fields_dict.contact_by.df.options.match(/^User/)) {
|
||||
this.frm.set_query("contact_by", erpnext.queries.user);
|
||||
}
|
||||
|
||||
me.frm.set_query('customer_address', erpnext.queries.address_query);
|
||||
|
||||
this.frm.set_query("item_code", "items", function() {
|
||||
@ -287,6 +287,14 @@ erpnext.crm.Opportunity = class Opportunity extends frappe.ui.form.Controller {
|
||||
}
|
||||
else if (me.frm.doc.opportunity_from == "Customer") {
|
||||
me.frm.set_query('party_name', erpnext.queries['customer']);
|
||||
} else if (me.frm.doc.opportunity_from == "Prospect") {
|
||||
me.frm.set_query('party_name', function() {
|
||||
return {
|
||||
filters: {
|
||||
"company": me.frm.doc.company
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -303,6 +311,24 @@ erpnext.crm.Opportunity = class Opportunity extends frappe.ui.form.Controller {
|
||||
frm: cur_frm
|
||||
})
|
||||
}
|
||||
|
||||
show_notes() {
|
||||
const crm_notes = new erpnext.utils.CRMNotes({
|
||||
frm: this.frm,
|
||||
notes_wrapper: $(this.frm.fields_dict.notes_html.wrapper),
|
||||
});
|
||||
crm_notes.refresh();
|
||||
}
|
||||
|
||||
show_activities() {
|
||||
const crm_activities = new erpnext.utils.CRMActivities({
|
||||
frm: this.frm,
|
||||
open_activities_wrapper: $(this.frm.fields_dict.open_activities_html.wrapper),
|
||||
all_activities_wrapper: $(this.frm.fields_dict.all_activities_html.wrapper),
|
||||
form_wrapper: $(this.frm.wrapper),
|
||||
});
|
||||
crm_activities.refresh();
|
||||
}
|
||||
};
|
||||
|
||||
extend_cscript(cur_frm.cscript, new erpnext.crm.Opportunity({frm: cur_frm}));
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_events_in_timeline": 1,
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "naming_series:",
|
||||
@ -11,68 +12,87 @@
|
||||
"email_append_to": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"from_section",
|
||||
"naming_series",
|
||||
"opportunity_from",
|
||||
"party_name",
|
||||
"customer_name",
|
||||
"source",
|
||||
"column_break0",
|
||||
"title",
|
||||
"opportunity_type",
|
||||
"status",
|
||||
"converted_by",
|
||||
"column_break0",
|
||||
"opportunity_type",
|
||||
"source",
|
||||
"opportunity_owner",
|
||||
"column_break_10",
|
||||
"sales_stage",
|
||||
"first_response_time",
|
||||
"expected_closing",
|
||||
"next_contact",
|
||||
"contact_by",
|
||||
"contact_date",
|
||||
"column_break2",
|
||||
"to_discuss",
|
||||
"probability",
|
||||
"organization_details_section",
|
||||
"no_of_employees",
|
||||
"annual_revenue",
|
||||
"customer_group",
|
||||
"column_break_23",
|
||||
"industry",
|
||||
"market_segment",
|
||||
"website",
|
||||
"column_break_31",
|
||||
"city",
|
||||
"state",
|
||||
"country",
|
||||
"territory",
|
||||
"section_break_14",
|
||||
"currency",
|
||||
"column_break_36",
|
||||
"conversion_rate",
|
||||
"base_opportunity_amount",
|
||||
"with_items",
|
||||
"column_break_17",
|
||||
"probability",
|
||||
"opportunity_amount",
|
||||
"base_opportunity_amount",
|
||||
"more_info",
|
||||
"company",
|
||||
"campaign",
|
||||
"transaction_date",
|
||||
"column_break1",
|
||||
"language",
|
||||
"amended_from",
|
||||
"title",
|
||||
"first_response_time",
|
||||
"lost_detail_section",
|
||||
"lost_reasons",
|
||||
"order_lost_reason",
|
||||
"column_break_56",
|
||||
"competitors",
|
||||
"contact_info",
|
||||
"primary_contact_section",
|
||||
"contact_person",
|
||||
"job_title",
|
||||
"column_break_54",
|
||||
"contact_email",
|
||||
"contact_mobile",
|
||||
"column_break_22",
|
||||
"whatsapp",
|
||||
"phone",
|
||||
"phone_ext",
|
||||
"address_contact_section",
|
||||
"address_html",
|
||||
"customer_address",
|
||||
"address_display",
|
||||
"column_break3",
|
||||
"contact_html",
|
||||
"contact_display",
|
||||
"items_section",
|
||||
"items",
|
||||
"section_break_32",
|
||||
"base_total",
|
||||
"column_break_33",
|
||||
"total",
|
||||
"contact_info",
|
||||
"customer_address",
|
||||
"address_display",
|
||||
"territory",
|
||||
"customer_group",
|
||||
"column_break3",
|
||||
"contact_person",
|
||||
"contact_display",
|
||||
"contact_email",
|
||||
"contact_mobile",
|
||||
"more_info",
|
||||
"company",
|
||||
"campaign",
|
||||
"column_break1",
|
||||
"transaction_date",
|
||||
"language",
|
||||
"amended_from",
|
||||
"lost_detail_section",
|
||||
"lost_reasons",
|
||||
"order_lost_reason",
|
||||
"column_break_56",
|
||||
"competitors"
|
||||
"activities_tab",
|
||||
"open_activities_html",
|
||||
"all_activities_section",
|
||||
"all_activities_html",
|
||||
"notes_tab",
|
||||
"notes_html",
|
||||
"notes",
|
||||
"dashboard_tab"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "from_section",
|
||||
"fieldtype": "Section Break",
|
||||
"options": "fa fa-user"
|
||||
},
|
||||
{
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Select",
|
||||
@ -113,8 +133,9 @@
|
||||
"bold": 1,
|
||||
"fieldname": "customer_name",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"in_global_search": 1,
|
||||
"label": "Customer / Lead Name",
|
||||
"label": "Customer Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@ -166,48 +187,10 @@
|
||||
"fieldtype": "Date",
|
||||
"label": "Expected Closing Date"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "contact_by",
|
||||
"fieldname": "next_contact",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Follow Up"
|
||||
},
|
||||
{
|
||||
"fieldname": "contact_by",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Next Contact By",
|
||||
"oldfieldname": "contact_by",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "User",
|
||||
"width": "75px"
|
||||
},
|
||||
{
|
||||
"fieldname": "contact_date",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Next Contact Date",
|
||||
"oldfieldname": "contact_date",
|
||||
"oldfieldtype": "Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break2",
|
||||
"fieldtype": "Column Break",
|
||||
"oldfieldtype": "Column Break",
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"fieldname": "to_discuss",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "To Discuss",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "to_discuss",
|
||||
"oldfieldtype": "Small Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_14",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Sales"
|
||||
"label": "Opportunity Value"
|
||||
},
|
||||
{
|
||||
"fieldname": "currency",
|
||||
@ -221,12 +204,6 @@
|
||||
"label": "Opportunity Amount",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "with_items",
|
||||
"fieldtype": "Check",
|
||||
"label": "With Items"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_17",
|
||||
"fieldtype": "Column Break"
|
||||
@ -245,9 +222,8 @@
|
||||
"label": "Probability (%)"
|
||||
},
|
||||
{
|
||||
"depends_on": "with_items",
|
||||
"fieldname": "items_section",
|
||||
"fieldtype": "Section Break",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Items",
|
||||
"oldfieldtype": "Section Break",
|
||||
"options": "fa fa-shopping-cart"
|
||||
@ -262,18 +238,16 @@
|
||||
"options": "Opportunity Item"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "next_contact_by",
|
||||
"depends_on": "eval:doc.party_name",
|
||||
"fieldname": "contact_info",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Contact Info",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Contacts",
|
||||
"options": "fa fa-bullhorn"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.party_name",
|
||||
"fieldname": "customer_address",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Customer / Lead Address",
|
||||
"options": "Address",
|
||||
"print_hide": 1
|
||||
@ -327,19 +301,16 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.party_name",
|
||||
"fieldname": "contact_email",
|
||||
"fieldtype": "Data",
|
||||
"label": "Contact Email",
|
||||
"options": "Email",
|
||||
"read_only": 1
|
||||
"options": "Email"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.party_name",
|
||||
"fieldname": "contact_mobile",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Contact Mobile No",
|
||||
"read_only": 1
|
||||
"fieldtype": "Data",
|
||||
"label": "Contact Mobile",
|
||||
"options": "Phone"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
@ -416,12 +387,6 @@
|
||||
"options": "Opportunity Lost Reason Detail",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "converted_by",
|
||||
"fieldtype": "Link",
|
||||
"label": "Converted By",
|
||||
"options": "User"
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"fieldname": "first_response_time",
|
||||
@ -474,6 +439,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.status===\"Lost\"",
|
||||
"fieldname": "lost_detail_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Lost Reasons"
|
||||
@ -488,12 +454,179 @@
|
||||
"label": "Competitors",
|
||||
"options": "Competitor Detail",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_10",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "organization_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Organization"
|
||||
},
|
||||
{
|
||||
"fieldname": "no_of_employees",
|
||||
"fieldtype": "Int",
|
||||
"label": "No of Employees"
|
||||
},
|
||||
{
|
||||
"fieldname": "annual_revenue",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Annual Revenue"
|
||||
},
|
||||
{
|
||||
"fieldname": "industry",
|
||||
"fieldtype": "Link",
|
||||
"label": "Industry",
|
||||
"options": "Industry Type"
|
||||
},
|
||||
{
|
||||
"fieldname": "market_segment",
|
||||
"fieldtype": "Link",
|
||||
"label": "Market Segment",
|
||||
"options": "Market Segment"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_23",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "address_contact_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Address & Contact"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_36",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "opportunity_owner",
|
||||
"fieldtype": "Link",
|
||||
"label": "Opportunity Owner",
|
||||
"options": "User"
|
||||
},
|
||||
{
|
||||
"fieldname": "website",
|
||||
"fieldtype": "Data",
|
||||
"label": "Website"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_22",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "whatsapp",
|
||||
"fieldtype": "Data",
|
||||
"label": "WhatsApp",
|
||||
"options": "Phone"
|
||||
},
|
||||
{
|
||||
"fieldname": "phone",
|
||||
"fieldtype": "Data",
|
||||
"label": "Phone",
|
||||
"options": "Phone"
|
||||
},
|
||||
{
|
||||
"fieldname": "phone_ext",
|
||||
"fieldtype": "Data",
|
||||
"label": "Phone Ext."
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_31",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "primary_contact_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Primary Contact"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_54",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "dashboard_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Dashboard",
|
||||
"show_dashboard": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "notes_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Notes"
|
||||
},
|
||||
{
|
||||
"fieldname": "notes_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Notes HTML"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "activities_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Activities"
|
||||
},
|
||||
{
|
||||
"fieldname": "job_title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Job Title"
|
||||
},
|
||||
{
|
||||
"fieldname": "address_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Address HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "contact_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Contact HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "open_activities_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Open Activities HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "all_activities_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "All Activities"
|
||||
},
|
||||
{
|
||||
"fieldname": "all_activities_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "All Activities HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "notes",
|
||||
"fieldtype": "Table",
|
||||
"hidden": 1,
|
||||
"label": "Notes",
|
||||
"no_copy": 1,
|
||||
"options": "CRM Note"
|
||||
},
|
||||
{
|
||||
"fieldname": "city",
|
||||
"fieldtype": "Data",
|
||||
"label": "City"
|
||||
},
|
||||
{
|
||||
"fieldname": "state",
|
||||
"fieldtype": "Data",
|
||||
"label": "State"
|
||||
},
|
||||
{
|
||||
"fieldname": "country",
|
||||
"fieldtype": "Link",
|
||||
"label": "Country",
|
||||
"options": "Country"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-info-sign",
|
||||
"idx": 195,
|
||||
"links": [],
|
||||
"modified": "2022-01-29 19:32:26.382896",
|
||||
"modified": "2022-06-27 18:44:32.858696",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Opportunity",
|
||||
|
@ -6,53 +6,54 @@ import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.contacts.address_and_contact import load_address_and_contact
|
||||
from frappe.email.inbox import link_communication_to_document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.query_builder import DocType, Interval
|
||||
from frappe.query_builder.functions import Now
|
||||
from frappe.utils import cint, flt, get_fullname
|
||||
from frappe.utils import flt, get_fullname
|
||||
|
||||
from erpnext.crm.utils import add_link_in_communication, copy_comments
|
||||
from erpnext.crm.utils import (
|
||||
CRMNote,
|
||||
copy_comments,
|
||||
link_communications,
|
||||
link_open_events,
|
||||
link_open_tasks,
|
||||
)
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
from erpnext.utilities.transaction_base import TransactionBase
|
||||
|
||||
|
||||
class Opportunity(TransactionBase):
|
||||
class Opportunity(TransactionBase, CRMNote):
|
||||
def onload(self):
|
||||
ref_doc = frappe.get_doc(self.opportunity_from, self.party_name)
|
||||
load_address_and_contact(ref_doc)
|
||||
self.set("__onload", ref_doc.get("__onload"))
|
||||
|
||||
def after_insert(self):
|
||||
if self.opportunity_from == "Lead":
|
||||
frappe.get_doc("Lead", self.party_name).set_status(update=True)
|
||||
self.disable_lead()
|
||||
|
||||
if self.opportunity_from in ["Lead", "Prospect"]:
|
||||
link_open_tasks(self.opportunity_from, self.party_name, self)
|
||||
link_open_events(self.opportunity_from, self.party_name, self)
|
||||
if frappe.db.get_single_value("CRM Settings", "carry_forward_communication_and_comments"):
|
||||
copy_comments(self.opportunity_from, self.party_name, self)
|
||||
add_link_in_communication(self.opportunity_from, self.party_name, self)
|
||||
link_communications(self.opportunity_from, self.party_name, self)
|
||||
|
||||
def validate(self):
|
||||
self._prev = frappe._dict(
|
||||
{
|
||||
"contact_date": frappe.db.get_value("Opportunity", self.name, "contact_date")
|
||||
if (not cint(self.get("__islocal")))
|
||||
else None,
|
||||
"contact_by": frappe.db.get_value("Opportunity", self.name, "contact_by")
|
||||
if (not cint(self.get("__islocal")))
|
||||
else None,
|
||||
}
|
||||
)
|
||||
|
||||
self.make_new_lead_if_required()
|
||||
self.validate_item_details()
|
||||
self.validate_uom_is_integer("uom", "qty")
|
||||
self.validate_cust_name()
|
||||
self.map_fields()
|
||||
self.set_exchange_rate()
|
||||
|
||||
if not self.title:
|
||||
self.title = self.customer_name
|
||||
|
||||
if not self.with_items:
|
||||
self.items = []
|
||||
|
||||
else:
|
||||
self.calculate_totals()
|
||||
self.update_prospect()
|
||||
|
||||
def map_fields(self):
|
||||
for field in self.meta.get_valid_columns():
|
||||
@ -63,18 +64,65 @@ class Opportunity(TransactionBase):
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
def set_exchange_rate(self):
|
||||
company_currency = frappe.get_cached_value("Company", self.company, "default_currency")
|
||||
if self.currency == company_currency:
|
||||
self.conversion_rate = 1.0
|
||||
return
|
||||
|
||||
if not self.conversion_rate or self.conversion_rate == 1.0:
|
||||
self.conversion_rate = get_exchange_rate(self.currency, company_currency, self.transaction_date)
|
||||
|
||||
def calculate_totals(self):
|
||||
total = base_total = 0
|
||||
for item in self.get("items"):
|
||||
item.amount = flt(item.rate) * flt(item.qty)
|
||||
item.base_rate = flt(self.conversion_rate * item.rate)
|
||||
item.base_amount = flt(self.conversion_rate * item.amount)
|
||||
item.base_rate = flt(self.conversion_rate) * flt(item.rate)
|
||||
item.base_amount = flt(self.conversion_rate) * flt(item.amount)
|
||||
total += item.amount
|
||||
base_total += item.base_amount
|
||||
|
||||
self.total = flt(total)
|
||||
self.base_total = flt(base_total)
|
||||
|
||||
def update_prospect(self):
|
||||
prospect_name = None
|
||||
if self.opportunity_from == "Prospect" and self.party_name:
|
||||
prospect_name = self.party_name
|
||||
elif self.opportunity_from == "Lead":
|
||||
prospect_name = frappe.db.get_value("Prospect Lead", {"lead": self.party_name}, "parent")
|
||||
|
||||
if prospect_name:
|
||||
prospect = frappe.get_doc("Prospect", prospect_name)
|
||||
|
||||
opportunity_values = {
|
||||
"opportunity": self.name,
|
||||
"amount": self.opportunity_amount,
|
||||
"stage": self.sales_stage,
|
||||
"deal_owner": self.opportunity_owner,
|
||||
"probability": self.probability,
|
||||
"expected_closing": self.expected_closing,
|
||||
"currency": self.currency,
|
||||
"contact_person": self.contact_person,
|
||||
}
|
||||
|
||||
opportunity_already_added = False
|
||||
for d in prospect.get("opportunities", []):
|
||||
if d.opportunity == self.name:
|
||||
opportunity_already_added = True
|
||||
d.update(opportunity_values)
|
||||
d.db_update()
|
||||
|
||||
if not opportunity_already_added:
|
||||
prospect.append("opportunities", opportunity_values)
|
||||
prospect.flags.ignore_permissions = True
|
||||
prospect.flags.ignore_mandatory = True
|
||||
prospect.save()
|
||||
|
||||
def disable_lead(self):
|
||||
if self.opportunity_from == "Lead":
|
||||
frappe.db.set_value("Lead", self.party_name, {"disabled": 1, "docstatus": 1})
|
||||
|
||||
def make_new_lead_if_required(self):
|
||||
"""Set lead against new opportunity"""
|
||||
if (not self.get("party_name")) and self.contact_email:
|
||||
@ -144,11 +192,8 @@ class Opportunity(TransactionBase):
|
||||
else:
|
||||
frappe.throw(_("Cannot declare as lost, because Quotation has been made."))
|
||||
|
||||
def on_trash(self):
|
||||
self.delete_events()
|
||||
|
||||
def has_active_quotation(self):
|
||||
if not self.with_items:
|
||||
if not self.get("items", []):
|
||||
return frappe.get_all(
|
||||
"Quotation",
|
||||
{"opportunity": self.name, "status": ("not in", ["Lost", "Closed"]), "docstatus": 1},
|
||||
@ -165,7 +210,7 @@ class Opportunity(TransactionBase):
|
||||
)
|
||||
|
||||
def has_ordered_quotation(self):
|
||||
if not self.with_items:
|
||||
if not self.get("items", []):
|
||||
return frappe.get_all(
|
||||
"Quotation", {"opportunity": self.name, "status": "Ordered", "docstatus": 1}, "name"
|
||||
)
|
||||
@ -195,43 +240,20 @@ class Opportunity(TransactionBase):
|
||||
return True
|
||||
|
||||
def validate_cust_name(self):
|
||||
if self.party_name and self.opportunity_from == "Customer":
|
||||
if self.party_name:
|
||||
if self.opportunity_from == "Customer":
|
||||
self.customer_name = frappe.db.get_value("Customer", self.party_name, "customer_name")
|
||||
elif self.party_name and self.opportunity_from == "Lead":
|
||||
elif self.opportunity_from == "Lead":
|
||||
customer_name = frappe.db.get_value("Prospect Lead", {"lead": self.party_name}, "parent")
|
||||
if not customer_name:
|
||||
lead_name, company_name = frappe.db.get_value(
|
||||
"Lead", self.party_name, ["lead_name", "company_name"]
|
||||
)
|
||||
self.customer_name = company_name or lead_name
|
||||
customer_name = company_name or lead_name
|
||||
|
||||
def on_update(self):
|
||||
self.add_calendar_event()
|
||||
|
||||
def add_calendar_event(self, opts=None, force=False):
|
||||
if frappe.db.get_single_value("CRM Settings", "create_event_on_next_contact_date_opportunity"):
|
||||
if not opts:
|
||||
opts = frappe._dict()
|
||||
|
||||
opts.description = ""
|
||||
opts.contact_date = self.contact_date
|
||||
|
||||
if self.party_name and self.opportunity_from == "Customer":
|
||||
if self.contact_person:
|
||||
opts.description = f"Contact {self.contact_person}"
|
||||
else:
|
||||
opts.description = f"Contact customer {self.party_name}"
|
||||
elif self.party_name and self.opportunity_from == "Lead":
|
||||
if self.contact_display:
|
||||
opts.description = f"Contact {self.contact_display}"
|
||||
else:
|
||||
opts.description = f"Contact lead {self.party_name}"
|
||||
|
||||
opts.subject = opts.description
|
||||
opts.description += f". By : {self.contact_by}"
|
||||
|
||||
if self.to_discuss:
|
||||
opts.description += f" To Discuss : {frappe.render_template(self.to_discuss, {'doc': self})}"
|
||||
|
||||
super(Opportunity, self).add_calendar_event(opts, force)
|
||||
self.customer_name = customer_name
|
||||
elif self.opportunity_from == "Prospect":
|
||||
self.customer_name = self.party_name
|
||||
|
||||
def validate_item_details(self):
|
||||
if not self.get("items"):
|
||||
@ -295,7 +317,7 @@ def make_quotation(source_name, target_doc=None):
|
||||
|
||||
quotation.run_method("set_missing_values")
|
||||
quotation.run_method("calculate_taxes_and_totals")
|
||||
if not source.with_items:
|
||||
if not source.get("items", []):
|
||||
quotation.opportunity = source.name
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
@ -440,34 +462,3 @@ def make_opportunity_from_communication(communication, company, ignore_communica
|
||||
link_communication_to_document(doc, "Opportunity", opportunity.name, ignore_communication_links)
|
||||
|
||||
return opportunity.name
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_events(start, end, filters=None):
|
||||
"""Returns events for Gantt / Calendar view rendering.
|
||||
:param start: Start date-time.
|
||||
:param end: End date-time.
|
||||
:param filters: Filters (JSON).
|
||||
"""
|
||||
from frappe.desk.calendar import get_event_conditions
|
||||
|
||||
conditions = get_event_conditions("Opportunity", filters)
|
||||
|
||||
data = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
distinct `tabOpportunity`.name, `tabOpportunity`.customer_name, `tabOpportunity`.opportunity_amount,
|
||||
`tabOpportunity`.title, `tabOpportunity`.contact_date
|
||||
from
|
||||
`tabOpportunity`
|
||||
where
|
||||
(`tabOpportunity`.contact_date between %(start)s and %(end)s)
|
||||
{conditions}
|
||||
""".format(
|
||||
conditions=conditions
|
||||
),
|
||||
{"start": start, "end": end},
|
||||
as_dict=True,
|
||||
update={"allDay": 0},
|
||||
)
|
||||
return data
|
||||
|
@ -1,19 +0,0 @@
|
||||
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
frappe.views.calendar["Opportunity"] = {
|
||||
field_map: {
|
||||
"start": "contact_date",
|
||||
"end": "contact_date",
|
||||
"id": "name",
|
||||
"title": "customer_name",
|
||||
"allDay": "allDay"
|
||||
},
|
||||
options: {
|
||||
header: {
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: 'month'
|
||||
}
|
||||
},
|
||||
get_events_method: 'erpnext.crm.doctype.opportunity.opportunity.get_events'
|
||||
}
|
@ -77,42 +77,6 @@ class TestOpportunity(unittest.TestCase):
|
||||
create_communication(opp_doc.doctype, opp_doc.name, opp_doc.contact_email)
|
||||
create_communication(opp_doc.doctype, opp_doc.name, opp_doc.contact_email)
|
||||
|
||||
quotation_doc = make_quotation(opp_doc.name)
|
||||
quotation_doc.append("items", {"item_code": "_Test Item", "qty": 1})
|
||||
quotation_doc.run_method("set_missing_values")
|
||||
quotation_doc.run_method("calculate_taxes_and_totals")
|
||||
quotation_doc.save()
|
||||
|
||||
quotation_comment_count = frappe.db.count(
|
||||
"Comment",
|
||||
{
|
||||
"reference_doctype": quotation_doc.doctype,
|
||||
"reference_name": quotation_doc.name,
|
||||
"comment_type": "Comment",
|
||||
},
|
||||
)
|
||||
quotation_communication_count = len(
|
||||
get_linked_communication_list(quotation_doc.doctype, quotation_doc.name)
|
||||
)
|
||||
self.assertEqual(quotation_comment_count, 4)
|
||||
self.assertEqual(quotation_communication_count, 4)
|
||||
|
||||
def test_render_template_for_to_discuss(self):
|
||||
doc = make_opportunity(with_items=0, opportunity_from="Lead")
|
||||
doc.contact_by = "test@example.com"
|
||||
doc.contact_date = add_days(today(), days=2)
|
||||
doc.to_discuss = "{{ doc.name }} test data"
|
||||
doc.save()
|
||||
|
||||
event = frappe.get_all(
|
||||
"Event Participants",
|
||||
fields=["parent"],
|
||||
filters={"reference_doctype": doc.doctype, "reference_docname": doc.name},
|
||||
)
|
||||
|
||||
event_description = frappe.db.get_value("Event", event[0].parent, "description")
|
||||
self.assertTrue(doc.name in event_description)
|
||||
|
||||
|
||||
def make_opportunity_from_lead():
|
||||
new_lead_email_id = "new{}@example.com".format(random_string(5))
|
||||
@ -139,7 +103,6 @@ def make_opportunity(**args):
|
||||
"opportunity_from": args.opportunity_from or "Customer",
|
||||
"opportunity_type": "Sales",
|
||||
"conversion_rate": 1.0,
|
||||
"with_items": args.with_items or 0,
|
||||
"transaction_date": today(),
|
||||
}
|
||||
)
|
||||
|
@ -8,7 +8,9 @@
|
||||
"transaction_date": "2013-12-12",
|
||||
"items": [{
|
||||
"item_name": "Test Item",
|
||||
"description": "Some description"
|
||||
"description": "Some description",
|
||||
"qty": 5,
|
||||
"rate": 100
|
||||
}]
|
||||
}
|
||||
]
|
||||
|
@ -27,5 +27,26 @@ frappe.ui.form.on('Prospect', {
|
||||
} else {
|
||||
frappe.contacts.clear_address_and_contact(frm);
|
||||
}
|
||||
frm.trigger("show_notes");
|
||||
frm.trigger("show_activities");
|
||||
},
|
||||
|
||||
show_notes (frm) {
|
||||
const crm_notes = new erpnext.utils.CRMNotes({
|
||||
frm: frm,
|
||||
notes_wrapper: $(frm.fields_dict.notes_html.wrapper),
|
||||
});
|
||||
crm_notes.refresh();
|
||||
},
|
||||
|
||||
show_activities (frm) {
|
||||
const crm_activities = new erpnext.utils.CRMActivities({
|
||||
frm: frm,
|
||||
open_activities_wrapper: $(frm.fields_dict.open_activities_html.wrapper),
|
||||
all_activities_wrapper: $(frm.fields_dict.all_activities_html.wrapper),
|
||||
form_wrapper: $(frm.wrapper),
|
||||
});
|
||||
crm_activities.refresh();
|
||||
}
|
||||
|
||||
});
|
||||
|
@ -1,33 +1,42 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_events_in_timeline": 1,
|
||||
"autoname": "field:company_name",
|
||||
"creation": "2021-08-19 00:21:06.995448",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"overview_tab",
|
||||
"company_name",
|
||||
"industry",
|
||||
"market_segment",
|
||||
"customer_group",
|
||||
"no_of_employees",
|
||||
"annual_revenue",
|
||||
"column_break_4",
|
||||
"market_segment",
|
||||
"industry",
|
||||
"territory",
|
||||
"column_break_6",
|
||||
"no_of_employees",
|
||||
"currency",
|
||||
"annual_revenue",
|
||||
"more_details_section",
|
||||
"fax",
|
||||
"website",
|
||||
"column_break_13",
|
||||
"prospect_owner",
|
||||
"website",
|
||||
"fax",
|
||||
"company",
|
||||
"leads_section",
|
||||
"prospect_lead",
|
||||
"address_and_contact_section",
|
||||
"column_break_16",
|
||||
"contacts_tab",
|
||||
"address_html",
|
||||
"column_break_17",
|
||||
"column_break_18",
|
||||
"contact_html",
|
||||
"leads_section",
|
||||
"leads",
|
||||
"opportunities_tab",
|
||||
"opportunities",
|
||||
"activities_tab",
|
||||
"open_activities_html",
|
||||
"all_activities_section",
|
||||
"all_activities_html",
|
||||
"notes_section",
|
||||
"notes_html",
|
||||
"notes"
|
||||
],
|
||||
"fields": [
|
||||
@ -71,15 +80,9 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "no_of_employees",
|
||||
"fieldtype": "Int",
|
||||
"label": "No. of Employees"
|
||||
},
|
||||
{
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Currency",
|
||||
"options": "Currency"
|
||||
"fieldtype": "Select",
|
||||
"label": "No. of Employees",
|
||||
"options": "1-10\n11-20\n21-30\n31-100\n11-50\n51-200\n201-500\n101-500\n500-1000\n501-1000\n>1000\n1000+"
|
||||
},
|
||||
{
|
||||
"fieldname": "annual_revenue",
|
||||
@ -97,8 +100,7 @@
|
||||
{
|
||||
"fieldname": "website",
|
||||
"fieldtype": "Data",
|
||||
"label": "Website",
|
||||
"options": "URL"
|
||||
"label": "Website"
|
||||
},
|
||||
{
|
||||
"fieldname": "prospect_owner",
|
||||
@ -108,23 +110,14 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "leads_section",
|
||||
"fieldtype": "Section Break",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Leads"
|
||||
},
|
||||
{
|
||||
"fieldname": "prospect_lead",
|
||||
"fieldtype": "Table",
|
||||
"options": "Prospect Lead"
|
||||
},
|
||||
{
|
||||
"fieldname": "address_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Address HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_17",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "contact_html",
|
||||
"fieldtype": "HTML",
|
||||
@ -132,28 +125,16 @@
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "notes_section",
|
||||
"fieldtype": "Section Break",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Notes"
|
||||
},
|
||||
{
|
||||
"fieldname": "notes",
|
||||
"fieldtype": "Text Editor"
|
||||
},
|
||||
{
|
||||
"fieldname": "more_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "More Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_13",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: !doc.__islocal",
|
||||
"fieldname": "address_and_contact_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Address and Contact"
|
||||
"label": "Address"
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
@ -161,11 +142,83 @@
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "opportunities_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Opportunities"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "activities_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Activities"
|
||||
},
|
||||
{
|
||||
"fieldname": "notes_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Notes HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "opportunities",
|
||||
"fieldtype": "Table",
|
||||
"label": "Opportunities",
|
||||
"options": "Prospect Opportunity"
|
||||
},
|
||||
{
|
||||
"fieldname": "contacts_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Address & Contact"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_18",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "leads",
|
||||
"fieldtype": "Table",
|
||||
"options": "Prospect Lead"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_16",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "overview_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Overview"
|
||||
},
|
||||
{
|
||||
"fieldname": "open_activities_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Open Activities HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "all_activities_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "All Activities"
|
||||
},
|
||||
{
|
||||
"fieldname": "all_activities_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "All Activities HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "notes",
|
||||
"fieldtype": "Table",
|
||||
"hidden": 1,
|
||||
"label": "Notes",
|
||||
"no_copy": 1,
|
||||
"options": "CRM Note"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-11-01 13:10:36.759249",
|
||||
"modified": "2022-06-21 15:10:26.887502",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Prospect",
|
||||
@ -207,6 +260,7 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "company_name",
|
||||
"track_changes": 1
|
||||
}
|
@ -3,19 +3,15 @@
|
||||
|
||||
import frappe
|
||||
from frappe.contacts.address_and_contact import load_address_and_contact
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
|
||||
from erpnext.crm.utils import add_link_in_communication, copy_comments
|
||||
from erpnext.crm.utils import CRMNote, copy_comments, link_communications, link_open_events
|
||||
|
||||
|
||||
class Prospect(Document):
|
||||
class Prospect(CRMNote):
|
||||
def onload(self):
|
||||
load_address_and_contact(self)
|
||||
|
||||
def validate(self):
|
||||
self.update_lead_details()
|
||||
|
||||
def on_update(self):
|
||||
self.link_with_lead_contact_and_address()
|
||||
|
||||
@ -23,23 +19,24 @@ class Prospect(Document):
|
||||
self.unlink_dynamic_links()
|
||||
|
||||
def after_insert(self):
|
||||
if frappe.db.get_single_value("CRM Settings", "carry_forward_communication_and_comments"):
|
||||
for row in self.get("prospect_lead"):
|
||||
copy_comments("Lead", row.lead, self)
|
||||
add_link_in_communication("Lead", row.lead, self)
|
||||
|
||||
def update_lead_details(self):
|
||||
for row in self.get("prospect_lead"):
|
||||
lead = frappe.get_value(
|
||||
"Lead", row.lead, ["lead_name", "status", "email_id", "mobile_no"], as_dict=True
|
||||
carry_forward_communication_and_comments = frappe.db.get_single_value(
|
||||
"CRM Settings", "carry_forward_communication_and_comments"
|
||||
)
|
||||
row.lead_name = lead.lead_name
|
||||
row.status = lead.status
|
||||
row.email = lead.email_id
|
||||
row.mobile_no = lead.mobile_no
|
||||
|
||||
for row in self.get("leads"):
|
||||
if carry_forward_communication_and_comments:
|
||||
copy_comments("Lead", row.lead, self)
|
||||
link_communications("Lead", row.lead, self)
|
||||
link_open_events("Lead", row.lead, self)
|
||||
|
||||
for row in self.get("opportunities"):
|
||||
if carry_forward_communication_and_comments:
|
||||
copy_comments("Opportunity", row.opportunity, self)
|
||||
link_communications("Opportunity", row.opportunity, self)
|
||||
link_open_events("Opportunity", row.opportunity, self)
|
||||
|
||||
def link_with_lead_contact_and_address(self):
|
||||
for row in self.prospect_lead:
|
||||
for row in self.leads:
|
||||
links = frappe.get_all(
|
||||
"Dynamic Link",
|
||||
filters={"link_doctype": "Lead", "link_name": row.lead},
|
||||
@ -116,9 +113,7 @@ def make_opportunity(source_name, target_doc=None):
|
||||
{
|
||||
"Prospect": {
|
||||
"doctype": "Opportunity",
|
||||
"field_map": {
|
||||
"name": "party_name",
|
||||
},
|
||||
"field_map": {"name": "party_name", "prospect_owner": "opportunity_owner"},
|
||||
}
|
||||
},
|
||||
target_doc,
|
||||
@ -127,3 +122,25 @@ def make_opportunity(source_name, target_doc=None):
|
||||
)
|
||||
|
||||
return doclist
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_opportunities(prospect):
|
||||
return frappe.get_all(
|
||||
"Opportunity",
|
||||
filters={"opportunity_from": "Prospect", "party_name": prospect},
|
||||
fields=[
|
||||
"opportunity_owner",
|
||||
"sales_stage",
|
||||
"status",
|
||||
"expected_closing",
|
||||
"probability",
|
||||
"opportunity_amount",
|
||||
"currency",
|
||||
"contact_person",
|
||||
"contact_email",
|
||||
"contact_mobile",
|
||||
"creation",
|
||||
"name",
|
||||
],
|
||||
)
|
||||
|
@ -20,7 +20,7 @@ class TestProspect(unittest.TestCase):
|
||||
add_lead_to_prospect(lead_doc.name, prospect_doc.name)
|
||||
prospect_doc.reload()
|
||||
lead_exists_in_prosoect = False
|
||||
for rec in prospect_doc.get("prospect_lead"):
|
||||
for rec in prospect_doc.get("leads"):
|
||||
if rec.lead == lead_doc.name:
|
||||
lead_exists_in_prosoect = True
|
||||
self.assertEqual(lead_exists_in_prosoect, True)
|
||||
|
@ -7,12 +7,15 @@
|
||||
"field_order": [
|
||||
"lead",
|
||||
"lead_name",
|
||||
"status",
|
||||
"email",
|
||||
"mobile_no"
|
||||
"column_break_4",
|
||||
"mobile_no",
|
||||
"lead_owner",
|
||||
"status"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "lead",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
@ -21,6 +24,8 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fetch_from": "lead.lead_name",
|
||||
"fieldname": "lead_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
@ -28,14 +33,17 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"fetch_from": "lead.status",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Status",
|
||||
"options": "Lead\nOpen\nReplied\nOpportunity\nQuotation\nLost Quotation\nInterested\nConverted\nDo Not Contact",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fetch_from": "lead.email_id",
|
||||
"fieldname": "email",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
@ -44,18 +52,32 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fetch_from": "lead.mobile_no",
|
||||
"fieldname": "mobile_no",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Mobile No",
|
||||
"options": "Phone",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"fetch_from": "lead.lead_owner",
|
||||
"fieldname": "lead_owner",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Lead Owner"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-08-25 12:58:24.638054",
|
||||
"modified": "2022-04-28 20:27:58.805970",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Prospect Lead",
|
||||
@ -63,5 +85,6 @@
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "autoincrement",
|
||||
"creation": "2022-04-27 17:40:37.965161",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"opportunity",
|
||||
"amount",
|
||||
"stage",
|
||||
"deal_owner",
|
||||
"column_break_4",
|
||||
"probability",
|
||||
"expected_closing",
|
||||
"currency",
|
||||
"contact_person"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "opportunity",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Opportunity",
|
||||
"options": "Opportunity"
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fetch_from": "opportunity.opportunity_amount",
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Amount",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fetch_from": "opportunity.sales_stage",
|
||||
"fieldname": "stage",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Stage"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"fetch_from": "opportunity.probability",
|
||||
"fieldname": "probability",
|
||||
"fieldtype": "Percent",
|
||||
"in_list_view": 1,
|
||||
"label": "Probability"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"fetch_from": "opportunity.expected_closing",
|
||||
"fieldname": "expected_closing",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Closing"
|
||||
},
|
||||
{
|
||||
"fetch_from": "opportunity.currency",
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Currency",
|
||||
"options": "Currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fetch_from": "opportunity.opportunity_owner",
|
||||
"fieldname": "deal_owner",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Deal Owner"
|
||||
},
|
||||
{
|
||||
"fetch_from": "opportunity.contact_person",
|
||||
"fieldname": "contact_person",
|
||||
"fieldtype": "Link",
|
||||
"label": "Contact Person",
|
||||
"options": "Contact"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-04-28 10:05:38.730368",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Prospect Opportunity",
|
||||
"naming_rule": "Autoincrement",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class ProspectOpportunity(Document):
|
||||
pass
|
@ -57,11 +57,5 @@ frappe.query_reports["Lost Opportunity"] = {
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "opportunity_from"
|
||||
},
|
||||
{
|
||||
"fieldname":"contact_by",
|
||||
"label": __("Next Contact By"),
|
||||
"fieldtype": "Link",
|
||||
"options": "User"
|
||||
},
|
||||
]
|
||||
};
|
||||
|
@ -7,8 +7,8 @@
|
||||
"doctype": "Report",
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"json": "{\"order_by\": \"`tabOpportunity`.`modified` desc\", \"filters\": [[\"Opportunity\", \"status\", \"=\", \"Lost\"]], \"fields\": [[\"name\", \"Opportunity\"], [\"opportunity_from\", \"Opportunity\"], [\"party_name\", \"Opportunity\"], [\"customer_name\", \"Opportunity\"], [\"opportunity_type\", \"Opportunity\"], [\"status\", \"Opportunity\"], [\"contact_by\", \"Opportunity\"], [\"docstatus\", \"Opportunity\"], [\"lost_reason\", \"Lost Reason Detail\"]], \"add_totals_row\": 0, \"add_total_row\": 0, \"page_length\": 20}",
|
||||
"modified": "2020-07-29 15:49:02.848845",
|
||||
"json": "{\"order_by\": \"`tabOpportunity`.`modified` desc\", \"filters\": [[\"Opportunity\", \"status\", \"=\", \"Lost\"]], \"fields\": [[\"name\", \"Opportunity\"], [\"opportunity_from\", \"Opportunity\"], [\"party_name\", \"Opportunity\"], [\"customer_name\", \"Opportunity\"], [\"opportunity_type\", \"Opportunity\"], [\"status\", \"Opportunity\"], [\"docstatus\", \"Opportunity\"], [\"lost_reason\", \"Lost Reason Detail\"]], \"add_totals_row\": 0, \"add_total_row\": 0, \"page_length\": 20}",
|
||||
"modified": "2022-06-04 15:49:02.848845",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Lost Opportunity",
|
||||
|
@ -61,13 +61,6 @@ def get_columns():
|
||||
"options": "Territory",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"label": _("Next Contact By"),
|
||||
"fieldname": "contact_by",
|
||||
"fieldtype": "Link",
|
||||
"options": "User",
|
||||
"width": 150,
|
||||
},
|
||||
]
|
||||
return columns
|
||||
|
||||
@ -81,7 +74,6 @@ def get_data(filters):
|
||||
`tabOpportunity`.party_name,
|
||||
`tabOpportunity`.customer_name,
|
||||
`tabOpportunity`.opportunity_type,
|
||||
`tabOpportunity`.contact_by,
|
||||
GROUP_CONCAT(`tabOpportunity Lost Reason Detail`.lost_reason separator ', ') lost_reason,
|
||||
`tabOpportunity`.sales_stage,
|
||||
`tabOpportunity`.territory
|
||||
@ -115,9 +107,6 @@ def get_conditions(filters):
|
||||
if filters.get("party_name"):
|
||||
conditions.append(" and `tabOpportunity`.party_name=%(party_name)s")
|
||||
|
||||
if filters.get("contact_by"):
|
||||
conditions.append(" and `tabOpportunity`.contact_by=%(contact_by)s")
|
||||
|
||||
return " ".join(conditions) if conditions else ""
|
||||
|
||||
|
||||
|
@ -1,4 +1,7 @@
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cstr, now, today
|
||||
from pypika import functions
|
||||
|
||||
|
||||
def update_lead_phone_numbers(contact, method):
|
||||
@ -41,7 +44,7 @@ def copy_comments(doctype, docname, doc):
|
||||
comment.insert()
|
||||
|
||||
|
||||
def add_link_in_communication(doctype, docname, doc):
|
||||
def link_communications(doctype, docname, doc):
|
||||
communication_list = get_linked_communication_list(doctype, docname)
|
||||
|
||||
for communication in communication_list:
|
||||
@ -60,3 +63,159 @@ def get_linked_communication_list(doctype, docname):
|
||||
)
|
||||
|
||||
return communications + communication_links
|
||||
|
||||
|
||||
def link_communications_with_prospect(communication, method):
|
||||
prospect = get_linked_prospect(communication.reference_doctype, communication.reference_name)
|
||||
|
||||
if prospect:
|
||||
already_linked = any(
|
||||
[
|
||||
d.name
|
||||
for d in communication.get("timeline_links")
|
||||
if d.link_doctype == "Prospect" and d.link_name == prospect
|
||||
]
|
||||
)
|
||||
if not already_linked:
|
||||
row = communication.append("timeline_links")
|
||||
row.link_doctype = "Prospect"
|
||||
row.link_name = prospect
|
||||
row.db_update()
|
||||
|
||||
|
||||
def get_linked_prospect(reference_doctype, reference_name):
|
||||
prospect = None
|
||||
if reference_doctype == "Lead":
|
||||
prospect = frappe.db.get_value("Prospect Lead", {"lead": reference_name}, "parent")
|
||||
|
||||
elif reference_doctype == "Opportunity":
|
||||
opportunity_from, party_name = frappe.db.get_value(
|
||||
"Opportunity", reference_name, ["opportunity_from", "party_name"]
|
||||
)
|
||||
if opportunity_from == "Lead":
|
||||
prospect = frappe.db.get_value(
|
||||
"Prospect Opportunity", {"opportunity": reference_name}, "parent"
|
||||
)
|
||||
if opportunity_from == "Prospect":
|
||||
prospect = party_name
|
||||
|
||||
return prospect
|
||||
|
||||
|
||||
def link_events_with_prospect(event, method):
|
||||
if event.event_participants:
|
||||
ref_doctype = event.event_participants[0].reference_doctype
|
||||
ref_docname = event.event_participants[0].reference_docname
|
||||
prospect = get_linked_prospect(ref_doctype, ref_docname)
|
||||
if prospect:
|
||||
event.add_participant("Prospect", prospect)
|
||||
event.save()
|
||||
|
||||
|
||||
def link_open_tasks(ref_doctype, ref_docname, doc):
|
||||
todos = get_open_todos(ref_doctype, ref_docname)
|
||||
|
||||
for todo in todos:
|
||||
todo_doc = frappe.get_doc("ToDo", todo.name)
|
||||
todo_doc.reference_type = doc.doctype
|
||||
todo_doc.reference_name = doc.name
|
||||
todo_doc.db_update()
|
||||
|
||||
|
||||
def link_open_events(ref_doctype, ref_docname, doc):
|
||||
events = get_open_events(ref_doctype, ref_docname)
|
||||
for event in events:
|
||||
event_doc = frappe.get_doc("Event", event.name)
|
||||
event_doc.add_participant(doc.doctype, doc.name)
|
||||
event_doc.save()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_open_activities(ref_doctype, ref_docname):
|
||||
tasks = get_open_todos(ref_doctype, ref_docname)
|
||||
events = get_open_events(ref_doctype, ref_docname)
|
||||
|
||||
return {"tasks": tasks, "events": events}
|
||||
|
||||
|
||||
def get_open_todos(ref_doctype, ref_docname):
|
||||
return frappe.get_all(
|
||||
"ToDo",
|
||||
filters={"reference_type": ref_doctype, "reference_name": ref_docname, "status": "Open"},
|
||||
fields=[
|
||||
"name",
|
||||
"description",
|
||||
"allocated_to",
|
||||
"date",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def get_open_events(ref_doctype, ref_docname):
|
||||
event = frappe.qb.DocType("Event")
|
||||
event_link = frappe.qb.DocType("Event Participants")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(event)
|
||||
.join(event_link)
|
||||
.on(event_link.parent == event.name)
|
||||
.select(
|
||||
event.name,
|
||||
event.subject,
|
||||
event.event_category,
|
||||
event.starts_on,
|
||||
event.ends_on,
|
||||
event.description,
|
||||
)
|
||||
.where(
|
||||
(event_link.reference_doctype == ref_doctype)
|
||||
& (event_link.reference_docname == ref_docname)
|
||||
& (event.status == "Open")
|
||||
)
|
||||
)
|
||||
data = query.run(as_dict=True)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def open_leads_opportunities_based_on_todays_event():
|
||||
event = frappe.qb.DocType("Event")
|
||||
event_link = frappe.qb.DocType("Event Participants")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(event)
|
||||
.join(event_link)
|
||||
.on(event_link.parent == event.name)
|
||||
.select(event_link.reference_doctype, event_link.reference_docname)
|
||||
.where(
|
||||
(event_link.reference_doctype.isin(["Lead", "Opportunity"]))
|
||||
& (event.status == "Open")
|
||||
& (functions.Date(event.starts_on) == today())
|
||||
)
|
||||
)
|
||||
data = query.run(as_dict=True)
|
||||
|
||||
for d in data:
|
||||
frappe.db.set_value(d.reference_doctype, d.reference_docname, "status", "Open")
|
||||
|
||||
|
||||
class CRMNote(Document):
|
||||
@frappe.whitelist()
|
||||
def add_note(self, note):
|
||||
self.append("notes", {"note": note, "added_by": frappe.session.user, "added_on": now()})
|
||||
self.save()
|
||||
|
||||
@frappe.whitelist()
|
||||
def edit_note(self, note, row_id):
|
||||
for d in self.notes:
|
||||
if cstr(d.name) == row_id:
|
||||
d.note = note
|
||||
d.db_update()
|
||||
|
||||
@frappe.whitelist()
|
||||
def delete_note(self, row_id):
|
||||
for d in self.notes:
|
||||
if cstr(d.name) == row_id:
|
||||
self.remove(d)
|
||||
break
|
||||
self.save()
|
||||
|
@ -299,7 +299,11 @@ doc_events = {
|
||||
"on_update": [
|
||||
"erpnext.support.doctype.service_level_agreement.service_level_agreement.on_communication_update",
|
||||
"erpnext.support.doctype.issue.issue.set_first_response_time",
|
||||
]
|
||||
],
|
||||
"after_insert": "erpnext.crm.utils.link_communications_with_prospect",
|
||||
},
|
||||
"Event": {
|
||||
"after_insert": "erpnext.crm.utils.link_events_with_prospect",
|
||||
},
|
||||
"Sales Taxes and Charges Template": {
|
||||
"on_update": "erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings.validate_cart_settings"
|
||||
@ -398,6 +402,14 @@ scheduler_events = {
|
||||
"0/30 * * * *": [
|
||||
"erpnext.utilities.doctype.video.video.update_youtube_data",
|
||||
],
|
||||
# Hourly but offset by 30 minutes
|
||||
"30 * * * *": [
|
||||
"erpnext.accounts.doctype.gl_entry.gl_entry.rename_gle_sle_docs",
|
||||
],
|
||||
# Daily but offset by 45 minutes
|
||||
"45 0 * * *": [
|
||||
"erpnext.stock.reorder_item.reorder_item",
|
||||
],
|
||||
},
|
||||
"all": [
|
||||
"erpnext.projects.doctype.project.project.project_status_update_reminder",
|
||||
@ -407,7 +419,6 @@ scheduler_events = {
|
||||
"hourly": [
|
||||
"erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails",
|
||||
"erpnext.accounts.doctype.subscription.subscription.process_all",
|
||||
"erpnext.accounts.doctype.gl_entry.gl_entry.rename_gle_sle_docs",
|
||||
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
|
||||
"erpnext.projects.doctype.project.project.hourly_reminder",
|
||||
"erpnext.projects.doctype.project.project.collect_project_status",
|
||||
@ -418,7 +429,6 @@ scheduler_events = {
|
||||
"erpnext.bulk_transaction.doctype.bulk_transaction_log.bulk_transaction_log.retry_failing_transaction",
|
||||
],
|
||||
"daily": [
|
||||
"erpnext.stock.reorder_item.reorder_item",
|
||||
"erpnext.support.doctype.issue.issue.auto_close_tickets",
|
||||
"erpnext.crm.doctype.opportunity.opportunity.auto_close_opportunity",
|
||||
"erpnext.controllers.accounts_controller.update_invoice_status",
|
||||
@ -453,7 +463,7 @@ scheduler_events = {
|
||||
"erpnext.hr.utils.allocate_earned_leaves",
|
||||
"erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall.create_process_loan_security_shortfall",
|
||||
"erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans",
|
||||
"erpnext.crm.doctype.lead.lead.daily_open_lead",
|
||||
"erpnext.crm.utils.open_leads_opportunities_based_on_todays_event",
|
||||
],
|
||||
"weekly": ["erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_weekly"],
|
||||
"monthly": ["erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_monthly"],
|
||||
|
@ -445,6 +445,7 @@ class BOM(WebsiteGenerator):
|
||||
and self.is_active
|
||||
):
|
||||
frappe.db.set(self, "is_default", 1)
|
||||
frappe.db.set_value("Item", self.item, "default_bom", self.name)
|
||||
else:
|
||||
frappe.db.set(self, "is_default", 0)
|
||||
item = frappe.get_doc("Item", self.item)
|
||||
|
@ -575,6 +575,42 @@ class TestBOM(FrappeTestCase):
|
||||
bom.submit()
|
||||
self.assertEqual(bom.items[0].rate, 42)
|
||||
|
||||
def test_set_default_bom_for_item_having_single_bom(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
fg_item = make_item(properties={"is_stock_item": 1})
|
||||
bom_item = make_item(properties={"is_stock_item": 1})
|
||||
|
||||
# Step 1: Create BOM
|
||||
bom = frappe.new_doc("BOM")
|
||||
bom.item = fg_item.item_code
|
||||
bom.quantity = 1
|
||||
bom.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": bom_item.item_code,
|
||||
"qty": 1,
|
||||
"uom": bom_item.stock_uom,
|
||||
"stock_uom": bom_item.stock_uom,
|
||||
"rate": 100.0,
|
||||
},
|
||||
)
|
||||
bom.save()
|
||||
bom.submit()
|
||||
self.assertEqual(frappe.get_value("Item", fg_item.item_code, "default_bom"), bom.name)
|
||||
|
||||
# Step 2: Uncheck is_active field
|
||||
bom.is_active = 0
|
||||
bom.save()
|
||||
bom.reload()
|
||||
self.assertIsNone(frappe.get_value("Item", fg_item.item_code, "default_bom"))
|
||||
|
||||
# Step 3: Check is_active field
|
||||
bom.is_active = 1
|
||||
bom.save()
|
||||
bom.reload()
|
||||
self.assertEqual(frappe.get_value("Item", fg_item.item_code, "default_bom"), bom.name)
|
||||
|
||||
|
||||
def get_default_bom(item_code="_Test FG Item 2"):
|
||||
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
|
||||
|
@ -7,11 +7,11 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"current_bom",
|
||||
"new_bom",
|
||||
"column_break_3",
|
||||
"update_type",
|
||||
"status",
|
||||
"column_break_3",
|
||||
"current_bom",
|
||||
"new_bom",
|
||||
"error_log",
|
||||
"progress_section",
|
||||
"current_level",
|
||||
@ -37,6 +37,7 @@
|
||||
"options": "BOM"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.update_type === \"Replace BOM\"",
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
@ -87,6 +88,7 @@
|
||||
"options": "BOM Update Batch"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.status !== \"Completed\"",
|
||||
"fieldname": "current_level",
|
||||
"fieldtype": "Int",
|
||||
"label": "Current Level"
|
||||
@ -96,7 +98,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-06-06 15:15:23.883251",
|
||||
"modified": "2022-06-20 15:43:55.696388",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM Update Log",
|
||||
|
@ -6,6 +6,8 @@ from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import DocType, Interval
|
||||
from frappe.query_builder.functions import Now
|
||||
from frappe.utils import cint, cstr
|
||||
|
||||
from erpnext.manufacturing.doctype.bom_update_log.bom_updation_utils import (
|
||||
@ -22,6 +24,17 @@ class BOMMissingError(frappe.ValidationError):
|
||||
|
||||
|
||||
class BOMUpdateLog(Document):
|
||||
@staticmethod
|
||||
def clear_old_logs(days=None):
|
||||
days = days or 90
|
||||
table = DocType("BOM Update Log")
|
||||
frappe.db.delete(
|
||||
table,
|
||||
filters=(
|
||||
(table.modified < (Now() - Interval(days=days))) & (table.update_type == "Update Cost")
|
||||
),
|
||||
)
|
||||
|
||||
def validate(self):
|
||||
if self.update_type == "Replace BOM":
|
||||
self.validate_boms_are_specified()
|
||||
@ -77,7 +90,11 @@ class BOMUpdateLog(Document):
|
||||
now=frappe.flags.in_test,
|
||||
)
|
||||
else:
|
||||
process_boms_cost_level_wise(self)
|
||||
frappe.enqueue(
|
||||
method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.process_boms_cost_level_wise",
|
||||
update_doc=self,
|
||||
now=frappe.flags.in_test,
|
||||
)
|
||||
|
||||
|
||||
def run_replace_bom_job(
|
||||
@ -112,6 +129,7 @@ def process_boms_cost_level_wise(
|
||||
current_boms = {}
|
||||
values = {}
|
||||
|
||||
try:
|
||||
if update_doc.status == "Queued":
|
||||
# First level yet to process. On Submit.
|
||||
current_level = 0
|
||||
@ -134,6 +152,8 @@ def process_boms_cost_level_wise(
|
||||
|
||||
set_values_in_log(update_doc.name, values, commit=True)
|
||||
queue_bom_cost_jobs(current_boms, update_doc, current_level)
|
||||
except Exception:
|
||||
handle_exception(update_doc)
|
||||
|
||||
|
||||
def queue_bom_cost_jobs(
|
||||
@ -199,16 +219,22 @@ def resume_bom_cost_update_jobs():
|
||||
current_boms, processed_boms = get_processed_current_boms(log, bom_batches)
|
||||
parent_boms = get_next_higher_level_boms(child_boms=current_boms, processed_boms=processed_boms)
|
||||
|
||||
# Unset processed BOMs if log is complete, it is used for next level BOMs
|
||||
# Unset processed BOMs (it is used for next level BOMs) & change status if log is complete
|
||||
status = "Completed" if not parent_boms else "In Progress"
|
||||
processed_boms = json.dumps([] if not parent_boms else processed_boms)
|
||||
set_values_in_log(
|
||||
log.name,
|
||||
values={
|
||||
"processed_boms": json.dumps([] if not parent_boms else processed_boms),
|
||||
"status": "Completed" if not parent_boms else "In Progress",
|
||||
"processed_boms": processed_boms,
|
||||
"status": status,
|
||||
},
|
||||
commit=True,
|
||||
)
|
||||
|
||||
# clear progress section
|
||||
if status == "Completed":
|
||||
frappe.db.delete("BOM Update Batch", {"parent": log.name})
|
||||
|
||||
if parent_boms: # there is a next level to process
|
||||
process_boms_cost_level_wise(
|
||||
update_doc=frappe.get_doc("BOM Update Log", log.name), parent_boms=parent_boms
|
||||
|
@ -1,6 +1,6 @@
|
||||
frappe.listview_settings['BOM Update Log'] = {
|
||||
add_fields: ["status"],
|
||||
get_indicator: function(doc) {
|
||||
get_indicator: (doc) => {
|
||||
let status_map = {
|
||||
"Queued": "orange",
|
||||
"In Progress": "blue",
|
||||
@ -9,5 +9,22 @@ frappe.listview_settings['BOM Update Log'] = {
|
||||
};
|
||||
|
||||
return [__(doc.status), status_map[doc.status], "status,=," + doc.status];
|
||||
},
|
||||
onload: () => {
|
||||
if (!frappe.model.can_write("Log Settings")) {
|
||||
return;
|
||||
}
|
||||
|
||||
let sidebar_entry = $(
|
||||
'<ul class="list-unstyled sidebar-menu log-retention-note"></ul>'
|
||||
).appendTo(cur_list.page.sidebar);
|
||||
let message = __("Note: Automatic log deletion only applies to logs of type <i>Update Cost</i>");
|
||||
$(`<hr><div class='text-muted'>${message}</div>`).appendTo(sidebar_entry);
|
||||
|
||||
frappe.require("logtypes.bundle.js", () => {
|
||||
frappe.utils.logtypes.show_log_retention_message(cur_list.doctype);
|
||||
});
|
||||
|
||||
|
||||
},
|
||||
};
|
@ -25,6 +25,7 @@ from erpnext.manufacturing.doctype.bom.bom import get_children as get_bom_childr
|
||||
from erpnext.manufacturing.doctype.bom.bom import validate_bom_no
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
|
||||
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
|
||||
from erpnext.utilities.transaction_base import validate_uom_is_integer
|
||||
|
||||
|
||||
class ProductionPlan(Document):
|
||||
@ -33,6 +34,7 @@ class ProductionPlan(Document):
|
||||
self.calculate_total_planned_qty()
|
||||
self.set_status()
|
||||
self._rename_temporary_references()
|
||||
validate_uom_is_integer(self, "stock_uom", "planned_qty")
|
||||
|
||||
def set_pending_qty_in_row_without_reference(self):
|
||||
"Set Pending Qty in independent rows (not from SO or MR)."
|
||||
|
@ -679,15 +679,23 @@ class TestProductionPlan(FrappeTestCase):
|
||||
self.assertFalse(pp.all_items_completed())
|
||||
|
||||
def test_production_plan_planned_qty(self):
|
||||
pln = create_production_plan(item_code="_Test FG Item", planned_qty=0.55)
|
||||
pln.make_work_order()
|
||||
work_order = frappe.db.get_value("Work Order", {"production_plan": pln.name}, "name")
|
||||
wo_doc = frappe.get_doc("Work Order", work_order)
|
||||
wo_doc.update(
|
||||
{"wip_warehouse": "Work In Progress - _TC", "fg_warehouse": "Finished Goods - _TC"}
|
||||
# Case 1: When Planned Qty is non-integer and UOM is integer.
|
||||
from erpnext.utilities.transaction_base import UOMMustBeIntegerError
|
||||
|
||||
self.assertRaises(
|
||||
UOMMustBeIntegerError, create_production_plan, item_code="_Test FG Item", planned_qty=0.55
|
||||
)
|
||||
wo_doc.submit()
|
||||
self.assertEqual(wo_doc.qty, 0.55)
|
||||
|
||||
# Case 2: When Planned Qty is non-integer and UOM is also non-integer.
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
fg_item = make_item(properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1"}).name
|
||||
bom_item = make_item().name
|
||||
|
||||
make_bom(item=fg_item, raw_materials=[bom_item], source_warehouse="_Test Warehouse - _TC")
|
||||
|
||||
pln = create_production_plan(item_code=fg_item, planned_qty=0.55, stock_uom="_Test UOM 1")
|
||||
self.assertEqual(pln.po_items[0].planned_qty, 0.55)
|
||||
|
||||
def test_temporary_name_relinking(self):
|
||||
|
||||
@ -751,6 +759,7 @@ def create_production_plan(**args):
|
||||
"bom_no": frappe.db.get_value("Item", args.item_code, "default_bom"),
|
||||
"planned_qty": args.planned_qty or 1,
|
||||
"planned_start_date": args.planned_start_date or now_datetime(),
|
||||
"stock_uom": args.stock_uom or "Nos",
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import copy
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings, timeout
|
||||
from frappe.utils import add_days, add_months, cint, flt, now, today
|
||||
@ -19,6 +21,7 @@ from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
)
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.stock.doctype.item.test_item import create_item, make_item
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
from erpnext.stock.doctype.stock_entry import test_stock_entry
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
from erpnext.stock.utils import get_bin
|
||||
@ -28,6 +31,7 @@ class TestWorkOrder(FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.warehouse = "_Test Warehouse 2 - _TC"
|
||||
self.item = "_Test Item"
|
||||
prepare_data_for_backflush_based_on_materials_transferred()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
@ -527,6 +531,8 @@ class TestWorkOrder(FrappeTestCase):
|
||||
work_order.cancel()
|
||||
|
||||
def test_work_order_with_non_transfer_item(self):
|
||||
frappe.db.set_value("Manufacturing Settings", None, "backflush_raw_materials_based_on", "BOM")
|
||||
|
||||
items = {"Finished Good Transfer Item": 1, "_Test FG Item": 1, "_Test FG Item 1": 0}
|
||||
for item, allow_transfer in items.items():
|
||||
make_item(item, {"include_item_in_manufacturing": allow_transfer})
|
||||
@ -1071,7 +1077,7 @@ class TestWorkOrder(FrappeTestCase):
|
||||
sm = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 100))
|
||||
for row in sm.get("items"):
|
||||
if row.get("item_code") == "_Test Item":
|
||||
row.qty = 110
|
||||
row.qty = 120
|
||||
|
||||
sm.submit()
|
||||
cancel_stock_entry.append(sm.name)
|
||||
@ -1079,21 +1085,21 @@ class TestWorkOrder(FrappeTestCase):
|
||||
s = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 90))
|
||||
for row in s.get("items"):
|
||||
if row.get("item_code") == "_Test Item":
|
||||
self.assertEqual(row.get("qty"), 100)
|
||||
self.assertEqual(row.get("qty"), 108)
|
||||
s.submit()
|
||||
cancel_stock_entry.append(s.name)
|
||||
|
||||
s1 = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 5))
|
||||
for row in s1.get("items"):
|
||||
if row.get("item_code") == "_Test Item":
|
||||
self.assertEqual(row.get("qty"), 5)
|
||||
self.assertEqual(row.get("qty"), 6)
|
||||
s1.submit()
|
||||
cancel_stock_entry.append(s1.name)
|
||||
|
||||
s2 = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 5))
|
||||
for row in s2.get("items"):
|
||||
if row.get("item_code") == "_Test Item":
|
||||
self.assertEqual(row.get("qty"), 5)
|
||||
self.assertEqual(row.get("qty"), 6)
|
||||
|
||||
cancel_stock_entry.reverse()
|
||||
for ste in cancel_stock_entry:
|
||||
@ -1203,6 +1209,269 @@ class TestWorkOrder(FrappeTestCase):
|
||||
self.assertEqual(work_order.required_items[0].transferred_qty, 1)
|
||||
self.assertEqual(work_order.required_items[1].transferred_qty, 2)
|
||||
|
||||
def test_backflushed_batch_raw_materials_based_on_transferred(self):
|
||||
frappe.db.set_value(
|
||||
"Manufacturing Settings",
|
||||
None,
|
||||
"backflush_raw_materials_based_on",
|
||||
"Material Transferred for Manufacture",
|
||||
)
|
||||
|
||||
batch_item = "Test Batch MCC Keyboard"
|
||||
fg_item = "Test FG Item with Batch Raw Materials"
|
||||
|
||||
ste_doc = test_stock_entry.make_stock_entry(
|
||||
item_code=batch_item, target="Stores - _TC", qty=2, basic_rate=100, do_not_save=True
|
||||
)
|
||||
|
||||
ste_doc.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": batch_item,
|
||||
"item_name": batch_item,
|
||||
"description": batch_item,
|
||||
"basic_rate": 100,
|
||||
"t_warehouse": "Stores - _TC",
|
||||
"qty": 2,
|
||||
"uom": "Nos",
|
||||
"stock_uom": "Nos",
|
||||
"conversion_factor": 1,
|
||||
},
|
||||
)
|
||||
|
||||
# Inward raw materials in Stores warehouse
|
||||
ste_doc.insert()
|
||||
ste_doc.submit()
|
||||
|
||||
batch_list = [row.batch_no for row in ste_doc.items]
|
||||
|
||||
wo_doc = make_wo_order_test_record(production_item=fg_item, qty=4)
|
||||
transferred_ste_doc = frappe.get_doc(
|
||||
make_stock_entry(wo_doc.name, "Material Transfer for Manufacture", 4)
|
||||
)
|
||||
|
||||
transferred_ste_doc.items[0].qty = 2
|
||||
transferred_ste_doc.items[0].batch_no = batch_list[0]
|
||||
|
||||
new_row = copy.deepcopy(transferred_ste_doc.items[0])
|
||||
new_row.name = ""
|
||||
new_row.batch_no = batch_list[1]
|
||||
|
||||
# Transferred two batches from Stores to WIP Warehouse
|
||||
transferred_ste_doc.append("items", new_row)
|
||||
transferred_ste_doc.submit()
|
||||
|
||||
# First Manufacture stock entry
|
||||
manufacture_ste_doc1 = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 1))
|
||||
|
||||
# Batch no should be same as transferred Batch no
|
||||
self.assertEqual(manufacture_ste_doc1.items[0].batch_no, batch_list[0])
|
||||
self.assertEqual(manufacture_ste_doc1.items[0].qty, 1)
|
||||
|
||||
manufacture_ste_doc1.submit()
|
||||
|
||||
# Second Manufacture stock entry
|
||||
manufacture_ste_doc2 = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 2))
|
||||
|
||||
# Batch no should be same as transferred Batch no
|
||||
self.assertEqual(manufacture_ste_doc2.items[0].batch_no, batch_list[0])
|
||||
self.assertEqual(manufacture_ste_doc2.items[0].qty, 1)
|
||||
self.assertEqual(manufacture_ste_doc2.items[1].batch_no, batch_list[1])
|
||||
self.assertEqual(manufacture_ste_doc2.items[1].qty, 1)
|
||||
|
||||
def test_backflushed_serial_no_raw_materials_based_on_transferred(self):
|
||||
frappe.db.set_value(
|
||||
"Manufacturing Settings",
|
||||
None,
|
||||
"backflush_raw_materials_based_on",
|
||||
"Material Transferred for Manufacture",
|
||||
)
|
||||
|
||||
sn_item = "Test Serial No BTT Headphone"
|
||||
fg_item = "Test FG Item with Serial No Raw Materials"
|
||||
|
||||
ste_doc = test_stock_entry.make_stock_entry(
|
||||
item_code=sn_item, target="Stores - _TC", qty=4, basic_rate=100, do_not_save=True
|
||||
)
|
||||
|
||||
# Inward raw materials in Stores warehouse
|
||||
ste_doc.submit()
|
||||
|
||||
serial_nos_list = sorted(get_serial_nos(ste_doc.items[0].serial_no))
|
||||
|
||||
wo_doc = make_wo_order_test_record(production_item=fg_item, qty=4)
|
||||
transferred_ste_doc = frappe.get_doc(
|
||||
make_stock_entry(wo_doc.name, "Material Transfer for Manufacture", 4)
|
||||
)
|
||||
|
||||
transferred_ste_doc.items[0].serial_no = "\n".join(serial_nos_list)
|
||||
transferred_ste_doc.submit()
|
||||
|
||||
# First Manufacture stock entry
|
||||
manufacture_ste_doc1 = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 1))
|
||||
|
||||
# Serial nos should be same as transferred Serial nos
|
||||
self.assertEqual(get_serial_nos(manufacture_ste_doc1.items[0].serial_no), serial_nos_list[0:1])
|
||||
self.assertEqual(manufacture_ste_doc1.items[0].qty, 1)
|
||||
|
||||
manufacture_ste_doc1.submit()
|
||||
|
||||
# Second Manufacture stock entry
|
||||
manufacture_ste_doc2 = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 2))
|
||||
|
||||
# Serial nos should be same as transferred Serial nos
|
||||
self.assertEqual(get_serial_nos(manufacture_ste_doc2.items[0].serial_no), serial_nos_list[1:3])
|
||||
self.assertEqual(manufacture_ste_doc2.items[0].qty, 2)
|
||||
|
||||
def test_backflushed_serial_no_batch_raw_materials_based_on_transferred(self):
|
||||
frappe.db.set_value(
|
||||
"Manufacturing Settings",
|
||||
None,
|
||||
"backflush_raw_materials_based_on",
|
||||
"Material Transferred for Manufacture",
|
||||
)
|
||||
|
||||
sn_batch_item = "Test Batch Serial No WebCam"
|
||||
fg_item = "Test FG Item with Serial & Batch No Raw Materials"
|
||||
|
||||
ste_doc = test_stock_entry.make_stock_entry(
|
||||
item_code=sn_batch_item, target="Stores - _TC", qty=2, basic_rate=100, do_not_save=True
|
||||
)
|
||||
|
||||
ste_doc.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": sn_batch_item,
|
||||
"item_name": sn_batch_item,
|
||||
"description": sn_batch_item,
|
||||
"basic_rate": 100,
|
||||
"t_warehouse": "Stores - _TC",
|
||||
"qty": 2,
|
||||
"uom": "Nos",
|
||||
"stock_uom": "Nos",
|
||||
"conversion_factor": 1,
|
||||
},
|
||||
)
|
||||
|
||||
# Inward raw materials in Stores warehouse
|
||||
ste_doc.insert()
|
||||
ste_doc.submit()
|
||||
|
||||
batch_dict = {row.batch_no: get_serial_nos(row.serial_no) for row in ste_doc.items}
|
||||
batches = list(batch_dict.keys())
|
||||
|
||||
wo_doc = make_wo_order_test_record(production_item=fg_item, qty=4)
|
||||
transferred_ste_doc = frappe.get_doc(
|
||||
make_stock_entry(wo_doc.name, "Material Transfer for Manufacture", 4)
|
||||
)
|
||||
|
||||
transferred_ste_doc.items[0].qty = 2
|
||||
transferred_ste_doc.items[0].batch_no = batches[0]
|
||||
transferred_ste_doc.items[0].serial_no = "\n".join(batch_dict.get(batches[0]))
|
||||
|
||||
new_row = copy.deepcopy(transferred_ste_doc.items[0])
|
||||
new_row.name = ""
|
||||
new_row.batch_no = batches[1]
|
||||
new_row.serial_no = "\n".join(batch_dict.get(batches[1]))
|
||||
|
||||
# Transferred two batches from Stores to WIP Warehouse
|
||||
transferred_ste_doc.append("items", new_row)
|
||||
transferred_ste_doc.submit()
|
||||
|
||||
# First Manufacture stock entry
|
||||
manufacture_ste_doc1 = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 1))
|
||||
|
||||
# Batch no & Serial Nos should be same as transferred Batch no & Serial Nos
|
||||
batch_no = manufacture_ste_doc1.items[0].batch_no
|
||||
self.assertEqual(
|
||||
get_serial_nos(manufacture_ste_doc1.items[0].serial_no)[0], batch_dict.get(batch_no)[0]
|
||||
)
|
||||
self.assertEqual(manufacture_ste_doc1.items[0].qty, 1)
|
||||
|
||||
manufacture_ste_doc1.submit()
|
||||
|
||||
# Second Manufacture stock entry
|
||||
manufacture_ste_doc2 = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 2))
|
||||
|
||||
# Batch no & Serial Nos should be same as transferred Batch no & Serial Nos
|
||||
batch_no = manufacture_ste_doc2.items[0].batch_no
|
||||
self.assertEqual(
|
||||
get_serial_nos(manufacture_ste_doc2.items[0].serial_no)[0], batch_dict.get(batch_no)[1]
|
||||
)
|
||||
self.assertEqual(manufacture_ste_doc2.items[0].qty, 1)
|
||||
|
||||
batch_no = manufacture_ste_doc2.items[1].batch_no
|
||||
self.assertEqual(
|
||||
get_serial_nos(manufacture_ste_doc2.items[1].serial_no)[0], batch_dict.get(batch_no)[0]
|
||||
)
|
||||
self.assertEqual(manufacture_ste_doc2.items[1].qty, 1)
|
||||
|
||||
|
||||
def prepare_data_for_backflush_based_on_materials_transferred():
|
||||
batch_item_doc = make_item(
|
||||
"Test Batch MCC Keyboard",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "TBMK.#####",
|
||||
"valuation_rate": 100,
|
||||
"stock_uom": "Nos",
|
||||
},
|
||||
)
|
||||
|
||||
item = make_item(
|
||||
"Test FG Item with Batch Raw Materials",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
},
|
||||
)
|
||||
|
||||
make_bom(item=item.name, source_warehouse="Stores - _TC", raw_materials=[batch_item_doc.name])
|
||||
|
||||
sn_item_doc = make_item(
|
||||
"Test Serial No BTT Headphone",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_serial_no": 1,
|
||||
"serial_no_series": "TSBH.#####",
|
||||
"valuation_rate": 100,
|
||||
"stock_uom": "Nos",
|
||||
},
|
||||
)
|
||||
|
||||
item = make_item(
|
||||
"Test FG Item with Serial No Raw Materials",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
},
|
||||
)
|
||||
|
||||
make_bom(item=item.name, source_warehouse="Stores - _TC", raw_materials=[sn_item_doc.name])
|
||||
|
||||
sn_batch_item_doc = make_item(
|
||||
"Test Batch Serial No WebCam",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "TBSW.#####",
|
||||
"has_serial_no": 1,
|
||||
"serial_no_series": "TBSWC.#####",
|
||||
"valuation_rate": 100,
|
||||
"stock_uom": "Nos",
|
||||
},
|
||||
)
|
||||
|
||||
item = make_item(
|
||||
"Test FG Item with Serial & Batch No Raw Materials",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
},
|
||||
)
|
||||
|
||||
make_bom(item=item.name, source_warehouse="Stores - _TC", raw_materials=[sn_batch_item_doc.name])
|
||||
|
||||
|
||||
def update_job_card(job_card, jc_qty=None):
|
||||
employee = frappe.db.get_value("Employee", {"status": "Active"}, "name")
|
||||
|
@ -376,3 +376,4 @@ erpnext.patches.v13_0.set_payroll_entry_status
|
||||
erpnext.patches.v13_0.job_card_status_on_hold
|
||||
erpnext.patches.v14_0.copy_is_subcontracted_value_to_is_old_subcontracting_flow
|
||||
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
||||
erpnext.patches.v14_0.crm_ux_cleanup
|
||||
|
94
erpnext/patches/v14_0/crm_ux_cleanup.py
Normal file
94
erpnext/patches/v14_0/crm_ux_cleanup.py
Normal file
@ -0,0 +1,94 @@
|
||||
import frappe
|
||||
from frappe.model.utils.rename_field import rename_field
|
||||
from frappe.utils import add_months, cstr, today
|
||||
|
||||
|
||||
def execute():
|
||||
for doctype in ("CRM Note", "Lead", "Opportunity", "Prospect", "Prospect Lead"):
|
||||
frappe.reload_doc("crm", "doctype", doctype)
|
||||
|
||||
try:
|
||||
rename_field("Lead", "designation", "job_title")
|
||||
rename_field("Opportunity", "converted_by", "opportunity_owner")
|
||||
|
||||
frappe.db.sql(
|
||||
"""
|
||||
update `tabProspect Lead`
|
||||
set parentfield='leads'
|
||||
where parentfield='partner_lead'
|
||||
"""
|
||||
)
|
||||
except Exception as e:
|
||||
if e.args[0] != 1054:
|
||||
raise
|
||||
|
||||
add_calendar_event_for_leads()
|
||||
add_calendar_event_for_opportunities()
|
||||
|
||||
|
||||
def add_calendar_event_for_leads():
|
||||
# create events based on next contact date
|
||||
leads = frappe.db.sql(
|
||||
"""
|
||||
select name, contact_date, contact_by, ends_on, lead_name, lead_owner
|
||||
from tabLead
|
||||
where contact_date >= %s
|
||||
""",
|
||||
add_months(today(), -1),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
for d in leads:
|
||||
event = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Event",
|
||||
"owner": d.lead_owner,
|
||||
"subject": ("Contact " + cstr(d.lead_name)),
|
||||
"description": (
|
||||
("Contact " + cstr(d.lead_name)) + (("<br>By: " + cstr(d.contact_by)) if d.contact_by else "")
|
||||
),
|
||||
"starts_on": d.contact_date,
|
||||
"ends_on": d.ends_on,
|
||||
"event_type": "Private",
|
||||
}
|
||||
)
|
||||
|
||||
event.append("event_participants", {"reference_doctype": "Lead", "reference_docname": d.name})
|
||||
|
||||
event.insert(ignore_permissions=True)
|
||||
|
||||
|
||||
def add_calendar_event_for_opportunities():
|
||||
# create events based on next contact date
|
||||
opportunities = frappe.db.sql(
|
||||
"""
|
||||
select name, contact_date, contact_by, to_discuss,
|
||||
party_name, opportunity_owner, contact_person
|
||||
from tabOpportunity
|
||||
where contact_date >= %s
|
||||
""",
|
||||
add_months(today(), -1),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
for d in opportunities:
|
||||
event = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Event",
|
||||
"owner": d.opportunity_owner,
|
||||
"subject": ("Contact " + cstr(d.contact_person or d.party_name)),
|
||||
"description": (
|
||||
("Contact " + cstr(d.contact_person or d.party_name))
|
||||
+ (("<br>By: " + cstr(d.contact_by)) if d.contact_by else "")
|
||||
+ (("<br>Agenda: " + cstr(d.to_discuss)) if d.to_discuss else "")
|
||||
),
|
||||
"starts_on": d.contact_date,
|
||||
"event_type": "Private",
|
||||
}
|
||||
)
|
||||
|
||||
event.append(
|
||||
"event_participants", {"reference_doctype": "Opportunity", "reference_docname": d.name}
|
||||
)
|
||||
|
||||
event.insert(ignore_permissions=True)
|
@ -1,6 +1,6 @@
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder import Case, CustomFunction
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder.functions import IfNull
|
||||
|
||||
@ -87,6 +87,7 @@ def execute():
|
||||
|
||||
gl = qb.DocType("GL Entry")
|
||||
account = qb.DocType("Account")
|
||||
ifelse = CustomFunction("IF", ["condition", "then", "else"])
|
||||
|
||||
gl_entries = (
|
||||
qb.from_(gl)
|
||||
@ -96,8 +97,12 @@ def execute():
|
||||
gl.star,
|
||||
ConstantColumn(1).as_("docstatus"),
|
||||
account.account_type.as_("account_type"),
|
||||
IfNull(gl.against_voucher_type, gl.voucher_type).as_("against_voucher_type"),
|
||||
IfNull(gl.against_voucher, gl.voucher_no).as_("against_voucher_no"),
|
||||
IfNull(
|
||||
ifelse(gl.against_voucher_type == "", None, gl.against_voucher_type), gl.voucher_type
|
||||
).as_("against_voucher_type"),
|
||||
IfNull(ifelse(gl.against_voucher == "", None, gl.against_voucher), gl.voucher_no).as_(
|
||||
"against_voucher_no"
|
||||
),
|
||||
# convert debit/credit to amount
|
||||
Case()
|
||||
.when(account.account_type == "Receivable", gl.debit - gl.credit)
|
||||
|
@ -84,7 +84,9 @@ class TestTimesheet(unittest.TestCase):
|
||||
emp = make_employee("test_employee_6@salary.com")
|
||||
|
||||
timesheet = make_timesheet(emp, simulate=True, is_billable=1)
|
||||
sales_invoice = make_sales_invoice(timesheet.name, "_Test Item", "_Test Customer")
|
||||
sales_invoice = make_sales_invoice(
|
||||
timesheet.name, "_Test Item", "_Test Customer", currency="INR"
|
||||
)
|
||||
sales_invoice.due_date = nowdate()
|
||||
sales_invoice.submit()
|
||||
timesheet = frappe.get_doc("Timesheet", timesheet.name)
|
||||
|
@ -22,5 +22,8 @@ import "./utils/barcode_scanner";
|
||||
import "./telephony";
|
||||
import "./templates/call_link.html";
|
||||
import "./bulk_transaction_processing";
|
||||
import "./utils/crm_activities";
|
||||
import "./templates/crm_activities.html";
|
||||
import "./templates/crm_notes.html";
|
||||
|
||||
// import { sum } from 'frappe/public/utils/util.js'
|
||||
|
176
erpnext/public/js/templates/crm_activities.html
Normal file
176
erpnext/public/js/templates/crm_activities.html
Normal file
@ -0,0 +1,176 @@
|
||||
<div class="open-activities">
|
||||
<div class="new-btn pb-3">
|
||||
<span>
|
||||
<button class="btn btn-sm small new-task-btn mr-1">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-small-message"></use>
|
||||
</svg>
|
||||
{{ __("New Task") }}
|
||||
</button>
|
||||
<button class="btn btn-sm small new-event-btn">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-calendar"></use>
|
||||
</svg>
|
||||
{{ __("New Event") }}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<div class="open-tasks pr-1">
|
||||
<div class="open-section-head">
|
||||
<span class="ml-2">{{ __("Open Tasks") }}</span>
|
||||
</div>
|
||||
{% if (tasks.length) { %}
|
||||
{% for(var i=0, l=tasks.length; i<l; i++) { %}
|
||||
<div class="single-activity">
|
||||
<div class="flex justify-between mb-2">
|
||||
<div class="row label-area font-md ml-1">
|
||||
<span class="mr-2">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-small-message"></use>
|
||||
</svg>
|
||||
</span>
|
||||
<a href="/app/todo/{{ tasks[i].name }}" title="{{ __('Open Task') }}">
|
||||
{%= tasks[i].description %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<input type="checkbox" class="completion-checkbox"
|
||||
name="{{tasks[i].name}}" title="{{ __('Mark As Closed') }}">
|
||||
</div>
|
||||
</div>
|
||||
{% if(tasks[i].date) { %}
|
||||
<div class="text-muted ml-1">
|
||||
{%= frappe.datetime.global_date_format(tasks[i].date) %}
|
||||
</div>
|
||||
{% } %}
|
||||
{% if(tasks[i].allocated_to) { %}
|
||||
<div class="text-muted ml-1">
|
||||
{{ __("Allocated To:") }}
|
||||
{%= tasks[i].allocated_to %}
|
||||
</div>
|
||||
{% } %}
|
||||
</div>
|
||||
{% } %}
|
||||
{% } else { %}
|
||||
<div class="single-activity no-activity text-muted">
|
||||
{{ __("No open task") }}
|
||||
</div>
|
||||
{% } %}
|
||||
</div>
|
||||
<div class="open-events pl-1">
|
||||
<div class="open-section-head">
|
||||
<span class="ml-2">{{ __("Open Events") }}</span>
|
||||
</div>
|
||||
{% if (events.length) { %}
|
||||
{% let icon_set = {"Sent/Received Email": "mail", "Call": "call", "Meeting": "share-people"}; %}
|
||||
{% for(var i=0, l=events.length; i<l; i++) { %}
|
||||
<div class="single-activity">
|
||||
<div class="flex justify-between mb-2">
|
||||
<div class="row label-area font-md ml-1 title">
|
||||
<span class="mr-2">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-{{ icon_set[events[i].event_category] || 'calendar' }}"></use>
|
||||
</svg>
|
||||
</span>
|
||||
<a href="/app/event/{{ events[i].name }}" title="{{ __('Open Event') }}">
|
||||
{%= events[i].subject %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<input type="checkbox" class="completion-checkbox"
|
||||
name="{{ events[i].name }}" title="{{ __('Mark As Closed') }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-muted ml-1">
|
||||
{%= frappe.datetime.global_date_format(events[i].starts_on) %}
|
||||
|
||||
{% if (events[i].ends_on) { %}
|
||||
{% if (frappe.datetime.obj_to_user(events[i].starts_on) != frappe.datetime.obj_to_user(events[i].ends_on)) %}
|
||||
-
|
||||
{%= frappe.datetime.global_date_format(frappe.datetime.obj_to_user(events[i].ends_on)) %}
|
||||
{%= frappe.datetime.get_time(events[i].ends_on) %}
|
||||
{% } else if (events[i].ends_on) { %}
|
||||
-
|
||||
{%= frappe.datetime.get_time(events[i].ends_on) %}
|
||||
{% } %}
|
||||
{% } %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% } %}
|
||||
{% } else { %}
|
||||
<div class="single-activity no-activity text-muted">
|
||||
{{ __("No open event") }}
|
||||
</div>
|
||||
{% } %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<style>
|
||||
.open-activities {
|
||||
min-height: 50px;
|
||||
padding-left: 0px;
|
||||
padding-bottom: 15px !important;
|
||||
}
|
||||
|
||||
.open-activities .new-btn {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.single-activity {
|
||||
min-height: 90px;
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 10px;
|
||||
border-bottom: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.single-activity:last-child {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.single-activity:hover .completion-checkbox{
|
||||
display: block;
|
||||
}
|
||||
|
||||
.completion-checkbox {
|
||||
vertical-align: middle;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
min-width: 22px;
|
||||
}
|
||||
|
||||
.open-tasks {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.open-tasks:first-child {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.open-events {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.open-section-head {
|
||||
background-color: var(--bg-color);
|
||||
min-height: 30px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.no-activity {
|
||||
text-align: center;
|
||||
padding-top: 30px;
|
||||
}
|
||||
|
||||
.form-footer {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
</style>
|
74
erpnext/public/js/templates/crm_notes.html
Normal file
74
erpnext/public/js/templates/crm_notes.html
Normal file
@ -0,0 +1,74 @@
|
||||
<div class="notes-section col-xs-12">
|
||||
<div class="new-btn pb-3">
|
||||
<button class="btn btn-sm small new-note-btn mr-1">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-add"></use>
|
||||
</svg>
|
||||
{{ __("New Note") }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="all-notes">
|
||||
{% if (notes.length) { %}
|
||||
{% for(var i=0, l=notes.length; i<l; i++) { %}
|
||||
<div class="comment-content p-3 row" name="{{ notes[i].name }}">
|
||||
<div class="mb-2 head col-xs-3">
|
||||
<div class="row">
|
||||
<div class="col-xs-2">
|
||||
{{ frappe.avatar(notes[i].added_by) }}
|
||||
</div>
|
||||
<div class="col-xs-10">
|
||||
<div class="mr-2 title font-weight-bold">
|
||||
{{ strip_html(notes[i].added_by) }}
|
||||
</div>
|
||||
<div class="time small text-muted">
|
||||
{{ frappe.datetime.global_date_format(notes[i].added_on) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content col-xs-8">
|
||||
{{ notes[i].note }}
|
||||
</div>
|
||||
<div class="col-xs-1 text-right">
|
||||
<span class="edit-note-btn btn btn-link">
|
||||
<svg class="icon icon-sm"><use xlink:href="#icon-edit"></use></svg>
|
||||
</span>
|
||||
<span class="delete-note-btn btn btn-link pl-2">
|
||||
<svg class="icon icon-xs"><use xlink:href="#icon-delete"></use></svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{% } %}
|
||||
{% } else { %}
|
||||
<div class="no-activity text-muted pt-6">
|
||||
{{ __("No Notes") }}
|
||||
</div>
|
||||
{% } %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
.comment-content {
|
||||
border: 1px solid var(--border-color);
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.comment-content:last-child {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.new-btn {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.notes-section .no-activity {
|
||||
min-height: 100px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.notes-section .btn {
|
||||
padding: 0.2rem 0.2rem;
|
||||
}
|
||||
|
||||
</style>
|
234
erpnext/public/js/utils/crm_activities.js
Normal file
234
erpnext/public/js/utils/crm_activities.js
Normal file
@ -0,0 +1,234 @@
|
||||
erpnext.utils.CRMActivities = class CRMActivities {
|
||||
constructor(opts) {
|
||||
$.extend(this, opts);
|
||||
}
|
||||
|
||||
refresh() {
|
||||
var me = this;
|
||||
$(this.open_activities_wrapper).empty();
|
||||
let cur_form_footer = this.form_wrapper.find('.form-footer');
|
||||
|
||||
// all activities
|
||||
if (!$(this.all_activities_wrapper).find('.form-footer').length) {
|
||||
this.all_activities_wrapper.empty();
|
||||
$(cur_form_footer).appendTo(this.all_activities_wrapper);
|
||||
|
||||
// remove frappe-control class to avoid absolute position for action-btn
|
||||
$(this.all_activities_wrapper).removeClass('frappe-control');
|
||||
// hide new event button
|
||||
$('.timeline-actions').find('.btn-default').hide();
|
||||
// hide new comment box
|
||||
$(".comment-box").hide();
|
||||
// show only communications by default
|
||||
$($('.timeline-content').find('.nav-link')[0]).tab('show');
|
||||
}
|
||||
|
||||
// open activities
|
||||
frappe.call({
|
||||
method: "erpnext.crm.utils.get_open_activities",
|
||||
args: {
|
||||
ref_doctype: this.frm.doc.doctype,
|
||||
ref_docname: this.frm.doc.name
|
||||
},
|
||||
callback: (r) => {
|
||||
if (!r.exc) {
|
||||
var activities_html = frappe.render_template('crm_activities', {
|
||||
tasks: r.message.tasks,
|
||||
events: r.message.events
|
||||
});
|
||||
|
||||
$(activities_html).appendTo(me.open_activities_wrapper);
|
||||
|
||||
$(".open-tasks").find(".completion-checkbox").on("click", function() {
|
||||
me.update_status(this, "ToDo");
|
||||
});
|
||||
|
||||
$(".open-events").find(".completion-checkbox").on("click", function() {
|
||||
me.update_status(this, "Event");
|
||||
});
|
||||
|
||||
me.create_task();
|
||||
me.create_event();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
create_task () {
|
||||
let me = this;
|
||||
let _create_task = () => {
|
||||
const args = {
|
||||
doc: me.frm.doc,
|
||||
frm: me.frm,
|
||||
title: __("New Task")
|
||||
};
|
||||
let composer = new frappe.views.InteractionComposer(args);
|
||||
composer.dialog.get_field('interaction_type').set_value("ToDo");
|
||||
// hide column having interaction type field
|
||||
$(composer.dialog.get_field('interaction_type').wrapper).closest('.form-column').hide();
|
||||
// hide summary field
|
||||
$(composer.dialog.get_field('summary').wrapper).closest('.form-section').hide();
|
||||
};
|
||||
$(".new-task-btn").click(_create_task);
|
||||
}
|
||||
|
||||
create_event () {
|
||||
let me = this;
|
||||
let _create_event = () => {
|
||||
const args = {
|
||||
doc: me.frm.doc,
|
||||
frm: me.frm,
|
||||
title: __("New Event")
|
||||
};
|
||||
let composer = new frappe.views.InteractionComposer(args);
|
||||
composer.dialog.get_field('interaction_type').set_value("Event");
|
||||
$(composer.dialog.get_field('interaction_type').wrapper).hide();
|
||||
};
|
||||
$(".new-event-btn").click(_create_event);
|
||||
}
|
||||
|
||||
async update_status (input_field, doctype) {
|
||||
let completed = $(input_field).prop("checked") ? 1 : 0;
|
||||
let docname = $(input_field).attr("name");
|
||||
if (completed) {
|
||||
await frappe.db.set_value(doctype, docname, "status", "Closed");
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
erpnext.utils.CRMNotes = class CRMNotes {
|
||||
constructor(opts) {
|
||||
$.extend(this, opts);
|
||||
}
|
||||
|
||||
refresh() {
|
||||
var me = this;
|
||||
this.notes_wrapper.find('.notes-section').remove();
|
||||
|
||||
let notes = this.frm.doc.notes || [];
|
||||
notes.sort(
|
||||
function(a, b) {
|
||||
return new Date(b.added_on) - new Date(a.added_on);
|
||||
}
|
||||
);
|
||||
|
||||
let notes_html = frappe.render_template(
|
||||
'crm_notes',
|
||||
{
|
||||
notes: notes
|
||||
}
|
||||
);
|
||||
$(notes_html).appendTo(this.notes_wrapper);
|
||||
|
||||
this.add_note();
|
||||
|
||||
$(".notes-section").find(".edit-note-btn").on("click", function() {
|
||||
me.edit_note(this);
|
||||
});
|
||||
|
||||
$(".notes-section").find(".delete-note-btn").on("click", function() {
|
||||
me.delete_note(this);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
add_note () {
|
||||
let me = this;
|
||||
let _add_note = () => {
|
||||
var d = new frappe.ui.Dialog({
|
||||
title: __('Add a Note'),
|
||||
fields: [
|
||||
{
|
||||
"label": "Note",
|
||||
"fieldname": "note",
|
||||
"fieldtype": "Text Editor",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
primary_action: function() {
|
||||
var data = d.get_values();
|
||||
frappe.call({
|
||||
method: "add_note",
|
||||
doc: me.frm.doc,
|
||||
args: {
|
||||
note: data.note
|
||||
},
|
||||
freeze: true,
|
||||
callback: function(r) {
|
||||
if (!r.exc) {
|
||||
me.frm.refresh_field("notes");
|
||||
me.refresh();
|
||||
}
|
||||
d.hide();
|
||||
}
|
||||
});
|
||||
},
|
||||
primary_action_label: __('Add')
|
||||
});
|
||||
d.show();
|
||||
};
|
||||
$(".new-note-btn").click(_add_note);
|
||||
}
|
||||
|
||||
edit_note (edit_btn) {
|
||||
var me = this;
|
||||
let row = $(edit_btn).closest('.comment-content');
|
||||
let row_id = row.attr("name");
|
||||
let row_content = $(row).find(".content").html();
|
||||
if (row_content) {
|
||||
var d = new frappe.ui.Dialog({
|
||||
title: __('Edit Note'),
|
||||
fields: [
|
||||
{
|
||||
"label": "Note",
|
||||
"fieldname": "note",
|
||||
"fieldtype": "Text Editor",
|
||||
"default": row_content
|
||||
}
|
||||
],
|
||||
primary_action: function() {
|
||||
var data = d.get_values();
|
||||
frappe.call({
|
||||
method: "edit_note",
|
||||
doc: me.frm.doc,
|
||||
args: {
|
||||
note: data.note,
|
||||
row_id: row_id
|
||||
},
|
||||
freeze: true,
|
||||
callback: function(r) {
|
||||
if (!r.exc) {
|
||||
me.frm.refresh_field("notes");
|
||||
me.refresh();
|
||||
d.hide();
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
},
|
||||
primary_action_label: __('Done')
|
||||
});
|
||||
d.show();
|
||||
}
|
||||
}
|
||||
|
||||
delete_note (delete_btn) {
|
||||
var me = this;
|
||||
let row_id = $(delete_btn).closest('.comment-content').attr("name");
|
||||
frappe.call({
|
||||
method: "delete_note",
|
||||
doc: me.frm.doc,
|
||||
args: {
|
||||
row_id: row_id
|
||||
},
|
||||
freeze: true,
|
||||
callback: function(r) {
|
||||
if (!r.exc) {
|
||||
me.frm.refresh_field("notes");
|
||||
me.refresh();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
@ -375,6 +375,12 @@ def create_internal_customer(
|
||||
if not allowed_to_interact_with:
|
||||
allowed_to_interact_with = represents_company
|
||||
|
||||
exisiting_representative = frappe.db.get_value(
|
||||
"Customer", {"represents_company": represents_company}
|
||||
)
|
||||
if exisiting_representative:
|
||||
return exisiting_representative
|
||||
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
customer = frappe.get_doc(
|
||||
{
|
||||
|
@ -1,19 +1,13 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
cur_frm.cscript.refresh = function(doc, cdt, cdn) {
|
||||
cur_frm.toggle_enable('new_item_code', doc.__islocal);
|
||||
}
|
||||
|
||||
cur_frm.fields_dict.new_item_code.get_query = function() {
|
||||
return{
|
||||
query: "erpnext.selling.doctype.product_bundle.product_bundle.get_new_item_code"
|
||||
}
|
||||
}
|
||||
cur_frm.fields_dict.new_item_code.query_description = __('Please select Item where "Is Stock Item" is "No" and "Is Sales Item" is "Yes" and there is no other Product Bundle');
|
||||
|
||||
cur_frm.cscript.onload = function() {
|
||||
// set add fetch for item_code's item_name and description
|
||||
cur_frm.add_fetch('item_code', 'stock_uom', 'uom');
|
||||
cur_frm.add_fetch('item_code', 'description', 'description');
|
||||
}
|
||||
frappe.ui.form.on("Product Bundle", {
|
||||
refresh: function (frm) {
|
||||
frm.toggle_enable("new_item_code", frm.is_new());
|
||||
frm.set_query("new_item_code", () => {
|
||||
return {
|
||||
query: "erpnext.selling.doctype.product_bundle.product_bundle.get_new_item_code",
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
@ -33,6 +33,8 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.description",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text Editor",
|
||||
"in_list_view": 1,
|
||||
@ -51,6 +53,8 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.stock_uom",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "uom",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
@ -64,7 +68,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-02-28 14:06:05.725655",
|
||||
"modified": "2022-06-27 05:30:18.475150",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Product Bundle Item",
|
||||
@ -72,5 +76,6 @@
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -8,7 +8,6 @@ from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils import flt, getdate, nowdate
|
||||
|
||||
from erpnext.controllers.selling_controller import SellingController
|
||||
from erpnext.crm.utils import add_link_in_communication, copy_comments
|
||||
|
||||
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
|
||||
|
||||
@ -36,16 +35,6 @@ class Quotation(SellingController):
|
||||
|
||||
make_packing_list(self)
|
||||
|
||||
def after_insert(self):
|
||||
if frappe.db.get_single_value("CRM Settings", "carry_forward_communication_and_comments"):
|
||||
if self.opportunity:
|
||||
copy_comments("Opportunity", self.opportunity, self)
|
||||
add_link_in_communication("Opportunity", self.opportunity, self)
|
||||
|
||||
elif self.quotation_to == "Lead" and self.party_name:
|
||||
copy_comments("Lead", self.party_name, self)
|
||||
add_link_in_communication("Lead", self.party_name, self)
|
||||
|
||||
def validate_valid_till(self):
|
||||
if self.valid_till and getdate(self.valid_till) < getdate(self.transaction_date):
|
||||
frappe.throw(_("Valid till date cannot be before transaction date"))
|
||||
@ -218,6 +207,15 @@ def make_sales_order(source_name, target_doc=None):
|
||||
|
||||
def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
|
||||
customer = _make_customer(source_name, ignore_permissions)
|
||||
ordered_items = frappe._dict(
|
||||
frappe.db.get_all(
|
||||
"Sales Order Item",
|
||||
{"prevdoc_docname": source_name, "docstatus": 1},
|
||||
["item_code", "sum(qty)"],
|
||||
group_by="item_code",
|
||||
as_list=1,
|
||||
)
|
||||
)
|
||||
|
||||
def set_missing_values(source, target):
|
||||
if customer:
|
||||
@ -233,7 +231,9 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
|
||||
target.run_method("calculate_taxes_and_totals")
|
||||
|
||||
def update_item(obj, target, source_parent):
|
||||
target.stock_qty = flt(obj.qty) * flt(obj.conversion_factor)
|
||||
balance_qty = obj.qty - ordered_items.get(obj.item_code, 0.0)
|
||||
target.qty = balance_qty if balance_qty > 0 else 0
|
||||
target.stock_qty = flt(target.qty) * flt(obj.conversion_factor)
|
||||
|
||||
if obj.against_blanket_order:
|
||||
target.against_blanket_order = obj.against_blanket_order
|
||||
@ -249,6 +249,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
|
||||
"doctype": "Sales Order Item",
|
||||
"field_map": {"parent": "prevdoc_docname"},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: doc.qty > 0,
|
||||
},
|
||||
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True},
|
||||
"Sales Team": {"doctype": "Sales Team", "add_if_empty": True},
|
||||
|
@ -55,6 +55,7 @@ frappe.query_reports["Sales Order Analysis"] = {
|
||||
for (let option of status){
|
||||
options.push({
|
||||
"value": option,
|
||||
"label": __(option),
|
||||
"description": ""
|
||||
})
|
||||
}
|
||||
|
@ -1064,6 +1064,33 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
|
||||
self.assertEqual(dn.items[0].rate, rate)
|
||||
|
||||
def test_internal_transfer_precision_gle(self):
|
||||
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
|
||||
|
||||
item = make_item(properties={"valuation_method": "Moving Average"}).name
|
||||
company = "_Test Company with perpetual inventory"
|
||||
warehouse = "Stores - TCP1"
|
||||
target = "Finished Goods - TCP1"
|
||||
customer = create_internal_customer(represents_company=company)
|
||||
|
||||
# average rate = 128.015
|
||||
rates = [101.45, 150.46, 138.25, 121.9]
|
||||
|
||||
for rate in rates:
|
||||
make_stock_entry(item_code=item, target=warehouse, qty=1, rate=rate)
|
||||
|
||||
dn = create_delivery_note(
|
||||
item_code=item,
|
||||
company=company,
|
||||
customer=customer,
|
||||
qty=4,
|
||||
warehouse=warehouse,
|
||||
target_warehouse=target,
|
||||
)
|
||||
self.assertFalse(
|
||||
frappe.db.exists("GL Entry", {"voucher_no": dn.name, "voucher_type": dn.doctype})
|
||||
)
|
||||
|
||||
|
||||
def create_delivery_note(**args):
|
||||
dn = frappe.new_doc("Delivery Note")
|
||||
|
@ -619,21 +619,6 @@ class StockEntry(StockController):
|
||||
title=_("Insufficient Stock"),
|
||||
)
|
||||
|
||||
def set_serial_nos(self, work_order):
|
||||
previous_se = frappe.db.get_value(
|
||||
"Stock Entry",
|
||||
{"work_order": work_order, "purpose": "Material Transfer for Manufacture"},
|
||||
"name",
|
||||
)
|
||||
|
||||
for d in self.get("items"):
|
||||
transferred_serial_no = frappe.db.get_value(
|
||||
"Stock Entry Detail", {"parent": previous_se, "item_code": d.item_code}, "serial_no"
|
||||
)
|
||||
|
||||
if transferred_serial_no:
|
||||
d.serial_no = transferred_serial_no
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_stock_and_rate(self):
|
||||
"""
|
||||
@ -1384,7 +1369,7 @@ class StockEntry(StockController):
|
||||
and not self.pro_doc.skip_transfer
|
||||
and self.flags.backflush_based_on == "Material Transferred for Manufacture"
|
||||
):
|
||||
self.get_transfered_raw_materials()
|
||||
self.add_transfered_raw_materials_in_items()
|
||||
|
||||
elif (
|
||||
self.work_order
|
||||
@ -1428,7 +1413,6 @@ class StockEntry(StockController):
|
||||
|
||||
# fetch the serial_no of the first stock entry for the second stock entry
|
||||
if self.work_order and self.purpose == "Manufacture":
|
||||
self.set_serial_nos(self.work_order)
|
||||
work_order = frappe.get_doc("Work Order", self.work_order)
|
||||
add_additional_cost(self, work_order)
|
||||
|
||||
@ -1720,120 +1704,79 @@ class StockEntry(StockController):
|
||||
}
|
||||
)
|
||||
|
||||
def get_transfered_raw_materials(self):
|
||||
transferred_materials = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
item_name, original_item, item_code, sum(qty) as qty, sed.t_warehouse as warehouse,
|
||||
description, stock_uom, expense_account, cost_center
|
||||
from `tabStock Entry` se,`tabStock Entry Detail` sed
|
||||
where
|
||||
se.name = sed.parent and se.docstatus=1 and se.purpose='Material Transfer for Manufacture'
|
||||
and se.work_order= %s and ifnull(sed.t_warehouse, '') != ''
|
||||
group by sed.item_code, sed.t_warehouse
|
||||
""",
|
||||
def add_transfered_raw_materials_in_items(self) -> None:
|
||||
available_materials = get_available_materials(self.work_order)
|
||||
|
||||
wo_data = frappe.db.get_value(
|
||||
"Work Order",
|
||||
self.work_order,
|
||||
["qty", "produced_qty", "material_transferred_for_manufacturing as trans_qty"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
materials_already_backflushed = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
item_code, sed.s_warehouse as warehouse, sum(qty) as qty
|
||||
from
|
||||
`tabStock Entry` se, `tabStock Entry Detail` sed
|
||||
where
|
||||
se.name = sed.parent and se.docstatus=1
|
||||
and (se.purpose='Manufacture' or se.purpose='Material Consumption for Manufacture')
|
||||
and se.work_order= %s and ifnull(sed.s_warehouse, '') != ''
|
||||
group by sed.item_code, sed.s_warehouse
|
||||
""",
|
||||
self.work_order,
|
||||
as_dict=1,
|
||||
)
|
||||
for key, row in available_materials.items():
|
||||
remaining_qty_to_produce = flt(wo_data.trans_qty) - flt(wo_data.produced_qty)
|
||||
if remaining_qty_to_produce <= 0:
|
||||
continue
|
||||
|
||||
backflushed_materials = {}
|
||||
for d in materials_already_backflushed:
|
||||
backflushed_materials.setdefault(d.item_code, []).append({d.warehouse: d.qty})
|
||||
|
||||
po_qty = frappe.db.sql(
|
||||
"""select qty, produced_qty, material_transferred_for_manufacturing from
|
||||
`tabWork Order` where name=%s""",
|
||||
self.work_order,
|
||||
as_dict=1,
|
||||
)[0]
|
||||
|
||||
manufacturing_qty = flt(po_qty.qty) or 1
|
||||
produced_qty = flt(po_qty.produced_qty)
|
||||
trans_qty = flt(po_qty.material_transferred_for_manufacturing) or 1
|
||||
|
||||
for item in transferred_materials:
|
||||
qty = item.qty
|
||||
item_code = item.original_item or item.item_code
|
||||
req_items = frappe.get_all(
|
||||
"Work Order Item",
|
||||
filters={"parent": self.work_order, "item_code": item_code},
|
||||
fields=["required_qty", "consumed_qty"],
|
||||
)
|
||||
|
||||
req_qty = flt(req_items[0].required_qty) if req_items else flt(4)
|
||||
req_qty_each = flt(req_qty / manufacturing_qty)
|
||||
consumed_qty = flt(req_items[0].consumed_qty) if req_items else 0
|
||||
|
||||
if trans_qty and manufacturing_qty > (produced_qty + flt(self.fg_completed_qty)):
|
||||
if qty >= req_qty:
|
||||
qty = (req_qty / trans_qty) * flt(self.fg_completed_qty)
|
||||
else:
|
||||
qty = qty - consumed_qty
|
||||
|
||||
if self.purpose == "Manufacture":
|
||||
# If Material Consumption is booked, must pull only remaining components to finish product
|
||||
if consumed_qty != 0:
|
||||
remaining_qty = consumed_qty - (produced_qty * req_qty_each)
|
||||
exhaust_qty = req_qty_each * produced_qty
|
||||
if remaining_qty > exhaust_qty:
|
||||
if (remaining_qty / (req_qty_each * flt(self.fg_completed_qty))) >= 1:
|
||||
qty = 0
|
||||
else:
|
||||
qty = (req_qty_each * flt(self.fg_completed_qty)) - remaining_qty
|
||||
else:
|
||||
if self.flags.backflush_based_on == "Material Transferred for Manufacture":
|
||||
qty = (item.qty / trans_qty) * flt(self.fg_completed_qty)
|
||||
else:
|
||||
qty = req_qty_each * flt(self.fg_completed_qty)
|
||||
|
||||
elif backflushed_materials.get(item.item_code):
|
||||
precision = frappe.get_precision("Stock Entry Detail", "qty")
|
||||
for d in backflushed_materials.get(item.item_code):
|
||||
if d.get(item.warehouse) > 0:
|
||||
if qty > req_qty:
|
||||
qty = (
|
||||
(flt(qty, precision) - flt(d.get(item.warehouse), precision))
|
||||
/ (flt(trans_qty, precision) - flt(produced_qty, precision))
|
||||
) * flt(self.fg_completed_qty)
|
||||
|
||||
d[item.warehouse] -= qty
|
||||
qty = (flt(row.qty) * flt(self.fg_completed_qty)) / remaining_qty_to_produce
|
||||
|
||||
item = row.item_details
|
||||
if cint(frappe.get_cached_value("UOM", item.stock_uom, "must_be_whole_number")):
|
||||
qty = frappe.utils.ceil(qty)
|
||||
|
||||
if qty > 0:
|
||||
self.add_to_stock_entry_detail(
|
||||
{
|
||||
item.item_code: {
|
||||
if row.batch_details:
|
||||
for batch_no, batch_qty in row.batch_details.items():
|
||||
if qty <= 0 or batch_qty <= 0:
|
||||
continue
|
||||
|
||||
if batch_qty > qty:
|
||||
batch_qty = qty
|
||||
|
||||
item.batch_no = batch_no
|
||||
self.update_item_in_stock_entry_detail(row, item, batch_qty)
|
||||
|
||||
row.batch_details[batch_no] -= batch_qty
|
||||
qty -= batch_qty
|
||||
else:
|
||||
self.update_item_in_stock_entry_detail(row, item, qty)
|
||||
|
||||
def update_item_in_stock_entry_detail(self, row, item, qty) -> None:
|
||||
ste_item_details = {
|
||||
"from_warehouse": item.warehouse,
|
||||
"to_warehouse": "",
|
||||
"qty": qty,
|
||||
"item_name": item.item_name,
|
||||
"batch_no": item.batch_no,
|
||||
"description": item.description,
|
||||
"stock_uom": item.stock_uom,
|
||||
"expense_account": item.expense_account,
|
||||
"cost_center": item.buying_cost_center,
|
||||
"original_item": item.original_item,
|
||||
}
|
||||
}
|
||||
|
||||
if row.serial_nos:
|
||||
serial_nos = row.serial_nos
|
||||
if item.batch_no:
|
||||
serial_nos = self.get_serial_nos_based_on_transferred_batch(item.batch_no, row.serial_nos)
|
||||
|
||||
serial_nos = serial_nos[0 : cint(qty)]
|
||||
ste_item_details["serial_no"] = "\n".join(serial_nos)
|
||||
|
||||
# remove consumed serial nos from list
|
||||
for sn in serial_nos:
|
||||
row.serial_nos.remove(sn)
|
||||
|
||||
self.add_to_stock_entry_detail({item.item_code: ste_item_details})
|
||||
|
||||
@staticmethod
|
||||
def get_serial_nos_based_on_transferred_batch(batch_no, serial_nos) -> list:
|
||||
serial_nos = frappe.get_all(
|
||||
"Serial No", filters={"batch_no": batch_no, "name": ("in", serial_nos)}, order_by="creation"
|
||||
)
|
||||
|
||||
return [d.name for d in serial_nos]
|
||||
|
||||
def get_pending_raw_materials(self, backflush_based_on=None):
|
||||
"""
|
||||
issue (item quantity) that is pending to issue or desire to transfer,
|
||||
@ -2639,3 +2582,81 @@ def get_items_from_subcontracting_order(source_name, target_doc=None):
|
||||
)
|
||||
|
||||
return target_doc
|
||||
|
||||
|
||||
def get_available_materials(work_order) -> dict:
|
||||
data = get_stock_entry_data(work_order)
|
||||
|
||||
available_materials = {}
|
||||
for row in data:
|
||||
key = (row.item_code, row.warehouse)
|
||||
if row.purpose != "Material Transfer for Manufacture":
|
||||
key = (row.item_code, row.s_warehouse)
|
||||
|
||||
if key not in available_materials:
|
||||
available_materials.setdefault(
|
||||
key,
|
||||
frappe._dict(
|
||||
{"item_details": row, "batch_details": defaultdict(float), "qty": 0, "serial_nos": []}
|
||||
),
|
||||
)
|
||||
|
||||
item_data = available_materials[key]
|
||||
|
||||
if row.purpose == "Material Transfer for Manufacture":
|
||||
item_data.qty += row.qty
|
||||
if row.batch_no:
|
||||
item_data.batch_details[row.batch_no] += row.qty
|
||||
|
||||
if row.serial_no:
|
||||
item_data.serial_nos.extend(get_serial_nos(row.serial_no))
|
||||
item_data.serial_nos.sort()
|
||||
else:
|
||||
# Consume raw material qty in case of 'Manufacture' or 'Material Consumption for Manufacture'
|
||||
|
||||
item_data.qty -= row.qty
|
||||
if row.batch_no:
|
||||
item_data.batch_details[row.batch_no] -= row.qty
|
||||
|
||||
if row.serial_no:
|
||||
for serial_no in get_serial_nos(row.serial_no):
|
||||
item_data.serial_nos.remove(serial_no)
|
||||
|
||||
return available_materials
|
||||
|
||||
|
||||
def get_stock_entry_data(work_order):
|
||||
stock_entry = frappe.qb.DocType("Stock Entry")
|
||||
stock_entry_detail = frappe.qb.DocType("Stock Entry Detail")
|
||||
|
||||
return (
|
||||
frappe.qb.from_(stock_entry)
|
||||
.from_(stock_entry_detail)
|
||||
.select(
|
||||
stock_entry_detail.item_name,
|
||||
stock_entry_detail.original_item,
|
||||
stock_entry_detail.item_code,
|
||||
stock_entry_detail.qty,
|
||||
(stock_entry_detail.t_warehouse).as_("warehouse"),
|
||||
(stock_entry_detail.s_warehouse).as_("s_warehouse"),
|
||||
stock_entry_detail.description,
|
||||
stock_entry_detail.stock_uom,
|
||||
stock_entry_detail.expense_account,
|
||||
stock_entry_detail.cost_center,
|
||||
stock_entry_detail.batch_no,
|
||||
stock_entry_detail.serial_no,
|
||||
stock_entry.purpose,
|
||||
)
|
||||
.where(
|
||||
(stock_entry.name == stock_entry_detail.parent)
|
||||
& (stock_entry.work_order == work_order)
|
||||
& (stock_entry.docstatus == 1)
|
||||
& (stock_entry_detail.s_warehouse.isnotnull())
|
||||
& (
|
||||
stock_entry.purpose.isin(
|
||||
["Manufacture", "Material Consumption for Manufacture", "Material Transfer for Manufacture"]
|
||||
)
|
||||
)
|
||||
)
|
||||
.orderby(stock_entry.creation, stock_entry_detail.item_code, stock_entry_detail.idx)
|
||||
).run(as_dict=1)
|
||||
|
@ -34,7 +34,6 @@ def send_message(subject="Website Query", message="", sender="", status="Open"):
|
||||
status="Open",
|
||||
title=subject,
|
||||
contact_email=sender,
|
||||
to_discuss=message,
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -44,7 +44,7 @@ Accessable Value,Доступная стоимость,
|
||||
Account,Аккаунт,
|
||||
Account Number,Номер аккаунта,
|
||||
Account Number {0} already used in account {1},"Номер счета {0}, уже использованный в учетной записи {1}",
|
||||
Account Pay Only,Счет Оплатить только,
|
||||
Account Pay Only,Только оплатить счет,
|
||||
Account Type,Тип учетной записи,
|
||||
Account Type for {0} must be {1},Тип счета для {0} должен быть {1},
|
||||
"Account balance already in Credit, you are not allowed to set 'Balance Must Be' as 'Debit'","Баланс счета в Кредите, запрещена установка 'Баланс должен быть' как 'Дебет'",
|
||||
@ -117,7 +117,7 @@ Add Item,Добавить продукт,
|
||||
Add Items,Добавить продукты,
|
||||
Add Leads,Добавить лид,
|
||||
Add Multiple Tasks,Добавить несколько задач,
|
||||
Add Row,Добавить ряд,
|
||||
Add Row,Добавить строку,
|
||||
Add Sales Partners,Добавить партнеров по продажам,
|
||||
Add Serial No,Добавить серийный номер,
|
||||
Add Students,Добавить студентов,
|
||||
@ -692,7 +692,7 @@ Created {0} scorecards for {1} between: ,Созданы {0} оценочные
|
||||
Creating Company and Importing Chart of Accounts,Создание компании и импорт плана счетов,
|
||||
Creating Fees,Создание сборов,
|
||||
Creating Payment Entries......,Создание платежных записей......,
|
||||
Creating Salary Slips...,Создание зарплатных листков...,
|
||||
Creating Salary Slips...,Создание зарплатных ведомостей...,
|
||||
Creating student groups,Создание групп студентов,
|
||||
Creating {0} Invoice,Создание {0} счета,
|
||||
Credit,Кредит,
|
||||
@ -995,7 +995,7 @@ Expenses,Расходы,
|
||||
Expenses Included In Asset Valuation,"Расходы, включенные в оценку активов",
|
||||
Expenses Included In Valuation,"Затрат, включаемых в оценке",
|
||||
Expired Batches,Просроченные партии,
|
||||
Expires On,Годен до,
|
||||
Expires On,Актуален до,
|
||||
Expiring On,Срок действия,
|
||||
Expiry (In Days),Срок действия (в днях),
|
||||
Explore,Обзор,
|
||||
@ -1411,7 +1411,7 @@ Lab Test UOM,Лабораторная проверка UOM,
|
||||
Lab Tests and Vital Signs,Лабораторные тесты и жизненные знаки,
|
||||
Lab result datetime cannot be before testing datetime,Лабораторный результат datetime не может быть до тестирования даты и времени,
|
||||
Lab testing datetime cannot be before collection datetime,Лабораторное тестирование datetime не может быть до даты сбора данных,
|
||||
Label,Ярлык,
|
||||
Label,Метка,
|
||||
Laboratory,Лаборатория,
|
||||
Language Name,Название языка,
|
||||
Large,Большой,
|
||||
@ -2874,7 +2874,7 @@ Supplier Id,Id поставщика,
|
||||
Supplier Invoice Date cannot be greater than Posting Date,"Дата Поставщик Счет не может быть больше, чем Дата публикации",
|
||||
Supplier Invoice No,Поставщик Счет №,
|
||||
Supplier Invoice No exists in Purchase Invoice {0},Номер счета поставщика отсутствует в счете на покупку {0},
|
||||
Supplier Name,наименование поставщика,
|
||||
Supplier Name,Наименование поставщика,
|
||||
Supplier Part No,Деталь поставщика №,
|
||||
Supplier Quotation,Предложение поставщика,
|
||||
Supplier Scorecard,Оценочная карта поставщика,
|
||||
@ -3091,7 +3091,7 @@ Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total,
|
||||
Total Payments,Всего платежей,
|
||||
Total Present,Итого Текущая,
|
||||
Total Qty,Общее количество,
|
||||
Total Quantity,Общая численность,
|
||||
Total Quantity,Общее количество,
|
||||
Total Revenue,Общий доход,
|
||||
Total Student,Всего учеников,
|
||||
Total Target,Всего целей,
|
||||
@ -3498,7 +3498,7 @@ Postal,Почтовый,
|
||||
Postal Code,Почтовый индекс,
|
||||
Previous,Предыдущая,
|
||||
Provider,Поставщик,
|
||||
Read Only,Только чтения,
|
||||
Read Only,Только чтение,
|
||||
Recipient,Сторона-реципиент,
|
||||
Reviews,Отзывы,
|
||||
Sender,Отправитель,
|
||||
@ -3879,7 +3879,7 @@ On Lead Creation,Создание лида,
|
||||
On Supplier Creation,Создание поставщика,
|
||||
On Customer Creation,Создание клиента,
|
||||
Only .csv and .xlsx files are supported currently,В настоящее время поддерживаются только файлы .csv и .xlsx,
|
||||
Only expired allocation can be cancelled,Только истекшее распределение может быть отменено,
|
||||
Only expired allocation can be cancelled,Отменить можно только просроченное распределение,
|
||||
Only users with the {0} role can create backdated leave applications,Только пользователи с ролью {0} могут создавать оставленные приложения с задним сроком действия,
|
||||
Open,Открыт,
|
||||
Open Contact,Открытый контакт,
|
||||
@ -4046,7 +4046,7 @@ Server Error,Ошибка сервера,
|
||||
Service Level Agreement has been changed to {0}.,Соглашение об уровне обслуживания изменено на {0}.,
|
||||
Service Level Agreement was reset.,Соглашение об уровне обслуживания было сброшено.,
|
||||
Service Level Agreement with Entity Type {0} and Entity {1} already exists.,Соглашение об уровне обслуживания с типом объекта {0} и объектом {1} уже существует.,
|
||||
Set,Задать,
|
||||
Set,Комплект,
|
||||
Set Meta Tags,Установить метатеги,
|
||||
Set {0} in company {1},Установить {0} в компании {1},
|
||||
Setup,Настройки,
|
||||
@ -4059,7 +4059,7 @@ Show Stock Ageing Data,Показать данные о старении зап
|
||||
Show Warehouse-wise Stock,Показать складской запас,
|
||||
Size,Размер,
|
||||
Something went wrong while evaluating the quiz.,Что-то пошло не так при оценке теста.,
|
||||
Sr,Sr,
|
||||
Sr,№,
|
||||
Start,Начать,
|
||||
Start Date cannot be before the current date,Дата начала не может быть раньше текущей даты,
|
||||
Start Time,Время начала,
|
||||
@ -4513,7 +4513,7 @@ Mandatory For Profit and Loss Account,Обязательно для счета
|
||||
Accounting Period,Период учета,
|
||||
Period Name,Название периода,
|
||||
Closed Documents,Закрытые документы,
|
||||
Accounts Settings,Настройки аккаунта,
|
||||
Accounts Settings,Настройка счетов,
|
||||
Settings for Accounts,Настройки для счетов,
|
||||
Make Accounting Entry For Every Stock Movement,Создавать бухгалтерские проводки при каждом перемещении запасов,
|
||||
Users with this role are allowed to set frozen accounts and create / modify accounting entries against frozen accounts,"Пользователи с этой ролью могут замороживать счета, а также создавать / изменять бухгалтерские проводки замороженных счетов",
|
||||
@ -5084,8 +5084,8 @@ Allow Zero Valuation Rate,Разрешить нулевую оценку,
|
||||
Item Tax Rate,Ставка налогов на продукт,
|
||||
Tax detail table fetched from item master as a string and stored in this field.\nUsed for Taxes and Charges,Налоговый Подробная таблица выбирается из мастера элемента в виде строки и хранится в этой области.\n Используется по налогам и сборам,
|
||||
Purchase Order Item,Заказ товара,
|
||||
Purchase Receipt Detail,Деталь квитанции о покупке,
|
||||
Item Weight Details,Деталь Вес Подробности,
|
||||
Purchase Receipt Detail,Сведения о квитанции о покупке,
|
||||
Item Weight Details,Сведения о весе товара,
|
||||
Weight Per Unit,Вес на единицу,
|
||||
Total Weight,Общий вес,
|
||||
Weight UOM,Вес Единица измерения,
|
||||
@ -5198,7 +5198,7 @@ Address and Contacts,Адрес и контакты,
|
||||
Contact List,Список контактов,
|
||||
Hidden list maintaining the list of contacts linked to Shareholder,"Скрытый список, поддерживающий список контактов, связанных с Акционером",
|
||||
Specify conditions to calculate shipping amount,Укажите условия для расчета суммы доставки,
|
||||
Shipping Rule Label,Название правила доставки,
|
||||
Shipping Rule Label,Метка правила доставки,
|
||||
example: Next Day Shipping,Пример: доставка на следующий день,
|
||||
Shipping Rule Type,Тип правила доставки,
|
||||
Shipping Account,Счет доставки,
|
||||
@ -5236,7 +5236,7 @@ Billing Interval,Интервал выставления счетов,
|
||||
Billing Interval Count,Счет интервала фактурирования,
|
||||
"Number of intervals for the interval field e.g if Interval is 'Days' and Billing Interval Count is 3, invoices will be generated every 3 days","Количество интервалов для поля интервалов, например, если Interval является «Days», а количество интервалов фактурирования - 3, счета-фактуры будут генерироваться каждые 3 дня",
|
||||
Payment Plan,Платежный план,
|
||||
Subscription Plan Detail,Деталь плана подписки,
|
||||
Subscription Plan Detail,Сведения о плана подписки,
|
||||
Plan,План,
|
||||
Subscription Settings,Настройки подписки,
|
||||
Grace Period,Льготный период,
|
||||
@ -5802,7 +5802,7 @@ Make Academic Term Mandatory,Сделать академический срок
|
||||
Skip User creation for new Student,Пропустить создание пользователя для нового студента,
|
||||
"By default, a new User is created for every new Student. If enabled, no new User will be created when a new Student is created.","По умолчанию для каждого нового Студента создается новый Пользователь. Если этот параметр включен, при создании нового Студента новый Пользователь не создается.",
|
||||
Instructor Records to be created by,Записи инструкторов должны быть созданы,
|
||||
Employee Number,Общее число сотрудников,
|
||||
Employee Number,Номер сотрудника,
|
||||
Fee Category,Категория платы,
|
||||
Fee Component,Компонент платы,
|
||||
Fees Category,Категория плат,
|
||||
@ -6196,7 +6196,7 @@ Inpatient Occupancy,Стационарное размещение,
|
||||
Occupancy Status,Статус занятости,
|
||||
Vacant,Вакантно,
|
||||
Occupied,Занято,
|
||||
Item Details,Детальная информация о товаре,
|
||||
Item Details,Детальная информация о продукте,
|
||||
UOM Conversion in Hours,Преобразование UOM в часы,
|
||||
Rate / UOM,Скорость / UOM,
|
||||
Change in Item,Изменение продукта,
|
||||
@ -6868,8 +6868,8 @@ Only Tax Impact (Cannot Claim But Part of Taxable Income),Только нало
|
||||
Create Separate Payment Entry Against Benefit Claim,Создать отдельную заявку на подачу заявки на получение пособия,
|
||||
Condition and Formula,Состояние и формула,
|
||||
Amount based on formula,Сумма на основе формулы,
|
||||
Formula,формула,
|
||||
Salary Detail,Заработная плата: Подробности,
|
||||
Formula,Формула,
|
||||
Salary Detail,Подробно об заработной плате,
|
||||
Component,Компонент,
|
||||
Do not include in total,Не включать в общей сложности,
|
||||
Default Amount,По умолчанию количество,
|
||||
@ -6891,7 +6891,7 @@ Total Principal Amount,Общая сумма,
|
||||
Total Interest Amount,Общая сумма процентов,
|
||||
Total Loan Repayment,Общая сумма погашения кредита,
|
||||
net pay info,Чистая информация платить,
|
||||
Gross Pay - Total Deduction - Loan Repayment,Gross Pay - Итого Вычет - Погашение кредита,
|
||||
Gross Pay - Total Deduction - Loan Repayment,Валовая заработная плата - Общий вычет - Погашение кредита,
|
||||
Total in words,Всего в словах,
|
||||
Net Pay (in words) will be visible once you save the Salary Slip.,"Чистая плата (прописью) будет видна, как только вы сохраните зарплатную ведомость.",
|
||||
Salary Component for timesheet based payroll.,Компонент заработной платы для расчета зарплаты на основе расписания.,
|
||||
@ -6961,7 +6961,7 @@ Trainer Email,Электронная почта тренера,
|
||||
Attendees,Присутствующие,
|
||||
Employee Emails,Электронные почты сотрудников,
|
||||
Training Event Employee,Обучение сотрудников Событие,
|
||||
Invited,приглашенный,
|
||||
Invited,Приглашенный,
|
||||
Feedback Submitted,Отзыв отправлен,
|
||||
Optional,Необязательный,
|
||||
Training Result Employee,Результат обучения сотрудника,
|
||||
@ -7185,7 +7185,7 @@ Ordered Quantity,Заказанное количество,
|
||||
Item to be manufactured or repacked,Продукт должен быть произведен или переупакован,
|
||||
Quantity of item obtained after manufacturing / repacking from given quantities of raw materials,Количество пункта получены после изготовления / переупаковка от заданных величин сырья,
|
||||
Set rate of sub-assembly item based on BOM,Установить скорость сборки на основе спецификации,
|
||||
Allow Alternative Item,Разрешить альтернативный элемент,
|
||||
Allow Alternative Item,Разрешить альтернативный продукт,
|
||||
Item UOM,Единиц продукта,
|
||||
Conversion Rate,Коэффициент конверсии,
|
||||
Rate Of Materials Based On,Оценить материалов на основе,
|
||||
@ -7600,7 +7600,7 @@ Invoices with no Place Of Supply,Счета без места поставки,
|
||||
Import Supplier Invoice,Импортная накладная поставщика,
|
||||
Invoice Series,Серия счетов,
|
||||
Upload XML Invoices,Загрузить XML-счета,
|
||||
Zip File,Zip-файл,
|
||||
Zip File,Zip файл,
|
||||
Import Invoices,Импорт счетов,
|
||||
Click on Import Invoices button once the zip file has been attached to the document. Any errors related to processing will be shown in the Error Log.,"Нажмите кнопку «Импортировать счета-фактуры», когда файл zip прикреплен к документу. Любые ошибки, связанные с обработкой, будут отображаться в журнале ошибок.",
|
||||
Lower Deduction Certificate,Свидетельство о нижнем удержании,
|
||||
@ -7635,7 +7635,7 @@ Restaurant Order Entry Item,Номер заказа заказа рестора
|
||||
Served,Подается,
|
||||
Restaurant Reservation,Бронирование ресторанов,
|
||||
Waitlisted,Лист ожидания,
|
||||
No Show,Нет шоу,
|
||||
No Show,Не показывать,
|
||||
No of People,Нет людей,
|
||||
Reservation Time,Время резервирования,
|
||||
Reservation End Time,Время окончания бронирования,
|
||||
@ -7873,8 +7873,8 @@ Disable In Words,Отключить в словах,
|
||||
"If disable, 'In Words' field will not be visible in any transaction","Если отключить, "В словах" поле не будет видно в любой сделке",
|
||||
Item Classification,Продуктовая классификация,
|
||||
General Settings,Основные настройки,
|
||||
Item Group Name,Пункт Название группы,
|
||||
Parent Item Group,Родитель Пункт Группа,
|
||||
Item Group Name,Название группы продуктов,
|
||||
Parent Item Group,Родительская группа продукта,
|
||||
Item Group Defaults,Элемент группы по умолчанию,
|
||||
Item Tax,Налог на продукт,
|
||||
Check this if you want to show in website,"Проверьте это, если вы хотите показать в веб-сайт",
|
||||
@ -7971,13 +7971,13 @@ Customs Tariff Number,Номер таможенного тарифа,
|
||||
Tariff Number,Тарифный номер,
|
||||
Delivery To,Доставка,
|
||||
MAT-DN-.YYYY.-,MAT-DN-.YYYY.-,
|
||||
Is Return,Является Вернуться,
|
||||
Is Return,Возврат,
|
||||
Issue Credit Note,Кредитная кредитная карта,
|
||||
Return Against Delivery Note,Вернуться На накладной,
|
||||
Customer's Purchase Order No,Клиентам Заказ Нет,
|
||||
Return Against Delivery Note,Возврат по накладной,
|
||||
Customer's Purchase Order No,Заказ клиента №,
|
||||
Billing Address Name,Название адреса для выставления счета,
|
||||
Required only for sample item.,Требуется только для образца пункта.,
|
||||
"If you have created a standard template in Sales Taxes and Charges Template, select one and click on the button below.","Если вы создали стандартный шаблон в шаблонах Налоги с налогами и сбором платежей, выберите его и нажмите кнопку ниже.",
|
||||
"If you have created a standard template in Sales Taxes and Charges Template, select one and click on the button below.","Если вы создали стандартный шаблон в Шаблоне налогов и сборов с продаж, выберите его и нажмите кнопку ниже.",
|
||||
In Words will be visible once you save the Delivery Note.,По словам будет виден только вы сохраните накладной.,
|
||||
In Words (Export) will be visible once you save the Delivery Note.,В Слов (Экспорт) будут видны только вы сохраните накладной.,
|
||||
Transporter Info,Информация для транспортировки,
|
||||
@ -7991,8 +7991,8 @@ Installation Status,Состояние установки,
|
||||
Excise Page Number,Количество Акцизный Страница,
|
||||
Instructions,Инструкции,
|
||||
From Warehouse,Со склада,
|
||||
Against Sales Order,По Сделке,
|
||||
Against Sales Order Item,По Продукту Сделки,
|
||||
Against Sales Order,По сделке,
|
||||
Against Sales Order Item,По позиции сделки,
|
||||
Against Sales Invoice,Повторная накладная,
|
||||
Against Sales Invoice Item,Счет на продажу продукта,
|
||||
Available Batch Qty at From Warehouse,Доступные Пакетная Кол-во на со склада,
|
||||
@ -8008,7 +8008,7 @@ Delivery Stop,Остановить доставку,
|
||||
Lock,Заблокировано,
|
||||
Visited,Посещен,
|
||||
Order Information,запросить информацию,
|
||||
Contact Information,Контакты,
|
||||
Contact Information,Контактная информация,
|
||||
Email sent to,Письмо отправлено,
|
||||
Dispatch Information,Информация о доставке,
|
||||
Estimated Arrival,Ожидаемое прибытие,
|
||||
@ -8121,7 +8121,7 @@ Two-way,Двусторонний,
|
||||
Alternative Item Name,Альтернативное название продукта,
|
||||
Attribute Name,Название атрибута,
|
||||
Numeric Values,Числовые значения,
|
||||
From Range,От хребта,
|
||||
From Range,Из диапазона,
|
||||
Increment,Приращение,
|
||||
To Range,В диапазоне,
|
||||
Item Attribute Values,Пункт значений атрибутов,
|
||||
@ -8143,7 +8143,7 @@ Default Supplier,Поставщик по умолчанию,
|
||||
Default Expense Account,Счет учета затрат по умолчанию,
|
||||
Sales Defaults,По умолчанию,
|
||||
Default Selling Cost Center,По умолчанию Продажа Стоимость центр,
|
||||
Item Manufacturer,Пункт Производитель,
|
||||
Item Manufacturer,Производитель товара,
|
||||
Item Price,Цена продукта,
|
||||
Packing Unit,Упаковочный блок,
|
||||
Quantity that must be bought or sold per UOM,"Количество, которое необходимо купить или продать за UOM",
|
||||
@ -8177,7 +8177,7 @@ Purchase Receipts,Покупка Поступления,
|
||||
Purchase Receipt Items,Покупка продуктов,
|
||||
Get Items From Purchase Receipts,Получить продукты из покупки.,
|
||||
Distribute Charges Based On,Распределите платежи на основе,
|
||||
Landed Cost Help,Земельные Стоимость Помощь,
|
||||
Landed Cost Help,Справка по стоимости доставки,
|
||||
Manufacturers used in Items,Производители использовали в пунктах,
|
||||
Limited to 12 characters,Ограничено до 12 символов,
|
||||
MAT-MR-.YYYY.-,МАТ-MR-.YYYY.-,
|
||||
@ -8186,13 +8186,13 @@ Transferred,Переданы,
|
||||
% Ordered,% заказано,
|
||||
Terms and Conditions Content,Условия Содержимое,
|
||||
Quantity and Warehouse,Количество и Склад,
|
||||
Lead Time Date,Время и Дата Лида,
|
||||
Min Order Qty,Минимальный заказ Кол-во,
|
||||
Lead Time Date,Дата выполнения заказа,
|
||||
Min Order Qty,Минимальное количество для заказа,
|
||||
Packed Item,Упаковано,
|
||||
To Warehouse (Optional),На склад (Необязательно),
|
||||
Actual Batch Quantity,Фактическое количество партий,
|
||||
Prevdoc DocType,Prevdoc DocType,
|
||||
Parent Detail docname,Родитель Деталь DOCNAME,
|
||||
Parent Detail docname,Сведения о родителе docname,
|
||||
"Generate packing slips for packages to be delivered. Used to notify package number, package contents and its weight.","Создаёт упаковочные листы к упаковкам для доставки. Содержит номер упаковки, перечень содержимого и вес.",
|
||||
Indicates that the package is a part of this delivery (Only Draft),"Указывает, что пакет является частью этой поставки (только проект)",
|
||||
MAT-PAC-.YYYY.-,MAT-PAC-.YYYY.-,
|
||||
@ -8353,7 +8353,7 @@ Automatically Set Serial Nos based on FIFO,Автоматически устан
|
||||
Auto Material Request,Автоматический запрос материалов,
|
||||
Inter Warehouse Transfer Settings,Настройки передачи между складами,
|
||||
Freeze Stock Entries,Замораживание поступления запасов,
|
||||
Stock Frozen Upto,остатки заморожены до,
|
||||
Stock Frozen Upto,Остатки заморожены до,
|
||||
Batch Identification,Идентификация партии,
|
||||
Use Naming Series,Использовать серийный номер,
|
||||
Naming Series Prefix,Префикс Идентификации по Имени,
|
||||
@ -8372,7 +8372,7 @@ Issue Split From,Выпуск Сплит От,
|
||||
Service Level,Уровень обслуживания,
|
||||
Response By,Ответ от,
|
||||
Response By Variance,Ответ по отклонениям,
|
||||
Ongoing,постоянный,
|
||||
Ongoing,Постоянный,
|
||||
Resolution By,Разрешение по,
|
||||
Resolution By Variance,Разрешение по отклонениям,
|
||||
Service Level Agreement Creation,Создание соглашения об уровне обслуживания,
|
||||
|
Can't render this file because it is too large.
|
@ -9,6 +9,7 @@ import frappe
|
||||
import pytz
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint
|
||||
from pyyoutube import Api
|
||||
|
||||
|
||||
@ -46,7 +47,7 @@ def is_tracking_enabled():
|
||||
def get_frequency(value):
|
||||
# Return numeric value from frequency field, return 1 as fallback default value: 1 hour
|
||||
if value != "Daily":
|
||||
return frappe.utils.cint(value[:2].strip())
|
||||
return cint(value[:2].strip())
|
||||
elif value:
|
||||
return 24
|
||||
return 1
|
||||
@ -120,24 +121,12 @@ def batch_update_youtube_data():
|
||||
video_stats = entry.to_dict().get("statistics")
|
||||
video_id = entry.to_dict().get("id")
|
||||
stats = {
|
||||
"like_count": video_stats.get("likeCount"),
|
||||
"view_count": video_stats.get("viewCount"),
|
||||
"dislike_count": video_stats.get("dislikeCount"),
|
||||
"comment_count": video_stats.get("commentCount"),
|
||||
"video_id": video_id,
|
||||
"like_count": cint(video_stats.get("likeCount")),
|
||||
"view_count": cint(video_stats.get("viewCount")),
|
||||
"dislike_count": cint(video_stats.get("dislikeCount")),
|
||||
"comment_count": cint(video_stats.get("commentCount")),
|
||||
}
|
||||
|
||||
frappe.db.sql(
|
||||
"""
|
||||
UPDATE `tabVideo`
|
||||
SET
|
||||
like_count = %(like_count)s,
|
||||
view_count = %(view_count)s,
|
||||
dislike_count = %(dislike_count)s,
|
||||
comment_count = %(comment_count)s
|
||||
WHERE youtube_video_id = %(video_id)s""",
|
||||
stats,
|
||||
)
|
||||
frappe.db.set_value("Video", video_id, stats)
|
||||
|
||||
video_list = frappe.get_all("Video", fields=["youtube_video_id"])
|
||||
if len(video_list) > 50:
|
||||
|
@ -5,7 +5,7 @@
|
||||
import frappe
|
||||
import frappe.share
|
||||
from frappe import _
|
||||
from frappe.utils import cint, cstr, flt, get_time, now_datetime
|
||||
from frappe.utils import cint, flt, get_time, now_datetime
|
||||
|
||||
from erpnext.controllers.status_updater import StatusUpdater
|
||||
|
||||
@ -30,64 +30,6 @@ class TransactionBase(StatusUpdater):
|
||||
except ValueError:
|
||||
frappe.throw(_("Invalid Posting Time"))
|
||||
|
||||
def add_calendar_event(self, opts, force=False):
|
||||
if (
|
||||
cstr(self.contact_by) != cstr(self._prev.contact_by)
|
||||
or cstr(self.contact_date) != cstr(self._prev.contact_date)
|
||||
or force
|
||||
or (hasattr(self, "ends_on") and cstr(self.ends_on) != cstr(self._prev.ends_on))
|
||||
):
|
||||
|
||||
self.delete_events()
|
||||
self._add_calendar_event(opts)
|
||||
|
||||
def delete_events(self):
|
||||
participations = frappe.get_all(
|
||||
"Event Participants",
|
||||
filters={
|
||||
"reference_doctype": self.doctype,
|
||||
"reference_docname": self.name,
|
||||
"parenttype": "Event",
|
||||
},
|
||||
fields=["name", "parent"],
|
||||
)
|
||||
|
||||
if participations:
|
||||
for participation in participations:
|
||||
total_participants = frappe.get_all(
|
||||
"Event Participants", filters={"parenttype": "Event", "parent": participation.parent}
|
||||
)
|
||||
|
||||
if len(total_participants) <= 1:
|
||||
frappe.db.sql("delete from `tabEvent` where name='%s'" % participation.parent)
|
||||
|
||||
frappe.db.sql("delete from `tabEvent Participants` where name='%s'" % participation.name)
|
||||
|
||||
def _add_calendar_event(self, opts):
|
||||
opts = frappe._dict(opts)
|
||||
|
||||
if self.contact_date:
|
||||
event = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Event",
|
||||
"owner": opts.owner or self.owner,
|
||||
"subject": opts.subject,
|
||||
"description": opts.description,
|
||||
"starts_on": self.contact_date,
|
||||
"ends_on": opts.ends_on,
|
||||
"event_type": "Private",
|
||||
}
|
||||
)
|
||||
|
||||
event.append(
|
||||
"event_participants", {"reference_doctype": self.doctype, "reference_docname": self.name}
|
||||
)
|
||||
|
||||
event.insert(ignore_permissions=True)
|
||||
|
||||
if frappe.db.exists("User", self.contact_by):
|
||||
frappe.share.add("Event", event.name, self.contact_by, flags={"ignore_share_permission": True})
|
||||
|
||||
def validate_uom_is_integer(self, uom_field, qty_fields):
|
||||
validate_uom_is_integer(self, uom_field, qty_fields)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user