solved conflicts

This commit is contained in:
Khushal Trivedi 2020-01-04 18:23:38 +05:30
commit 9e9f22b957
44 changed files with 1088 additions and 457 deletions

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"allow_import": 1, "allow_import": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
"creation": "2013-05-21 16:16:39", "creation": "2013-05-21 16:16:39",
@ -417,6 +418,7 @@
"fieldname": "contact_email", "fieldname": "contact_email",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Contact Email", "label": "Contact Email",
"options": "Email",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
@ -1287,7 +1289,8 @@
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"idx": 204, "idx": 204,
"is_submittable": 1, "is_submittable": 1,
"modified": "2019-09-17 22:31:42.666601", "links": [],
"modified": "2019-12-24 12:51:58.613538",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice", "name": "Purchase Invoice",

View File

@ -90,6 +90,7 @@ class SalesInvoice(SellingController):
self.validate_account_for_change_amount() self.validate_account_for_change_amount()
self.validate_fixed_asset() self.validate_fixed_asset()
self.set_income_account_for_fixed_assets() self.set_income_account_for_fixed_assets()
self.validate_item_cost_centers()
validate_inter_company_party(self.doctype, self.customer, self.company, self.inter_company_invoice_reference) validate_inter_company_party(self.doctype, self.customer, self.company, self.inter_company_invoice_reference)
if cint(self.is_pos): if cint(self.is_pos):
@ -147,6 +148,12 @@ class SalesInvoice(SellingController):
elif asset.status in ("Scrapped", "Cancelled", "Sold"): elif asset.status in ("Scrapped", "Cancelled", "Sold"):
frappe.throw(_("Row #{0}: Asset {1} cannot be submitted, it is already {2}").format(d.idx, d.asset, asset.status)) frappe.throw(_("Row #{0}: Asset {1} cannot be submitted, it is already {2}").format(d.idx, d.asset, asset.status))
def validate_item_cost_centers(self):
for item in self.items:
cost_center_company = frappe.get_cached_value("Cost Center", item.cost_center, "company")
if cost_center_company != self.company:
frappe.throw(_("Row #{0}: Cost Center {1} does not belong to company {2}").format(frappe.bold(item.idx), frappe.bold(item.cost_center), frappe.bold(self.company)))
def before_save(self): def before_save(self):
set_account_for_mode_of_payment(self) set_account_for_mode_of_payment(self)

View File

@ -18,6 +18,10 @@ def reconcile(bank_transaction, payment_doctype, payment_name):
account = frappe.db.get_value("Bank Account", transaction.bank_account, "account") account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
gl_entry = frappe.get_doc("GL Entry", dict(account=account, voucher_type=payment_doctype, voucher_no=payment_name)) gl_entry = frappe.get_doc("GL Entry", dict(account=account, voucher_type=payment_doctype, voucher_no=payment_name))
if payment_doctype == "Payment Entry" and payment_entry.unallocated_amount > transaction.unallocated_amount:
frappe.throw(_("The unallocated amount of Payment Entry {0} \
is greater than the Bank Transaction's unallocated amount").format(payment_name))
if transaction.unallocated_amount == 0: if transaction.unallocated_amount == 0:
frappe.throw(_("This bank transaction is already fully reconciled")) frappe.throw(_("This bank transaction is already fully reconciled"))
@ -373,4 +377,4 @@ def sales_invoices_query(doctype, txt, searchfield, start, page_len, filters):
'start': start, 'start': start,
'page_len': page_len 'page_len': page_len
} }
) )

View File

