Merge branch 'develop' into subcontracting

This commit is contained in:
Sagar Sharma 2022-06-28 22:20:32 +05:30
commit 78ff1783b1
71 changed files with 2985 additions and 1099 deletions

View File

@ -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

View File

@ -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");
}
});
}
}
},

View File

@ -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",

View File

@ -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):

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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)

View File

@ -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"):

View File

@ -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"):

View File

@ -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)

View File

@ -59,6 +59,7 @@ frappe.query_reports["Purchase Order Analysis"] = {
for (let option of status){
options.push({
"value": option,
"label": __(option),
"description": ""
})
}

View File

@ -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,8 +2710,9 @@ 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:
parent.update_reserved_qty_for_subcontract()
parent.create_raw_materials_supplied()
if should_update_supplied_items(parent):
parent.update_reserved_qty_for_subcontract()
parent.create_raw_materials_supplied()
parent.save()
else: # Sales Order
parent.validate_warehouse()

View File

@ -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",
},

View File

View 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": []
}

View 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

View File

@ -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
}

View File

@ -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"));
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 (!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
});
}
}
});

View File

@ -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"
}

View File

@ -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 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.link_to_contact()
def on_update(self):
self.update_prospect()
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.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
self.lead_name = " ".join(
filter(None, [self.salutation, self.first_name, self.middle_name, self.last_name])
)
def before_insert(self):
self.contact_doc = self.create_contact()
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 after_insert(self):
self.update_links()
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 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 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_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,50 +183,78 @@ 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()
if not self.lead_name:
self.set_full_name()
self.set_lead_name()
contact = frappe.new_doc("Contact")
contact.update(
contact = frappe.new_doc("Contact")
contact.update(
{
"first_name": self.first_name or self.lead_name,
"last_name": self.last_name,
"salutation": self.salutation,
"gender": self.gender,
"job_title": self.job_title,
"company_name": self.company_name,
}
)
if self.email_id:
contact.append("email_ids", {"email_id": self.email_id, "is_primary": 1})
if self.phone:
contact.append("phone_nos", {"phone": self.phone, "is_primary_phone": 1})
if self.mobile_no:
contact.append("phone_nos", {"phone": self.mobile_no, "is_primary_mobile_no": 1})
contact.insert(ignore_permissions=True)
contact.reload() # load changes by hooks on contact
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",
{
"first_name": self.first_name or self.lead_name,
"last_name": self.last_name,
"salutation": self.salutation,
"gender": self.gender,
"designation": self.designation,
"company_name": self.company_name,
}
"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,
},
)
if self.email_id:
contact.append("email_ids", {"email_id": self.email_id, "is_primary": 1})
if self.phone:
contact.append("phone_nos", {"phone": self.phone, "is_primary_phone": 1})
if self.mobile_no:
contact.append("phone_nos", {"phone": self.mobile_no, "is_primary_mobile_no": 1})
contact.insert(ignore_permissions=True)
contact.reload() # load changes by hooks on contact
return contact
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()
@ -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",
)

View File

@ -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);

View File

@ -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()

View File

@ -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}));

View File

@ -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",

View File

@ -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.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":
self.customer_name = frappe.db.get_value("Customer", self.party_name, "customer_name")
elif self.party_name and self.opportunity_from == "Lead":
lead_name, company_name = frappe.db.get_value(
"Lead", self.party_name, ["lead_name", "company_name"]
)
self.customer_name = company_name or lead_name
if self.party_name:
if self.opportunity_from == "Customer":
self.customer_name = frappe.db.get_value("Customer", self.party_name, "customer_name")
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"]
)
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

View File

@ -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'
}

View File

@ -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(),
}
)

View File

@ -8,7 +8,9 @@
"transaction_date": "2013-12-12",
"items": [{
"item_name": "Test Item",
"description": "Some description"
"description": "Some description",
"qty": 5,
"rate": 100
}]
}
]

View File

@ -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();
}
});

View File

@ -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
}

View File

@ -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)
carry_forward_communication_and_comments = frappe.db.get_single_value(
"CRM Settings", "carry_forward_communication_and_comments"
)
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
)
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",
],
)

View File

@ -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)

View File

@ -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
}

View File

@ -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": []
}

View 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 ProspectOpportunity(Document):
pass

View File

@ -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"
},
]
};

View File

@ -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",

View File

@ -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 ""

View File

@ -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()

View File

@ -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"],

View File

@ -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)

View File

@ -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})

View File

@ -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",

View File

@ -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,28 +129,31 @@ def process_boms_cost_level_wise(
current_boms = {}
values = {}
if update_doc.status == "Queued":
# First level yet to process. On Submit.
current_level = 0
current_boms = get_leaf_boms()
values = {
"processed_boms": json.dumps({}),
"status": "In Progress",
"current_level": current_level,
}
else:
# Resume next level. via Cron Job.
if not parent_boms:
return
try:
if update_doc.status == "Queued":
# First level yet to process. On Submit.
current_level = 0
current_boms = get_leaf_boms()
values = {
"processed_boms": json.dumps({}),
"status": "In Progress",
"current_level": current_level,
}
else:
# Resume next level. via Cron Job.
if not parent_boms:
return
current_level = cint(update_doc.current_level) + 1
current_level = cint(update_doc.current_level) + 1
# Process the next level BOMs. Stage parents as current BOMs.
current_boms = parent_boms.copy()
values = {"current_level": current_level}
# Process the next level BOMs. Stage parents as current BOMs.
current_boms = parent_boms.copy()
values = {"current_level": current_level}
set_values_in_log(update_doc.name, values, commit=True)
queue_bom_cost_jobs(current_boms, update_doc, current_level)
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

View File

@ -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);
});
},
};

View File

@ -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)."

View File

@ -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",
},
)

View File

@ -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")

View File

@ -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

View 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)

View File

@ -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)

View File

@ -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)

View File

@ -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'

View 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>

View 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>

View 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();
}
}
});
}
};

View File

@ -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(
{

View File

@ -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",
};
});
},
});

View File

@ -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
}

View File

@ -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},

View File

@ -55,6 +55,7 @@ frappe.query_reports["Sales Order Analysis"] = {
for (let option of status){
options.push({
"value": option,
"label": __(option),
"description": ""
})
}

View File

@ -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")

View File

@ -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,119 +1704,78 @@ 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: {
"from_warehouse": item.warehouse,
"to_warehouse": "",
"qty": qty,
"item_name": item.item_name,
"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.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):
"""
@ -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)

View File

@ -34,7 +34,6 @@ def send_message(subject="Website Query", message="", sender="", status="Open"):
status="Open",
title=subject,
contact_email=sender,
to_discuss=message,
)
)

View File

@ -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","Если отключить, &quot;В словах&quot; поле не будет видно в любой сделке",
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.

View File

@ -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:

View File

@ -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)