Project Margin Calculation Improvement (#11911)

* Project Margin Calculation Improvement

* Documentation modification

* Change Total Planned Sales to Total Sales Amount

* Change documentation screenshot
This commit is contained in:
Charles-Henri Decultot 2017-12-12 10:29:59 +01:00 committed by Nabin Hait
parent d06b7049c7
commit d18423d9c7
7 changed files with 136 additions and 63 deletions

View File

@ -143,6 +143,7 @@ class SalesInvoice(SellingController):
self.update_time_sheet(self.name)
self.update_current_month_sales()
self.update_project()
def validate_pos_paid_amount(self):
if len(self.payments) == 0 and self.is_pos:
@ -181,6 +182,7 @@ class SalesInvoice(SellingController):
frappe.db.set(self, 'status', 'Cancelled')
self.update_current_month_sales()
self.update_project()
def update_current_month_sales(self):
if frappe.flags.in_test:
@ -912,6 +914,13 @@ class SalesInvoice(SellingController):
serial_no, sales_invoice
)))
def update_project(self):
if self.project:
project = frappe.get_doc("Project", self.project)
project.flags.dont_sync_tasks = True
project.update_billed_amount()
project.save()
def get_list_context(context=None):
from erpnext.controllers.website_list_for_contact import get_list_context
list_context = get_list_context(context)
@ -991,4 +1000,4 @@ def make_sales_return(source_name, target_doc=None):
def set_account_for_mode_of_payment(self):
for data in self.payments:
if not data.account:
data.account = get_bank_cash_account(data.mode_of_payment, self.company).get("account")
data.account = get_bank_cash_account(data.mode_of_payment, self.company).get("account")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