@ -4,126 +4,141 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import formatdate, getdate, flt, add_days from frappe.utils import formatdate, flt, add_days
def execute(filters=None): def execute(filters=None):
filters.day_before_from_date = add_days(filters.from_date, -1) filters.day_before_from_date = add_days(filters.from_date, -1)
columns, data = get_columns(filters), get_data(filters) columns, data = get_columns(filters), get_data(filters)
return columns, data return columns, data
def get_data(filters): def get_data(filters):
data = [] data = []
asset_categories = get_asset_categories(filters) asset_categories = get_asset_categories(filters)
assets = get_assets(filters) assets = get_assets(filters)
asset_costs = get_asset_costs(assets, filters)
asset_depreciations = get_accumulated_depreciations(assets, filters)
for asset_category in asset_categories: for asset_category in asset_categories:
row = frappe._dict() row = frappe._dict()
row.asset_category = asset_category # row.asset_category = asset_category
row.update(asset_costs.get(asset_category)) row.update(asset_category)
row.cost_as_on_to_date = (flt(row.cost_as_on_from_date) + flt(row.cost_of_new_purchase) -
flt(row.cost_of_sold_asset) - flt(row.cost_of_scrapped_asset))
row.update(next(asset for asset in assets if asset["asset_category"] == asset_category.get("asset_category", "")))
row.accumulated_depreciation_as_on_to_date = (flt(row.accumulated_depreciation_as_on_from_date) +
flt(row.depreciation_amount_during_the_period) - flt(row.depreciation_eliminated))
row.net_asset_value_as_on_from_date = (flt(row.cost_as_on_from_date) -
flt(row.accumulated_depreciation_as_on_from_date))
row.net_asset_value_as_on_to_date = (flt(row.cost_as_on_to_date) -
flt(row.accumulated_depreciation_as_on_to_date))
row.cost_as_on_to_date = (flt(row.cost_as_on_from_date) + flt(row.cost_of_new_purchase)
- flt(row.cost_of_sold_asset) - flt(row.cost_of_scrapped_asset))
row.update(asset_depreciations.get(asset_category))
row.accumulated_depreciation_as_on_to_date = (flt(row.accumulated_depreciation_as_on_from_date) +
flt(row.depreciation_amount_during_the_period) - flt(row.depreciation_eliminated))
row.net_asset_value_as_on_from_date = (flt(row.cost_as_on_from_date) -
flt(row.accumulated_depreciation_as_on_from_date))
row.net_asset_value_as_on_to_date = (flt(row.cost_as_on_to_date) -
flt(row.accumulated_depreciation_as_on_to_date))
data.append(row) data.append(row)
return data return data
def get_asset_categories(filters): def get_asset_categories(filters):
return frappe.db.sql_list(""" return frappe.db.sql("""
select distinct asset_category from `tabAsset` SELECT asset_category,
where docstatus=1 and company=%s and purchase_date <= %s ifnull(sum(case when purchase_date < %(from_date)s then
""", (filters.company, filters.to_date)) case when ifnull(disposal_date, 0) = 0 or disposal_date >= %(from_date)s then
gross_purchase_amount
else
0
end
else
0
end), 0) as cost_as_on_from_date,
ifnull(sum(case when purchase_date >= %(from_date)s then
gross_purchase_amount
else
0
end), 0) as cost_of_new_purchase,
ifnull(sum(case when ifnull(disposal_date, 0) != 0
and disposal_date >= %(from_date)s
and disposal_date <= %(to_date)s then
case when status = "Sold" then
gross_purchase_amount
else
0
end
else
0
end), 0) as cost_of_sold_asset,
ifnull(sum(case when ifnull(disposal_date, 0) != 0
and disposal_date >= %(from_date)s
and disposal_date <= %(to_date)s then
case when status = "Scrapped" then
gross_purchase_amount
else
0
end
else
0
end), 0) as cost_of_scrapped_asset
from `tabAsset`
where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s
group by asset_category
""", {"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company}, as_dict=1)
def get_assets(filters): def get_assets(filters):
return frappe.db.sql(""" return frappe.db.sql("""
select name, asset_category, purchase_date, gross_purchase_amount, disposal_date, status SELECT results.asset_category,
from `tabAsset` sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
where docstatus=1 and company=%s and purchase_date <= %s""", sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
(filters.company, filters.to_date), as_dict=1) sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
from (SELECT a.asset_category,
def get_asset_costs(assets, filters): ifnull(sum(a.opening_accumulated_depreciation +
asset_costs = frappe._dict() case when ds.schedule_date < %(from_date)s and
for d in assets: (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
asset_costs.setdefault(d.asset_category, frappe._dict({ ds.depreciation_amount
"cost_as_on_from_date": 0, else
"cost_of_new_purchase": 0, 0
"cost_of_sold_asset": 0, end), 0) as accumulated_depreciation_as_on_from_date,
"cost_of_scrapped_asset": 0 ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
})) and a.disposal_date <= %(to_date)s and ds.schedule_date <= a.disposal_date then
ds.depreciation_amount
costs = asset_costs[d.asset_category] else
0
if getdate(d.purchase_date) < getdate(filters.from_date): end), 0) as depreciation_eliminated_during_the_period,
if not d.disposal_date or getdate(d.disposal_date) >= getdate(filters.from_date):
costs.cost_as_on_from_date += flt(d.gross_purchase_amount)
else:
costs.cost_of_new_purchase += flt(d.gross_purchase_amount)
if d.disposal_date and getdate(d.disposal_date) >= getdate(filters.from_date) \
and getdate(d.disposal_date) <= getdate(filters.to_date):
if d.status == "Sold":
costs.cost_of_sold_asset += flt(d.gross_purchase_amount)
elif d.status == "Scrapped":
costs.cost_of_scrapped_asset += flt(d.gross_purchase_amount)
return asset_costs
def get_accumulated_depreciations(assets, filters):
asset_depreciations = frappe._dict()
for d in assets:
asset = frappe.get_doc("Asset", d.name)
if d.asset_category in asset_depreciations:
asset_depreciations[d.asset_category]['accumulated_depreciation_as_on_from_date'] += asset.opening_accumulated_depreciation
else:
asset_depreciations.setdefault(d.asset_category, frappe._dict({
"accumulated_depreciation_as_on_from_date": asset.opening_accumulated_depreciation,
"depreciation_amount_during_the_period": 0,
"depreciation_eliminated_during_the_period": 0
}))
depr = asset_depreciations[d.asset_category] ifnull(sum(case when ds.schedule_date >= %(from_date)s and ds.schedule_date <= %(to_date)s
and (ifnull(a.disposal_date, 0) = 0 or ds.schedule_date <= a.disposal_date) then
ds.depreciation_amount
else
0
end), 0) as depreciation_amount_during_the_period
from `tabAsset` a, `tabDepreciation Schedule` ds
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and a.name = ds.parent
group by a.asset_category
union
SELECT a.asset_category,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
and (a.disposal_date < %(from_date)s or a.disposal_date > %(to_date)s) then
0
else
a.opening_accumulated_depreciation
end), 0) as accumulated_depreciation_as_on_from_date,
ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then
a.opening_accumulated_depreciation
else
0
end), 0) as depreciation_eliminated_during_the_period,
0 as depreciation_amount_during_the_period
from `tabAsset` a
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s
and not exists(select * from `tabDepreciation Schedule` ds where a.name = ds.parent)
group by a.asset_category) as results
group by results.asset_category
""", {"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company}, as_dict=1)
if not asset.schedules: # if no schedule,
if asset.disposal_date:
# and disposal is NOT within the period, then opening accumulated depreciation not included
if getdate(asset.disposal_date) < getdate(filters.from_date) or getdate(asset.disposal_date) > getdate(filters.to_date):
asset_depreciations[d.asset_category]['accumulated_depreciation_as_on_from_date'] = 0
# if no schedule, and disposal is within period, accumulated dep is the amount eliminated
if getdate(asset.disposal_date) >= getdate(filters.from_date) and getdate(asset.disposal_date) <= getdate(filters.to_date):
depr.depreciation_eliminated_during_the_period += asset.opening_accumulated_depreciation
for schedule in asset.get("schedules"):
if getdate(schedule.schedule_date) < getdate(filters.from_date):
if not asset.disposal_date or getdate(asset.disposal_date) >= getdate(filters.from_date):
depr.accumulated_depreciation_as_on_from_date += flt(schedule.depreciation_amount)
elif getdate(schedule.schedule_date) <= getdate(filters.to_date):
if not asset.disposal_date:
depr.depreciation_amount_during_the_period += flt(schedule.depreciation_amount)
else:
if getdate(schedule.schedule_date) <= getdate(asset.disposal_date):
depr.depreciation_amount_during_the_period += flt(schedule.depreciation_amount)
if asset.disposal_date and getdate(asset.disposal_date) >= getdate(filters.from_date) and getdate(asset.disposal_date) <= getdate(filters.to_date):
if getdate(schedule.schedule_date) <= getdate(asset.disposal_date):
depr.depreciation_eliminated_during_the_period += flt(schedule.depreciation_amount)
return asset_depreciations
def get_columns(filters): def get_columns(filters):
return [ return [
{ {

View File

@ -46,13 +46,24 @@ frappe.query_reports["Budget Variance Report"] = {
fieldtype: "Select", fieldtype: "Select",
options: ["Cost Center", "Project"], options: ["Cost Center", "Project"],
default: "Cost Center", default: "Cost Center",
reqd: 1 reqd: 1,
on_change: function() {
frappe.query_report.set_filter_value("budget_against_filter", []);
frappe.query_report.refresh();
}
}, },
{ {
fieldname: "cost_center", fieldname:"budget_against_filter",
label: __("Cost Center"), label: __('Dimension Filter'),
fieldtype: "Link", fieldtype: "MultiSelectList",
options: "Cost Center" get_data: function(txt) {
if (!frappe.query_report.filters) return;
let budget_against = frappe.query_report.get_filter_value('budget_against');
if (!budget_against) return;
return frappe.db.get_link_options(budget_against, txt);
}
}, },
{ {
fieldname:"show_cumulative", fieldname:"show_cumulative",

View File

@ -12,22 +12,22 @@ from six import iteritems
from pprint import pprint from pprint import pprint
def execute(filters=None): def execute(filters=None):
if not filters: filters = {} if not filters: filters = {}
validate_filters(filters)
columns = get_columns(filters) columns = get_columns(filters)
if filters.get("cost_center"): if filters.get("budget_against_filter"):
cost_centers = [filters.get("cost_center")] dimensions = filters.get("budget_against_filter")
else: else:
cost_centers = get_cost_centers(filters) dimensions = get_cost_centers(filters)
period_month_ranges = get_period_month_ranges(filters["period"], filters["from_fiscal_year"]) period_month_ranges = get_period_month_ranges(filters["period"], filters["from_fiscal_year"])
cam_map = get_cost_center_account_month_map(filters) cam_map = get_dimension_account_month_map(filters)
data = [] data = []
for cost_center in cost_centers: for dimension in dimensions:
cost_center_items = cam_map.get(cost_center) dimension_items = cam_map.get(dimension)
if cost_center_items: if dimension_items:
for account, monthwise_data in iteritems(cost_center_items): for account, monthwise_data in iteritems(dimension_items):
row = [cost_center, account] row = [dimension, account]
totals = [0, 0, 0] totals = [0, 0, 0]
for year in get_fiscal_years(filters): for year in get_fiscal_years(filters):
last_total = 0 last_total = 0
@ -55,10 +55,6 @@ def execute(filters=None):
return columns, data return columns, data
def validate_filters(filters):
if filters.get("budget_against") != "Cost Center" and filters.get("cost_center"):
frappe.throw(_("Filter based on Cost Center is only applicable if Budget Against is selected as Cost Center"))
def get_columns(filters): def get_columns(filters):
columns = [_(filters.get("budget_against")) + ":Link/%s:150"%(filters.get("budget_against")), _("Account") + ":Link/Account:150"] columns = [_(filters.get("budget_against")) + ":Link/%s:150"%(filters.get("budget_against")), _("Account") + ":Link/Account:150"]
@ -98,11 +94,12 @@ def get_cost_centers(filters):
else: else:
return frappe.db.sql_list("""select name from `tab{tab}`""".format(tab=filters.get("budget_against"))) #nosec return frappe.db.sql_list("""select name from `tab{tab}`""".format(tab=filters.get("budget_against"))) #nosec
#Get cost center & target details #Get dimension & target details
def get_cost_center_target_details(filters): def get_dimension_target_details(filters):
cond = "" cond = ""
if filters.get("cost_center"): if filters.get("budget_against_filter"):
cond += " and b.cost_center=%s" % frappe.db.escape(filters.get("cost_center")) cond += " and b.{budget_against} in (%s)".format(budget_against = \
frappe.scrub(filters.get('budget_against'))) % ', '.join(['%s']* len(filters.get('budget_against_filter')))
return frappe.db.sql(""" return frappe.db.sql("""
select b.{budget_against} as budget_against, b.monthly_distribution, ba.account, ba.budget_amount,b.fiscal_year select b.{budget_against} as budget_against, b.monthly_distribution, ba.account, ba.budget_amount,b.fiscal_year
@ -110,8 +107,8 @@ def get_cost_center_target_details(filters):
where b.name=ba.parent and b.docstatus = 1 and b.fiscal_year between %s and %s where b.name=ba.parent and b.docstatus = 1 and b.fiscal_year between %s and %s
and b.budget_against = %s and b.company=%s {cond} order by b.fiscal_year and b.budget_against = %s and b.company=%s {cond} order by b.fiscal_year
""".format(budget_against=filters.get("budget_against").replace(" ", "_").lower(), cond=cond), """.format(budget_against=filters.get("budget_against").replace(" ", "_").lower(), cond=cond),
(filters.from_fiscal_year,filters.to_fiscal_year,filters.budget_against, filters.company), as_dict=True) tuple([filters.from_fiscal_year,filters.to_fiscal_year,filters.budget_against, filters.company] + filters.get('budget_against_filter')),
as_dict=True)
#Get target distribution details of accounts of cost center #Get target distribution details of accounts of cost center
@ -153,14 +150,14 @@ def get_actual_details(name, filters):
return cc_actual_details return cc_actual_details
def get_cost_center_account_month_map(filters): def get_dimension_account_month_map(filters):
import datetime import datetime
cost_center_target_details = get_cost_center_target_details(filters) dimension_target_details = get_dimension_target_details(filters)
tdd = get_target_distribution_details(filters) tdd = get_target_distribution_details(filters)
cam_map = {} cam_map = {}
for ccd in cost_center_target_details: for ccd in dimension_target_details:
actual_details = get_actual_details(ccd.budget_against, filters) actual_details = get_actual_details(ccd.budget_against, filters)
for month_id in range(1, 13): for month_id in range(1, 13):

View File

@ -342,6 +342,7 @@
"fieldname": "contact_email", "fieldname": "contact_email",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Contact Email", "label": "Contact Email",
"options": "Email",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
@ -1054,7 +1055,7 @@
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2019-12-18 13:13:22.852412", "modified": "2019-12-24 12:44:13.137194",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order", "name": "Purchase Order",

View File

@ -118,6 +118,73 @@ class TestPurchaseOrder(unittest.TestCase):
self.assertEqual(po.get("items")[0].amount, 1400) self.assertEqual(po.get("items")[0].amount, 1400)
self.assertEqual(get_ordered_qty(), existing_ordered_qty + 3) self.assertEqual(get_ordered_qty(), existing_ordered_qty + 3)
def test_add_new_item_in_update_child_qty_rate(self):
po = create_purchase_order(do_not_save=1)
po.items[0].qty = 4
po.save()
po.submit()
pr = make_pr_against_po(po.name, 2)
po.load_from_db()
first_item_of_po = po.get("items")[0]
trans_item = json.dumps([
{
'item_code': first_item_of_po.item_code,
'rate': first_item_of_po.rate,
'qty': first_item_of_po.qty,
'docname': first_item_of_po.name
},
{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 7}
])
update_child_qty_rate('Purchase Order', trans_item, po.name)
po.reload()
self.assertEquals(len(po.get('items')), 2)
self.assertEqual(po.status, 'To Receive and Bill')
def test_remove_item_in_update_child_qty_rate(self):
po = create_purchase_order(do_not_save=1)
po.items[0].qty = 4
po.save()
po.submit()
pr = make_pr_against_po(po.name, 2)
po.reload()
first_item_of_po = po.get("items")[0]
# add an item
trans_item = json.dumps([
{
'item_code': first_item_of_po.item_code,
'rate': first_item_of_po.rate,
'qty': first_item_of_po.qty,
'docname': first_item_of_po.name
},
{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 7}])
update_child_qty_rate('Purchase Order', trans_item, po.name)
po.reload()
# check if can remove received item
trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 7, 'docname': po.get("items")[1].name}])
self.assertRaises(frappe.ValidationError, update_child_qty_rate, 'Purchase Order', trans_item, po.name)
first_item_of_po = po.get("items")[0]
trans_item = json.dumps([
{
'item_code': first_item_of_po.item_code,
'rate': first_item_of_po.rate,
'qty': first_item_of_po.qty,
'docname': first_item_of_po.name
}
])
update_child_qty_rate('Purchase Order', trans_item, po.name)
po.reload()
self.assertEquals(len(po.get('items')), 1)
self.assertEqual(po.status, 'To Receive and Bill')
def test_update_qty(self): def test_update_qty(self):
po = create_purchase_order() po = create_purchase_order()

View File

@ -1155,6 +1155,25 @@ def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docna
child_item.base_amount = 1 # Initiallize value will update in parent validation child_item.base_amount = 1 # Initiallize value will update in parent validation
return child_item return child_item
def check_and_delete_children(parent, data):
deleted_children = []
updated_item_names = [d.get("docname") for d in data]
for item in parent.items:
if item.name not in updated_item_names:
deleted_children.append(item)
for d in deleted_children:
if parent.doctype == "Sales Order" and flt(d.delivered_qty):
frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been delivered").format(d.idx, d.item_code))
if parent.doctype == "Purchase Order" and flt(d.received_qty):
frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been received").format(d.idx, d.item_code))
if flt(d.billed_amt):
frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been billed.").format(d.idx, d.item_code))
d.cancel()
d.delete()
@frappe.whitelist() @frappe.whitelist()
def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"): def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
@ -1163,6 +1182,8 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
sales_doctypes = ['Sales Order', 'Sales Invoice', 'Delivery Note', 'Quotation'] sales_doctypes = ['Sales Order', 'Sales Invoice', 'Delivery Note', 'Quotation']
parent = frappe.get_doc(parent_doctype, parent_doctype_name) parent = frappe.get_doc(parent_doctype, parent_doctype_name)
check_and_delete_children(parent, data)
for d in data: for d in data:
new_child_flag = False new_child_flag = False
if not d.get("docname"): if not d.get("docname"):

View File

@ -311,6 +311,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
and sle.item_code = %(item_code)s and sle.item_code = %(item_code)s
and sle.warehouse = %(warehouse)s and sle.warehouse = %(warehouse)s
and (sle.batch_no like %(txt)s and (sle.batch_no like %(txt)s
or batch.expiry_date like %(txt)s
or batch.manufacturing_date like %(txt)s) or batch.manufacturing_date like %(txt)s)
and batch.docstatus < 2 and batch.docstatus < 2
{cond} {cond}
@ -329,6 +330,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
where batch.disabled = 0 where batch.disabled = 0
and item = %(item_code)s and item = %(item_code)s
and (name like %(txt)s and (name like %(txt)s
or expiry_date like %(txt)s
or manufacturing_date like %(txt)s) or manufacturing_date like %(txt)s)
and docstatus < 2 and docstatus < 2
{0} {0}

View File

@ -148,13 +148,6 @@ class SellingController(StockController):
if sales_team and total != 100.0: if sales_team and total != 100.0:
throw(_("Total allocated percentage for sales team should be 100")) throw(_("Total allocated percentage for sales team should be 100"))
def validate_order_type(self):
valid_types = ["Sales", "Maintenance", "Shopping Cart"]
if not self.order_type:
self.order_type = "Sales"
elif self.order_type not in valid_types:
throw(_("Order Type must be one of {0}").format(comma_or(valid_types)))
def validate_max_discount(self): def validate_max_discount(self):
for d in self.get("items"): for d in self.get("items"):
if d.item_code: if d.item_code:

View File

@ -1,4 +1,4 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
frappe.provide("erpnext"); frappe.provide("erpnext");
@ -7,57 +7,54 @@ cur_frm.email_field = "email_id";
erpnext.LeadController = frappe.ui.form.Controller.extend({ erpnext.LeadController = frappe.ui.form.Controller.extend({
setup: function () { setup: function () {
this.frm.make_methods = { this.frm.make_methods = {
'Customer': this.make_customer,
'Quotation': this.make_quotation, 'Quotation': this.make_quotation,
'Opportunity': this.create_opportunity 'Opportunity': this.make_opportunity
} };
this.frm.fields_dict.customer.get_query = function (doc, cdt, cdn) {
return { query: "erpnext.controllers.queries.customer_query" }
}
this.frm.toggle_reqd("lead_name", !this.frm.doc.organization_lead); this.frm.toggle_reqd("lead_name", !this.frm.doc.organization_lead);
}, },
onload: function () { onload: function () {
if (cur_frm.fields_dict.lead_owner.df.options.match(/^User/)) { this.frm.set_query("customer", function (doc, cdt, cdn) {
cur_frm.fields_dict.lead_owner.get_query = function (doc, cdt, cdn) { return { query: "erpnext.controllers.queries.customer_query" }
return { query: "frappe.core.doctype.user.user.user_query" } });
}
}
if (cur_frm.fields_dict.contact_by.df.options.match(/^User/)) { this.frm.set_query("lead_owner", function (doc, cdt, cdn) {
cur_frm.fields_dict.contact_by.get_query = function (doc, cdt, cdn) { return { query: "frappe.core.doctype.user.user.user_query" }
return { query: "frappe.core.doctype.user.user.user_query" } });
}
} this.frm.set_query("contact_by", function (doc, cdt, cdn) {
return { query: "frappe.core.doctype.user.user.user_query" }
});
}, },
refresh: function () { refresh: function () {
var doc = this.frm.doc; let doc = this.frm.doc;
erpnext.toggle_naming_series(); erpnext.toggle_naming_series();
frappe.dynamic_link = { doc: doc, fieldname: 'name', doctype: 'Lead' } frappe.dynamic_link = { doc: doc, fieldname: 'name', doctype: 'Lead' }
if(!doc.__islocal && doc.__onload && !doc.__onload.is_customer) { if (!this.frm.is_new() && doc.__onload && !doc.__onload.is_customer) {
this.frm.add_custom_button(__("Customer"), this.create_customer, __('Create')); this.frm.add_custom_button(__("Customer"), this.make_customer, __("Create"));
this.frm.add_custom_button(__("Opportunity"), this.create_opportunity, __('Create')); this.frm.add_custom_button(__("Opportunity"), this.make_opportunity, __("Create"));
this.frm.add_custom_button(__("Quotation"), this.make_quotation, __('Create')); this.frm.add_custom_button(__("Quotation"), this.make_quotation, __("Create"));
} }
if (!this.frm.doc.__islocal) { if (!this.frm.is_new()) {
frappe.contacts.render_address_and_contact(cur_frm); frappe.contacts.render_address_and_contact(this.frm);
} else { } else {
frappe.contacts.clear_address_and_contact(cur_frm); frappe.contacts.clear_address_and_contact(this.frm);
} }
}, },
create_customer: function () { make_customer: function () {
frappe.model.open_mapped_doc({ frappe.model.open_mapped_doc({
method: "erpnext.crm.doctype.lead.lead.make_customer", method: "erpnext.crm.doctype.lead.lead.make_customer",
frm: cur_frm frm: cur_frm
}) })
}, },
create_opportunity: function () { make_opportunity: function () {
frappe.model.open_mapped_doc({ frappe.model.open_mapped_doc({
method: "erpnext.crm.doctype.lead.lead.make_opportunity", method: "erpnext.crm.doctype.lead.lead.make_opportunity",
frm: cur_frm frm: cur_frm
@ -77,7 +74,7 @@ erpnext.LeadController = frappe.ui.form.Controller.extend({
}, },
company_name: function () { company_name: function () {
if (this.frm.doc.organization_lead == 1) { if (this.frm.doc.organization_lead && !this.frm.doc.lead_name) {
this.frm.set_value("lead_name", this.frm.doc.company_name); this.frm.set_value("lead_name", this.frm.doc.company_name);
} }
}, },
@ -85,7 +82,7 @@ erpnext.LeadController = frappe.ui.form.Controller.extend({
contact_date: function () { contact_date: function () {
if (this.frm.doc.contact_date) { if (this.frm.doc.contact_date) {
let d = moment(this.frm.doc.contact_date); let d = moment(this.frm.doc.contact_date);
d.add(1, "hours"); d.add(1, "day");
this.frm.set_value("ends_on", d.format(frappe.defaultDatetimeFormat)); this.frm.set_value("ends_on", d.format(frappe.defaultDatetimeFormat));
} }
} }

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"allow_events_in_timeline": 1, "allow_events_in_timeline": 1,
"allow_import": 1, "allow_import": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
@ -16,6 +17,8 @@
"col_break123", "col_break123",
"lead_owner", "lead_owner",
"status", "status",
"salutation",
"designation",
"gender", "gender",
"source", "source",
"customer", "customer",
@ -28,17 +31,22 @@
"ends_on", "ends_on",
"notes_section", "notes_section",
"notes", "notes",
"contact_info", "address_info",
"address_desc",
"address_html", "address_html",
"address_title",
"address_line1",
"address_line2",
"city",
"county",
"column_break2", "column_break2",
"contact_html", "contact_html",
"state",
"country",
"pincode",
"contact_section",
"phone", "phone",
"salutation",
"mobile_no", "mobile_no",
"fax", "fax",
"website",
"territory",
"more_info", "more_info",
"type", "type",
"market_segment", "market_segment",
@ -46,8 +54,11 @@
"request_type", "request_type",
"column_break3", "column_break3",
"company", "company",
"website",
"territory",
"unsubscribed", "unsubscribed",
"blog_subscriber" "blog_subscriber",
"title"
], ],
"fields": [ "fields": [
{ {
@ -73,7 +84,6 @@
"set_only_once": 1 "set_only_once": 1
}, },
{ {
"depends_on": "eval:!doc.organization_lead",
"fieldname": "lead_name", "fieldname": "lead_name",
"fieldtype": "Data", "fieldtype": "Data",
"in_global_search": 1, "in_global_search": 1,
@ -130,7 +140,13 @@
"search_index": 1 "search_index": 1
}, },
{ {
"depends_on": "eval:!doc.organization_lead", "depends_on": "eval: doc.__islocal",
"fieldname": "salutation",
"fieldtype": "Link",
"label": "Salutation",
"options": "Salutation"
},
{
"fieldname": "gender", "fieldname": "gender",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Gender", "label": "Gender",
@ -216,40 +232,74 @@
"fieldtype": "Text Editor", "fieldtype": "Text Editor",
"label": "Notes" "label": "Notes"
}, },
{
"collapsible": 1,
"fieldname": "contact_info",
"fieldtype": "Section Break",
"label": "Address & Contact",
"oldfieldtype": "Column Break",
"options": "fa fa-map-marker"
},
{
"depends_on": "eval:doc.__islocal",
"fieldname": "address_desc",
"fieldtype": "HTML",
"label": "Address Desc",
"print_hide": 1
},
{ {
"fieldname": "address_html", "fieldname": "address_html",
"fieldtype": "HTML", "fieldtype": "HTML",
"label": "Address HTML", "label": "Address HTML",
"read_only": 1 "read_only": 1
}, },
{
"depends_on": "eval: doc.__islocal",
"fieldname": "address_title",
"fieldtype": "Data",
"label": "Address Title"
},
{
"depends_on": "eval: doc.__islocal",
"fieldname": "address_line1",
"fieldtype": "Data",
"label": "Address Line 1"
},
{
"depends_on": "eval: doc.__islocal",
"fieldname": "address_line2",
"fieldtype": "Data",
"label": "Address Line 2"
},
{
"depends_on": "eval: doc.__islocal",
"fieldname": "city",
"fieldtype": "Data",
"label": "City/Town"
},
{
"depends_on": "eval: doc.__islocal",
"fieldname": "county",
"fieldtype": "Data",
"label": "County"
},
{
"depends_on": "eval: doc.__islocal",
"fieldname": "state",
"fieldtype": "Data",
"label": "State"
},
{
"depends_on": "eval: doc.__islocal",
"fieldname": "country",
"fieldtype": "Link",
"label": "Country",
"options": "Country"
},
{
"depends_on": "eval: doc.__islocal",
"fieldname": "pincode",
"fieldtype": "Data",
"label": "Postal Code",
"options": "Country"
},
{ {
"fieldname": "column_break2", "fieldname": "column_break2",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"depends_on": "eval:doc.organization_lead",
"fieldname": "contact_html", "fieldname": "contact_html",
"fieldtype": "HTML", "fieldtype": "HTML",
"label": "Contact HTML", "label": "Contact HTML",
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval:!doc.organization_lead", "depends_on": "eval: doc.__islocal",
"fieldname": "phone", "fieldname": "phone",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Phone", "label": "Phone",
@ -257,14 +307,7 @@
"oldfieldtype": "Data" "oldfieldtype": "Data"
}, },
{ {
"depends_on": "eval:!doc.organization_lead", "depends_on": "eval: doc.__islocal",
"fieldname": "salutation",
"fieldtype": "Link",
"label": "Salutation",
"options": "Salutation"
},
{
"depends_on": "eval:!doc.organization_lead",
"fieldname": "mobile_no", "fieldname": "mobile_no",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Mobile No.", "label": "Mobile No.",
@ -272,29 +315,13 @@
"oldfieldtype": "Data" "oldfieldtype": "Data"
}, },
{ {
"depends_on": "eval:!doc.organization_lead", "depends_on": "eval: doc.__islocal",
"fieldname": "fax", "fieldname": "fax",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Fax", "label": "Fax",
"oldfieldname": "fax", "oldfieldname": "fax",
"oldfieldtype": "Data" "oldfieldtype": "Data"
}, },
{
"fieldname": "website",
"fieldtype": "Data",
"label": "Website",
"oldfieldname": "website",
"oldfieldtype": "Data"
},
{
"fieldname": "territory",
"fieldtype": "Link",
"label": "Territory",
"oldfieldname": "territory",
"oldfieldtype": "Link",
"options": "Territory",
"print_hide": 1
},
{ {
"collapsible": 1, "collapsible": 1,
"fieldname": "more_info", "fieldname": "more_info",
@ -350,6 +377,22 @@
"options": "Company", "options": "Company",
"remember_last_selected_value": 1 "remember_last_selected_value": 1
}, },
{
"fieldname": "website",
"fieldtype": "Data",
"label": "Website",
"oldfieldname": "website",
"oldfieldtype": "Data"
},
{
"fieldname": "territory",
"fieldtype": "Link",
"label": "Territory",
"oldfieldname": "territory",
"oldfieldtype": "Link",
"options": "Territory",
"print_hide": 1
},
{ {
"default": "0", "default": "0",
"fieldname": "unsubscribed", "fieldname": "unsubscribed",
@ -361,12 +404,42 @@
"fieldname": "blog_subscriber", "fieldname": "blog_subscriber",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Blog Subscriber" "label": "Blog Subscriber"
},
{
"fieldname": "title",
"fieldtype": "Data",
"hidden": 1,
"label": "Title",
"print_hide": 1
},
{
"fieldname": "designation",
"fieldtype": "Link",
"label": "Designation",
"options": "Designation"
},
{
"collapsible": 1,
"collapsible_depends_on": "eval: doc.__islocal",
"fieldname": "address_info",
"fieldtype": "Section Break",
"label": "Address & Contact",
"oldfieldtype": "Column Break",
"options": "fa fa-map-marker"
},
{
"collapsible": 1,
"collapsible_depends_on": "eval: doc.__islocal",
"fieldname": "contact_section",
"fieldtype": "Section Break",
"label": "Contact"
} }
], ],
"icon": "fa fa-user", "icon": "fa fa-user",
"idx": 5, "idx": 5,
"image_field": "image", "image_field": "image",
"modified": "2019-09-19 12:49:02.536647", "links": [],
"modified": "2019-12-24 16:00:44.239168",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Lead", "name": "Lead",
@ -438,5 +511,5 @@
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"title_field": "lead_name" "title_field": "title"
} }

View File

@ -2,18 +2,19 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import (cstr, validate_email_address, cint, comma_and, has_gravatar, now, getdate, nowdate)
from frappe.model.mapper import get_mapped_doc
from erpnext.controllers.selling_controller import SellingController import frappe
from frappe.contacts.address_and_contact import load_address_and_contact
from erpnext.accounts.party import set_taxes from erpnext.accounts.party import set_taxes
from erpnext.controllers.selling_controller import SellingController
from frappe import _
from frappe.contacts.address_and_contact import load_address_and_contact
from frappe.email.inbox import link_communication_to_document from frappe.email.inbox import link_communication_to_document
from frappe.model.mapper import get_mapped_doc
from frappe.utils import cint, comma_and, cstr, getdate, has_gravatar, nowdate, validate_email_address
sender_field = "email_id" sender_field = "email_id"
class Lead(SellingController): class Lead(SellingController):
def get_feed(self): def get_feed(self):
return '{0}: {1}'.format(_(self.status), self.lead_name) return '{0}: {1}'.format(_(self.status), self.lead_name)
@ -23,15 +24,23 @@ class Lead(SellingController):
self.get("__onload").is_customer = customer self.get("__onload").is_customer = customer
load_address_and_contact(self) load_address_and_contact(self)
def before_insert(self):
self.address_doc = self.create_address()
self.contact_doc = self.create_contact()
def after_insert(self):
self.update_links()
# after the address and contact are created, flush the field values
# to avoid inconsistent reporting in case the documents are changed
self.flush_address_and_contact_fields()
def validate(self): def validate(self):
self.set_lead_name() self.set_lead_name()
self.set_title()
self._prev = frappe._dict({ self._prev = frappe._dict({
"contact_date": frappe.db.get_value("Lead", self.name, "contact_date") if \ "contact_date": frappe.db.get_value("Lead", self.name, "contact_date") if (not cint(self.is_new())) else None,
(not cint(self.get("__islocal"))) else None, "ends_on": frappe.db.get_value("Lead", self.name, "ends_on") if (not cint(self.is_new())) else None,
"ends_on": frappe.db.get_value("Lead", self.name, "ends_on") if \ "contact_by": frappe.db.get_value("Lead", self.name, "contact_by") if (not cint(self.is_new())) else None,
(not cint(self.get("__islocal"))) else None,
"contact_by": frappe.db.get_value("Lead", self.name, "contact_by") if \
(not cint(self.get("__islocal"))) else None,
}) })
self.set_status() self.set_status()
@ -39,7 +48,7 @@ class Lead(SellingController):
if self.email_id: if self.email_id:
if not self.flags.ignore_email_validation: if not self.flags.ignore_email_validation:
validate_email_address(self.email_id, True) validate_email_address(self.email_id, throw=True)
if self.email_id == self.lead_owner: if self.email_id == self.lead_owner:
frappe.throw(_("Lead Owner cannot be same as the Lead")) frappe.throw(_("Lead Owner cannot be same as the Lead"))
@ -53,8 +62,7 @@ class Lead(SellingController):
if self.contact_date and getdate(self.contact_date) < getdate(nowdate()): if self.contact_date and getdate(self.contact_date) < getdate(nowdate()):
frappe.throw(_("Next Contact Date cannot be in the past")) frappe.throw(_("Next Contact Date cannot be in the past"))
if self.ends_on and self.contact_date and\ if self.ends_on and self.contact_date and (self.ends_on < self.contact_date):
(self.ends_on < self.contact_date):
frappe.throw(_("Ends On date cannot be before Next Contact Date.")) frappe.throw(_("Ends On date cannot be before Next Contact Date."))
def on_update(self): def on_update(self):
@ -66,23 +74,21 @@ class Lead(SellingController):
"starts_on": self.contact_date, "starts_on": self.contact_date,
"ends_on": self.ends_on or "", "ends_on": self.ends_on or "",
"subject": ('Contact ' + cstr(self.lead_name)), "subject": ('Contact ' + cstr(self.lead_name)),
"description": ('Contact ' + cstr(self.lead_name)) + \ "description": ('Contact ' + cstr(self.lead_name)) + (self.contact_by and ('. By : ' + cstr(self.contact_by)) or '')
(self.contact_by and ('. By : ' + cstr(self.contact_by)) or '')
}, force) }, force)
def check_email_id_is_unique(self): def check_email_id_is_unique(self):
if self.email_id: if self.email_id:
# validate email is unique # validate email is unique
duplicate_leads = frappe.db.sql_list("""select name from tabLead duplicate_leads = frappe.get_all("Lead", filters={"email_id": self.email_id, "name": ["!=", self.name]})
where email_id=%s and name!=%s""", (self.email_id, self.name)) duplicate_leads = [lead.name for lead in duplicate_leads]
if duplicate_leads: if duplicate_leads:
frappe.throw(_("Email Address must be unique, already exists for {0}") frappe.throw(_("Email Address must be unique, already exists for {0}")
.format(comma_and(duplicate_leads)), frappe.DuplicateEntryError) .format(comma_and(duplicate_leads)), frappe.DuplicateEntryError)
def on_trash(self): def on_trash(self):
frappe.db.sql("""update `tabIssue` set lead='' where lead=%s""", frappe.db.sql("""update `tabIssue` set lead='' where lead=%s""", self.name)
self.name)
self.delete_events() self.delete_events()
@ -115,10 +121,101 @@ class Lead(SellingController):
self.lead_name = self.company_name self.lead_name = self.company_name
def set_title(self):
if self.organization_lead:
self.title = self.company_name
else:
self.title = self.lead_name
def create_address(self):
address_fields = ["address_title", "address_line1", "address_line2",
"city", "county", "state", "country", "pincode"]
info_fields = ["email_id", "phone", "fax"]
# do not create an address if no fields are available,
# skipping country since the system auto-sets it from system defaults
if not any([self.get(field) for field in address_fields if field != "country"]):
return
address = frappe.new_doc("Address")
address.update({addr_field: self.get(addr_field) for addr_field in address_fields})
address.update({info_field: self.get(info_field) for info_field in info_fields})
address.insert()
return address
def create_contact(self):
if not self.lead_name:
self.set_lead_name()
names = self.lead_name.split(" ")
if len(names) > 1:
first_name, last_name = names[0], " ".join(names[1:])
else:
first_name, last_name = self.lead_name, None
contact = frappe.new_doc("Contact")
contact.update({
"first_name": first_name,
"last_name": last_name,
"salutation": self.salutation,
"gender": self.gender,
"designation": self.designation,
})
if self.email_id:
contact.append("email_ids", {
"email_id": self.email_id,
"is_primary": 1
})
if self.phone:
contact.append("phone_nos", {
"phone": self.phone,
"is_primary": 1
})
if self.mobile_no:
contact.append("phone_nos", {
"phone": self.mobile_no
})
contact.insert()
return contact
def update_links(self):
# update address links
if self.address_doc:
self.address_doc.append("links", {
"link_doctype": "Lead",
"link_name": self.name,
"link_title": self.lead_name
})
self.address_doc.save()
# update contact links
if self.contact_doc:
self.contact_doc.append("links", {
"link_doctype": "Lead",
"link_name": self.name,
"link_title": self.lead_name
})
self.contact_doc.save()
def flush_address_and_contact_fields(self):
fields = ['address_line1', 'address_line2', 'address_title',
'city', 'county', 'country', 'fax', 'pincode', 'state']
for field in fields:
self.set(field, None)
@frappe.whitelist() @frappe.whitelist()
def make_customer(source_name, target_doc=None): def make_customer(source_name, target_doc=None):
return _make_customer(source_name, target_doc) return _make_customer(source_name, target_doc)
def _make_customer(source_name, target_doc=None, ignore_permissions=False): def _make_customer(source_name, target_doc=None, ignore_permissions=False):
def set_missing_values(source, target): def set_missing_values(source, target):
if source.company_name: if source.company_name:
@ -143,6 +240,7 @@ def _make_customer(source_name, target_doc=None, ignore_permissions=False):
return doclist return doclist
@frappe.whitelist() @frappe.whitelist()
def make_opportunity(source_name, target_doc=None): def make_opportunity(source_name, target_doc=None):
def set_missing_values(source, target): def set_missing_values(source, target):
@ -164,6 +262,7 @@ def make_opportunity(source_name, target_doc=None):
return target_doc return target_doc
@frappe.whitelist() @frappe.whitelist()
def make_quotation(source_name, target_doc=None): def make_quotation(source_name, target_doc=None):
def set_missing_values(source, target): def set_missing_values(source, target):
@ -205,7 +304,8 @@ def _set_missing_values(source, target):
@frappe.whitelist() @frappe.whitelist()
def get_lead_details(lead, posting_date=None, company=None): def get_lead_details(lead, posting_date=None, company=None):
if not lead: return {} if not lead:
return {}
from erpnext.accounts.party import set_address_details from erpnext.accounts.party import set_address_details
out = frappe._dict() out = frappe._dict()
@ -231,6 +331,7 @@ def get_lead_details(lead, posting_date=None, company=None):
return out return out
@frappe.whitelist() @frappe.whitelist()
def make_lead_from_communication(communication, ignore_communication_links=False): def make_lead_from_communication(communication, ignore_communication_links=False):
""" raise a issue from email """ """ raise a issue from email """
@ -267,4 +368,4 @@ def get_lead_with_phone_number(number):
lead = leads[0].name if leads else None lead = leads[0].name if leads else None
return lead return lead

View File

@ -27,7 +27,7 @@ class Instructor(Document):
self.validate_duplicate_employee() self.validate_duplicate_employee()
def validate_duplicate_employee(self): def validate_duplicate_employee(self):
if self.employee and frappe.db.get_value("Instructor", {'employee': self.employee}, 'name'): if self.employee and frappe.db.get_value("Instructor", {'employee': self.employee, 'name': ['!=', self.name]}, 'name'):
frappe.throw(_("Employee ID is linked with another instructor")) frappe.throw(_("Employee ID is linked with another instructor"))

View File

@ -164,6 +164,12 @@ class Employee(NestedSet):
if self.personal_email: if self.personal_email:
validate_email_address(self.personal_email, True) validate_email_address(self.personal_email, True)
def set_preferred_email(self):
preferred_email_field = frappe.scrub(self.prefered_contact_email)
if preferred_email_field:
preferred_email = self.get(preferred_email_field)
self.prefered_email = preferred_email
def validate_status(self): def validate_status(self):
if self.status == 'Left': if self.status == 'Left':
reports_to = frappe.db.get_all('Employee', reports_to = frappe.db.get_all('Employee',

View File

@ -115,6 +115,16 @@ def get_valid_items(search_value=''):
return valid_items return valid_items
@frappe.whitelist()
def update_item(ref_doc, data):
data = json.loads(data)
data.update(dict(doctype='Hub Item', name=ref_doc))
try:
connection = get_hub_connection()
connection.update(data)
except Exception as e:
frappe.log_error(message=e, title='Hub Sync Error')
@frappe.whitelist() @frappe.whitelist()
def publish_selected_items(items_to_publish): def publish_selected_items(items_to_publish):

View File

@ -856,4 +856,4 @@ def create_pick_list(source_name, target_doc=None, for_qty=None):
doc.set_item_locations() doc.set_item_locations()
return doc return doc

View File

@ -649,5 +649,7 @@ erpnext.patches.v12_0.add_export_type_field_in_party_master
erpnext.patches.v12_0.remove_denied_leaves_from_leave_ledger erpnext.patches.v12_0.remove_denied_leaves_from_leave_ledger
erpnext.patches.v12_0.update_price_or_product_discount erpnext.patches.v12_0.update_price_or_product_discount
erpnext.patches.v12_0.set_production_capacity_in_workstation erpnext.patches.v12_0.set_production_capacity_in_workstation
erpnext.patches.v12_0.set_employee_preferred_emails
erpnext.patches.v12_0.set_against_blanket_order_in_sales_and_purchase_order erpnext.patches.v12_0.set_against_blanket_order_in_sales_and_purchase_order
erpnext.patches.v12_0.set_cost_center_in_child_table_of_expense_claim erpnext.patches.v12_0.set_cost_center_in_child_table_of_expense_claim
erpnext.patches.v12_0.set_lead_title_field

View File

@ -0,0 +1,16 @@
import frappe
def execute():
employees = frappe.get_all("Employee",
filters={"prefered_email": ""},
fields=["name", "prefered_contact_email", "company_email", "personal_email", "user_id"])
for employee in employees:
preferred_email_field = frappe.scrub(employee.prefered_contact_email)
if not preferred_email_field:
continue
preferred_email = employee.get(preferred_email_field)
frappe.db.set_value("Employee", employee.name, "prefered_email", preferred_email, update_modified=False)

View File

@ -0,0 +1,11 @@
import frappe
def execute():
frappe.reload_doc("crm", "doctype", "lead")
frappe.db.sql("""
UPDATE
`tabLead`
SET
title = IF(organization_lead = 1, company_name, lead_name)
""")

View File

@ -1808,14 +1808,44 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
} }
}); });
erpnext.show_serial_batch_selector = function(frm, d, callback, on_close, show_dialog) { erpnext.show_serial_batch_selector = function (frm, d, callback, on_close, show_dialog) {
let warehouse, receiving_stock, existing_stock;
if (frm.doc.is_return) {
if (["Purchase Receipt", "Purchase Invoice"].includes(frm.doc.doctype)) {
existing_stock = true;
warehouse = d.warehouse;
} else if (["Delivery Note", "Sales Invoice"].includes(frm.doc.doctype)) {
receiving_stock = true;
}
} else {
if (frm.doc.doctype == "Stock Entry") {
if (frm.doc.purpose == "Material Receipt") {
receiving_stock = true;
} else {
existing_stock = true;
warehouse = d.s_warehouse;
}
} else {
existing_stock = true;
warehouse = d.warehouse;
}
}
if (!warehouse) {
if (receiving_stock) {
warehouse = ["like", ""];
} else if (existing_stock) {
warehouse = ["!=", ""];
}
}
frappe.require("assets/erpnext/js/utils/serial_no_batch_selector.js", function() { frappe.require("assets/erpnext/js/utils/serial_no_batch_selector.js", function() {
new erpnext.SerialNoBatchSelector({ new erpnext.SerialNoBatchSelector({
frm: frm, frm: frm,
item: d, item: d,
warehouse_details: { warehouse_details: {
type: "Warehouse", type: "Warehouse",
name: d.warehouse name: warehouse
}, },
callback: callback, callback: callback,
on_close: on_close on_close: on_close

View File

@ -0,0 +1,41 @@
function edit_details_dialog(params) {
let dialog = new frappe.ui.Dialog({
title: __('Update Details'),
fields: [
{
label: 'Item Name',
fieldname: 'item_name',
fieldtype: 'Data',
default: params.defaults.item_name,
reqd: 1
},
{
label: 'Hub Category',
fieldname: 'hub_category',
fieldtype: 'Autocomplete',
default: params.defaults.hub_category,
options: [],
reqd: 1
},
{
label: 'Description',
fieldname: 'description',
fieldtype: 'Text',
default: params.defaults.description,
options: [],
reqd: 1
}
],
primary_action_label: params.primary_action.label || __('Update Details'),
primary_action: params.primary_action.fn
});
hub.call('get_categories').then(categories => {
categories = categories.map(d => d.name);
dialog.fields_dict.hub_category.set_data(categories);
});
return dialog;
}
export { edit_details_dialog };

View File

@ -1,10 +1,5 @@
<template> <template>
<div <div class="marketplace-page" :data-page-name="page_name" v-if="init || item">
class="marketplace-page"
:data-page-name="page_name"
v-if="init || item"
>
<detail-view <detail-view
:title="title" :title="title"
:image="image" :image="image"
@ -12,20 +7,15 @@
:menu_items="menu_items" :menu_items="menu_items"
:show_skeleton="init" :show_skeleton="init"
> >
<detail-header-item slot="detail-header-item" <detail-header-item slot="detail-header-item" :value="item_subtitle"></detail-header-item>
:value="item_subtitle" <detail-header-item slot="detail-header-item" :value="item_views_and_ratings"></detail-header-item>
></detail-header-item>
<detail-header-item slot="detail-header-item"
:value="item_views_and_ratings"
></detail-header-item>
<button v-if="primary_action" slot="detail-header-item" <button
v-if="primary_action"
slot="detail-header-item"
class="btn btn-primary btn-sm margin-top" class="btn btn-primary btn-sm margin-top"
@click="primary_action.action" @click="primary_action.action"
> >{{ primary_action.label }}</button>
{{ primary_action.label }}
</button>
</detail-view> </detail-view>
<review-area v-if="!init" :hub_item_name="hub_item_name"></review-area> <review-area v-if="!init" :hub_item_name="hub_item_name"></review-area>
@ -35,6 +25,7 @@
<script> <script>
import ReviewArea from '../components/ReviewArea.vue'; import ReviewArea from '../components/ReviewArea.vue';
import { get_rating_html } from '../components/reviews'; import { get_rating_html } from '../components/reviews';
import { edit_details_dialog } from '../components/edit_details_dialog';
export default { export default {
name: 'item-page', name: 'item-page',
@ -51,21 +42,20 @@ export default {
item: null, item: null,
title: null, title: null,
image: null, image: null,
sections: [], sections: []
}; };
}, },
computed: { computed: {
is_own_item() { is_own_item() {
let is_own_item = false; let is_own_item = false;
if(this.item) { if (this.item) {
if(this.item.hub_seller === hub.settings.hub_seller_name) { if (this.item.hub_seller === hub.settings.hub_seller_name) {
is_own_item = true; is_own_item = true;
} }
} }
return is_own_item; return is_own_item;
}, },
menu_items(){ menu_items() {
return [ return [
{ {
label: __('Save Item'), label: __('Save Item'),
@ -92,11 +82,11 @@ export default {
condition: hub.is_user_registered() && this.is_own_item, condition: hub.is_user_registered() && this.is_own_item,
action: this.unpublish_item action: this.unpublish_item
} }
] ];
}, },
item_subtitle() { item_subtitle() {
if(!this.item) { if (!this.item) {
return ''; return '';
} }
@ -105,25 +95,31 @@ export default {
const rating = this.item.average_rating; const rating = this.item.average_rating;
if (rating > 0) { if (rating > 0) {
subtitle_items.push(rating + `<i class='fa fa-fw fa-star-o'></i>`) subtitle_items.push(rating + `<i class='fa fa-fw fa-star-o'></i>`);
} }
subtitle_items.push({value:this.item.company,on_click:this.go_to_seller_profile_page}); subtitle_items.push({
value: this.item.company,
on_click: this.go_to_seller_profile_page
});
return subtitle_items; return subtitle_items;
}, },
item_views_and_ratings() { item_views_and_ratings() {
if(!this.item) { if (!this.item) {
return ''; return '';
} }
let stats = __('No views yet'); let stats = __('No views yet');
if(this.item.view_count) { if (this.item.view_count) {
const views_message = __(`${this.item.view_count} Views`); const views_message = __(`${this.item.view_count} Views`);
const rating_html = get_rating_html(this.item.average_rating); const rating_html = get_rating_html(this.item.average_rating);
const rating_count = this.item.no_of_ratings > 0 ? `${this.item.no_of_ratings} reviews` : __('No reviews yet'); const rating_count =
this.item.no_of_ratings > 0
? `${this.item.no_of_ratings} reviews`
: __('No reviews yet');
stats = [views_message, rating_html, rating_count]; stats = [views_message, rating_html, rating_count];
} }
@ -136,7 +132,7 @@ export default {
return { return {
label: __('Contact Seller'), label: __('Contact Seller'),
action: this.contact_seller.bind(this) action: this.contact_seller.bind(this)
} };
} else { } else {
return undefined; return undefined;
} }
@ -156,7 +152,7 @@ export default {
setTimeout(() => { setTimeout(() => {
hub.call('add_item_view', { hub.call('add_item_view', {
hub_item_name: this.hub_item_name hub_item_name: this.hub_item_name
}) });
// .then(() => { // .then(() => {
// erpnext.hub.item_view_cache.push(this.hub_item_name); // erpnext.hub.item_view_cache.push(this.hub_item_name);
// }); // });
@ -167,12 +163,12 @@ export default {
get_item_details() { get_item_details() {
this.item_received = hub.call('get_item_details', { hub_item_name: this.hub_item_name }) this.item_received = hub.call('get_item_details', { hub_item_name: this.hub_item_name })
.then(item => { .then(item => {
this.init = false; this.init = false;
this.item = item; this.item = item;
this.build_data(); this.build_data();
this.make_dialogs(); this.make_dialogs();
}); });
}, },
go_to_seller_profile_page(seller_name) { go_to_seller_profile_page(seller_name) {
frappe.set_route(`marketplace/seller/${seller_name}`); frappe.set_route(`marketplace/seller/${seller_name}`);
@ -200,36 +196,41 @@ export default {
make_dialogs() { make_dialogs() {
this.make_contact_seller_dialog(); this.make_contact_seller_dialog();
this.make_report_item_dialog(); this.make_report_item_dialog();
this.make_editing_dialog();
}, },
add_to_saved_items() { add_to_saved_items() {
hub.call('add_item_to_user_saved_items', { hub.call('add_item_to_user_saved_items', {
hub_item_name: this.hub_item_name, hub_item_name: this.hub_item_name,
hub_user: frappe.session.user hub_user: frappe.session.user
}) })
.then(() => { .then(() => {
const saved_items_link = `<b><a href="#marketplace/saved-items">${__('Saved')}</a></b>` const saved_items_link = `<b><a href="#marketplace/saved-items">${__(
frappe.show_alert(saved_items_link); 'Saved'
erpnext.hub.trigger('action:item_save'); )}</a></b>`;
}) frappe.show_alert(saved_items_link);
.catch(e => { erpnext.hub.trigger('action:item_save');
console.error(e); })
}); .catch(e => {
console.error(e);
});
}, },
add_to_featured_items() { add_to_featured_items() {
hub.call('add_item_to_seller_featured_items', { hub.call('add_item_to_seller_featured_items', {
hub_item_name: this.hub_item_name, hub_item_name: this.hub_item_name,
hub_user: frappe.session.user hub_user: frappe.session.user
},) })
.then(() => { .then(() => {
const featured_items_link = `<b><a href="#marketplace/featured-items">${__('Added to Featured Items')}</a></b>` const featured_items_link = `<b><a href="#marketplace/featured-items">${__(
frappe.show_alert(featured_items_link); 'Added to Featured Items'
erpnext.hub.trigger('action:item_feature'); )}</a></b>`;
}) frappe.show_alert(featured_items_link);
.catch(e => { erpnext.hub.trigger('action:item_feature');
console.error(e); })
}); .catch(e => {
console.error(e);
});
}, },
make_contact_seller_dialog() { make_contact_seller_dialog() {
@ -252,13 +253,13 @@ export default {
if (!message) return; if (!message) return;
hub.call('send_message', { hub.call('send_message', {
hub_item: this.item.name, hub_item: this.item.name,
message message
}) })
.then(() => { .then(() => {
this.contact_seller_dialog.hide(); this.contact_seller_dialog.hide();
frappe.set_route('marketplace', 'buying', this.item.name); frappe.set_route('marketplace', 'buying', this.item.name);
erpnext.hub.trigger('action:send_message') erpnext.hub.trigger('action:send_message');
}); });
} }
}); });
@ -275,7 +276,10 @@ export default {
} }
], ],
primary_action: ({ message }) => { primary_action: ({ message }) => {
hub.call('add_reported_item', { hub_item_name: this.item.name, message }) hub.call('add_reported_item', {
hub_item_name: this.item.name,
message
})
.then(() => { .then(() => {
d.hide(); d.hide();
frappe.show_alert(__('Item Reported')); frappe.show_alert(__('Item Reported'));
@ -284,26 +288,62 @@ export default {
}); });
}, },
make_editing_dialog() {
this.edit_dialog = edit_details_dialog({
primary_action: {
fn: values => {
this.update_details(values);
this.edit_dialog.hide();
}
},
defaults: {
item_name: this.item.item_name,
hub_category: this.item.hub_category,
description: this.item.description
}
});
},
update_details(values) {
frappe.call('erpnext.hub_node.api.update_item', {
ref_doc: this.item.name,
data: values
})
.then(r => {
return this.get_item_details();
})
.then(() => {
frappe.show_alert(__(`${this.item.item_name} Updated`));
});
},
contact_seller() { contact_seller() {
this.contact_seller_dialog.show(); this.contact_seller_dialog.show();
}, },
report_item() { report_item() {
if (!hub.is_seller_registered()) { if (!hub.is_seller_registered()) {
frappe.throw(__('Please login as a Marketplace User to report this item.')); frappe.throw(
__('Please login as a Marketplace User to report this item.')
);
} }
this.report_item_dialog.show(); this.report_item_dialog.show();
}, },
edit_details() { edit_details() {
frappe.msgprint(__('This feature is under development...')); if (!hub.is_seller_registered()) {
frappe.throw(
__('Please login as a Marketplace User to edit this item.')
);
}
this.edit_dialog.show();
}, },
unpublish_item() { unpublish_item() {
frappe.msgprint(__('This feature is under development...')); frappe.msgprint(__('This feature is under development...'));
} }
} }
} };
</script> </script>
<style scoped></style> <style scoped></style>

View File

@ -1,23 +1,22 @@
<div class="clearfix"></div> <div class="clearfix"></div>
{% for(var i=0, l=addr_list.length; i<l; i++) { %} {% for(var i=0, l=addr_list.length; i<l; i++) { %}
<div class="address-box"> <div class="address-box">
<p class="h6"> <p class="h6">
{%= i+1 %}. {%= addr_list[i].address_type!="Other" ? __(addr_list[i].address_type) : addr_list[i].address_title %} {%= i+1 %}. {%= addr_list[i].address_title %}{% if(addr_list[i].address_type!="Other") { %}
{% if(addr_list[i].is_primary_address) { %} <span class="text-muted">({%= __(addr_list[i].address_type) %})</span>{% } %}
<span class="text-muted">({%= __("Primary") %})</span>{% } %} {% if(addr_list[i].is_primary_address) { %}
{% if(addr_list[i].is_shipping_address) { %} <span class="text-muted">({%= __("Primary") %})</span>{% } %}
<span class="text-muted">({%= __("Shipping") %})</span>{% } %} {% if(addr_list[i].is_shipping_address) { %}
<span class="text-muted">({%= __("Shipping") %})</span>{% } %}
<a href="#Form/Address/{%= encodeURIComponent(addr_list[i].name) %}" <a href="#Form/Address/{%= encodeURIComponent(addr_list[i].name) %}" class="btn btn-default btn-xs pull-right"
class="btn btn-default btn-xs pull-right" style="margin-top:-3px; margin-right: -5px;">
style="margin-top:-3px; margin-right: -5px;"> {%= __("Edit") %}</a>
{%= __("Edit") %}</a> </p>
</p> <p>{%= addr_list[i].display %}</p>
<p>{%= addr_list[i].display %}</p> </div>
</div>
{% } %} {% } %}
{% if(!addr_list.length) { %} {% if(!addr_list.length) { %}
<p class="text-muted small">{%= __("No address added yet.") %}</p> <p class="text-muted small">{%= __("No address added yet.") %}</p>
{% } %} {% } %}
<p><button class="btn btn-xs btn-default btn-address">{{ __("New Address") }}</button></p> <p><button class="btn btn-xs btn-default btn-address">{{ __("New Address") }}</button></p>

View File

@ -458,7 +458,8 @@ erpnext.utils.update_child_items = function(opts) {
fieldname:"item_code", fieldname:"item_code",
options: 'Item', options: 'Item',
in_list_view: 1, in_list_view: 1,
read_only: 1, read_only: 0,
disabled: 0,
label: __('Item Code') label: __('Item Code')
}, { }, {
fieldtype:'Float', fieldtype:'Float',

View File

@ -389,12 +389,14 @@ erpnext.SerialNoBatchSelector = Class.extend({
let serial_no_filters = { let serial_no_filters = {
item_code: me.item_code, item_code: me.item_code,
batch_no: this.doc.batch_no || null,
delivery_document_no: "" delivery_document_no: ""
} }
if (me.warehouse_details.name) { if (me.warehouse_details.name) {
serial_no_filters['warehouse'] = me.warehouse_details.name; serial_no_filters['warehouse'] = me.warehouse_details.name;
} }
return [ return [
{fieldtype: 'Section Break', label: __('Serial Numbers')}, {fieldtype: 'Section Break', label: __('Serial Numbers')},
{ {

View File

@ -26,7 +26,6 @@ class Quotation(SellingController):
super(Quotation, self).validate() super(Quotation, self).validate()
self.set_status() self.set_status()
self.update_opportunity() self.update_opportunity()
self.validate_order_type()
self.validate_uom_is_integer("stock_uom", "qty") self.validate_uom_is_integer("stock_uom", "qty")
self.validate_valid_till() self.validate_valid_till()
self.set_customer_name() self.set_customer_name()
@ -40,9 +39,6 @@ class Quotation(SellingController):
def has_sales_order(self): def has_sales_order(self):
return frappe.db.get_value("Sales Order Item", {"prevdoc_docname": self.name, "docstatus": 1}) return frappe.db.get_value("Sales Order Item", {"prevdoc_docname": self.name, "docstatus": 1})
def validate_order_type(self):
super(Quotation, self).validate_order_type()
def update_lead(self): def update_lead(self):
if self.quotation_to == "Lead" and self.party_name: if self.quotation_to == "Lead" and self.party_name:
frappe.get_doc("Lead", self.party_name).set_status(update=True) frappe.get_doc("Lead", self.party_name).set_status(update=True)

View File

@ -34,7 +34,6 @@ class SalesOrder(SellingController):
def validate(self): def validate(self):
super(SalesOrder, self).validate() super(SalesOrder, self).validate()
self.validate_order_type()
self.validate_delivery_date() self.validate_delivery_date()
self.validate_proj_cust() self.validate_proj_cust()
self.validate_po() self.validate_po()
@ -100,9 +99,6 @@ class SalesOrder(SellingController):
frappe.msgprint(_("Quotation {0} not of type {1}") frappe.msgprint(_("Quotation {0} not of type {1}")
.format(d.prevdoc_docname, self.order_type)) .format(d.prevdoc_docname, self.order_type))
def validate_order_type(self):
super(SalesOrder, self).validate_order_type()
def validate_delivery_date(self): def validate_delivery_date(self):
if self.order_type == 'Sales' and not self.skip_delivery_note: if self.order_type == 'Sales' and not self.skip_delivery_note:
delivery_date_list = [d.delivery_date for d in self.get("items") if d.delivery_date] delivery_date_list = [d.delivery_date for d in self.get("items") if d.delivery_date]

View File

@ -321,7 +321,12 @@ class TestSalesOrder(unittest.TestCase):
create_dn_against_so(so.name, 4) create_dn_against_so(so.name, 4)
make_sales_invoice(so.name) make_sales_invoice(so.name)
trans_item = json.dumps([{'item_code' : '_Test Item 2', 'rate' : 200, 'qty' : 7}]) first_item_of_so = so.get("items")[0]
trans_item = json.dumps([
{'item_code' : first_item_of_so.item_code, 'rate' : first_item_of_so.rate, \
'qty' : first_item_of_so.qty, 'docname': first_item_of_so.name},
{'item_code' : '_Test Item 2', 'rate' : 200, 'qty' : 7}
])
update_child_qty_rate('Sales Order', trans_item, so.name) update_child_qty_rate('Sales Order', trans_item, so.name)
so.reload() so.reload()
@ -330,6 +335,48 @@ class TestSalesOrder(unittest.TestCase):
self.assertEqual(so.get("items")[-1].qty, 7) self.assertEqual(so.get("items")[-1].qty, 7)
self.assertEqual(so.get("items")[-1].amount, 1400) self.assertEqual(so.get("items")[-1].amount, 1400)
self.assertEqual(so.status, 'To Deliver and Bill') self.assertEqual(so.status, 'To Deliver and Bill')
def test_remove_item_in_update_child_qty_rate(self):
so = make_sales_order(**{
"item_list": [{
"item_code": '_Test Item',
"qty": 5,
"rate":1000
}]
})
create_dn_against_so(so.name, 2)
make_sales_invoice(so.name)
# add an item so as to try removing items
trans_item = json.dumps([
{"item_code": '_Test Item', "qty": 5, "rate":1000, "docname": so.get("items")[0].name},
{"item_code": '_Test Item 2', "qty": 2, "rate":500}
])
update_child_qty_rate('Sales Order', trans_item, so.name)
so.reload()
self.assertEqual(len(so.get("items")), 2)
# check if delivered items can be removed
trans_item = json.dumps([{
"item_code": '_Test Item 2',
"qty": 2,
"rate":500,
"docname": so.get("items")[1].name
}])
self.assertRaises(frappe.ValidationError, update_child_qty_rate, 'Sales Order', trans_item, so.name)
#remove last added item
trans_item = json.dumps([{
"item_code": '_Test Item',
"qty": 5,
"rate":1000,
"docname": so.get("items")[0].name
}])
update_child_qty_rate('Sales Order', trans_item, so.name)
so.reload()
self.assertEqual(len(so.get("items")), 1)
self.assertEqual(so.status, 'To Deliver and Bill')
def test_update_child_qty_rate(self): def test_update_child_qty_rate(self):

View File

@ -77,10 +77,12 @@
"ordered_qty", "ordered_qty",
"planned_qty", "planned_qty",
"column_break_69", "column_break_69",
"delivered_qty",
"work_order_qty", "work_order_qty",
"delivered_qty",
"produced_qty", "produced_qty",
"returned_qty", "returned_qty",
"shopping_cart_section",
"additional_notes",
"section_break_63", "section_break_63",
"page_break", "page_break",
"item_tax_rate", "item_tax_rate",
@ -746,15 +748,20 @@
"label": "Image" "label": "Image"
}, },
{ {
"default": "0", "collapsible": 1,
"fieldname": "against_blanket_order", "fieldname": "shopping_cart_section",
"fieldtype": "Check", "fieldtype": "Section Break",
"label": "Against Blanket Order" "label": "Shopping Cart"
},
{
"fieldname": "additional_notes",
"fieldtype": "Text",
"label": "Additional Notes"
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"modified": "2019-11-19 14:19:29.491945", "modified": "2019-12-11 18:06:26.238169",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Sales Order Item", "name": "Sales Order Item",

View File

@ -261,15 +261,14 @@ def get_batch_no(item_code, warehouse, qty=1, throw=False):
def get_batches(item_code, warehouse, qty=1, throw=False): def get_batches(item_code, warehouse, qty=1, throw=False):
batches = frappe.db.sql(
'select batch_id, sum(actual_qty) as qty from `tabBatch` join `tabStock Ledger Entry` ignore index (item_code, warehouse) '
'on (`tabBatch`.batch_id = `tabStock Ledger Entry`.batch_no )'
'where `tabStock Ledger Entry`.item_code = %s and `tabStock Ledger Entry`.warehouse = %s '
'and (`tabBatch`.expiry_date >= CURDATE() or `tabBatch`.expiry_date IS NULL)'
'group by batch_id '
'order by `tabBatch`.expiry_date ASC, `tabBatch`.creation ASC',
(item_code, warehouse),
as_dict=True
)
return batches return frappe.db.sql("""
select batch_id, sum(`tabStock Ledger Entry`.actual_qty) as qty
from `tabBatch`
join `tabStock Ledger Entry` ignore index (item_code, warehouse)
on (`tabBatch`.batch_id = `tabStock Ledger Entry`.batch_no )
where `tabStock Ledger Entry`.item_code = %s and `tabStock Ledger Entry`.warehouse = %s
and (`tabBatch`.expiry_date >= CURDATE() or `tabBatch`.expiry_date IS NULL)
group by batch_id
order by `tabBatch`.expiry_date ASC, `tabBatch`.creation ASC
""", (item_code, warehouse), as_dict=True)

View File

@ -79,6 +79,21 @@ frappe.ui.form.on('Delivery Trip', {
}, () => { }, () => {
frm.reload_doc(); frm.reload_doc();
}); });
},
driver: function (frm) {
if (frm.doc.driver) {
frappe.call({
method: "erpnext.stock.doctype.delivery_trip.delivery_trip.get_driver_email",
args: {
driver: frm.doc.driver
},
callback: (data) => {
frm.set_value("driver_email", data.message.email);
}
});
};
},
}, },
@ -196,4 +211,4 @@ frappe.ui.form.on('Delivery Stop', {
frappe.model.set_value(cdt, cdn, "customer_contact", ""); frappe.model.set_value(cdt, cdn, "customer_contact", "");
} }
} }
}); });

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"autoname": "naming_series:", "autoname": "naming_series:",
"creation": "2017-10-16 16:45:48.293335", "creation": "2017-10-16 16:45:48.293335",
"doctype": "DocType", "doctype": "DocType",
@ -13,6 +14,7 @@
"section_break_3", "section_break_3",
"driver", "driver",
"driver_name", "driver_name",
"driver_email",
"driver_address", "driver_address",
"total_distance", "total_distance",
"uom", "uom",
@ -167,10 +169,17 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Driver Address", "label": "Driver Address",
"options": "Address" "options": "Address"
},
{
"fieldname": "driver_email",
"fieldtype": "Data",
"label": "Driver Email",
"read_only": 1
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"modified": "2019-09-27 15:43:01.975139", "links": [],
"modified": "2019-12-06 17:06:59.681952",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Delivery Trip", "name": "Delivery Trip",

View File

@ -387,3 +387,9 @@ def get_attachments(delivery_stop):
file_name="Delivery Note", print_format=dispatch_attachment) file_name="Delivery Note", print_format=dispatch_attachment)
return [attachments] return [attachments]
@frappe.whitelist()
def get_driver_email(driver):
employee = frappe.db.get_value("Driver", driver, "employee")
email = frappe.db.get_value("Employee", employee, "prefered_email")
return {"email": email}

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"allow_import": 1, "allow_import": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
"creation": "2013-05-21 16:16:39", "creation": "2013-05-21 16:16:39",
@ -305,6 +306,7 @@
"fieldname": "contact_email", "fieldname": "contact_email",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Contact Email", "label": "Contact Email",
"options": "Email",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
@ -1056,7 +1058,8 @@
"icon": "fa fa-truck", "icon": "fa fa-truck",
"idx": 261, "idx": 261,
"is_submittable": 1, "is_submittable": 1,
"modified": "2019-09-27 14:24:49.044505", "links": [],
"modified": "2019-12-24 12:52:17.216304",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Purchase Receipt", "name": "Purchase Receipt",

View File

@ -88,7 +88,7 @@ class PurchaseReceipt(BuyingController):
if getdate(self.posting_date) > getdate(nowdate()): if getdate(self.posting_date) > getdate(nowdate()):
throw(_("Posting Date cannot be future date")) throw(_("Posting Date cannot be future date"))
def validate_cwip_accounts(self): def validate_cwip_accounts(self):
for item in self.get('items'): for item in self.get('items'):
if item.is_fixed_asset and is_cwip_accounting_enabled(item.asset_category): if item.is_fixed_asset and is_cwip_accounting_enabled(item.asset_category):
@ -362,7 +362,7 @@ class PurchaseReceipt(BuyingController):
# valuation rate is total of net rate, raw mat supp cost, tax amount, lcv amount per item # valuation rate is total of net rate, raw mat supp cost, tax amount, lcv amount per item
self.update_assets(item, item.valuation_rate) self.update_assets(item, item.valuation_rate)
return gl_entries return gl_entries
def add_asset_gl_entries(self, item, gl_entries): def add_asset_gl_entries(self, item, gl_entries):
arbnb_account = self.get_company_default("asset_received_but_not_billed") arbnb_account = self.get_company_default("asset_received_but_not_billed")
# This returns category's cwip account if not then fallback to company's default cwip account # This returns category's cwip account if not then fallback to company's default cwip account
@ -395,7 +395,7 @@ class PurchaseReceipt(BuyingController):
"credit_in_account_currency": (base_asset_amount "credit_in_account_currency": (base_asset_amount
if asset_rbnb_currency == self.company_currency else asset_amount) if asset_rbnb_currency == self.company_currency else asset_amount)
}, item=item)) }, item=item))
def add_lcv_gl_entries(self, item, gl_entries): def add_lcv_gl_entries(self, item, gl_entries):
expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation") expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation")
if not is_cwip_accounting_enabled(item.asset_category): if not is_cwip_accounting_enabled(item.asset_category):
@ -404,7 +404,7 @@ class PurchaseReceipt(BuyingController):
else: else:
# This returns company's default cwip account # This returns company's default cwip account
asset_account = get_asset_account("capital_work_in_progress_account", company=self.company) asset_account = get_asset_account("capital_work_in_progress_account", company=self.company)
gl_entries.append(self.get_gl_dict({ gl_entries.append(self.get_gl_dict({
"account": expenses_included_in_asset_valuation, "account": expenses_included_in_asset_valuation,
"against": asset_account, "against": asset_account,
@ -424,7 +424,7 @@ class PurchaseReceipt(BuyingController):
}, item=item)) }, item=item))
def update_assets(self, item, valuation_rate): def update_assets(self, item, valuation_rate):
assets = frappe.db.get_all('Asset', assets = frappe.db.get_all('Asset',
filters={ 'purchase_receipt': self.name, 'item_code': item.item_code } filters={ 'purchase_receipt': self.name, 'item_code': item.item_code }
) )
@ -610,27 +610,36 @@ def make_stock_entry(source_name,target_doc=None):
return doclist return doclist
def get_item_account_wise_additional_cost(purchase_document): def get_item_account_wise_additional_cost(purchase_document):
landed_cost_voucher = frappe.get_value("Landed Cost Purchase Receipt", landed_cost_vouchers = frappe.get_all("Landed Cost Purchase Receipt", fields=["parent"],
{"receipt_document": purchase_document, "docstatus": 1}, "parent") filters = {"receipt_document": purchase_document, "docstatus": 1})
if not landed_cost_voucher: if not landed_cost_vouchers:
return return
total_item_cost = 0 total_item_cost = 0
item_account_wise_cost = {} item_account_wise_cost = {}
landed_cost_voucher_doc = frappe.get_doc("Landed Cost Voucher", landed_cost_voucher) item_cost_allocated = []
based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on)
for item in landed_cost_voucher_doc.items: for lcv in landed_cost_vouchers:
total_item_cost += item.get(based_on_field) landed_cost_voucher_doc = frappe.get_cached_doc("Landed Cost Voucher", lcv.parent)
based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on)
for item in landed_cost_voucher_doc.items: for item in landed_cost_voucher_doc.items:
if item.receipt_document == purchase_document: if item.purchase_receipt_item not in item_cost_allocated:
for account in landed_cost_voucher_doc.taxes: total_item_cost += item.get(based_on_field)
item_account_wise_cost.setdefault((item.item_code, item.purchase_receipt_item), {}) item_cost_allocated.append(item.purchase_receipt_item)
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)].setdefault(account.expense_account, 0.0)
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account] += \ for lcv in landed_cost_vouchers:
account.amount * item.get(based_on_field) / total_item_cost landed_cost_voucher_doc = frappe.get_cached_doc("Landed Cost Voucher", lcv.parent)
based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on)
for item in landed_cost_voucher_doc.items:
if item.receipt_document == purchase_document:
for account in landed_cost_voucher_doc.taxes:
item_account_wise_cost.setdefault((item.item_code, item.purchase_receipt_item), {})
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)].setdefault(account.expense_account, 0.0)
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account] += \
account.amount * item.get(based_on_field) / total_item_cost
return item_account_wise_cost return item_account_wise_cost

