Merge branch 'develop' of https://github.com/frappe/erpnext into tds_validity
This commit is contained in:
commit
ee1486209d
2
.github/helper/documentation.py
vendored
2
.github/helper/documentation.py
vendored
@ -24,6 +24,8 @@ def docs_link_exists(body):
|
|||||||
parts = parsed_url.path.split('/')
|
parts = parsed_url.path.split('/')
|
||||||
if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos:
|
if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos:
|
||||||
return True
|
return True
|
||||||
|
elif parsed_url.netloc == "docs.erpnext.com":
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
@ -100,15 +100,15 @@ def convert_to_presentation_currency(gl_entries, currency_info, company):
|
|||||||
if entry.get('credit'):
|
if entry.get('credit'):
|
||||||
entry['credit'] = credit_in_account_currency
|
entry['credit'] = credit_in_account_currency
|
||||||
else:
|
else:
|
||||||
value = debit or credit
|
|
||||||
date = currency_info['report_date']
|
date = currency_info['report_date']
|
||||||
converted_value = convert(value, presentation_currency, company_currency, date)
|
converted_debit_value = convert(debit, presentation_currency, company_currency, date)
|
||||||
|
converted_credit_value = convert(credit, presentation_currency, company_currency, date)
|
||||||
|
|
||||||
if entry.get('debit'):
|
if entry.get('debit'):
|
||||||
entry['debit'] = converted_value
|
entry['debit'] = converted_debit_value
|
||||||
|
|
||||||
if entry.get('credit'):
|
if entry.get('credit'):
|
||||||
entry['credit'] = converted_value
|
entry['credit'] = converted_credit_value
|
||||||
|
|
||||||
converted_gl_list.append(entry)
|
converted_gl_list.append(entry)
|
||||||
|
|
||||||
|
@ -5,21 +5,48 @@ import unittest
|
|||||||
from frappe.test_runner import make_test_objects
|
from frappe.test_runner import make_test_objects
|
||||||
|
|
||||||
from erpnext.accounts.party import get_party_shipping_address
|
from erpnext.accounts.party import get_party_shipping_address
|
||||||
|
from erpnext.accounts.utils import get_future_stock_vouchers, get_voucherwise_gl_entries
|
||||||
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||||
|
|
||||||
|
|
||||||
class TestUtils(unittest.TestCase):
|
class TestUtils(unittest.TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
super(TestUtils, cls).setUpClass()
|
super(TestUtils, cls).setUpClass()
|
||||||
make_test_objects('Address', ADDRESS_RECORDS)
|
make_test_objects("Address", ADDRESS_RECORDS)
|
||||||
|
|
||||||
def test_get_party_shipping_address(self):
|
def test_get_party_shipping_address(self):
|
||||||
address = get_party_shipping_address('Customer', '_Test Customer 1')
|
address = get_party_shipping_address("Customer", "_Test Customer 1")
|
||||||
self.assertEqual(address, '_Test Billing Address 2 Title-Billing')
|
self.assertEqual(address, "_Test Billing Address 2 Title-Billing")
|
||||||
|
|
||||||
def test_get_party_shipping_address2(self):
|
def test_get_party_shipping_address2(self):
|
||||||
address = get_party_shipping_address('Customer', '_Test Customer 2')
|
address = get_party_shipping_address("Customer", "_Test Customer 2")
|
||||||
self.assertEqual(address, '_Test Shipping Address 2 Title-Shipping')
|
self.assertEqual(address, "_Test Shipping Address 2 Title-Shipping")
|
||||||
|
|
||||||
|
def test_get_voucher_wise_gl_entry(self):
|
||||||
|
|
||||||
|
pr = make_purchase_receipt(
|
||||||
|
item_code="_Test Item",
|
||||||
|
posting_date="2021-02-01",
|
||||||
|
rate=100,
|
||||||
|
qty=1,
|
||||||
|
warehouse="Stores - TCP1",
|
||||||
|
company="_Test Company with perpetual inventory",
|
||||||
|
)
|
||||||
|
|
||||||
|
future_vouchers = get_future_stock_vouchers("2021-01-01", "00:00:00", for_items=["_Test Item"])
|
||||||
|
|
||||||
|
voucher_type_and_no = ("Purchase Receipt", pr.name)
|
||||||
|
self.assertTrue(
|
||||||
|
voucher_type_and_no in future_vouchers,
|
||||||
|
msg="get_future_stock_vouchers not returning correct value",
|
||||||
|
)
|
||||||
|
|
||||||
|
posting_date = "2021-01-01"
|
||||||
|
gl_entries = get_voucherwise_gl_entries(future_vouchers, posting_date)
|
||||||
|
self.assertTrue(
|
||||||
|
voucher_type_and_no in gl_entries, msg="get_voucherwise_gl_entries not returning expected GLes",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
ADDRESS_RECORDS = [
|
ADDRESS_RECORDS = [
|
||||||
@ -31,12 +58,8 @@ ADDRESS_RECORDS = [
|
|||||||
"city": "Lagos",
|
"city": "Lagos",
|
||||||
"country": "Nigeria",
|
"country": "Nigeria",
|
||||||
"links": [
|
"links": [
|
||||||
{
|
{"link_doctype": "Customer", "link_name": "_Test Customer 2", "doctype": "Dynamic Link"}
|
||||||
"link_doctype": "Customer",
|
],
|
||||||
"link_name": "_Test Customer 2",
|
|
||||||
"doctype": "Dynamic Link"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"doctype": "Address",
|
"doctype": "Address",
|
||||||
@ -46,12 +69,8 @@ ADDRESS_RECORDS = [
|
|||||||
"city": "Lagos",
|
"city": "Lagos",
|
||||||
"country": "Nigeria",
|
"country": "Nigeria",
|
||||||
"links": [
|
"links": [
|
||||||
{
|
{"link_doctype": "Customer", "link_name": "_Test Customer 2", "doctype": "Dynamic Link"}
|
||||||
"link_doctype": "Customer",
|
],
|
||||||
"link_name": "_Test Customer 2",
|
|
||||||
"doctype": "Dynamic Link"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"doctype": "Address",
|
"doctype": "Address",
|
||||||
@ -62,12 +81,8 @@ ADDRESS_RECORDS = [
|
|||||||
"country": "Nigeria",
|
"country": "Nigeria",
|
||||||
"is_shipping_address": "1",
|
"is_shipping_address": "1",
|
||||||
"links": [
|
"links": [
|
||||||
{
|
{"link_doctype": "Customer", "link_name": "_Test Customer 2", "doctype": "Dynamic Link"}
|
||||||
"link_doctype": "Customer",
|
],
|
||||||
"link_name": "_Test Customer 2",
|
|
||||||
"doctype": "Dynamic Link"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"doctype": "Address",
|
"doctype": "Address",
|
||||||
@ -78,11 +93,7 @@ ADDRESS_RECORDS = [
|
|||||||
"country": "Nigeria",
|
"country": "Nigeria",
|
||||||
"is_shipping_address": "1",
|
"is_shipping_address": "1",
|
||||||
"links": [
|
"links": [
|
||||||
{
|
{"link_doctype": "Customer", "link_name": "_Test Customer 1", "doctype": "Dynamic Link"}
|
||||||
"link_doctype": "Customer",
|
],
|
||||||
"link_name": "_Test Customer 1",
|
},
|
||||||
"doctype": "Dynamic Link"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
|
@ -963,6 +963,9 @@ def get_voucherwise_gl_entries(future_stock_vouchers, posting_date):
|
|||||||
|
|
||||||
Only fetches GLE fields required for comparing with new GLE.
|
Only fetches GLE fields required for comparing with new GLE.
|
||||||
Check compare_existing_and_expected_gle function below.
|
Check compare_existing_and_expected_gle function below.
|
||||||
|
|
||||||
|
returns:
|
||||||
|
Dict[Tuple[voucher_type, voucher_no], List[GL Entries]]
|
||||||
"""
|
"""
|
||||||
gl_entries = {}
|
gl_entries = {}
|
||||||
if not future_stock_vouchers:
|
if not future_stock_vouchers:
|
||||||
@ -971,7 +974,7 @@ def get_voucherwise_gl_entries(future_stock_vouchers, posting_date):
|
|||||||
voucher_nos = [d[1] for d in future_stock_vouchers]
|
voucher_nos = [d[1] for d in future_stock_vouchers]
|
||||||
|
|
||||||
gles = frappe.db.sql("""
|
gles = frappe.db.sql("""
|
||||||
select name, account, credit, debit, cost_center, project
|
select name, account, credit, debit, cost_center, project, voucher_type, voucher_no
|
||||||
from `tabGL Entry`
|
from `tabGL Entry`
|
||||||
where
|
where
|
||||||
posting_date >= %s and voucher_no in (%s)""" %
|
posting_date >= %s and voucher_no in (%s)""" %
|
||||||
|
@ -132,10 +132,43 @@ frappe.ui.form.on("Opportunity", {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
currency: function(frm) {
|
||||||
|
let company_currency = erpnext.get_currency(frm.doc.company);
|
||||||
|
if (company_currency != frm.doc.company) {
|
||||||
|
frappe.call({
|
||||||
|
method: "erpnext.setup.utils.get_exchange_rate",
|
||||||
|
args: {
|
||||||
|
from_currency: frm.doc.currency,
|
||||||
|
to_currency: company_currency
|
||||||
|
},
|
||||||
|
callback: function(r) {
|
||||||
|
if (r.message) {
|
||||||
|
frm.set_value('conversion_rate', flt(r.message));
|
||||||
|
frm.set_df_property('conversion_rate', 'description', '1 ' + frm.doc.currency
|
||||||
|
+ ' = [?] ' + company_currency);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
frm.set_value('conversion_rate', 1.0);
|
||||||
|
frm.set_df_property('conversion_rate', 'hidden', 1);
|
||||||
|
frm.set_df_property('conversion_rate', 'description', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
frm.trigger('opportunity_amount');
|
||||||
|
frm.trigger('set_dynamic_field_label');
|
||||||
|
},
|
||||||
|
|
||||||
|
opportunity_amount: function(frm) {
|
||||||
|
frm.set_value('base_opportunity_amount', flt(frm.doc.opportunity_amount) * flt(frm.doc.conversion_rate));
|
||||||
|
},
|
||||||
|
|
||||||
set_dynamic_field_label: function(frm){
|
set_dynamic_field_label: function(frm){
|
||||||
if (frm.doc.opportunity_from) {
|
if (frm.doc.opportunity_from) {
|
||||||
frm.set_df_property("party_name", "label", frm.doc.opportunity_from);
|
frm.set_df_property("party_name", "label", frm.doc.opportunity_from);
|
||||||
}
|
}
|
||||||
|
frm.trigger('change_grid_labels');
|
||||||
|
frm.trigger('change_form_labels');
|
||||||
},
|
},
|
||||||
|
|
||||||
make_supplier_quotation: function(frm) {
|
make_supplier_quotation: function(frm) {
|
||||||
@ -152,6 +185,62 @@ frappe.ui.form.on("Opportunity", {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
change_form_labels: function(frm) {
|
||||||
|
let company_currency = erpnext.get_currency(frm.doc.company);
|
||||||
|
frm.set_currency_labels(["base_opportunity_amount", "base_total", "base_grand_total"], company_currency);
|
||||||
|
frm.set_currency_labels(["opportunity_amount", "total", "grand_total"], frm.doc.currency);
|
||||||
|
|
||||||
|
// toggle fields
|
||||||
|
frm.toggle_display(["conversion_rate", "base_opportunity_amount", "base_total", "base_grand_total"],
|
||||||
|
frm.doc.currency != company_currency);
|
||||||
|
},
|
||||||
|
|
||||||
|
change_grid_labels: function(frm) {
|
||||||
|
let company_currency = erpnext.get_currency(frm.doc.company);
|
||||||
|
frm.set_currency_labels(["base_rate", "base_amount"], company_currency, "items");
|
||||||
|
frm.set_currency_labels(["rate", "amount"], frm.doc.currency, "items");
|
||||||
|
|
||||||
|
let item_grid = frm.fields_dict.items.grid;
|
||||||
|
$.each(["base_rate", "base_amount"], function(i, fname) {
|
||||||
|
if(frappe.meta.get_docfield(item_grid.doctype, fname))
|
||||||
|
item_grid.set_column_disp(fname, frm.doc.currency != company_currency);
|
||||||
|
});
|
||||||
|
frm.refresh_fields();
|
||||||
|
},
|
||||||
|
|
||||||
|
calculate_total: function(frm) {
|
||||||
|
let total = 0, base_total = 0, grand_total = 0, base_grand_total = 0;
|
||||||
|
frm.doc.items.forEach(item => {
|
||||||
|
total += item.amount;
|
||||||
|
base_total += item.base_amount;
|
||||||
|
})
|
||||||
|
|
||||||
|
base_grand_total = base_total + frm.doc.base_opportunity_amount;
|
||||||
|
grand_total = total + frm.doc.opportunity_amount;
|
||||||
|
|
||||||
|
frm.set_value({
|
||||||
|
'total': flt(total),
|
||||||
|
'base_total': flt(base_total),
|
||||||
|
'grand_total': flt(grand_total),
|
||||||
|
'base_grand_total': flt(base_grand_total)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
frappe.ui.form.on("Opportunity Item", {
|
||||||
|
calculate: function(frm, cdt, cdn) {
|
||||||
|
let row = frappe.get_doc(cdt, cdn);
|
||||||
|
frappe.model.set_value(cdt, cdn, "amount", flt(row.qty) * flt(row.rate));
|
||||||
|
frappe.model.set_value(cdt, cdn, "base_rate", flt(frm.doc.conversion_rate) * flt(row.rate));
|
||||||
|
frappe.model.set_value(cdt, cdn, "base_amount", flt(frm.doc.conversion_rate) * flt(row.amount));
|
||||||
|
frm.trigger("calculate_total");
|
||||||
|
},
|
||||||
|
qty: function(frm, cdt, cdn) {
|
||||||
|
frm.trigger("calculate", cdt, cdn);
|
||||||
|
},
|
||||||
|
rate: function(frm, cdt, cdn) {
|
||||||
|
frm.trigger("calculate", cdt, cdn);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO commonify this code
|
// TODO commonify this code
|
||||||
@ -169,6 +258,7 @@ erpnext.crm.Opportunity = class Opportunity extends frappe.ui.form.Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.setup_queries();
|
this.setup_queries();
|
||||||
|
this.frm.trigger('currency');
|
||||||
}
|
}
|
||||||
|
|
||||||
setup_queries() {
|
setup_queries() {
|
||||||
|
@ -33,12 +33,20 @@
|
|||||||
"to_discuss",
|
"to_discuss",
|
||||||
"section_break_14",
|
"section_break_14",
|
||||||
"currency",
|
"currency",
|
||||||
"opportunity_amount",
|
"conversion_rate",
|
||||||
|
"base_opportunity_amount",
|
||||||
"with_items",
|
"with_items",
|
||||||
"column_break_17",
|
"column_break_17",
|
||||||
"probability",
|
"probability",
|
||||||
|
"opportunity_amount",
|
||||||
"items_section",
|
"items_section",
|
||||||
"items",
|
"items",
|
||||||
|
"section_break_32",
|
||||||
|
"base_total",
|
||||||
|
"base_grand_total",
|
||||||
|
"column_break_33",
|
||||||
|
"total",
|
||||||
|
"grand_total",
|
||||||
"contact_info",
|
"contact_info",
|
||||||
"customer_address",
|
"customer_address",
|
||||||
"address_display",
|
"address_display",
|
||||||
@ -425,12 +433,65 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Print Language",
|
"label": "Print Language",
|
||||||
"options": "Language"
|
"options": "Language"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "base_opportunity_amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Opportunity Amount (Company Currency)",
|
||||||
|
"options": "Company:company:default_currency",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "with_items",
|
||||||
|
"fieldname": "section_break_32",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"hide_border": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "base_total",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Total (Company Currency)",
|
||||||
|
"options": "Company:company:default_currency",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "total",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Total",
|
||||||
|
"options": "currency",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "conversion_rate",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Exchange Rate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_33",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "base_grand_total",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Grand Total (Company Currency)",
|
||||||
|
"options": "Company:company:default_currency",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "grand_total",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Grand Total",
|
||||||
|
"options": "currency",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-info-sign",
|
"icon": "fa fa-info-sign",
|
||||||
"idx": 195,
|
"idx": 195,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-08-25 10:28:24.923543",
|
"modified": "2021-09-06 10:02:18.609136",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "CRM",
|
"module": "CRM",
|
||||||
"name": "Opportunity",
|
"name": "Opportunity",
|
||||||
|
@ -9,9 +9,8 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.email.inbox import link_communication_to_document
|
from frappe.email.inbox import link_communication_to_document
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
from frappe.utils import cint, cstr, get_fullname
|
from frappe.utils import cint, cstr, flt, get_fullname
|
||||||
|
|
||||||
from erpnext.accounts.party import get_party_account_currency
|
|
||||||
from erpnext.setup.utils import get_exchange_rate
|
from erpnext.setup.utils import get_exchange_rate
|
||||||
from erpnext.utilities.transaction_base import TransactionBase
|
from erpnext.utilities.transaction_base import TransactionBase
|
||||||
|
|
||||||
@ -41,6 +40,23 @@ class Opportunity(TransactionBase):
|
|||||||
if not self.with_items:
|
if not self.with_items:
|
||||||
self.items = []
|
self.items = []
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.calculate_totals()
|
||||||
|
|
||||||
|
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)
|
||||||
|
total += item.amount
|
||||||
|
base_total += item.base_amount
|
||||||
|
|
||||||
|
self.total = flt(total)
|
||||||
|
self.base_total = flt(base_total)
|
||||||
|
self.grand_total = flt(self.total) + flt(self.opportunity_amount)
|
||||||
|
self.base_grand_total = flt(self.base_total) + flt(self.base_opportunity_amount)
|
||||||
|
|
||||||
def make_new_lead_if_required(self):
|
def make_new_lead_if_required(self):
|
||||||
"""Set lead against new opportunity"""
|
"""Set lead against new opportunity"""
|
||||||
if (not self.get("party_name")) and self.contact_email:
|
if (not self.get("party_name")) and self.contact_email:
|
||||||
@ -224,13 +240,6 @@ def make_quotation(source_name, target_doc=None):
|
|||||||
|
|
||||||
company_currency = frappe.get_cached_value('Company', quotation.company, "default_currency")
|
company_currency = frappe.get_cached_value('Company', quotation.company, "default_currency")
|
||||||
|
|
||||||
if quotation.quotation_to == 'Customer' and quotation.party_name:
|
|
||||||
party_account_currency = get_party_account_currency("Customer", quotation.party_name, quotation.company)
|
|
||||||
else:
|
|
||||||
party_account_currency = company_currency
|
|
||||||
|
|
||||||
quotation.currency = party_account_currency or company_currency
|
|
||||||
|
|
||||||
if company_currency == quotation.currency:
|
if company_currency == quotation.currency:
|
||||||
exchange_rate = 1
|
exchange_rate = 1
|
||||||
else:
|
else:
|
||||||
@ -254,7 +263,7 @@ def make_quotation(source_name, target_doc=None):
|
|||||||
"doctype": "Quotation",
|
"doctype": "Quotation",
|
||||||
"field_map": {
|
"field_map": {
|
||||||
"opportunity_from": "quotation_to",
|
"opportunity_from": "quotation_to",
|
||||||
"name": "enq_no",
|
"name": "enq_no"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Opportunity Item": {
|
"Opportunity Item": {
|
||||||
|
@ -63,6 +63,10 @@ class TestOpportunity(unittest.TestCase):
|
|||||||
self.assertEqual(opp_doc.opportunity_from, "Customer")
|
self.assertEqual(opp_doc.opportunity_from, "Customer")
|
||||||
self.assertEqual(opp_doc.party_name, customer.name)
|
self.assertEqual(opp_doc.party_name, customer.name)
|
||||||
|
|
||||||
|
def test_opportunity_item(self):
|
||||||
|
opportunity_doc = make_opportunity(with_items=1, rate=1100, qty=2)
|
||||||
|
self.assertEqual(opportunity_doc.total, 2200)
|
||||||
|
|
||||||
def make_opportunity(**args):
|
def make_opportunity(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|
||||||
@ -71,6 +75,7 @@ def make_opportunity(**args):
|
|||||||
"company": args.company or "_Test Company",
|
"company": args.company or "_Test Company",
|
||||||
"opportunity_from": args.opportunity_from or "Customer",
|
"opportunity_from": args.opportunity_from or "Customer",
|
||||||
"opportunity_type": "Sales",
|
"opportunity_type": "Sales",
|
||||||
|
"conversion_rate": 1.0,
|
||||||
"with_items": args.with_items or 0,
|
"with_items": args.with_items or 0,
|
||||||
"transaction_date": today()
|
"transaction_date": today()
|
||||||
})
|
})
|
||||||
@ -85,6 +90,7 @@ def make_opportunity(**args):
|
|||||||
opp_doc.append('items', {
|
opp_doc.append('items', {
|
||||||
"item_code": args.item_code or "_Test Item",
|
"item_code": args.item_code or "_Test Item",
|
||||||
"qty": args.qty or 1,
|
"qty": args.qty or 1,
|
||||||
|
"rate": args.rate or 1000,
|
||||||
"uom": "_Test UOM"
|
"uom": "_Test UOM"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,469 +1,177 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_events_in_timeline": 0,
|
|
||||||
"allow_guest_to_view": 0,
|
|
||||||
"allow_import": 0,
|
|
||||||
"allow_rename": 0,
|
|
||||||
"beta": 0,
|
|
||||||
"creation": "2013-02-22 01:27:51",
|
"creation": "2013-02-22 01:27:51",
|
||||||
"custom": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"item_code",
|
||||||
|
"item_name",
|
||||||
|
"col_break1",
|
||||||
|
"uom",
|
||||||
|
"qty",
|
||||||
|
"section_break_6",
|
||||||
|
"brand",
|
||||||
|
"item_group",
|
||||||
|
"description",
|
||||||
|
"column_break_8",
|
||||||
|
"image",
|
||||||
|
"image_view",
|
||||||
|
"quantity_and_rate_section",
|
||||||
|
"base_rate",
|
||||||
|
"base_amount",
|
||||||
|
"column_break_16",
|
||||||
|
"rate",
|
||||||
|
"amount"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "item_code",
|
"fieldname": "item_code",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Item Code",
|
"label": "Item Code",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"oldfieldname": "item_code",
|
"oldfieldname": "item_code",
|
||||||
"oldfieldtype": "Link",
|
"oldfieldtype": "Link",
|
||||||
"options": "Item",
|
"options": "Item"
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "col_break1",
|
"fieldname": "col_break1",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break"
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"default": "1",
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "qty",
|
"fieldname": "qty",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Qty",
|
"label": "Qty",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"oldfieldname": "qty",
|
"oldfieldname": "qty",
|
||||||
"oldfieldtype": "Currency",
|
"oldfieldtype": "Currency"
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"description": "",
|
|
||||||
"fieldname": "item_group",
|
"fieldname": "item_group",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Item Group",
|
"label": "Item Group",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"oldfieldname": "item_group",
|
"oldfieldname": "item_group",
|
||||||
"oldfieldtype": "Link",
|
"oldfieldtype": "Link",
|
||||||
"options": "Item Group",
|
"options": "Item Group",
|
||||||
"permlevel": 0,
|
"print_hide": 1
|
||||||
"print_hide": 1,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "brand",
|
"fieldname": "brand",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Brand",
|
"label": "Brand",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"oldfieldname": "brand",
|
"oldfieldname": "brand",
|
||||||
"oldfieldtype": "Link",
|
"oldfieldtype": "Link",
|
||||||
"options": "Brand",
|
"options": "Brand",
|
||||||
"permlevel": 0,
|
"print_hide": 1
|
||||||
"print_hide": 1,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"collapsible": 1,
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "section_break_6",
|
"fieldname": "section_break_6",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"hidden": 0,
|
"label": "Description"
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "uom",
|
"fieldname": "uom",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "UOM",
|
"label": "UOM",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"oldfieldname": "uom",
|
"oldfieldname": "uom",
|
||||||
"oldfieldtype": "Link",
|
"oldfieldtype": "Link",
|
||||||
"options": "UOM",
|
"options": "UOM"
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "item_name",
|
"fieldname": "item_name",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 1,
|
"in_global_search": 1,
|
||||||
"in_list_view": 1,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Item Name",
|
"label": "Item Name",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"oldfieldname": "item_name",
|
"oldfieldname": "item_name",
|
||||||
"oldfieldtype": "Data",
|
"oldfieldtype": "Data"
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "description",
|
"fieldname": "description",
|
||||||
"fieldtype": "Text Editor",
|
"fieldtype": "Text Editor",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Description",
|
"label": "Description",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"oldfieldname": "description",
|
"oldfieldname": "description",
|
||||||
"oldfieldtype": "Text",
|
"oldfieldtype": "Text",
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"print_width": "300px",
|
"print_width": "300px",
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0,
|
|
||||||
"width": "300px"
|
"width": "300px"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "column_break_8",
|
"fieldname": "column_break_8",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break"
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "image",
|
"fieldname": "image",
|
||||||
"fieldtype": "Attach",
|
"fieldtype": "Attach",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"ignore_user_permissions": 0,
|
"label": "Image"
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Image",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "image_view",
|
"fieldname": "image_view",
|
||||||
"fieldtype": "Image",
|
"fieldtype": "Image",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Image View",
|
"label": "Image View",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "image",
|
"options": "image",
|
||||||
"permlevel": 0,
|
"print_hide": 1
|
||||||
"precision": "",
|
|
||||||
"print_hide": 1,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"fieldname": "rate",
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "basic_rate",
|
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"hidden": 1,
|
"in_list_view": 1,
|
||||||
"ignore_user_permissions": 0,
|
"label": "Rate",
|
||||||
"ignore_xss_filter": 0,
|
"options": "currency",
|
||||||
"in_filter": 0,
|
"reqd": 1
|
||||||
"in_global_search": 0,
|
},
|
||||||
"in_list_view": 0,
|
{
|
||||||
"in_standard_filter": 0,
|
"fieldname": "quantity_and_rate_section",
|
||||||
"label": "Basic Rate",
|
"fieldtype": "Section Break",
|
||||||
"length": 0,
|
"label": "Quantity and Rate"
|
||||||
"no_copy": 0,
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "base_amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Amount (Company Currency)",
|
||||||
|
"options": "Company:company:default_currency",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1,
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_16",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Amount",
|
||||||
|
"options": "currency",
|
||||||
|
"read_only": 1,
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "base_rate",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Rate (Company Currency)",
|
||||||
"oldfieldname": "basic_rate",
|
"oldfieldname": "basic_rate",
|
||||||
"oldfieldtype": "Currency",
|
"oldfieldtype": "Currency",
|
||||||
"options": "Company:company:default_currency",
|
"options": "Company:company:default_currency",
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"print_hide_if_no_value": 0,
|
"read_only": 1,
|
||||||
"read_only": 0,
|
"reqd": 1
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_web_view": 0,
|
|
||||||
"hide_heading": 0,
|
|
||||||
"hide_toolbar": 0,
|
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"image_view": 0,
|
|
||||||
"in_create": 0,
|
|
||||||
"is_submittable": 0,
|
|
||||||
"issingle": 0,
|
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"max_attachments": 0,
|
"links": [],
|
||||||
"modified": "2018-12-28 15:43:09.382012",
|
"modified": "2021-07-30 16:39:09.775720",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "CRM",
|
"module": "CRM",
|
||||||
"name": "Opportunity Item",
|
"name": "Opportunity Item",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 0,
|
"sort_field": "modified",
|
||||||
"read_only": 0,
|
"sort_order": "DESC",
|
||||||
"read_only_onload": 0,
|
"track_changes": 1
|
||||||
"show_name_in_global_search": 0,
|
|
||||||
"track_changes": 1,
|
|
||||||
"track_seen": 0,
|
|
||||||
"track_views": 0
|
|
||||||
}
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
frappe.query_reports["Opportunity Summary by Sales Stage"] = {
|
||||||
|
"filters": [
|
||||||
|
{
|
||||||
|
fieldname: "based_on",
|
||||||
|
label: __("Based On"),
|
||||||
|
fieldtype: "Select",
|
||||||
|
options: "Opportunity Owner\nSource\nOpportunity Type",
|
||||||
|
default: "Opportunity Owner"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "data_based_on",
|
||||||
|
label: __("Data Based On"),
|
||||||
|
fieldtype: "Select",
|
||||||
|
options: "Number\nAmount",
|
||||||
|
default: "Number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "from_date",
|
||||||
|
label: __("From Date"),
|
||||||
|
fieldtype: "Date",
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "to_date",
|
||||||
|
label: __("To Date"),
|
||||||
|
fieldtype: "Date",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "status",
|
||||||
|
label: __("Status"),
|
||||||
|
fieldtype: "MultiSelectList",
|
||||||
|
get_data: function() {
|
||||||
|
return [
|
||||||
|
{value: "Open", description: "Status"},
|
||||||
|
{value: "Converted", description: "Status"},
|
||||||
|
{value: "Quotation", description: "Status"},
|
||||||
|
{value: "Replied", description: "Status"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "opportunity_source",
|
||||||
|
label: __("Oppoturnity Source"),
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Lead Source",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "opportunity_type",
|
||||||
|
label: __("Opportunity Type"),
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Opportunity Type",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "company",
|
||||||
|
label: __("Company"),
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Company",
|
||||||
|
default: frappe.defaults.get_user_default("Company")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"add_total_row": 0,
|
||||||
|
"columns": [],
|
||||||
|
"creation": "2021-07-28 12:18:24.028737",
|
||||||
|
"disable_prepared_report": 0,
|
||||||
|
"disabled": 0,
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Report",
|
||||||
|
"filters": [],
|
||||||
|
"idx": 0,
|
||||||
|
"is_standard": "Yes",
|
||||||
|
"modified": "2021-07-28 12:18:24.028737",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "CRM",
|
||||||
|
"name": "Opportunity Summary by Sales Stage",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"prepared_report": 0,
|
||||||
|
"ref_doctype": "Opportunity",
|
||||||
|
"report_name": "Opportunity Summary by Sales Stage ",
|
||||||
|
"report_type": "Script Report",
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"role": "Sales User"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Sales Manager"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,254 @@
|
|||||||
|
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
import json
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
import pandas
|
||||||
|
from frappe import _
|
||||||
|
from frappe.utils import flt
|
||||||
|
from six import iteritems
|
||||||
|
|
||||||
|
from erpnext.setup.utils import get_exchange_rate
|
||||||
|
|
||||||
|
|
||||||
|
def execute(filters=None):
|
||||||
|
return OpportunitySummaryBySalesStage(filters).run()
|
||||||
|
|
||||||
|
class OpportunitySummaryBySalesStage(object):
|
||||||
|
def __init__(self,filters=None):
|
||||||
|
self.filters = frappe._dict(filters or {})
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.get_columns()
|
||||||
|
self.get_data()
|
||||||
|
self.get_chart_data()
|
||||||
|
return self.columns, self.data, None, self.chart
|
||||||
|
|
||||||
|
def get_columns(self):
|
||||||
|
self.columns = []
|
||||||
|
|
||||||
|
if self.filters.get('based_on') == 'Opportunity Owner':
|
||||||
|
self.columns.append({
|
||||||
|
'label': _('Opportunity Owner'),
|
||||||
|
'fieldname': 'opportunity_owner',
|
||||||
|
'width': 200
|
||||||
|
})
|
||||||
|
|
||||||
|
if self.filters.get('based_on') == 'Source':
|
||||||
|
self.columns.append({
|
||||||
|
'label': _('Source'),
|
||||||
|
'fieldname': 'source',
|
||||||
|
'fieldtype': 'Link',
|
||||||
|
'options': 'Lead Source',
|
||||||
|
'width': 200
|
||||||
|
})
|
||||||
|
|
||||||
|
if self.filters.get('based_on') == 'Opportunity Type':
|
||||||
|
self.columns.append({
|
||||||
|
'label': _('Opportunity Type'),
|
||||||
|
'fieldname': 'opportunity_type',
|
||||||
|
'width': 200
|
||||||
|
})
|
||||||
|
|
||||||
|
self.set_sales_stage_columns()
|
||||||
|
|
||||||
|
def set_sales_stage_columns(self):
|
||||||
|
self.sales_stage_list = frappe.db.get_list('Sales Stage', pluck='name')
|
||||||
|
|
||||||
|
for sales_stage in self.sales_stage_list:
|
||||||
|
if self.filters.get('data_based_on') == 'Number':
|
||||||
|
self.columns.append({
|
||||||
|
'label': _(sales_stage),
|
||||||
|
'fieldname': sales_stage,
|
||||||
|
'fieldtype': 'Int',
|
||||||
|
'width': 150
|
||||||
|
})
|
||||||
|
|
||||||
|
elif self.filters.get('data_based_on') == 'Amount':
|
||||||
|
self.columns.append({
|
||||||
|
'label': _(sales_stage),
|
||||||
|
'fieldname': sales_stage,
|
||||||
|
'fieldtype': 'Currency',
|
||||||
|
'width': 150
|
||||||
|
})
|
||||||
|
|
||||||
|
def get_data(self):
|
||||||
|
self.data = []
|
||||||
|
|
||||||
|
based_on = {
|
||||||
|
'Opportunity Owner': '_assign',
|
||||||
|
'Source': 'source',
|
||||||
|
'Opportunity Type': 'opportunity_type'
|
||||||
|
}[self.filters.get('based_on')]
|
||||||
|
|
||||||
|
data_based_on = {
|
||||||
|
'Number': 'count(name) as count',
|
||||||
|
'Amount': 'opportunity_amount as amount',
|
||||||
|
}[self.filters.get('data_based_on')]
|
||||||
|
|
||||||
|
self.get_data_query(based_on, data_based_on)
|
||||||
|
|
||||||
|
self.get_rows()
|
||||||
|
|
||||||
|
def get_data_query(self, based_on, data_based_on):
|
||||||
|
if self.filters.get('data_based_on') == 'Number':
|
||||||
|
group_by = '{},{}'.format('sales_stage', based_on)
|
||||||
|
self.query_result = frappe.db.get_list('Opportunity',
|
||||||
|
filters=self.get_conditions(),
|
||||||
|
fields=['sales_stage', data_based_on, based_on],
|
||||||
|
group_by=group_by
|
||||||
|
)
|
||||||
|
|
||||||
|
elif self.filters.get('data_based_on') == 'Amount':
|
||||||
|
self.query_result = frappe.db.get_list('Opportunity',
|
||||||
|
filters=self.get_conditions(),
|
||||||
|
fields=['sales_stage', based_on, data_based_on, 'currency']
|
||||||
|
)
|
||||||
|
|
||||||
|
self.convert_to_base_currency()
|
||||||
|
|
||||||
|
dataframe = pandas.DataFrame.from_records(self.query_result)
|
||||||
|
dataframe.replace(to_replace=[None], value='Not Assigned', inplace=True)
|
||||||
|
result = dataframe.groupby(['sales_stage', based_on], as_index=False)['amount'].sum()
|
||||||
|
|
||||||
|
self.grouped_data = []
|
||||||
|
|
||||||
|
for i in range(len(result['amount'])):
|
||||||
|
self.grouped_data.append({
|
||||||
|
'sales_stage': result['sales_stage'][i],
|
||||||
|
based_on : result[based_on][i],
|
||||||
|
'amount': result['amount'][i]
|
||||||
|
})
|
||||||
|
|
||||||
|
self.query_result = self.grouped_data
|
||||||
|
|
||||||
|
def get_rows(self):
|
||||||
|
self.data = []
|
||||||
|
self.get_formatted_data()
|
||||||
|
|
||||||
|
for based_on,data in iteritems(self.formatted_data):
|
||||||
|
row_based_on={
|
||||||
|
'Opportunity Owner': 'opportunity_owner',
|
||||||
|
'Source': 'source',
|
||||||
|
'Opportunity Type': 'opportunity_type'
|
||||||
|
}[self.filters.get('based_on')]
|
||||||
|
|
||||||
|
row = {row_based_on: based_on}
|
||||||
|
|
||||||
|
for d in self.query_result:
|
||||||
|
sales_stage = d.get('sales_stage')
|
||||||
|
row[sales_stage] = data.get(sales_stage)
|
||||||
|
|
||||||
|
self.data.append(row)
|
||||||
|
|
||||||
|
def get_formatted_data(self):
|
||||||
|
self.formatted_data = frappe._dict()
|
||||||
|
|
||||||
|
for d in self.query_result:
|
||||||
|
data_based_on ={
|
||||||
|
'Number': 'count',
|
||||||
|
'Amount': 'amount'
|
||||||
|
}[self.filters.get('data_based_on')]
|
||||||
|
|
||||||
|
based_on ={
|
||||||
|
'Opportunity Owner': '_assign',
|
||||||
|
'Source': 'source',
|
||||||
|
'Opportunity Type': 'opportunity_type'
|
||||||
|
}[self.filters.get('based_on')]
|
||||||
|
|
||||||
|
if self.filters.get('based_on') == 'Opportunity Owner':
|
||||||
|
if d.get(based_on) == '[]' or d.get(based_on) is None or d.get(based_on) == 'Not Assigned':
|
||||||
|
assignments = ['Not Assigned']
|
||||||
|
else:
|
||||||
|
assignments = json.loads(d.get(based_on))
|
||||||
|
|
||||||
|
sales_stage = d.get('sales_stage')
|
||||||
|
count = d.get(data_based_on)
|
||||||
|
|
||||||
|
if assignments:
|
||||||
|
if len(assignments) > 1:
|
||||||
|
for assigned_to in assignments:
|
||||||
|
self.set_formatted_data_based_on_sales_stage(assigned_to, sales_stage, count)
|
||||||
|
else:
|
||||||
|
assigned_to = assignments[0]
|
||||||
|
self.set_formatted_data_based_on_sales_stage(assigned_to, sales_stage, count)
|
||||||
|
else:
|
||||||
|
value = d.get(based_on)
|
||||||
|
sales_stage = d.get('sales_stage')
|
||||||
|
count = d.get(data_based_on)
|
||||||
|
self.set_formatted_data_based_on_sales_stage(value, sales_stage, count)
|
||||||
|
|
||||||
|
def set_formatted_data_based_on_sales_stage(self, based_on, sales_stage, count):
|
||||||
|
self.formatted_data.setdefault(based_on, frappe._dict()).setdefault(sales_stage, 0)
|
||||||
|
self.formatted_data[based_on][sales_stage] += count
|
||||||
|
|
||||||
|
def get_conditions(self):
|
||||||
|
filters = []
|
||||||
|
|
||||||
|
if self.filters.get('company'):
|
||||||
|
filters.append({'company': self.filters.get('company')})
|
||||||
|
|
||||||
|
if self.filters.get('opportunity_type'):
|
||||||
|
filters.append({'opportunity_type': self.filters.get('opportunity_type')})
|
||||||
|
|
||||||
|
if self.filters.get('opportunity_source'):
|
||||||
|
filters.append({'source': self.filters.get('opportunity_source')})
|
||||||
|
|
||||||
|
if self.filters.get('status'):
|
||||||
|
filters.append({'status': ('in',self.filters.get('status'))})
|
||||||
|
|
||||||
|
if self.filters.get('from_date') and self.filters.get('to_date'):
|
||||||
|
filters.append(['transaction_date', 'between', [self.filters.get('from_date'), self.filters.get('to_date')]])
|
||||||
|
|
||||||
|
return filters
|
||||||
|
|
||||||
|
def get_chart_data(self):
|
||||||
|
labels = []
|
||||||
|
datasets = []
|
||||||
|
values = [0] * 8
|
||||||
|
|
||||||
|
for sales_stage in self.sales_stage_list:
|
||||||
|
labels.append(sales_stage)
|
||||||
|
|
||||||
|
options = {
|
||||||
|
'Number': 'count',
|
||||||
|
'Amount': 'amount'
|
||||||
|
}[self.filters.get('data_based_on')]
|
||||||
|
|
||||||
|
for data in self.query_result:
|
||||||
|
for count in range(len(values)):
|
||||||
|
if data['sales_stage'] == labels[count]:
|
||||||
|
values[count] = values[count] + data[options]
|
||||||
|
|
||||||
|
datasets.append({'name':options, 'values':values})
|
||||||
|
|
||||||
|
self.chart = {
|
||||||
|
'data':{
|
||||||
|
'labels': labels,
|
||||||
|
'datasets': datasets
|
||||||
|
},
|
||||||
|
'type':'line'
|
||||||
|
}
|
||||||
|
|
||||||
|
def currency_conversion(self,from_currency,to_currency):
|
||||||
|
cacheobj = frappe.cache()
|
||||||
|
|
||||||
|
if cacheobj.get(from_currency):
|
||||||
|
return flt(str(cacheobj.get(from_currency),'UTF-8'))
|
||||||
|
|
||||||
|
else:
|
||||||
|
value = get_exchange_rate(from_currency,to_currency)
|
||||||
|
cacheobj.set(from_currency,value)
|
||||||
|
return flt(str(cacheobj.get(from_currency),'UTF-8'))
|
||||||
|
|
||||||
|
def get_default_currency(self):
|
||||||
|
company = self.filters.get('company')
|
||||||
|
return frappe.db.get_value('Company', company, 'default_currency')
|
||||||
|
|
||||||
|
def convert_to_base_currency(self):
|
||||||
|
default_currency = self.get_default_currency()
|
||||||
|
for data in self.query_result:
|
||||||
|
if data.get('currency') != default_currency:
|
||||||
|
opportunity_currency = data.get('currency')
|
||||||
|
value = self.currency_conversion(opportunity_currency,default_currency)
|
||||||
|
data['amount'] = data['amount'] * value
|
@ -0,0 +1,94 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
from erpnext.crm.report.opportunity_summary_by_sales_stage.opportunity_summary_by_sales_stage import (
|
||||||
|
execute,
|
||||||
|
)
|
||||||
|
from erpnext.crm.report.sales_pipeline_analytics.test_sales_pipeline_analytics import (
|
||||||
|
create_company,
|
||||||
|
create_customer,
|
||||||
|
create_opportunity,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestOpportunitySummaryBySalesStage(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(self):
|
||||||
|
frappe.db.delete("Opportunity")
|
||||||
|
create_company()
|
||||||
|
create_customer()
|
||||||
|
create_opportunity()
|
||||||
|
|
||||||
|
def test_opportunity_summary_by_sales_stage(self):
|
||||||
|
self.check_for_opportunity_owner()
|
||||||
|
self.check_for_source()
|
||||||
|
self.check_for_opportunity_type()
|
||||||
|
self.check_all_filters()
|
||||||
|
|
||||||
|
def check_for_opportunity_owner(self):
|
||||||
|
filters = {
|
||||||
|
'based_on': "Opportunity Owner",
|
||||||
|
'data_based_on': "Number",
|
||||||
|
'company': "Best Test"
|
||||||
|
}
|
||||||
|
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected_data = [{
|
||||||
|
'opportunity_owner': "Not Assigned",
|
||||||
|
'Prospecting': 1
|
||||||
|
}]
|
||||||
|
|
||||||
|
self.assertEqual(expected_data, report[1])
|
||||||
|
|
||||||
|
def check_for_source(self):
|
||||||
|
filters = {
|
||||||
|
'based_on': "Source",
|
||||||
|
'data_based_on': "Number",
|
||||||
|
'company': "Best Test"
|
||||||
|
}
|
||||||
|
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected_data = [{
|
||||||
|
'source': 'Cold Calling',
|
||||||
|
'Prospecting': 1
|
||||||
|
}]
|
||||||
|
|
||||||
|
self.assertEqual(expected_data, report[1])
|
||||||
|
|
||||||
|
def check_for_opportunity_type(self):
|
||||||
|
filters = {
|
||||||
|
'based_on': "Opportunity Type",
|
||||||
|
'data_based_on': "Number",
|
||||||
|
'company': "Best Test"
|
||||||
|
}
|
||||||
|
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected_data = [{
|
||||||
|
'opportunity_type': 'Sales',
|
||||||
|
'Prospecting': 1
|
||||||
|
}]
|
||||||
|
|
||||||
|
self.assertEqual(expected_data, report[1])
|
||||||
|
|
||||||
|
def check_all_filters(self):
|
||||||
|
filters = {
|
||||||
|
'based_on': "Opportunity Type",
|
||||||
|
'data_based_on': "Number",
|
||||||
|
'company': "Best Test",
|
||||||
|
'opportunity_source': "Cold Calling",
|
||||||
|
'opportunity_type': "Sales",
|
||||||
|
'status': ["Open"]
|
||||||
|
}
|
||||||
|
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected_data = [{
|
||||||
|
'opportunity_type': 'Sales',
|
||||||
|
'Prospecting': 1
|
||||||
|
}]
|
||||||
|
|
||||||
|
self.assertEqual(expected_data, report[1])
|
@ -0,0 +1,70 @@
|
|||||||
|
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
frappe.query_reports["Sales Pipeline Analytics"] = {
|
||||||
|
"filters": [
|
||||||
|
{
|
||||||
|
fieldname: "pipeline_by",
|
||||||
|
label: __("Pipeline By"),
|
||||||
|
fieldtype: "Select",
|
||||||
|
options: "Owner\nSales Stage",
|
||||||
|
default: "Owner"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "from_date",
|
||||||
|
label: __("From Date"),
|
||||||
|
fieldtype: "Date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "to_date",
|
||||||
|
label: __("To Date"),
|
||||||
|
fieldtype: "Date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "range",
|
||||||
|
label: __("Range"),
|
||||||
|
fieldtype: "Select",
|
||||||
|
options: "Monthly\nQuarterly",
|
||||||
|
default: "Monthly"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "assigned_to",
|
||||||
|
label: __("Assigned To"),
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "User"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "status",
|
||||||
|
label: __("Status"),
|
||||||
|
fieldtype: "Select",
|
||||||
|
options: "Open\nQuotation\nConverted\nReplied"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "based_on",
|
||||||
|
label: __("Based On"),
|
||||||
|
fieldtype: "Select",
|
||||||
|
options: "Number\nAmount",
|
||||||
|
default: "Number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "company",
|
||||||
|
label: __("Company"),
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Company",
|
||||||
|
default: frappe.defaults.get_user_default("Company")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "opportunity_source",
|
||||||
|
label: __("Opportunity Source"),
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Lead Source"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "opportunity_type",
|
||||||
|
label: __("Opportunity Type"),
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Opportunity Type"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
};
|
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"add_total_row": 0,
|
||||||
|
"columns": [],
|
||||||
|
"creation": "2021-07-01 17:29:09.530787",
|
||||||
|
"disable_prepared_report": 0,
|
||||||
|
"disabled": 0,
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Report",
|
||||||
|
"filters": [],
|
||||||
|
"idx": 0,
|
||||||
|
"is_standard": "Yes",
|
||||||
|
"modified": "2021-07-01 17:45:17.612861",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "CRM",
|
||||||
|
"name": "Sales Pipeline Analytics",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"prepared_report": 0,
|
||||||
|
"ref_doctype": "Opportunity",
|
||||||
|
"report_name": "Sales Pipeline Analytics",
|
||||||
|
"report_type": "Script Report",
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"role": "Sales User"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Sales Manager"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,333 @@
|
|||||||
|
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import json
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
import pandas
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
from frappe import _
|
||||||
|
from frappe.utils import cint, flt
|
||||||
|
from six import iteritems
|
||||||
|
|
||||||
|
from erpnext.setup.utils import get_exchange_rate
|
||||||
|
|
||||||
|
|
||||||
|
def execute(filters=None):
|
||||||
|
return SalesPipelineAnalytics(filters).run()
|
||||||
|
|
||||||
|
class SalesPipelineAnalytics(object):
|
||||||
|
def __init__(self, filters=None):
|
||||||
|
self.filters = frappe._dict(filters or {})
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.get_columns()
|
||||||
|
self.get_data()
|
||||||
|
self.get_chart_data()
|
||||||
|
|
||||||
|
return self.columns, self.data, None, self.chart
|
||||||
|
|
||||||
|
def get_columns(self):
|
||||||
|
self.columns = []
|
||||||
|
|
||||||
|
self.set_range_columns()
|
||||||
|
self.set_pipeline_based_on_column()
|
||||||
|
|
||||||
|
def set_range_columns(self):
|
||||||
|
based_on = {
|
||||||
|
'Number': 'Int',
|
||||||
|
'Amount': 'Currency'
|
||||||
|
}[self.filters.get('based_on')]
|
||||||
|
|
||||||
|
if self.filters.get('range') == 'Monthly':
|
||||||
|
month_list = self.get_month_list()
|
||||||
|
|
||||||
|
for month in month_list:
|
||||||
|
self.columns.append({
|
||||||
|
'fieldname': month,
|
||||||
|
'fieldtype': based_on,
|
||||||
|
'label': month,
|
||||||
|
'width': 200
|
||||||
|
})
|
||||||
|
|
||||||
|
elif self.filters.get('range') == 'Quarterly':
|
||||||
|
for quarter in range(1, 5):
|
||||||
|
self.columns.append({
|
||||||
|
'fieldname': f'Q{quarter}',
|
||||||
|
'fieldtype': based_on,
|
||||||
|
'label': f'Q{quarter}',
|
||||||
|
'width': 200
|
||||||
|
})
|
||||||
|
|
||||||
|
def set_pipeline_based_on_column(self):
|
||||||
|
if self.filters.get('pipeline_by') == 'Owner':
|
||||||
|
self.columns.insert(0, {
|
||||||
|
'fieldname': 'opportunity_owner',
|
||||||
|
'label': _('Opportunity Owner'),
|
||||||
|
'width': 200
|
||||||
|
})
|
||||||
|
|
||||||
|
elif self.filters.get('pipeline_by') == 'Sales Stage':
|
||||||
|
self.columns.insert(0, {
|
||||||
|
'fieldname': 'sales_stage',
|
||||||
|
'label': _('Sales Stage'),
|
||||||
|
'width': 200
|
||||||
|
})
|
||||||
|
|
||||||
|
def get_fields(self):
|
||||||
|
self.based_on ={
|
||||||
|
'Owner': '_assign as opportunity_owner',
|
||||||
|
'Sales Stage': 'sales_stage'
|
||||||
|
}[self.filters.get('pipeline_by')]
|
||||||
|
|
||||||
|
self.data_based_on ={
|
||||||
|
'Number': 'count(name) as count',
|
||||||
|
'Amount': 'opportunity_amount as amount'
|
||||||
|
}[self.filters.get('based_on')]
|
||||||
|
|
||||||
|
self.group_by_based_on = {
|
||||||
|
'Owner': '_assign',
|
||||||
|
'Sales Stage': 'sales_stage'
|
||||||
|
}[self.filters.get('pipeline_by')]
|
||||||
|
|
||||||
|
self.group_by_period = {
|
||||||
|
'Monthly': 'month(expected_closing)',
|
||||||
|
'Quarterly': 'QUARTER(expected_closing)'
|
||||||
|
}[self.filters.get('range')]
|
||||||
|
|
||||||
|
self.pipeline_by = {
|
||||||
|
'Owner': 'opportunity_owner',
|
||||||
|
'Sales Stage': 'sales_stage'
|
||||||
|
}[self.filters.get('pipeline_by')]
|
||||||
|
|
||||||
|
self.duration = {
|
||||||
|
'Monthly': 'monthname(expected_closing) as month',
|
||||||
|
'Quarterly': 'QUARTER(expected_closing) as quarter'
|
||||||
|
}[self.filters.get('range')]
|
||||||
|
|
||||||
|
self.period_by = {
|
||||||
|
'Monthly': 'month',
|
||||||
|
'Quarterly': 'quarter'
|
||||||
|
}[self.filters.get('range')]
|
||||||
|
|
||||||
|
def get_data(self):
|
||||||
|
self.get_fields()
|
||||||
|
|
||||||
|
if self.filters.get('based_on') == 'Number':
|
||||||
|
self.query_result = frappe.db.get_list('Opportunity',
|
||||||
|
filters=self.get_conditions(),
|
||||||
|
fields=[self.based_on, self.data_based_on, self.duration],
|
||||||
|
group_by='{},{}'.format(self.group_by_based_on, self.group_by_period),
|
||||||
|
order_by=self.group_by_period
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.filters.get('based_on') == 'Amount':
|
||||||
|
self.query_result = frappe.db.get_list('Opportunity',
|
||||||
|
filters=self.get_conditions(),
|
||||||
|
fields=[self.based_on, self.data_based_on, self.duration, 'currency']
|
||||||
|
)
|
||||||
|
|
||||||
|
self.convert_to_base_currency()
|
||||||
|
|
||||||
|
dataframe = pandas.DataFrame.from_records(self.query_result)
|
||||||
|
dataframe.replace(to_replace=[None], value='Not Assigned', inplace=True)
|
||||||
|
result = dataframe.groupby([self.pipeline_by, self.period_by], as_index=False)['amount'].sum()
|
||||||
|
|
||||||
|
self.grouped_data = []
|
||||||
|
|
||||||
|
for i in range(len(result['amount'])):
|
||||||
|
self.grouped_data.append({
|
||||||
|
self.pipeline_by : result[self.pipeline_by][i],
|
||||||
|
self.period_by : result[self.period_by][i],
|
||||||
|
'amount': result['amount'][i]
|
||||||
|
})
|
||||||
|
|
||||||
|
self.query_result = self.grouped_data
|
||||||
|
|
||||||
|
self.get_periodic_data()
|
||||||
|
self.append_data(self.pipeline_by, self.period_by)
|
||||||
|
|
||||||
|
def get_conditions(self):
|
||||||
|
conditions = []
|
||||||
|
|
||||||
|
if self.filters.get('opportunity_source'):
|
||||||
|
conditions.append({'source': self.filters.get('opportunity_source')})
|
||||||
|
|
||||||
|
if self.filters.get('opportunity_type'):
|
||||||
|
conditions.append({'opportunity_type': self.filters.get('opportunity_type')})
|
||||||
|
|
||||||
|
if self.filters.get('status'):
|
||||||
|
conditions.append({'status': self.filters.get('status')})
|
||||||
|
|
||||||
|
if self.filters.get('company'):
|
||||||
|
conditions.append({'company': self.filters.get('company')})
|
||||||
|
|
||||||
|
if self.filters.get('from_date') and self.filters.get('to_date'):
|
||||||
|
conditions.append(['expected_closing', 'between',
|
||||||
|
[self.filters.get('from_date'), self.filters.get('to_date')]])
|
||||||
|
|
||||||
|
return conditions
|
||||||
|
|
||||||
|
def get_chart_data(self):
|
||||||
|
labels = []
|
||||||
|
datasets = []
|
||||||
|
|
||||||
|
self.append_to_dataset(datasets)
|
||||||
|
|
||||||
|
for column in self.columns:
|
||||||
|
if column['fieldname'] != 'opportunity_owner' and column['fieldname'] != 'sales_stage':
|
||||||
|
labels.append(column['fieldname'])
|
||||||
|
|
||||||
|
self.chart = {
|
||||||
|
'data':{
|
||||||
|
'labels': labels,
|
||||||
|
'datasets': datasets
|
||||||
|
},
|
||||||
|
'type':'line'
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.chart
|
||||||
|
|
||||||
|
def get_periodic_data(self):
|
||||||
|
self.periodic_data = frappe._dict()
|
||||||
|
|
||||||
|
based_on = {
|
||||||
|
'Number': 'count',
|
||||||
|
'Amount': 'amount'
|
||||||
|
}[self.filters.get('based_on')]
|
||||||
|
|
||||||
|
pipeline_by = {
|
||||||
|
'Owner': 'opportunity_owner',
|
||||||
|
'Sales Stage': 'sales_stage'
|
||||||
|
}[self.filters.get('pipeline_by')]
|
||||||
|
|
||||||
|
frequency = {
|
||||||
|
'Monthly': 'month',
|
||||||
|
'Quarterly': 'quarter'
|
||||||
|
}[self.filters.get('range')]
|
||||||
|
|
||||||
|
for info in self.query_result:
|
||||||
|
if self.filters.get('range') == 'Monthly':
|
||||||
|
period = info.get(frequency)
|
||||||
|
if self.filters.get('range') == 'Quarterly':
|
||||||
|
period = f'Q{cint(info.get("quarter"))}'
|
||||||
|
|
||||||
|
value = info.get(pipeline_by)
|
||||||
|
count_or_amount = info.get(based_on)
|
||||||
|
|
||||||
|
if self.filters.get('pipeline_by') == 'Owner':
|
||||||
|
if value == 'Not Assigned' or value == '[]' or value is None:
|
||||||
|
assigned_to = ['Not Assigned']
|
||||||
|
else:
|
||||||
|
assigned_to = json.loads(value)
|
||||||
|
self.check_for_assigned_to(period, value, count_or_amount, assigned_to, info)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.set_formatted_data(period, value, count_or_amount, None)
|
||||||
|
|
||||||
|
def set_formatted_data(self, period, value, count_or_amount, assigned_to):
|
||||||
|
if assigned_to:
|
||||||
|
if len(assigned_to) > 1:
|
||||||
|
if self.filters.get('assigned_to'):
|
||||||
|
for user in assigned_to:
|
||||||
|
if self.filters.get('assigned_to') == user:
|
||||||
|
value = user
|
||||||
|
self.periodic_data.setdefault(value, frappe._dict()).setdefault(period, 0)
|
||||||
|
self.periodic_data[value][period] += count_or_amount
|
||||||
|
else:
|
||||||
|
for user in assigned_to:
|
||||||
|
value = user
|
||||||
|
self.periodic_data.setdefault(value, frappe._dict()).setdefault(period, 0)
|
||||||
|
self.periodic_data[value][period] += count_or_amount
|
||||||
|
else:
|
||||||
|
value = assigned_to[0]
|
||||||
|
self.periodic_data.setdefault(value, frappe._dict()).setdefault(period, 0)
|
||||||
|
self.periodic_data[value][period] += count_or_amount
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.periodic_data.setdefault(value, frappe._dict()).setdefault(period, 0)
|
||||||
|
self.periodic_data[value][period] += count_or_amount
|
||||||
|
|
||||||
|
def check_for_assigned_to(self, period, value, count_or_amount, assigned_to, info):
|
||||||
|
if self.filters.get('assigned_to'):
|
||||||
|
for data in json.loads(info.get('opportunity_owner')):
|
||||||
|
if data == self.filters.get('assigned_to'):
|
||||||
|
self.set_formatted_data(period, data, count_or_amount, assigned_to)
|
||||||
|
else:
|
||||||
|
self.set_formatted_data(period, value, count_or_amount, assigned_to)
|
||||||
|
|
||||||
|
def get_month_list(self):
|
||||||
|
month_list= []
|
||||||
|
current_date = date.today()
|
||||||
|
month_number = date.today().month
|
||||||
|
|
||||||
|
for month in range(month_number,13):
|
||||||
|
month_list.append(current_date.strftime('%B'))
|
||||||
|
current_date = current_date + relativedelta(months=1)
|
||||||
|
|
||||||
|
return month_list
|
||||||
|
|
||||||
|
def append_to_dataset(self, datasets):
|
||||||
|
range_by = {
|
||||||
|
'Monthly': 'month',
|
||||||
|
'Quarterly': 'quarter'
|
||||||
|
}[self.filters.get('range')]
|
||||||
|
|
||||||
|
based_on = {
|
||||||
|
'Amount': 'amount',
|
||||||
|
'Number': 'count'
|
||||||
|
}[self.filters.get('based_on')]
|
||||||
|
|
||||||
|
if self.filters.get('range') == 'Quarterly':
|
||||||
|
frequency_list = [1,2,3,4]
|
||||||
|
count = [0] * 4
|
||||||
|
|
||||||
|
if self.filters.get('range') == 'Monthly':
|
||||||
|
frequency_list = self.get_month_list()
|
||||||
|
count = [0] * 12
|
||||||
|
|
||||||
|
for info in self.query_result:
|
||||||
|
for i in range(len(frequency_list)):
|
||||||
|
if info[range_by] == frequency_list[i]:
|
||||||
|
count[i] = count[i] + info[based_on]
|
||||||
|
datasets.append({'name': based_on, 'values': count})
|
||||||
|
|
||||||
|
def append_data(self, pipeline_by, period_by):
|
||||||
|
self.data = []
|
||||||
|
for pipeline,period_data in iteritems(self.periodic_data):
|
||||||
|
row = {pipeline_by : pipeline}
|
||||||
|
for info in self.query_result:
|
||||||
|
if self.filters.get('range') == 'Monthly':
|
||||||
|
period = info.get(period_by)
|
||||||
|
|
||||||
|
if self.filters.get('range') == 'Quarterly':
|
||||||
|
period = f'Q{cint(info.get(period_by))}'
|
||||||
|
|
||||||
|
count = period_data.get(period,0.0)
|
||||||
|
row[period] = count
|
||||||
|
|
||||||
|
self.data.append(row)
|
||||||
|
|
||||||
|
def get_default_currency(self):
|
||||||
|
company = self.filters.get('company')
|
||||||
|
return frappe.db.get_value('Company',company,['default_currency'])
|
||||||
|
|
||||||
|
def get_currency_rate(self, from_currency, to_currency):
|
||||||
|
cacheobj = frappe.cache()
|
||||||
|
|
||||||
|
if cacheobj.get(from_currency):
|
||||||
|
return flt(str(cacheobj.get(from_currency),'UTF-8'))
|
||||||
|
|
||||||
|
else:
|
||||||
|
value = get_exchange_rate(from_currency, to_currency)
|
||||||
|
cacheobj.set(from_currency, value)
|
||||||
|
return flt(str(cacheobj.get(from_currency),'UTF-8'))
|
||||||
|
|
||||||
|
def convert_to_base_currency(self):
|
||||||
|
default_currency = self.get_default_currency()
|
||||||
|
for data in self.query_result:
|
||||||
|
if data.get('currency') != default_currency:
|
||||||
|
opportunity_currency = data.get('currency')
|
||||||
|
value = self.get_currency_rate(opportunity_currency,default_currency)
|
||||||
|
data['amount'] = data['amount'] * value
|
@ -0,0 +1,238 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
from erpnext.crm.report.sales_pipeline_analytics.sales_pipeline_analytics import execute
|
||||||
|
|
||||||
|
|
||||||
|
class TestSalesPipelineAnalytics(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(self):
|
||||||
|
frappe.db.delete("Opportunity")
|
||||||
|
create_company()
|
||||||
|
create_customer()
|
||||||
|
create_opportunity()
|
||||||
|
|
||||||
|
def test_sales_pipeline_analytics(self):
|
||||||
|
self.check_for_monthly_and_number()
|
||||||
|
self.check_for_monthly_and_amount()
|
||||||
|
self.check_for_quarterly_and_number()
|
||||||
|
self.check_for_quarterly_and_amount()
|
||||||
|
self.check_for_all_filters()
|
||||||
|
|
||||||
|
def check_for_monthly_and_number(self):
|
||||||
|
filters = {
|
||||||
|
'pipeline_by':"Owner",
|
||||||
|
'range':"Monthly",
|
||||||
|
'based_on':"Number",
|
||||||
|
'status':"Open",
|
||||||
|
'opportunity_type':"Sales",
|
||||||
|
'company':"Best Test"
|
||||||
|
}
|
||||||
|
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected_data = [
|
||||||
|
{
|
||||||
|
'opportunity_owner':'Not Assigned',
|
||||||
|
'August':1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertEqual(expected_data,report[1])
|
||||||
|
|
||||||
|
filters = {
|
||||||
|
'pipeline_by':"Sales Stage",
|
||||||
|
'range':"Monthly",
|
||||||
|
'based_on':"Number",
|
||||||
|
'status':"Open",
|
||||||
|
'opportunity_type':"Sales",
|
||||||
|
'company':"Best Test"
|
||||||
|
}
|
||||||
|
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected_data = [
|
||||||
|
{
|
||||||
|
'sales_stage':'Prospecting',
|
||||||
|
'August':1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertEqual(expected_data,report[1])
|
||||||
|
|
||||||
|
def check_for_monthly_and_amount(self):
|
||||||
|
filters = {
|
||||||
|
'pipeline_by':"Owner",
|
||||||
|
'range':"Monthly",
|
||||||
|
'based_on':"Amount",
|
||||||
|
'status':"Open",
|
||||||
|
'opportunity_type':"Sales",
|
||||||
|
'company':"Best Test"
|
||||||
|
}
|
||||||
|
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected_data = [
|
||||||
|
{
|
||||||
|
'opportunity_owner':'Not Assigned',
|
||||||
|
'August':150000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertEqual(expected_data,report[1])
|
||||||
|
|
||||||
|
filters = {
|
||||||
|
'pipeline_by':"Sales Stage",
|
||||||
|
'range':"Monthly",
|
||||||
|
'based_on':"Amount",
|
||||||
|
'status':"Open",
|
||||||
|
'opportunity_type':"Sales",
|
||||||
|
'company':"Best Test"
|
||||||
|
}
|
||||||
|
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected_data = [
|
||||||
|
{
|
||||||
|
'sales_stage':'Prospecting',
|
||||||
|
'August':150000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertEqual(expected_data,report[1])
|
||||||
|
|
||||||
|
def check_for_quarterly_and_number(self):
|
||||||
|
filters = {
|
||||||
|
'pipeline_by':"Owner",
|
||||||
|
'range':"Quarterly",
|
||||||
|
'based_on':"Number",
|
||||||
|
'status':"Open",
|
||||||
|
'opportunity_type':"Sales",
|
||||||
|
'company':"Best Test"
|
||||||
|
}
|
||||||
|
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected_data = [
|
||||||
|
{
|
||||||
|
'opportunity_owner':'Not Assigned',
|
||||||
|
'Q3':1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertEqual(expected_data,report[1])
|
||||||
|
|
||||||
|
filters = {
|
||||||
|
'pipeline_by':"Sales Stage",
|
||||||
|
'range':"Quarterly",
|
||||||
|
'based_on':"Number",
|
||||||
|
'status':"Open",
|
||||||
|
'opportunity_type':"Sales",
|
||||||
|
'company':"Best Test"
|
||||||
|
}
|
||||||
|
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected_data = [
|
||||||
|
{
|
||||||
|
'sales_stage':'Prospecting',
|
||||||
|
'Q3':1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertEqual(expected_data,report[1])
|
||||||
|
|
||||||
|
def check_for_quarterly_and_amount(self):
|
||||||
|
filters = {
|
||||||
|
'pipeline_by':"Owner",
|
||||||
|
'range':"Quarterly",
|
||||||
|
'based_on':"Amount",
|
||||||
|
'status':"Open",
|
||||||
|
'opportunity_type':"Sales",
|
||||||
|
'company':"Best Test"
|
||||||
|
}
|
||||||
|
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected_data = [
|
||||||
|
{
|
||||||
|
'opportunity_owner':'Not Assigned',
|
||||||
|
'Q3':150000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertEqual(expected_data,report[1])
|
||||||
|
|
||||||
|
filters = {
|
||||||
|
'pipeline_by':"Sales Stage",
|
||||||
|
'range':"Quarterly",
|
||||||
|
'based_on':"Amount",
|
||||||
|
'status':"Open",
|
||||||
|
'opportunity_type':"Sales",
|
||||||
|
'company':"Best Test"
|
||||||
|
}
|
||||||
|
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected_data = [
|
||||||
|
{
|
||||||
|
'sales_stage':'Prospecting',
|
||||||
|
'Q3':150000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertEqual(expected_data,report[1])
|
||||||
|
|
||||||
|
def check_for_all_filters(self):
|
||||||
|
filters = {
|
||||||
|
'pipeline_by':"Owner",
|
||||||
|
'range':"Monthly",
|
||||||
|
'based_on':"Number",
|
||||||
|
'status':"Open",
|
||||||
|
'opportunity_type':"Sales",
|
||||||
|
'company':"Best Test",
|
||||||
|
'opportunity_source':'Cold Calling',
|
||||||
|
'from_date': '2021-08-01',
|
||||||
|
'to_date':'2021-08-31'
|
||||||
|
}
|
||||||
|
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected_data = [
|
||||||
|
{
|
||||||
|
'opportunity_owner':'Not Assigned',
|
||||||
|
'August': 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertEqual(expected_data,report[1])
|
||||||
|
|
||||||
|
def create_company():
|
||||||
|
doc = frappe.db.exists('Company','Best Test')
|
||||||
|
if not doc:
|
||||||
|
doc = frappe.new_doc('Company')
|
||||||
|
doc.company_name = 'Best Test'
|
||||||
|
doc.default_currency = "INR"
|
||||||
|
doc.insert()
|
||||||
|
|
||||||
|
def create_customer():
|
||||||
|
doc = frappe.db.exists("Customer","_Test NC")
|
||||||
|
if not doc:
|
||||||
|
doc = frappe.new_doc("Customer")
|
||||||
|
doc.customer_name = '_Test NC'
|
||||||
|
doc.insert()
|
||||||
|
|
||||||
|
def create_opportunity():
|
||||||
|
doc = frappe.db.exists({"doctype":"Opportunity","party_name":"_Test NC"})
|
||||||
|
if not doc:
|
||||||
|
doc = frappe.new_doc("Opportunity")
|
||||||
|
doc.opportunity_from = "Customer"
|
||||||
|
customer_name = frappe.db.get_value("Customer",{"customer_name":'_Test NC'},['customer_name'])
|
||||||
|
doc.party_name = customer_name
|
||||||
|
doc.opportunity_amount = 150000
|
||||||
|
doc.source = "Cold Calling"
|
||||||
|
doc.currency = "INR"
|
||||||
|
doc.expected_closing = "2021-08-31"
|
||||||
|
doc.company = 'Best Test'
|
||||||
|
doc.insert()
|
@ -147,6 +147,24 @@
|
|||||||
"onboard": 1,
|
"onboard": 1,
|
||||||
"type": "Link"
|
"type": "Link"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 1,
|
||||||
|
"label": "Sales Pipeline Analytics",
|
||||||
|
"link_to": "Sales Pipeline Analytics",
|
||||||
|
"link_type": "Report",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 1,
|
||||||
|
"label": "Opportunity Summary by Sales Stage",
|
||||||
|
"link_to": "Opportunity Summary by Sales Stage",
|
||||||
|
"link_type": "Report",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"dependencies": "",
|
"dependencies": "",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
@ -403,7 +421,7 @@
|
|||||||
"type": "Link"
|
"type": "Link"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2021-08-05 12:15:56.913091",
|
"modified": "2021-08-19 19:08:08.728876",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "CRM",
|
"module": "CRM",
|
||||||
"name": "CRM",
|
"name": "CRM",
|
||||||
|
@ -13,7 +13,7 @@ from erpnext.hr.utils import get_holidays_for_employee
|
|||||||
# HOLIDAY REMINDERS
|
# HOLIDAY REMINDERS
|
||||||
# -----------------
|
# -----------------
|
||||||
def send_reminders_in_advance_weekly():
|
def send_reminders_in_advance_weekly():
|
||||||
to_send_in_advance = int(frappe.db.get_single_value("HR Settings", "send_holiday_reminders") or 1)
|
to_send_in_advance = int(frappe.db.get_single_value("HR Settings", "send_holiday_reminders"))
|
||||||
frequency = frappe.db.get_single_value("HR Settings", "frequency")
|
frequency = frappe.db.get_single_value("HR Settings", "frequency")
|
||||||
if not (to_send_in_advance and frequency == "Weekly"):
|
if not (to_send_in_advance and frequency == "Weekly"):
|
||||||
return
|
return
|
||||||
@ -21,7 +21,7 @@ def send_reminders_in_advance_weekly():
|
|||||||
send_advance_holiday_reminders("Weekly")
|
send_advance_holiday_reminders("Weekly")
|
||||||
|
|
||||||
def send_reminders_in_advance_monthly():
|
def send_reminders_in_advance_monthly():
|
||||||
to_send_in_advance = int(frappe.db.get_single_value("HR Settings", "send_holiday_reminders") or 1)
|
to_send_in_advance = int(frappe.db.get_single_value("HR Settings", "send_holiday_reminders"))
|
||||||
frequency = frappe.db.get_single_value("HR Settings", "frequency")
|
frequency = frappe.db.get_single_value("HR Settings", "frequency")
|
||||||
if not (to_send_in_advance and frequency == "Monthly"):
|
if not (to_send_in_advance and frequency == "Monthly"):
|
||||||
return
|
return
|
||||||
@ -79,7 +79,7 @@ def send_holidays_reminder_in_advance(employee, holidays):
|
|||||||
# ------------------
|
# ------------------
|
||||||
def send_birthday_reminders():
|
def send_birthday_reminders():
|
||||||
"""Send Employee birthday reminders if no 'Stop Birthday Reminders' is not set."""
|
"""Send Employee birthday reminders if no 'Stop Birthday Reminders' is not set."""
|
||||||
to_send = int(frappe.db.get_single_value("HR Settings", "send_birthday_reminders") or 1)
|
to_send = int(frappe.db.get_single_value("HR Settings", "send_birthday_reminders"))
|
||||||
if not to_send:
|
if not to_send:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -305,3 +305,4 @@ erpnext.patches.v13_0.validate_options_for_data_field
|
|||||||
erpnext.patches.v13_0.create_gst_payment_entry_fields
|
erpnext.patches.v13_0.create_gst_payment_entry_fields
|
||||||
erpnext.patches.v14_0.delete_shopify_doctypes
|
erpnext.patches.v14_0.delete_shopify_doctypes
|
||||||
erpnext.patches.v13_0.update_dates_in_tax_withholding_category
|
erpnext.patches.v13_0.update_dates_in_tax_withholding_category
|
||||||
|
erpnext.patches.v14_0.update_opportunity_currency_fields
|
||||||
|
@ -12,6 +12,7 @@ def execute():
|
|||||||
return
|
return
|
||||||
|
|
||||||
frappe.reload_doc('regional', 'doctype', 'south_africa_vat_settings')
|
frappe.reload_doc('regional', 'doctype', 'south_africa_vat_settings')
|
||||||
|
frappe.reload_doc('regional', 'report', 'vat_audit_report')
|
||||||
frappe.reload_doc('accounts', 'doctype', 'south_africa_vat_account')
|
frappe.reload_doc('accounts', 'doctype', 'south_africa_vat_account')
|
||||||
|
|
||||||
make_custom_fields()
|
make_custom_fields()
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||||
|
|
||||||
|
|
||||||
def execute():
|
def execute():
|
||||||
frappe.reload_doc('accounts', 'doctype', 'advance_taxes_and_charges')
|
frappe.reload_doc('accounts', 'doctype', 'advance_taxes_and_charges')
|
||||||
frappe.reload_doc('accounts', 'doctype', 'payment_entry')
|
frappe.reload_doc('accounts', 'doctype', 'payment_entry')
|
||||||
|
36
erpnext/patches/v14_0/update_opportunity_currency_fields.py
Normal file
36
erpnext/patches/v14_0/update_opportunity_currency_fields.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.utils import flt
|
||||||
|
|
||||||
|
import erpnext
|
||||||
|
from erpnext.setup.utils import get_exchange_rate
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.reload_doc('crm', 'doctype', 'opportunity')
|
||||||
|
frappe.reload_doc('crm', 'doctype', 'opportunity_item')
|
||||||
|
|
||||||
|
opportunities = frappe.db.get_list('Opportunity', filters={
|
||||||
|
'opportunity_amount': ['>', 0]
|
||||||
|
}, fields=['name', 'company', 'currency', 'opportunity_amount'])
|
||||||
|
|
||||||
|
for opportunity in opportunities:
|
||||||
|
company_currency = erpnext.get_company_currency(opportunity.company)
|
||||||
|
|
||||||
|
# base total and total will be 0 only since item table did not have amount field earlier
|
||||||
|
if opportunity.currency != company_currency:
|
||||||
|
conversion_rate = get_exchange_rate(opportunity.currency, company_currency)
|
||||||
|
base_opportunity_amount = flt(conversion_rate) * flt(opportunity.opportunity_amount)
|
||||||
|
grand_total = flt(opportunity.opportunity_amount)
|
||||||
|
base_grand_total = flt(conversion_rate) * flt(opportunity.opportunity_amount)
|
||||||
|
else:
|
||||||
|
conversion_rate = 1
|
||||||
|
base_opportunity_amount = grand_total = base_grand_total = flt(opportunity.opportunity_amount)
|
||||||
|
|
||||||
|
frappe.db.set_value('Opportunity', opportunity.name, {
|
||||||
|
'conversion_rate': conversion_rate,
|
||||||
|
'base_opportunity_amount': base_opportunity_amount,
|
||||||
|
'grand_total': grand_total,
|
||||||
|
'base_grand_total': base_grand_total
|
||||||
|
}, update_modified=False)
|
@ -487,6 +487,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
var me = this;
|
var me = this;
|
||||||
var item = frappe.get_doc(cdt, cdn);
|
var item = frappe.get_doc(cdt, cdn);
|
||||||
var update_stock = 0, show_batch_dialog = 0;
|
var update_stock = 0, show_batch_dialog = 0;
|
||||||
|
|
||||||
|
item.weight_per_unit = 0;
|
||||||
|
item.weight_uom = '';
|
||||||
|
|
||||||
if(['Sales Invoice'].includes(this.frm.doc.doctype)) {
|
if(['Sales Invoice'].includes(this.frm.doc.doctype)) {
|
||||||
update_stock = cint(me.frm.doc.update_stock);
|
update_stock = cint(me.frm.doc.update_stock);
|
||||||
show_batch_dialog = update_stock;
|
show_batch_dialog = update_stock;
|
||||||
|
@ -96,35 +96,36 @@ class Gstr1Report(object):
|
|||||||
def get_b2c_data(self):
|
def get_b2c_data(self):
|
||||||
b2cs_output = {}
|
b2cs_output = {}
|
||||||
|
|
||||||
for inv, items_based_on_rate in self.items_based_on_tax_rate.items():
|
if self.invoices:
|
||||||
invoice_details = self.invoices.get(inv)
|
for inv, items_based_on_rate in self.items_based_on_tax_rate.items():
|
||||||
for rate, items in items_based_on_rate.items():
|
invoice_details = self.invoices.get(inv)
|
||||||
place_of_supply = invoice_details.get("place_of_supply")
|
for rate, items in items_based_on_rate.items():
|
||||||
ecommerce_gstin = invoice_details.get("ecommerce_gstin")
|
place_of_supply = invoice_details.get("place_of_supply")
|
||||||
|
ecommerce_gstin = invoice_details.get("ecommerce_gstin")
|
||||||
|
|
||||||
b2cs_output.setdefault((rate, place_of_supply, ecommerce_gstin),{
|
b2cs_output.setdefault((rate, place_of_supply, ecommerce_gstin), {
|
||||||
"place_of_supply": "",
|
"place_of_supply": "",
|
||||||
"ecommerce_gstin": "",
|
"ecommerce_gstin": "",
|
||||||
"rate": "",
|
"rate": "",
|
||||||
"taxable_value": 0,
|
"taxable_value": 0,
|
||||||
"cess_amount": 0,
|
"cess_amount": 0,
|
||||||
"type": "",
|
"type": "",
|
||||||
"invoice_number": invoice_details.get("invoice_number"),
|
"invoice_number": invoice_details.get("invoice_number"),
|
||||||
"posting_date": invoice_details.get("posting_date"),
|
"posting_date": invoice_details.get("posting_date"),
|
||||||
"invoice_value": invoice_details.get("base_grand_total"),
|
"invoice_value": invoice_details.get("base_grand_total"),
|
||||||
})
|
})
|
||||||
|
|
||||||
row = b2cs_output.get((rate, place_of_supply, ecommerce_gstin))
|
row = b2cs_output.get((rate, place_of_supply, ecommerce_gstin))
|
||||||
row["place_of_supply"] = place_of_supply
|
row["place_of_supply"] = place_of_supply
|
||||||
row["ecommerce_gstin"] = ecommerce_gstin
|
row["ecommerce_gstin"] = ecommerce_gstin
|
||||||
row["rate"] = rate
|
row["rate"] = rate
|
||||||
row["taxable_value"] += sum([abs(net_amount)
|
row["taxable_value"] += sum([abs(net_amount)
|
||||||
for item_code, net_amount in self.invoice_items.get(inv).items() if item_code in items])
|
for item_code, net_amount in self.invoice_items.get(inv).items() if item_code in items])
|
||||||
row["cess_amount"] += flt(self.invoice_cess.get(inv), 2)
|
row["cess_amount"] += flt(self.invoice_cess.get(inv), 2)
|
||||||
row["type"] = "E" if ecommerce_gstin else "OE"
|
row["type"] = "E" if ecommerce_gstin else "OE"
|
||||||
|
|
||||||
for key, value in iteritems(b2cs_output):
|
for key, value in iteritems(b2cs_output):
|
||||||
self.data.append(value)
|
self.data.append(value)
|
||||||
|
|
||||||
def get_row_data_for_invoice(self, invoice, invoice_details, tax_rate, items):
|
def get_row_data_for_invoice(self, invoice, invoice_details, tax_rate, items):
|
||||||
row = []
|
row = []
|
||||||
@ -173,9 +174,10 @@ class Gstr1Report(object):
|
|||||||
|
|
||||||
company_gstins = get_company_gstin_number(self.filters.get('company'), all_gstins=True)
|
company_gstins = get_company_gstin_number(self.filters.get('company'), all_gstins=True)
|
||||||
|
|
||||||
self.filters.update({
|
if company_gstins:
|
||||||
'company_gstins': company_gstins
|
self.filters.update({
|
||||||
})
|
'company_gstins': company_gstins
|
||||||
|
})
|
||||||
|
|
||||||
invoice_data = frappe.db.sql("""
|
invoice_data = frappe.db.sql("""
|
||||||
select
|
select
|
||||||
@ -1050,6 +1052,7 @@ def get_company_gstin_number(company, address=None, all_gstins=False):
|
|||||||
["Dynamic Link", "link_doctype", "=", "Company"],
|
["Dynamic Link", "link_doctype", "=", "Company"],
|
||||||
["Dynamic Link", "link_name", "=", company],
|
["Dynamic Link", "link_name", "=", company],
|
||||||
["Dynamic Link", "parenttype", "=", "Address"],
|
["Dynamic Link", "parenttype", "=", "Address"],
|
||||||
|
["gstin", "!=", '']
|
||||||
]
|
]
|
||||||
gstin = frappe.get_all("Address", filters=filters, pluck="gstin", order_by="is_primary_address desc")
|
gstin = frappe.get_all("Address", filters=filters, pluck="gstin", order_by="is_primary_address desc")
|
||||||
if gstin and not all_gstins:
|
if gstin and not all_gstins:
|
||||||
|
@ -278,7 +278,7 @@ frappe.ui.form.on('Stock Entry', {
|
|||||||
get_query_filters: {
|
get_query_filters: {
|
||||||
docstatus: 1,
|
docstatus: 1,
|
||||||
material_request_type: ["in", allowed_request_types],
|
material_request_type: ["in", allowed_request_types],
|
||||||
status: ["not in", ["Transferred", "Issued"]]
|
status: ["not in", ["Transferred", "Issued", "Cancelled", "Stopped"]]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, __("Get Items From"));
|
}, __("Get Items From"));
|
||||||
|
@ -1503,7 +1503,8 @@ class StockEntry(StockController):
|
|||||||
qty_to_reserve -= reserved_qty[0][0]
|
qty_to_reserve -= reserved_qty[0][0]
|
||||||
if qty_to_reserve > 0:
|
if qty_to_reserve > 0:
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
if item.item_code == item_code:
|
has_serial_no = frappe.get_cached_value("Item", item.item_code, "has_serial_no")
|
||||||
|
if item.item_code == item_code and has_serial_no:
|
||||||
serial_nos = (item.serial_no).split("\n")
|
serial_nos = (item.serial_no).split("\n")
|
||||||
for serial_no in serial_nos:
|
for serial_no in serial_nos:
|
||||||
if qty_to_reserve > 0:
|
if qty_to_reserve > 0:
|
||||||
|
@ -321,8 +321,8 @@ def get_basic_details(args, item, overwrite_warehouse=True):
|
|||||||
"transaction_date": args.get("transaction_date"),
|
"transaction_date": args.get("transaction_date"),
|
||||||
"against_blanket_order": args.get("against_blanket_order"),
|
"against_blanket_order": args.get("against_blanket_order"),
|
||||||
"bom_no": item.get("default_bom"),
|
"bom_no": item.get("default_bom"),
|
||||||
"weight_per_unit": item.get("weight_per_unit"),
|
"weight_per_unit": args.get("weight_per_unit") or item.get("weight_per_unit"),
|
||||||
"weight_uom": item.get("weight_uom")
|
"weight_uom": args.get("weight_uom") or item.get("weight_uom")
|
||||||
})
|
})
|
||||||
|
|
||||||
if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"):
|
if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user