@ -81,13 +81,13 @@ You can make a [Cost Center](/docs/user/manual/en/accounts/setup/cost-center.htm
###Project Costing
The Project Costing section helps you track the time and expenses incurred against the project.
The Project Costing section helps you track the time, expenses and purchases incurred against the project.
<img class="screenshot" alt="Project - Costing" src="/docs/assets/img/project/project_costing.png">
* The Costing Section is updated based on Time Logs made.
* The Total Cost is composed of the costing amount from timesheets, the total cost from expense claims and the total cost from purchase invoices created against this project.
* Gross Margin is the difference between Total Costing Amount and Total Billing Amount
* The Gross Margin is the difference between Total Billed Amount and the Total Cost Amount for this project.
###Billing

View File

@ -893,7 +893,7 @@
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Total Costing Amount (via Time Logs)",
"label": "Total Costing Amount (via Timesheets)",
"length": 0,
"no_copy": 0,
"permlevel": 0,
@ -939,6 +939,36 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "total_purchase_cost",
"fieldtype": "Currency",
"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": "Total Purchase Cost (via Purchase Invoice)",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@ -969,36 +999,6 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "cost_center",
"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": "Default Cost Center",
"length": 0,
"no_copy": 0,
"options": "Cost Center",
"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,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@ -1028,6 +1028,36 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "total_sales_amount",
"fieldtype": "Currency",
"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": "Total Sales Amount (via Sales Order)",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@ -1035,7 +1065,7 @@
"collapsible": 0,
"columns": 0,
"description": "",
"fieldname": "total_billing_amount",
"fieldname": "total_billable_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
@ -1044,7 +1074,7 @@
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Total Billing Amount (via Time Logs)",
"label": "Total Billable Amount (via Timesheets)",
"length": 0,
"no_copy": 0,
"permlevel": 0,
@ -1065,7 +1095,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "total_purchase_cost",
"fieldname": "total_billed_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
@ -1074,7 +1104,7 @@
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Total Purchase Cost (via Purchase Invoice)",
"label": "Total Billed Amount (via Sales Invoices)",
"length": 0,
"no_copy": 0,
"permlevel": 0,
@ -1095,8 +1125,8 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "total_sales_cost",
"fieldtype": "Currency",
"fieldname": "cost_center",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
@ -1104,14 +1134,14 @@
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Total Sales Cost (via Sales Order)",
"label": "Default Cost Center",
"length": 0,
"no_copy": 0,
"options": "Cost Center",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
@ -1255,7 +1285,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 4,
"modified": "2017-07-26 14:36:20.857673",
"modified": "2017-12-10 08:40:46.843201",
"modified_by": "Administrator",
"module": "Projects",
"name": "Project",

View File

@ -1,4 +1,4 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
@ -25,6 +25,8 @@ class Project(Document):
from `tabTimesheet Detail` where project=%s and docstatus < 2 group by activity_type
order by total_hours desc''', self.name, as_dict=True))
self.update_costing()
def __setup__(self):
self.onload()
@ -68,7 +70,7 @@ class Project(Document):
if self.expected_start_date and self.expected_end_date:
if getdate(self.expected_end_date) < getdate(self.expected_start_date):
frappe.throw(_("Expected End Date can not be less than Expected Start Date"))
def validate_weights(self):
sum = 0
for task in self.tasks:
@ -174,28 +176,37 @@ class Project(Document):
self.actual_end_date = from_time_sheet.end_date
self.total_costing_amount = from_time_sheet.costing_amount
self.total_billing_amount = from_time_sheet.billing_amount
self.total_billable_amount = from_time_sheet.billing_amount
self.actual_time = from_time_sheet.time
self.total_expense_claim = from_expense_claim.total_sanctioned_amount
self.update_purchase_costing()
self.update_sales_amount()
self.update_billed_amount()
self.gross_margin = flt(self.total_billing_amount) - flt(self.total_costing_amount)
self.gross_margin = flt(self.total_billed_amount) - (flt(self.total_costing_amount) + flt(self.total_expense_claim) + flt(self.total_purchase_cost))
if self.total_billing_amount:
self.per_gross_margin = (self.gross_margin / flt(self.total_billing_amount)) *100
if self.total_billed_amount:
self.per_gross_margin = (self.gross_margin / flt(self.total_billed_amount)) *100
def update_purchase_costing(self):
total_purchase_cost = frappe.db.sql("""select sum(base_net_amount)
from `tabPurchase Invoice Item` where project = %s and docstatus=1""", self.name)
self.total_purchase_cost = total_purchase_cost and total_purchase_cost[0][0] or 0
def update_sales_costing(self):
total_sales_cost = frappe.db.sql("""select sum(base_grand_total)
def update_sales_amount(self):
total_sales_amount = frappe.db.sql("""select sum(base_grand_total)
from `tabSales Order` where project = %s and docstatus=1""", self.name)
self.total_sales_cost = total_sales_cost and total_sales_cost[0][0] or 0
self.total_sales_amount = total_sales_amount and total_sales_amount[0][0] or 0
def update_billed_amount(self):
total_billed_amount = frappe.db.sql("""select sum(base_grand_total)
from `tabSales Invoice` where project = %s and docstatus=1""", self.name)
self.total_billed_amount = total_billed_amount and total_billed_amount[0][0] or 0
def send_welcome_email(self):
url = get_url("/project/?name={0}".format(self.name))
@ -219,7 +230,7 @@ class Project(Document):
self.load_tasks()
self.sync_tasks()
self.update_dependencies_on_duplicated_project()
def update_dependencies_on_duplicated_project(self):
if self.flags.dont_sync_tasks: return
if not self.copied_from:
@ -289,10 +300,10 @@ def get_list_context(context=None):
def get_users_for_project(doctype, txt, searchfield, start, page_len, filters):
conditions = []
return frappe.db.sql("""select name, concat_ws(' ', first_name, middle_name, last_name)
return frappe.db.sql("""select name, concat_ws(' ', first_name, middle_name, last_name)
from `tabUser`
where enabled=1
and name not in ("Guest", "Administrator")
and name not in ("Guest", "Administrator")
and ({key} like %(txt)s
or full_name like %(txt)s)
{fcond} {mcond}

View File

@ -0,0 +1,23 @@
/* eslint-disable */
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line
QUnit.test("test: Project", function (assert) {
let done = assert.async();
// number of asserts
assert.expect(1);
frappe.run_serially([
// insert a new Project
() => frappe.tests.make('Project', [
// values to be set
{key: 'value'}
]),
() => {
assert.equal(cur_frm.doc.key, 'value');
},
() => done()
]);
});

View File

@ -110,7 +110,7 @@ class SalesOrder(SellingController):
for d in self.get("items"):
if not d.delivery_date:
d.delivery_date = self.delivery_date
if getdate(self.transaction_date) > getdate(d.delivery_date):
frappe.msgprint(_("Expected Delivery Date should be after Sales Order Date"),
indicator='orange', title=_('Warning'))
@ -191,7 +191,7 @@ class SalesOrder(SellingController):
if self.project:
project = frappe.get_doc("Project", self.project)
project.flags.dont_sync_tasks = True
project.update_sales_costing()
project.update_sales_amount()
project.save()
project_list.append(self.project)
@ -492,7 +492,7 @@ def make_delivery_note(source_name, target_doc=None):
target.ignore_pricing_rule = 1
target.run_method("set_missing_values")
target.run_method("calculate_taxes_and_totals")
# set company address
target.update(get_company_address(target.company))
if target.company_address:
@ -820,4 +820,4 @@ def get_default_bom_item(item_code):
order_by='is_default desc')
bom = bom[0].name if bom else None
return bom
return bom