View File

@ -808,24 +808,26 @@ class StockEntry(StockController):
if self.bom_no: if self.bom_no:
backflush_based_on = frappe.db.get_single_value("Manufacturing Settings",
"backflush_raw_materials_based_on")
if self.purpose in ["Material Issue", "Material Transfer", "Manufacture", "Repack", if self.purpose in ["Material Issue", "Material Transfer", "Manufacture", "Repack",
"Send to Subcontractor", "Material Transfer for Manufacture", "Material Consumption for Manufacture"]: "Send to Subcontractor", "Material Transfer for Manufacture", "Material Consumption for Manufacture"]:
if self.work_order and self.purpose == "Material Transfer for Manufacture": if self.work_order and self.purpose == "Material Transfer for Manufacture":
item_dict = self.get_pending_raw_materials() item_dict = self.get_pending_raw_materials(backflush_based_on)
if self.to_warehouse and self.pro_doc: if self.to_warehouse and self.pro_doc:
for item in itervalues(item_dict): for item in itervalues(item_dict):
item["to_warehouse"] = self.pro_doc.wip_warehouse item["to_warehouse"] = self.pro_doc.wip_warehouse
self.add_to_stock_entry_detail(item_dict) self.add_to_stock_entry_detail(item_dict)
elif (self.work_order and (self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture") elif (self.work_order and (self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture")
and not self.pro_doc.skip_transfer and frappe.db.get_single_value("Manufacturing Settings", and not self.pro_doc.skip_transfer and backflush_based_on == "Material Transferred for Manufacture"):
"backflush_raw_materials_based_on")== "Material Transferred for Manufacture"):
self.get_transfered_raw_materials() self.get_transfered_raw_materials()
elif self.work_order and (self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture") and \ elif (self.work_order and backflush_based_on== "BOM" and
frappe.db.get_single_value("Manufacturing Settings", "backflush_raw_materials_based_on")== "BOM" and \ (self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture")
frappe.db.get_single_value("Manufacturing Settings", "material_consumption")== 1: and frappe.db.get_single_value("Manufacturing Settings", "material_consumption")== 1):
self.get_unconsumed_raw_materials() self.get_unconsumed_raw_materials()
else: else:
@ -1034,10 +1036,6 @@ class StockEntry(StockController):
filters={'parent': self.work_order, 'item_code': item_code}, filters={'parent': self.work_order, 'item_code': item_code},
fields=["required_qty", "consumed_qty"] fields=["required_qty", "consumed_qty"]
) )
if not req_items:
frappe.msgprint(_("Did not found transfered item {0} in Work Order {1}, the item not added in Stock Entry")
.format(item_code, self.work_order))
continue
req_qty = flt(req_items[0].required_qty) req_qty = flt(req_items[0].required_qty)
req_qty_each = flt(req_qty / manufacturing_qty) req_qty_each = flt(req_qty / manufacturing_qty)
@ -1085,18 +1083,20 @@ class StockEntry(StockController):
} }
}) })
def get_pending_raw_materials(self): def get_pending_raw_materials(self, backflush_based_on=None):
""" """
issue (item quantity) that is pending to issue or desire to transfer, issue (item quantity) that is pending to issue or desire to transfer,
whichever is less whichever is less
""" """
item_dict = self.get_pro_order_required_items() item_dict = self.get_pro_order_required_items(backflush_based_on)
max_qty = flt(self.pro_doc.qty) max_qty = flt(self.pro_doc.qty)
for item, item_details in iteritems(item_dict): for item, item_details in iteritems(item_dict):
pending_to_issue = flt(item_details.required_qty) - flt(item_details.transferred_qty) pending_to_issue = flt(item_details.required_qty) - flt(item_details.transferred_qty)
desire_to_transfer = flt(self.fg_completed_qty) * flt(item_details.required_qty) / max_qty desire_to_transfer = flt(self.fg_completed_qty) * flt(item_details.required_qty) / max_qty
if desire_to_transfer <= pending_to_issue: if (desire_to_transfer <= pending_to_issue or
(desire_to_transfer > 0 and backflush_based_on == "Material Transferred for Manufacture")):
item_dict[item]["qty"] = desire_to_transfer item_dict[item]["qty"] = desire_to_transfer
elif pending_to_issue > 0: elif pending_to_issue > 0:
item_dict[item]["qty"] = pending_to_issue item_dict[item]["qty"] = pending_to_issue
@ -1114,7 +1114,7 @@ class StockEntry(StockController):
return item_dict return item_dict
def get_pro_order_required_items(self): def get_pro_order_required_items(self, backflush_based_on=None):
item_dict = frappe._dict() item_dict = frappe._dict()
pro_order = frappe.get_doc("Work Order", self.work_order) pro_order = frappe.get_doc("Work Order", self.work_order)
if not frappe.db.get_value("Warehouse", pro_order.wip_warehouse, "is_group"): if not frappe.db.get_value("Warehouse", pro_order.wip_warehouse, "is_group"):
@ -1123,7 +1123,8 @@ class StockEntry(StockController):
wip_warehouse = None wip_warehouse = None
for d in pro_order.get("required_items"): for d in pro_order.get("required_items"):
if (flt(d.required_qty) > flt(d.transferred_qty) and if ( ((flt(d.required_qty) > flt(d.transferred_qty)) or
(backflush_based_on == "Material Transferred for Manufacture")) and
(d.include_item_in_manufacturing or self.purpose != "Material Transfer for Manufacture")): (d.include_item_in_manufacturing or self.purpose != "Material Transfer for Manufacture")):
item_row = d.as_dict() item_row = d.as_dict()
if d.source_warehouse and not frappe.db.get_value("Warehouse", d.source_warehouse, "is_group"): if d.source_warehouse and not frappe.db.get_value("Warehouse", d.source_warehouse, "is_group"):

View File

@ -16,6 +16,29 @@ frappe.query_reports["Batch-Wise Balance History"] = {
"fieldtype": "Date", "fieldtype": "Date",
"width": "80", "width": "80",
"default": frappe.datetime.get_today() "default": frappe.datetime.get_today()
},
{
"fieldname": "item",
"label": __("Item"),
"fieldtype": "Link",
"options": "Item",
"width": "80"
} }
] ],
"formatter": function (value, row, column, data, default_formatter) {
if (column.fieldname == "Batch" && data && !!data["Batch"]) {
value = data["Batch"];
column.link_onclick = "frappe.query_reports['Batch-Wise Balance History'].set_batch_route_to_stock_ledger(" + JSON.stringify(data) + ")";
}
value = default_formatter(value, row, column, data);
return value;
},
"set_batch_route_to_stock_ledger": function (data) {
frappe.route_options = {
"batch_no": data["Batch"]
};
frappe.set_route("query-report", "Stock Ledger");
}
} }

View File

@ -2,9 +2,11 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import flt, cint, getdate from frappe.utils import cint, flt, getdate
def execute(filters=None): def execute(filters=None):
if not filters: filters = {} if not filters: filters = {}
@ -17,29 +19,31 @@ def execute(filters=None):
data = [] data = []
for item in sorted(iwb_map): for item in sorted(iwb_map):
for wh in sorted(iwb_map[item]): if not filters.get("item") or filters.get("item") == item:
for batch in sorted(iwb_map[item][wh]): for wh in sorted(iwb_map[item]):
qty_dict = iwb_map[item][wh][batch] for batch in sorted(iwb_map[item][wh]):
if qty_dict.opening_qty or qty_dict.in_qty or qty_dict.out_qty or qty_dict.bal_qty: qty_dict = iwb_map[item][wh][batch]
data.append([item, item_map[item]["item_name"], item_map[item]["description"], wh, batch, if qty_dict.opening_qty or qty_dict.in_qty or qty_dict.out_qty or qty_dict.bal_qty:
flt(qty_dict.opening_qty, float_precision), flt(qty_dict.in_qty, float_precision), data.append([item, item_map[item]["item_name"], item_map[item]["description"], wh, batch,
flt(qty_dict.out_qty, float_precision), flt(qty_dict.bal_qty, float_precision), flt(qty_dict.opening_qty, float_precision), flt(qty_dict.in_qty, float_precision),
item_map[item]["stock_uom"] flt(qty_dict.out_qty, float_precision), flt(qty_dict.bal_qty, float_precision),
]) item_map[item]["stock_uom"]
])
return columns, data return columns, data
def get_columns(filters): def get_columns(filters):
"""return columns based on filters""" """return columns based on filters"""
columns = [_("Item") + ":Link/Item:100"] + [_("Item Name") + "::150"] + [_("Description") + "::150"] + \ columns = [_("Item") + ":Link/Item:100"] + [_("Item Name") + "::150"] + [_("Description") + "::150"] + \
[_("Warehouse") + ":Link/Warehouse:100"] + [_("Batch") + ":Link/Batch:100"] + [_("Opening Qty") + ":Float:90"] + \ [_("Warehouse") + ":Link/Warehouse:100"] + [_("Batch") + ":Link/Batch:100"] + [_("Opening Qty") + ":Float:90"] + \
[_("In Qty") + ":Float:80"] + [_("Out Qty") + ":Float:80"] + [_("Balance Qty") + ":Float:90"] + \ [_("In Qty") + ":Float:80"] + [_("Out Qty") + ":Float:80"] + [_("Balance Qty") + ":Float:90"] + \
[_("UOM") + "::90"] [_("UOM") + "::90"]
return columns return columns
def get_conditions(filters): def get_conditions(filters):
conditions = "" conditions = ""
if not filters.get("from_date"): if not filters.get("from_date"):
@ -52,7 +56,8 @@ def get_conditions(filters):
return conditions return conditions
#get all details
# get all details
def get_stock_ledger_entries(filters): def get_stock_ledger_entries(filters):
conditions = get_conditions(filters) conditions = get_conditions(filters)
return frappe.db.sql(""" return frappe.db.sql("""
@ -63,6 +68,7 @@ def get_stock_ledger_entries(filters):
order by item_code, warehouse""" % order by item_code, warehouse""" %
conditions, as_dict=1) conditions, as_dict=1)
def get_item_warehouse_batch_map(filters, float_precision): def get_item_warehouse_batch_map(filters, float_precision):
sle = get_stock_ledger_entries(filters) sle = get_stock_ledger_entries(filters)
iwb_map = {} iwb_map = {}
@ -90,6 +96,7 @@ def get_item_warehouse_batch_map(filters, float_precision):
return iwb_map return iwb_map
def get_item_details(filters): def get_item_details(filters):
item_map = {} item_map = {}
for d in frappe.db.sql("select name, item_name, description, stock_uom from tabItem", as_dict=1): for d in frappe.db.sql("select name, item_name, description, stock_uom from tabItem", as_dict=1):

View File

@ -77,7 +77,15 @@ frappe.query_reports["Stock Ledger"] = {
"fieldtype": "Link", "fieldtype": "Link",
"options": "UOM" "options": "UOM"
} }
] ],
"formatter": function (value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
if (column.fieldname == "out_qty" && data.out_qty < 0) {
value = "<span style='color:red'>" + value + "</span>";
}
return value;
},
} }
// $(function() { // $(function() {

View File

@ -2,9 +2,11 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _
from erpnext.stock.utils import update_included_uom_in_report from erpnext.stock.utils import update_included_uom_in_report
from frappe import _
def execute(filters=None): def execute(filters=None):
include_uom = filters.get("include_uom") include_uom = filters.get("include_uom")
@ -36,7 +38,22 @@ def execute(filters=None):
sle.update({ sle.update({
"qty_after_transaction": actual_qty, "qty_after_transaction": actual_qty,
"stock_value": stock_value "stock_value": stock_value,
"in_qty": max(sle.actual_qty, 0),
"out_qty": min(sle.actual_qty, 0)
})
# get the name of the item that was produced using this item
if sle.voucher_type == "Stock Entry":
purpose, work_order, fg_completed_qty = frappe.db.get_value(sle.voucher_type, sle.voucher_no, ["purpose", "work_order", "fg_completed_qty"])
if purpose == "Manufacture" and work_order:
finished_product = frappe.db.get_value("Work Order", work_order, "item_name")
finished_qty = fg_completed_qty
sle.update({
"finished_product": finished_product,
"finished_qty": finished_qty,
}) })
data.append(sle) data.append(sle)
@ -47,53 +64,74 @@ def execute(filters=None):
update_included_uom_in_report(columns, data, include_uom, conversion_factors) update_included_uom_in_report(columns, data, include_uom, conversion_factors)
return columns, data return columns, data
def get_columns(): def get_columns():
columns = [ columns = [
{"label": _("Date"), "fieldname": "date", "fieldtype": "Datetime", "width": 95}, {"label": _("Date"), "fieldname": "date", "fieldtype": "Datetime", "width": 150},
{"label": _("Item"), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", "width": 130}, {"label": _("Item"), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", "width": 100},
{"label": _("Item Name"), "fieldname": "item_name", "width": 100}, {"label": _("Item Name"), "fieldname": "item_name", "width": 100},
{"label": _("Stock UOM"), "fieldname": "stock_uom", "fieldtype": "Link", "options": "UOM", "width": 90},
{"label": _("In Qty"), "fieldname": "in_qty", "fieldtype": "Float", "width": 80, "convertible": "qty"},
{"label": _("Out Qty"), "fieldname": "out_qty", "fieldtype": "Float", "width": 80, "convertible": "qty"},
{"label": _("Balance Qty"), "fieldname": "qty_after_transaction", "fieldtype": "Float", "width": 100, "convertible": "qty"},
{"label": _("Finished Product"), "fieldname": "finished_product", "width": 100},
{"label": _("Finished Qty"), "fieldname": "finished_qty", "fieldtype": "Float", "width": 100, "convertible": "qty"},
{"label": _("Voucher #"), "fieldname": "voucher_no", "fieldtype": "Dynamic Link", "options": "voucher_type", "width": 150},
{"label": _("Warehouse"), "fieldname": "warehouse", "fieldtype": "Link", "options": "Warehouse", "width": 150},
{"label": _("Item Group"), "fieldname": "item_group", "fieldtype": "Link", "options": "Item Group", "width": 100}, {"label": _("Item Group"), "fieldname": "item_group", "fieldtype": "Link", "options": "Item Group", "width": 100},
{"label": _("Brand"), "fieldname": "brand", "fieldtype": "Link", "options": "Brand", "width": 100}, {"label": _("Brand"), "fieldname": "brand", "fieldtype": "Link", "options": "Brand", "width": 100},
{"label": _("Description"), "fieldname": "description", "width": 200}, {"label": _("Description"), "fieldname": "description", "width": 200},
{"label": _("Warehouse"), "fieldname": "warehouse", "fieldtype": "Link", "options": "Warehouse", "width": 100}, {"label": _("Incoming Rate"), "fieldname": "incoming_rate", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency", "convertible": "rate"},
{"label": _("Stock UOM"), "fieldname": "stock_uom", "fieldtype": "Link", "options": "UOM", "width": 100}, {"label": _("Valuation Rate"), "fieldname": "valuation_rate", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency", "convertible": "rate"},
{"label": _("Qty"), "fieldname": "actual_qty", "fieldtype": "Float", "width": 50, "convertible": "qty"}, {"label": _("Balance Value"), "fieldname": "stock_value", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency"},
{"label": _("Balance Qty"), "fieldname": "qty_after_transaction", "fieldtype": "Float", "width": 100, "convertible": "qty"},
{"label": _("Incoming Rate"), "fieldname": "incoming_rate", "fieldtype": "Currency", "width": 110,
"options": "Company:company:default_currency", "convertible": "rate"},
{"label": _("Valuation Rate"), "fieldname": "valuation_rate", "fieldtype": "Currency", "width": 110,
"options": "Company:company:default_currency", "convertible": "rate"},
{"label": _("Balance Value"), "fieldname": "stock_value", "fieldtype": "Currency", "width": 110,
"options": "Company:company:default_currency"},
{"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 110}, {"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 110},
{"label": _("Voucher #"), "fieldname": "voucher_no", "fieldtype": "Dynamic Link", "options": "voucher_type", "width": 100}, {"label": _("Voucher #"), "fieldname": "voucher_no", "fieldtype": "Dynamic Link", "options": "voucher_type", "width": 100},
{"label": _("Batch"), "fieldname": "batch_no", "fieldtype": "Link", "options": "Batch", "width": 100}, {"label": _("Batch"), "fieldname": "batch_no", "fieldtype": "Link", "options": "Batch", "width": 100},
{"label": _("Serial #"), "fieldname": "serial_no", "width": 100}, {"label": _("Serial #"), "fieldname": "serial_no", "fieldtype": "Link", "options": "Serial No", "width": 100},
{"label": _("Project"), "fieldname": "project", "fieldtype": "Link", "options": "Project", "width": 100}, {"label": _("Project"), "fieldname": "project", "fieldtype": "Link", "options": "Project", "width": 100},
{"label": _("Company"), "fieldname": "company", "fieldtype": "Link", "options": "Company", "width": 110} {"label": _("Company"), "fieldname": "company", "fieldtype": "Link", "options": "Company", "width": 110}
] ]
return columns return columns
def get_stock_ledger_entries(filters, items): def get_stock_ledger_entries(filters, items):
item_conditions_sql = '' item_conditions_sql = ''
if items: if items:
item_conditions_sql = 'and sle.item_code in ({})'\ item_conditions_sql = 'and sle.item_code in ({})'\
.format(', '.join([frappe.db.escape(i) for i in items])) .format(', '.join([frappe.db.escape(i) for i in items]))
return frappe.db.sql("""select concat_ws(" ", posting_date, posting_time) as date, sl_entries = frappe.db.sql("""
item_code, warehouse, actual_qty, qty_after_transaction, incoming_rate, valuation_rate, SELECT
stock_value, voucher_type, voucher_no, batch_no, serial_no, company, project, stock_value_difference concat_ws(" ", posting_date, posting_time) AS date,
from `tabStock Ledger Entry` sle item_code,
where company = %(company)s and warehouse,
posting_date between %(from_date)s and %(to_date)s actual_qty,
{sle_conditions} qty_after_transaction,
{item_conditions_sql} incoming_rate,
order by posting_date asc, posting_time asc, creation asc"""\ valuation_rate,
.format( stock_value,
sle_conditions=get_sle_conditions(filters), voucher_type,
item_conditions_sql = item_conditions_sql voucher_no,
), filters, as_dict=1) batch_no,
serial_no,
company,
project,
stock_value_difference
FROM
`tabStock Ledger Entry` sle
WHERE
company = %(company)s
AND posting_date BETWEEN %(from_date)s AND %(to_date)s
{sle_conditions}
{item_conditions_sql}
ORDER BY
posting_date asc, posting_time asc, creation asc
""".format(sle_conditions=get_sle_conditions(filters), item_conditions_sql=item_conditions_sql),
filters, as_dict=1)
return sl_entries
def get_items(filters): def get_items(filters):
conditions = [] conditions = []
@ -111,6 +149,7 @@ def get_items(filters):
.format(" and ".join(conditions)), filters) .format(" and ".join(conditions)), filters)
return items return items
def get_item_details(items, sl_entries, include_uom): def get_item_details(items, sl_entries, include_uom):
item_details = {} item_details = {}
if not items: if not items:
@ -140,6 +179,7 @@ def get_item_details(items, sl_entries, include_uom):
return item_details return item_details
def get_sle_conditions(filters): def get_sle_conditions(filters):
conditions = [] conditions = []
if filters.get("warehouse"): if filters.get("warehouse"):
@ -155,6 +195,7 @@ def get_sle_conditions(filters):
return "and {}".format(" and ".join(conditions)) if conditions else "" return "and {}".format(" and ".join(conditions)) if conditions else ""
def get_opening_balance(filters, columns): def get_opening_balance(filters, columns):
if not (filters.item_code and filters.warehouse and filters.from_date): if not (filters.item_code and filters.warehouse and filters.from_date):
return return
@ -166,13 +207,17 @@ def get_opening_balance(filters, columns):
"posting_date": filters.from_date, "posting_date": filters.from_date,
"posting_time": "00:00:00" "posting_time": "00:00:00"
}) })
row = {}
row["item_code"] = _("'Opening'") row = {
for dummy, v in ((9, 'qty_after_transaction'), (11, 'valuation_rate'), (12, 'stock_value')): "item_code": _("'Opening'"),
row[v] = last_entry.get(v, 0) "qty_after_transaction": last_entry.get("qty_after_transaction", 0),
"valuation_rate": last_entry.get("valuation_rate", 0),
"stock_value": last_entry.get("stock_value", 0)
}
return row return row
def get_warehouse_condition(warehouse): def get_warehouse_condition(warehouse):
warehouse_details = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"], as_dict=1) warehouse_details = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"], as_dict=1)
if warehouse_details: if warehouse_details:
@ -182,6 +227,7 @@ def get_warehouse_condition(warehouse):
return '' return ''
def get_item_group_condition(item_group): def get_item_group_condition(item_group):
item_group_details = frappe.db.get_value("Item Group", item_group, ["lft", "rgt"], as_dict=1) item_group_details = frappe.db.get_value("Item Group", item_group, ["lft", "rgt"], as_dict=1)
if item_group_details: if item_group_details:

View File

@ -64,16 +64,6 @@ frappe.ready(() => {
fieldtype: 'Data', fieldtype: 'Data',
reqd: 1 reqd: 1
}, },
{
label: __('Address Type'),
fieldname: 'address_type',
fieldtype: 'Select',
options: [
'Billing',
'Shipping'
],
reqd: 1
},
{ {
label: __('Address Line 1'), label: __('Address Line 1'),
fieldname: 'address_line1', fieldname: 'address_line1',
@ -96,16 +86,37 @@ frappe.ready(() => {
fieldname: 'state', fieldname: 'state',
fieldtype: 'Data' fieldtype: 'Data'
}, },
{
label: __('Country'),
fieldname: 'country',
fieldtype: 'Link',
options: 'Country',
reqd: 1
},
{
fieldname: "column_break0",
fieldtype: "Column Break",
width: "50%"
},
{
label: __('Address Type'),
fieldname: 'address_type',
fieldtype: 'Select',
options: [
'Billing',
'Shipping'
],
reqd: 1
},
{ {
label: __('Pin Code'), label: __('Pin Code'),
fieldname: 'pincode', fieldname: 'pincode',
fieldtype: 'Data' fieldtype: 'Data'
}, },
{ {
label: __('Country'), fieldname: "phone",
fieldname: 'country', fieldtype: "Data",
fieldtype: 'Link', label: "Phone"
reqd: 1
}, },
], ],
primary_action_label: __('Save'), primary_action_label: __('Save'),

View File

@ -83,12 +83,10 @@
</div> </div>
{% endif %} {% endif %}
{% if cart_settings.enable_checkout %}
<div class="cart-addresses mt-5"> <div class="cart-addresses mt-5">
{% include "templates/includes/cart/cart_address.html" %} {% include "templates/includes/cart/cart_address.html" %}
</div> </div>
{% endif %} {% endif %}
{% endif %}
</div> </div>
<div class="row mt-5"> <div class="row mt-5">