Merge branch 'develop' into hsn_wise_json

This commit is contained in:
Deepesh Garg 2020-08-26 12:05:59 +05:30 committed by GitHub
commit 29f187f4f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 801 additions and 151 deletions

View File

@ -9,6 +9,8 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde
from erpnext.stock.get_item_details import get_item_details from erpnext.stock.get_item_details import get_item_details
from frappe.test_runner import make_test_objects from frappe.test_runner import make_test_objects
test_dependencies = ['Item']
def test_create_test_data(): def test_create_test_data():
frappe.set_user("Administrator") frappe.set_user("Administrator")
# create test item # create test item
@ -95,7 +97,6 @@ def test_create_test_data():
}) })
coupon_code.insert() coupon_code.insert()
class TestCouponCode(unittest.TestCase): class TestCouponCode(unittest.TestCase):
def setUp(self): def setUp(self):
test_create_test_data() test_create_test_data()

View File

@ -21,7 +21,7 @@ from six import iteritems
class POSInvoice(SalesInvoice): class POSInvoice(SalesInvoice):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(POSInvoice, self).__init__(*args, **kwargs) super(POSInvoice, self).__init__(*args, **kwargs)
def validate(self): def validate(self):
if not cint(self.is_pos): if not cint(self.is_pos):
frappe.throw(_("POS Invoice should have {} field checked.").format(frappe.bold("Include Payment"))) frappe.throw(_("POS Invoice should have {} field checked.").format(frappe.bold("Include Payment")))
@ -58,7 +58,7 @@ class POSInvoice(SalesInvoice):
if self.redeem_loyalty_points and self.loyalty_points: if self.redeem_loyalty_points and self.loyalty_points:
self.apply_loyalty_points() self.apply_loyalty_points()
self.set_status(update=True) self.set_status(update=True)
def on_cancel(self): def on_cancel(self):
# run on cancel method of selling controller # run on cancel method of selling controller
super(SalesInvoice, self).on_cancel() super(SalesInvoice, self).on_cancel()
@ -68,10 +68,10 @@ class POSInvoice(SalesInvoice):
against_psi_doc = frappe.get_doc("POS Invoice", self.return_against) against_psi_doc = frappe.get_doc("POS Invoice", self.return_against)
against_psi_doc.delete_loyalty_point_entry() against_psi_doc.delete_loyalty_point_entry()
against_psi_doc.make_loyalty_point_entry() against_psi_doc.make_loyalty_point_entry()
def validate_stock_availablility(self): def validate_stock_availablility(self):
allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock') allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock')
for d in self.get('items'): for d in self.get('items'):
if d.serial_no: if d.serial_no:
filters = { filters = {
@ -89,11 +89,11 @@ class POSInvoice(SalesInvoice):
for s in serial_nos: for s in serial_nos:
if s in reserved_serial_nos: if s in reserved_serial_nos:
invalid_serial_nos.append(s) invalid_serial_nos.append(s)
if len(invalid_serial_nos): if len(invalid_serial_nos):
multiple_nos = 's' if len(invalid_serial_nos) > 1 else '' multiple_nos = 's' if len(invalid_serial_nos) > 1 else ''
frappe.throw(_("Row #{}: Serial No{}. {} has already been transacted into another POS Invoice. \ frappe.throw(_("Row #{}: Serial No{}. {} has already been transacted into another POS Invoice. \
Please select valid serial no.".format(d.idx, multiple_nos, Please select valid serial no.".format(d.idx, multiple_nos,
frappe.bold(', '.join(invalid_serial_nos)))), title=_("Not Available")) frappe.bold(', '.join(invalid_serial_nos)))), title=_("Not Available"))
else: else:
if allow_negative_stock: if allow_negative_stock:
@ -105,9 +105,9 @@ class POSInvoice(SalesInvoice):
.format(d.idx, frappe.bold(d.item_code), frappe.bold(d.warehouse))), title=_("Not Available")) .format(d.idx, frappe.bold(d.item_code), frappe.bold(d.warehouse))), title=_("Not Available"))
elif flt(available_stock) < flt(d.qty): elif flt(available_stock) < flt(d.qty):
frappe.msgprint(_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. \ frappe.msgprint(_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. \
Available quantity {}.'.format(d.idx, frappe.bold(d.item_code), Available quantity {}.'.format(d.idx, frappe.bold(d.item_code),
frappe.bold(d.warehouse), frappe.bold(d.qty))), title=_("Not Available")) frappe.bold(d.warehouse), frappe.bold(d.qty))), title=_("Not Available"))
def validate_serialised_or_batched_item(self): def validate_serialised_or_batched_item(self):
for d in self.get("items"): for d in self.get("items"):
serialized = d.get("has_serial_no") serialized = d.get("has_serial_no")
@ -125,7 +125,7 @@ class POSInvoice(SalesInvoice):
if batched and no_batch_selected: if batched and no_batch_selected:
frappe.throw(_('Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction.' frappe.throw(_('Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction.'
.format(d.idx, frappe.bold(d.item_code))), title=_("Invalid Item")) .format(d.idx, frappe.bold(d.item_code))), title=_("Invalid Item"))
def validate_return_items(self): def validate_return_items(self):
if not self.get("is_return"): return if not self.get("is_return"): return
@ -158,7 +158,7 @@ class POSInvoice(SalesInvoice):
frappe.throw(_("Row #{0} (Payment Table): Amount must be positive").format(entry.idx)) frappe.throw(_("Row #{0} (Payment Table): Amount must be positive").format(entry.idx))
if self.is_return and entry.amount > 0: if self.is_return and entry.amount > 0:
frappe.throw(_("Row #{0} (Payment Table): Amount must be negative").format(entry.idx)) frappe.throw(_("Row #{0} (Payment Table): Amount must be negative").format(entry.idx))
def validate_pos_return(self): def validate_pos_return(self):
if self.is_pos and self.is_return: if self.is_pos and self.is_return:
total_amount_in_payments = 0 total_amount_in_payments = 0
@ -167,12 +167,12 @@ class POSInvoice(SalesInvoice):
invoice_total = self.rounded_total or self.grand_total invoice_total = self.rounded_total or self.grand_total
if total_amount_in_payments < invoice_total: if total_amount_in_payments < invoice_total:
frappe.throw(_("Total payments amount can't be greater than {}".format(-invoice_total))) frappe.throw(_("Total payments amount can't be greater than {}".format(-invoice_total)))
def validate_loyalty_transaction(self): def validate_loyalty_transaction(self):
if self.redeem_loyalty_points and (not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center): if self.redeem_loyalty_points and (not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center):
expense_account, cost_center = frappe.db.get_value('Loyalty Program', self.loyalty_program, ["expense_account", "cost_center"]) expense_account, cost_center = frappe.db.get_value('Loyalty Program', self.loyalty_program, ["expense_account", "cost_center"])
if not self.loyalty_redemption_account: if not self.loyalty_redemption_account:
self.loyalty_redemption_account = expense_account self.loyalty_redemption_account = expense_account
if not self.loyalty_redemption_cost_center: if not self.loyalty_redemption_cost_center:
self.loyalty_redemption_cost_center = cost_center self.loyalty_redemption_cost_center = cost_center
@ -212,7 +212,7 @@ class POSInvoice(SalesInvoice):
if update: if update:
self.db_set('status', self.status, update_modified = update_modified) self.db_set('status', self.status, update_modified = update_modified)
def set_pos_fields(self, for_validate=False): def set_pos_fields(self, for_validate=False):
"""Set retail related fields from POS Profiles""" """Set retail related fields from POS Profiles"""
from erpnext.stock.get_item_details import get_pos_profile_item_details, get_pos_profile from erpnext.stock.get_item_details import get_pos_profile_item_details, get_pos_profile
@ -315,25 +315,25 @@ class POSInvoice(SalesInvoice):
@frappe.whitelist() @frappe.whitelist()
def get_stock_availability(item_code, warehouse): def get_stock_availability(item_code, warehouse):
latest_sle = frappe.db.sql("""select qty_after_transaction latest_sle = frappe.db.sql("""select qty_after_transaction
from `tabStock Ledger Entry` from `tabStock Ledger Entry`
where item_code = %s and warehouse = %s where item_code = %s and warehouse = %s
order by posting_date desc, posting_time desc order by posting_date desc, posting_time desc
limit 1""", (item_code, warehouse), as_dict=1) limit 1""", (item_code, warehouse), as_dict=1)
pos_sales_qty = frappe.db.sql("""select sum(p_item.qty) as qty pos_sales_qty = frappe.db.sql("""select sum(p_item.qty) as qty
from `tabPOS Invoice` p, `tabPOS Invoice Item` p_item from `tabPOS Invoice` p, `tabPOS Invoice Item` p_item
where p.name = p_item.parent where p.name = p_item.parent
and p.consolidated_invoice is NULL and p.consolidated_invoice is NULL
and p.docstatus = 1 and p.docstatus = 1
and p_item.docstatus = 1 and p_item.docstatus = 1
and p_item.item_code = %s and p_item.item_code = %s
and p_item.warehouse = %s and p_item.warehouse = %s
""", (item_code, warehouse), as_dict=1) """, (item_code, warehouse), as_dict=1)
sle_qty = latest_sle[0].qty_after_transaction or 0 if latest_sle else 0 sle_qty = latest_sle[0].qty_after_transaction or 0 if latest_sle else 0
pos_sales_qty = pos_sales_qty[0].qty or 0 if pos_sales_qty else 0 pos_sales_qty = pos_sales_qty[0].qty or 0 if pos_sales_qty else 0
if sle_qty and pos_sales_qty and sle_qty > pos_sales_qty: if sle_qty and pos_sales_qty and sle_qty > pos_sales_qty:
return sle_qty - pos_sales_qty return sle_qty - pos_sales_qty
else: else:
@ -360,14 +360,14 @@ def make_merge_log(invoices):
merge_log = frappe.new_doc("POS Invoice Merge Log") merge_log = frappe.new_doc("POS Invoice Merge Log")
merge_log.posting_date = getdate(nowdate()) merge_log.posting_date = getdate(nowdate())
for inv in invoices: for inv in invoices:
inv_data = frappe.db.get_values("POS Invoice", inv.get('name'), inv_data = frappe.db.get_values("POS Invoice", inv.get('name'),
["customer", "posting_date", "grand_total"], as_dict=1)[0] ["customer", "posting_date", "grand_total"], as_dict=1)[0]
merge_log.customer = inv_data.customer merge_log.customer = inv_data.customer
merge_log.append("pos_invoices", { merge_log.append("pos_invoices", {
'pos_invoice': inv.get('name'), 'pos_invoice': inv.get('name'),
'customer': inv_data.customer, 'customer': inv_data.customer,
'posting_date': inv_data.posting_date, 'posting_date': inv_data.posting_date,
'grand_total': inv_data.grand_total 'grand_total': inv_data.grand_total
}) })
if merge_log.get('pos_invoices'): if merge_log.get('pos_invoices'):

View File

@ -8,6 +8,8 @@ import unittest
from erpnext.stock.get_item_details import get_pos_profile from erpnext.stock.get_item_details import get_pos_profile
from erpnext.accounts.doctype.pos_profile.pos_profile import get_child_nodes from erpnext.accounts.doctype.pos_profile.pos_profile import get_child_nodes
test_dependencies = ['Item']
class TestPOSProfile(unittest.TestCase): class TestPOSProfile(unittest.TestCase):
def test_pos_profile(self): def test_pos_profile(self):
make_pos_profile() make_pos_profile()
@ -88,7 +90,7 @@ def make_pos_profile(**args):
"write_off_account": args.write_off_account or "_Test Write Off - _TC", "write_off_account": args.write_off_account or "_Test Write Off - _TC",
"write_off_cost_center": args.write_off_cost_center or "_Test Write Off Cost Center - _TC" "write_off_cost_center": args.write_off_cost_center or "_Test Write Off Cost Center - _TC"
}) })
payments = [{ payments = [{
'mode_of_payment': 'Cash', 'mode_of_payment': 'Cash',
'default': 1 'default': 1

View File

@ -1,5 +1,4 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
# For license information, please see license.txt # For license information, please see license.txt
@ -208,7 +207,7 @@ def get_serial_no_for_item(args):
def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=False): def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=False):
from erpnext.accounts.doctype.pricing_rule.utils import (get_pricing_rules, from erpnext.accounts.doctype.pricing_rule.utils import (get_pricing_rules,
get_applied_pricing_rules, get_pricing_rule_items, get_product_discount_rule) get_applied_pricing_rules, get_pricing_rule_items, get_product_discount_rule)
if isinstance(doc, string_types): if isinstance(doc, string_types):
doc = json.loads(doc) doc = json.loads(doc)
@ -237,7 +236,7 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
update_args_for_pricing_rule(args) update_args_for_pricing_rule(args)
pricing_rules = (get_applied_pricing_rules(args) pricing_rules = (get_applied_pricing_rules(args.get('pricing_rules'))
if for_validate and args.get("pricing_rules") else get_pricing_rules(args, doc)) if for_validate and args.get("pricing_rules") else get_pricing_rules(args, doc))
if pricing_rules: if pricing_rules:
@ -365,8 +364,9 @@ def set_discount_amount(rate, item_details):
item_details.rate = rate item_details.rate = rate
def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None): def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None):
from erpnext.accounts.doctype.pricing_rule.utils import get_pricing_rule_items from erpnext.accounts.doctype.pricing_rule.utils import (get_applied_pricing_rules,
for d in json.loads(pricing_rules): get_pricing_rule_items)
for d in get_applied_pricing_rules(pricing_rules):
if not d or not frappe.db.exists("Pricing Rule", d): continue if not d or not frappe.db.exists("Pricing Rule", d): continue
pricing_rule = frappe.get_cached_doc('Pricing Rule', d) pricing_rule = frappe.get_cached_doc('Pricing Rule', d)

View File

@ -447,9 +447,14 @@ def apply_pricing_rule_on_transaction(doc):
apply_pricing_rule_for_free_items(doc, item_details.free_item_data) apply_pricing_rule_for_free_items(doc, item_details.free_item_data)
doc.set_missing_values() doc.set_missing_values()
def get_applied_pricing_rules(item_row): def get_applied_pricing_rules(pricing_rules):
return (json.loads(item_row.get("pricing_rules")) if pricing_rules:
if item_row.get("pricing_rules") else []) if pricing_rules.startswith('['):
return json.loads(pricing_rules)
else:
return pricing_rules.split(',')
return []
def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None): def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
free_item = pricing_rule.free_item free_item = pricing_rule.free_item

View File

@ -1,5 +1,4 @@
{ {
"actions": [],
"autoname": "hash", "autoname": "hash",
"creation": "2013-05-22 12:43:10", "creation": "2013-05-22 12:43:10",
"doctype": "DocType", "doctype": "DocType",
@ -82,6 +81,7 @@
"item_tax_rate", "item_tax_rate",
"bom", "bom",
"include_exploded_items", "include_exploded_items",
"purchase_invoice_item",
"col_break6", "col_break6",
"purchase_order", "purchase_order",
"po_detail", "po_detail",
@ -769,12 +769,21 @@
"collapsible": 1, "collapsible": 1,
"fieldname": "col_break7", "fieldname": "col_break7",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"depends_on": "eval:parent.update_stock == 1",
"fieldname": "purchase_invoice_item",
"fieldtype": "Data",
"ignore_user_permissions": 1,
"label": "Purchase Invoice Item",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "modified": "2020-08-20 11:48:01.398356",
"modified": "2020-04-22 10:37:35.103176",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice Item", "name": "Purchase Invoice Item",

View File

@ -13,7 +13,8 @@ def get_data():
'Auto Repeat': 'reference_document', 'Auto Repeat': 'reference_document',
}, },
'internal_links': { 'internal_links': {
'Sales Order': ['items', 'sales_order'] 'Sales Order': ['items', 'sales_order'],
'Delivery Note': ['items', 'delivery_note']
}, },
'transactions': [ 'transactions': [
{ {

View File

@ -1,5 +1,4 @@
{ {
"actions": [],
"autoname": "hash", "autoname": "hash",
"creation": "2013-06-04 11:02:19", "creation": "2013-06-04 11:02:19",
"doctype": "DocType", "doctype": "DocType",
@ -87,6 +86,7 @@
"edit_references", "edit_references",
"sales_order", "sales_order",
"so_detail", "so_detail",
"sales_invoice_item",
"column_break_74", "column_break_74",
"delivery_note", "delivery_note",
"dn_detail", "dn_detail",
@ -790,12 +790,22 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Project", "label": "Project",
"options": "Project" "options": "Project"
} },
{
"depends_on": "eval:parent.update_stock == 1",
"fieldname": "sales_invoice_item",
"fieldtype": "Data",
"ignore_user_permissions": 1,
"label": "Sales Invoice Item",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
}
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-07-18 12:24:41.749986", "modified": "2020-08-20 11:24:41.749986",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice Item", "name": "Sales Invoice Item",

View File

@ -378,7 +378,7 @@ def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, g
if filters and filters.get('presentation_currency') != d.default_currency: if filters and filters.get('presentation_currency') != d.default_currency:
currency_info['company'] = d.name currency_info['company'] = d.name
currency_info['company_currency'] = d.default_currency currency_info['company_currency'] = d.default_currency
convert_to_presentation_currency(gl_entries, currency_info) convert_to_presentation_currency(gl_entries, currency_info, filters.get('company'))
for entry in gl_entries: for entry in gl_entries:
key = entry.account_number or entry.account_name key = entry.account_number or entry.account_name

View File

@ -423,7 +423,7 @@ def set_gl_entries_by_account(
distributed_cost_center_query=distributed_cost_center_query), gl_filters, as_dict=True) #nosec distributed_cost_center_query=distributed_cost_center_query), gl_filters, as_dict=True) #nosec
if filters and filters.get('presentation_currency'): if filters and filters.get('presentation_currency'):
convert_to_presentation_currency(gl_entries, get_currency(filters)) convert_to_presentation_currency(gl_entries, get_currency(filters), filters.get('company'))
for entry in gl_entries: for entry in gl_entries:
gl_entries_by_account.setdefault(entry.account, []).append(entry) gl_entries_by_account.setdefault(entry.account, []).append(entry)

View File

@ -146,6 +146,12 @@ frappe.query_reports["General Ledger"] = {
return frappe.db.get_link_options('Project', txt); return frappe.db.get_link_options('Project', txt);
} }
}, },
{
"fieldname": "include_dimensions",
"label": __("Consider Accounting Dimensions"),
"fieldtype": "Check",
"default": 0
},
{ {
"fieldname": "show_opening_entries", "fieldname": "show_opening_entries",
"label": __("Show Opening Entries"), "label": __("Show Opening Entries"),

View File

@ -106,15 +106,20 @@ def set_account_currency(filters):
return filters return filters
def get_result(filters, account_details): def get_result(filters, account_details):
gl_entries = get_gl_entries(filters) accounting_dimensions = []
if filters.get("include_dimensions"):
accounting_dimensions = get_accounting_dimensions()
data = get_data_with_opening_closing(filters, account_details, gl_entries) gl_entries = get_gl_entries(filters, accounting_dimensions)
data = get_data_with_opening_closing(filters, account_details,
accounting_dimensions, gl_entries)
result = get_result_as_list(data, filters) result = get_result_as_list(data, filters)
return result return result
def get_gl_entries(filters): def get_gl_entries(filters, accounting_dimensions):
currency_map = get_currency(filters) currency_map = get_currency(filters)
select_fields = """, debit, credit, debit_in_account_currency, select_fields = """, debit, credit, debit_in_account_currency,
credit_in_account_currency """ credit_in_account_currency """
@ -128,6 +133,10 @@ def get_gl_entries(filters):
filters['company_fb'] = frappe.db.get_value("Company", filters['company_fb'] = frappe.db.get_value("Company",
filters.get("company"), 'default_finance_book') filters.get("company"), 'default_finance_book')
dimension_fields = ""
if accounting_dimensions:
dimension_fields = ', '.join(accounting_dimensions) + ','
distributed_cost_center_query = "" distributed_cost_center_query = ""
if filters and filters.get('cost_center'): if filters and filters.get('cost_center'):
select_fields_with_percentage = """, debit*(DCC_allocation.percentage_allocation/100) as debit, credit*(DCC_allocation.percentage_allocation/100) as credit, debit_in_account_currency*(DCC_allocation.percentage_allocation/100) as debit_in_account_currency, select_fields_with_percentage = """, debit*(DCC_allocation.percentage_allocation/100) as debit, credit*(DCC_allocation.percentage_allocation/100) as credit, debit_in_account_currency*(DCC_allocation.percentage_allocation/100) as debit_in_account_currency,
@ -141,7 +150,7 @@ def get_gl_entries(filters):
party_type, party_type,
party, party,
voucher_type, voucher_type,
voucher_no, voucher_no, {dimension_fields}
cost_center, project, cost_center, project,
against_voucher_type, against_voucher_type,
against_voucher, against_voucher,
@ -160,13 +169,14 @@ def get_gl_entries(filters):
{conditions} {conditions}
AND posting_date <= %(to_date)s AND posting_date <= %(to_date)s
AND cost_center = DCC_allocation.parent AND cost_center = DCC_allocation.parent
""".format(select_fields_with_percentage=select_fields_with_percentage, conditions=get_conditions(filters).replace("and cost_center in %(cost_center)s ", '')) """.format(dimension_fields=dimension_fields,select_fields_with_percentage=select_fields_with_percentage, conditions=get_conditions(filters).replace("and cost_center in %(cost_center)s ", ''))
gl_entries = frappe.db.sql( gl_entries = frappe.db.sql(
""" """
select select
name as gl_entry, posting_date, account, party_type, party, name as gl_entry, posting_date, account, party_type, party,
voucher_type, voucher_no, cost_center, project, voucher_type, voucher_no, {dimension_fields}
cost_center, project,
against_voucher_type, against_voucher, account_currency, against_voucher_type, against_voucher, account_currency,
remarks, against, is_opening, creation {select_fields} remarks, against, is_opening, creation {select_fields}
from `tabGL Entry` from `tabGL Entry`
@ -174,13 +184,13 @@ def get_gl_entries(filters):
{distributed_cost_center_query} {distributed_cost_center_query}
{order_by_statement} {order_by_statement}
""".format( """.format(
select_fields=select_fields, conditions=get_conditions(filters), distributed_cost_center_query=distributed_cost_center_query, dimension_fields=dimension_fields, select_fields=select_fields, conditions=get_conditions(filters), distributed_cost_center_query=distributed_cost_center_query,
order_by_statement=order_by_statement order_by_statement=order_by_statement
), ),
filters, as_dict=1) filters, as_dict=1)
if filters.get('presentation_currency'): if filters.get('presentation_currency'):
return convert_to_presentation_currency(gl_entries, currency_map) return convert_to_presentation_currency(gl_entries, currency_map, filters.get('company'))
else: else:
return gl_entries return gl_entries
@ -247,12 +257,12 @@ def get_conditions(filters):
return "and {}".format(" and ".join(conditions)) if conditions else "" return "and {}".format(" and ".join(conditions)) if conditions else ""
def get_data_with_opening_closing(filters, account_details, gl_entries): def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries):
data = [] data = []
gle_map = initialize_gle_map(gl_entries, filters) gle_map = initialize_gle_map(gl_entries, filters)
totals, entries = get_accountwise_gle(filters, gl_entries, gle_map) totals, entries = get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map)
# Opening for filtered account # Opening for filtered account
data.append(totals.opening) data.append(totals.opening)
@ -318,7 +328,7 @@ def initialize_gle_map(gl_entries, filters):
return gle_map return gle_map
def get_accountwise_gle(filters, gl_entries, gle_map): def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
totals = get_totals_dict() totals = get_totals_dict()
entries = [] entries = []
consolidated_gle = OrderedDict() consolidated_gle = OrderedDict()
@ -350,8 +360,11 @@ def get_accountwise_gle(filters, gl_entries, gle_map):
if filters.get("group_by") != _('Group by Voucher (Consolidated)'): if filters.get("group_by") != _('Group by Voucher (Consolidated)'):
gle_map[gle.get(group_by)].entries.append(gle) gle_map[gle.get(group_by)].entries.append(gle)
elif filters.get("group_by") == _('Group by Voucher (Consolidated)'): elif filters.get("group_by") == _('Group by Voucher (Consolidated)'):
key = (gle.get("voucher_type"), gle.get("voucher_no"), keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")]
gle.get("account"), gle.get("cost_center")) for dim in accounting_dimensions:
keylist.append(gle.get(dim))
keylist.append(gle.get("cost_center"))
key = tuple(keylist)
if key not in consolidated_gle: if key not in consolidated_gle:
consolidated_gle.setdefault(key, gle) consolidated_gle.setdefault(key, gle)
else: else:
@ -478,7 +491,19 @@ def get_columns(filters):
"options": "Project", "options": "Project",
"fieldname": "project", "fieldname": "project",
"width": 100 "width": 100
}, }
])
if filters.get("include_dimensions"):
for dim in get_accounting_dimensions(as_list = False):
columns.append({
"label": _(dim.label),
"options": dim.label,
"fieldname": dim.fieldname,
"width": 100
})
columns.extend([
{ {
"label": _("Cost Center"), "label": _("Cost Center"),
"options": "Cost Center", "options": "Cost Center",

View File

@ -6,10 +6,6 @@ from erpnext.accounts.doctype.fiscal_year.fiscal_year import get_from_and_to_dat
from frappe.utils import cint, get_datetime_str, formatdate, flt from frappe.utils import cint, get_datetime_str, formatdate, flt
__exchange_rates = {} __exchange_rates = {}
P_OR_L_ACCOUNTS = list(
sum(frappe.get_list('Account', fields=['name'], or_filters=[{'root_type': 'Income'}, {'root_type': 'Expense'}], as_list=True), ())
)
def get_currency(filters): def get_currency(filters):
""" """
@ -73,18 +69,7 @@ def get_rate_as_at(date, from_currency, to_currency):
return rate return rate
def convert_to_presentation_currency(gl_entries, currency_info, company):
def is_p_or_l_account(account_name):
"""
Check if the given `account name` is an `Account` with `root_type` of either 'Income'
or 'Expense'.
:param account_name:
:return: Boolean
"""
return account_name in P_OR_L_ACCOUNTS
def convert_to_presentation_currency(gl_entries, currency_info):
""" """
Take a list of GL Entries and change the 'debit' and 'credit' values to currencies Take a list of GL Entries and change the 'debit' and 'credit' values to currencies
in `currency_info`. in `currency_info`.
@ -96,6 +81,9 @@ def convert_to_presentation_currency(gl_entries, currency_info):
presentation_currency = currency_info['presentation_currency'] presentation_currency = currency_info['presentation_currency']
company_currency = currency_info['company_currency'] company_currency = currency_info['company_currency']
pl_accounts = [d.name for d in frappe.get_list('Account',
filters={'report_type': 'Profit and Loss', 'company': company})]
for entry in gl_entries: for entry in gl_entries:
account = entry['account'] account = entry['account']
debit = flt(entry['debit']) debit = flt(entry['debit'])
@ -107,7 +95,7 @@ def convert_to_presentation_currency(gl_entries, currency_info):
if account_currency != presentation_currency: if account_currency != presentation_currency:
value = debit or credit value = debit or credit
date = currency_info['report_date'] if not is_p_or_l_account(account) else entry['posting_date'] date = entry['posting_date'] if account in pl_accounts else currency_info['report_date']
converted_value = convert(value, presentation_currency, company_currency, date) converted_value = convert(value, presentation_currency, company_currency, date)
if entry.get('debit'): if entry.get('debit'):

View File

@ -325,7 +325,7 @@ class AccountsController(TransactionBase):
apply_pricing_rule_for_free_items(self, pricing_rule_args.get('free_item_data')) apply_pricing_rule_for_free_items(self, pricing_rule_args.get('free_item_data'))
elif pricing_rule_args.get("validate_applied_rule"): elif pricing_rule_args.get("validate_applied_rule"):
for pricing_rule in get_applied_pricing_rules(item): for pricing_rule in get_applied_pricing_rules(item.get('pricing_rules')):
pricing_rule_doc = frappe.get_cached_doc("Pricing Rule", pricing_rule) pricing_rule_doc = frappe.get_cached_doc("Pricing Rule", pricing_rule)
for field in ['discount_percentage', 'discount_amount', 'rate']: for field in ['discount_percentage', 'discount_amount', 'rate']:
if item.get(field) < pricing_rule_doc.get(field): if item.get(field) < pricing_rule_doc.get(field):

View File

@ -559,9 +559,19 @@ class BuyingController(StockController):
"serial_no": cstr(d.serial_no).strip() "serial_no": cstr(d.serial_no).strip()
}) })
if self.is_return: if self.is_return:
original_incoming_rate = frappe.db.get_value("Stock Ledger Entry", filters = {
{"voucher_type": "Purchase Receipt", "voucher_no": self.return_against, "voucher_type": self.doctype,
"item_code": d.item_code}, "incoming_rate") "voucher_no": self.return_against,
"item_code": d.item_code
}
if (self.doctype == "Purchase Invoice" and self.update_stock
and d.get("purchase_invoice_item")):
filters["voucher_detail_no"] = d.purchase_invoice_item
elif self.doctype == "Purchase Receipt" and d.get("purchase_receipt_item"):
filters["voucher_detail_no"] = d.purchase_receipt_item
original_incoming_rate = frappe.db.get_value("Stock Ledger Entry", filters, "incoming_rate")
sle.update({ sle.update({
"outgoing_rate": original_incoming_rate "outgoing_rate": original_incoming_rate

View File

@ -497,24 +497,18 @@ def warehouse_query(doctype, txt, searchfield, start, page_len, filters):
conditions, bin_conditions = [], [] conditions, bin_conditions = [], []
filter_dict = get_doctype_wise_filters(filters) filter_dict = get_doctype_wise_filters(filters)
sub_query = """ select round(`tabBin`.actual_qty, 2) from `tabBin`
where `tabBin`.warehouse = `tabWarehouse`.name
{bin_conditions} """.format(
bin_conditions=get_filters_cond(doctype, filter_dict.get("Bin"),
bin_conditions, ignore_permissions=True))
query = """select `tabWarehouse`.name, query = """select `tabWarehouse`.name,
CONCAT_WS(" : ", "Actual Qty", ifnull( ({sub_query}), 0) ) as actual_qty CONCAT_WS(" : ", "Actual Qty", ifnull(round(`tabBin`.actual_qty, 2), 0 )) actual_qty
from `tabWarehouse` from `tabWarehouse` left join `tabBin`
on `tabBin`.warehouse = `tabWarehouse`.name {bin_conditions}
where where
`tabWarehouse`.`{key}` like {txt} `tabWarehouse`.`{key}` like {txt}
{fcond} {mcond} {fcond} {mcond}
order by order by ifnull(`tabBin`.actual_qty, 0) desc
`tabWarehouse`.name desc
limit limit
{start}, {page_len} {start}, {page_len}
""".format( """.format(
sub_query=sub_query, bin_conditions=get_filters_cond(doctype, filter_dict.get("Bin"),bin_conditions, ignore_permissions=True),
key=searchfield, key=searchfield,
fcond=get_filters_cond(doctype, filter_dict.get("Warehouse"), conditions), fcond=get_filters_cond(doctype, filter_dict.get("Warehouse"), conditions),
mcond=get_match_cond(doctype), mcond=get_match_cond(doctype),

View File

@ -281,6 +281,8 @@ def make_return_doc(doctype, source_name, target_doc=None):
target_doc.rejected_warehouse = source_doc.rejected_warehouse target_doc.rejected_warehouse = source_doc.rejected_warehouse
target_doc.po_detail = source_doc.po_detail target_doc.po_detail = source_doc.po_detail
target_doc.pr_detail = source_doc.pr_detail target_doc.pr_detail = source_doc.pr_detail
target_doc.purchase_invoice_item = source_doc.name
elif doctype == "Delivery Note": elif doctype == "Delivery Note":
target_doc.against_sales_order = source_doc.against_sales_order target_doc.against_sales_order = source_doc.against_sales_order
target_doc.against_sales_invoice = source_doc.against_sales_invoice target_doc.against_sales_invoice = source_doc.against_sales_invoice
@ -296,6 +298,7 @@ def make_return_doc(doctype, source_name, target_doc=None):
target_doc.so_detail = source_doc.so_detail target_doc.so_detail = source_doc.so_detail
target_doc.dn_detail = source_doc.dn_detail target_doc.dn_detail = source_doc.dn_detail
target_doc.expense_account = source_doc.expense_account target_doc.expense_account = source_doc.expense_account
target_doc.sales_invoice_item = source_doc.name
if default_warehouse_for_sales_return: if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return target_doc.warehouse = default_warehouse_for_sales_return

View File

@ -217,7 +217,9 @@ class SellingController(StockController):
'target_warehouse': p.target_warehouse, 'target_warehouse': p.target_warehouse,
'company': self.company, 'company': self.company,
'voucher_type': self.doctype, 'voucher_type': self.doctype,
'allow_zero_valuation': d.allow_zero_valuation_rate 'allow_zero_valuation': d.allow_zero_valuation_rate,
'sales_invoice_item': d.get("sales_invoice_item"),
'delivery_note_item': d.get("dn_detail")
})) }))
else: else:
il.append(frappe._dict({ il.append(frappe._dict({
@ -233,7 +235,9 @@ class SellingController(StockController):
'target_warehouse': d.target_warehouse, 'target_warehouse': d.target_warehouse,
'company': self.company, 'company': self.company,
'voucher_type': self.doctype, 'voucher_type': self.doctype,
'allow_zero_valuation': d.allow_zero_valuation_rate 'allow_zero_valuation': d.allow_zero_valuation_rate,
'sales_invoice_item': d.get("sales_invoice_item"),
'delivery_note_item': d.get("dn_detail")
})) }))
return il return il
@ -302,7 +306,11 @@ class SellingController(StockController):
d.conversion_factor = get_conversion_factor(d.item_code, d.uom).get("conversion_factor") or 1.0 d.conversion_factor = get_conversion_factor(d.item_code, d.uom).get("conversion_factor") or 1.0
return_rate = 0 return_rate = 0
if cint(self.is_return) and self.return_against and self.docstatus==1: if cint(self.is_return) and self.return_against and self.docstatus==1:
return_rate = self.get_incoming_rate_for_return(d.item_code, self.return_against) against_document_no = (d.get("sales_invoice_item")
if self.doctype == "Sales Invoice" else d.get("delivery_note_item"))
return_rate = self.get_incoming_rate_for_return(d.item_code,
self.return_against, against_document_no)
# On cancellation or if return entry submission, make stock ledger entry for # On cancellation or if return entry submission, make stock ledger entry for
# target warehouse first, to update serial no values properly # target warehouse first, to update serial no values properly

View File

@ -301,14 +301,19 @@ class StockController(AccountsController):
return serialized_items return serialized_items
def get_incoming_rate_for_return(self, item_code, against_document): def get_incoming_rate_for_return(self, item_code, against_document, against_document_no=None):
incoming_rate = 0.0 incoming_rate = 0.0
cond = ''
if against_document and item_code: if against_document and item_code:
if against_document_no:
cond = " and voucher_detail_no = %s" %(frappe.db.escape(against_document_no))
incoming_rate = frappe.db.sql("""select abs(stock_value_difference / actual_qty) incoming_rate = frappe.db.sql("""select abs(stock_value_difference / actual_qty)
from `tabStock Ledger Entry` from `tabStock Ledger Entry`
where voucher_type = %s and voucher_no = %s where voucher_type = %s and voucher_no = %s
and item_code = %s limit 1""", and item_code = %s {0} limit 1""".format(cond),
(self.doctype, against_document, item_code)) (self.doctype, against_document, item_code))
incoming_rate = incoming_rate[0][0] if incoming_rate else 0.0 incoming_rate = incoming_rate[0][0] if incoming_rate else 0.0
return incoming_rate return incoming_rate

View File

@ -325,7 +325,7 @@ def auto_close_opportunity():
doc.save() doc.save()
@frappe.whitelist() @frappe.whitelist()
def make_opportunity_from_communication(communication, ignore_communication_links=False): def make_opportunity_from_communication(communication, company, ignore_communication_links=False):
from erpnext.crm.doctype.lead.lead import make_lead_from_communication from erpnext.crm.doctype.lead.lead import make_lead_from_communication
doc = frappe.get_doc("Communication", communication) doc = frappe.get_doc("Communication", communication)
@ -337,6 +337,7 @@ def make_opportunity_from_communication(communication, ignore_communication_link
opportunity = frappe.get_doc({ opportunity = frappe.get_doc({
"doctype": "Opportunity", "doctype": "Opportunity",
"company": company,
"opportunity_from": opportunity_from, "opportunity_from": opportunity_from,
"party_name": lead "party_name": lead
}).insert(ignore_permissions=True) }).insert(ignore_permissions=True)

View File

@ -30,14 +30,14 @@ frappe.ui.form.on('Social Media Post', {
let color = frm.doc.twitter_post_id ? "green" : "red"; let color = frm.doc.twitter_post_id ? "green" : "red";
let status = frm.doc.twitter_post_id ? "Posted" : "Not Posted"; let status = frm.doc.twitter_post_id ? "Posted" : "Not Posted";
html += `<div class="col-xs-6"> html += `<div class="col-xs-6">
<span class="indicator whitespace-nowrap ${color}"><span class="hidden-xs">Twitter : ${status} </span></span> <span class="indicator whitespace-nowrap ${color}"><span>Twitter : ${status} </span></span>
</div>` ; </div>` ;
} }
if (frm.doc.linkedin){ if (frm.doc.linkedin){
let color = frm.doc.linkedin_post_id ? "green" : "red"; let color = frm.doc.linkedin_post_id ? "green" : "red";
let status = frm.doc.linkedin_post_id ? "Posted" : "Not Posted"; let status = frm.doc.linkedin_post_id ? "Posted" : "Not Posted";
html += `<div class="col-xs-6"> html += `<div class="col-xs-6">
<span class="indicator whitespace-nowrap ${color}"><span class="hidden-xs">LinkedIn : ${status} </span></span> <span class="indicator whitespace-nowrap ${color}"><span>LinkedIn : ${status} </span></span>
</div>` ; </div>` ;
} }
html = `<div class="row">${html}</div>`; html = `<div class="row">${html}</div>`;

View File

@ -0,0 +1,52 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Lead Details"] = {
"filters": [
{
"fieldname":"company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
"default": frappe.defaults.get_user_default("Company"),
"reqd": 1
},
{
"fieldname":"from_date",
"label": __("From Date"),
"fieldtype": "Date",
"default": frappe.datetime.add_months(frappe.datetime.get_today(), -12),
"reqd": 1
},
{
"fieldname":"to_date",
"label": __("To Date"),
"fieldtype": "Date",
"default": frappe.datetime.get_today(),
"reqd": 1
},
{
"fieldname":"status",
"label": __("Status"),
"fieldtype": "Select",
options: [
{ "value": "Lead", "label": __("Lead") },
{ "value": "Open", "label": __("Open") },
{ "value": "Replied", "label": __("Replied") },
{ "value": "Opportunity", "label": __("Opportunity") },
{ "value": "Quotation", "label": __("Quotation") },
{ "value": "Lost Quotation", "label": __("Lost Quotation") },
{ "value": "Interested", "label": __("Interested") },
{ "value": "Converted", "label": __("Converted") },
{ "value": "Do Not Contact", "label": __("Do Not Contact") },
],
},
{
"fieldname":"territory",
"label": __("Territory"),
"fieldtype": "Link",
"options": "Territory",
}
]
};

View File

@ -7,16 +7,15 @@
"doctype": "Report", "doctype": "Report",
"idx": 3, "idx": 3,
"is_standard": "Yes", "is_standard": "Yes",
"modified": "2020-01-22 16:51:56.591110", "modified": "2020-07-26 23:59:49.897577",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Lead Details", "name": "Lead Details",
"owner": "Administrator", "owner": "Administrator",
"prepared_report": 0, "prepared_report": 0,
"query": "SELECT\n `tabLead`.name as \"Lead Id:Link/Lead:120\",\n `tabLead`.lead_name as \"Lead Name::120\",\n\t`tabLead`.company_name as \"Company Name::120\",\n\t`tabLead`.status as \"Status::120\",\n\tconcat_ws(', ', \n\t\ttrim(',' from `tabAddress`.address_line1), \n\t\ttrim(',' from tabAddress.address_line2)\n\t) as 'Address::180',\n\t`tabAddress`.state as \"State::100\",\n\t`tabAddress`.pincode as \"Pincode::70\",\n\t`tabAddress`.country as \"Country::100\",\n\t`tabLead`.phone as \"Phone::100\",\n\t`tabLead`.mobile_no as \"Mobile No::100\",\n\t`tabLead`.email_id as \"Email Id::120\",\n\t`tabLead`.lead_owner as \"Lead Owner::120\",\n\t`tabLead`.source as \"Source::120\",\n\t`tabLead`.territory as \"Territory::120\",\n\t`tabLead`.notes as \"Notes::360\",\n `tabLead`.owner as \"Owner:Link/User:120\"\nFROM\n\t`tabLead`\n\tleft join `tabDynamic Link` on (\n\t\t`tabDynamic Link`.link_name=`tabLead`.name \n\t\tand `tabDynamic Link`.parenttype = 'Address'\n\t)\n\tleft join `tabAddress` on (\n\t\t`tabAddress`.name=`tabDynamic Link`.parent\n\t)\nWHERE\n\t`tabLead`.docstatus<2\nORDER BY\n\t`tabLead`.name asc",
"ref_doctype": "Lead", "ref_doctype": "Lead",
"report_name": "Lead Details", "report_name": "Lead Details",
"report_type": "Query Report", "report_type": "Script Report",
"roles": [ "roles": [
{ {
"role": "Sales User" "role": "Sales User"

View File

@ -0,0 +1,158 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
from frappe import _
import frappe
def execute(filters=None):
columns, data = get_columns(), get_data(filters)
return columns, data
def get_columns():
columns = [
{
"label": _("Lead"),
"fieldname": "name",
"fieldtype": "Link",
"options": "Lead",
"width": 150,
},
{
"label": _("Lead Name"),
"fieldname": "lead_name",
"fieldtype": "Data",
"width": 120
},
{
"fieldname":"status",
"label": _("Status"),
"fieldtype": "Data",
"width": 100
},
{
"fieldname":"lead_owner",
"label": _("Lead Owner"),
"fieldtype": "Link",
"options": "User",
"width": 100
},
{
"label": _("Territory"),
"fieldname": "territory",
"fieldtype": "Link",
"options": "Territory",
"width": 100
},
{
"label": _("Source"),
"fieldname": "source",
"fieldtype": "Data",
"width": 120
},
{
"label": _("Email"),
"fieldname": "email_id",
"fieldtype": "Data",
"width": 120
},
{
"label": _("Mobile"),
"fieldname": "mobile_no",
"fieldtype": "Data",
"width": 120
},
{
"label": _("Phone"),
"fieldname": "phone",
"fieldtype": "Data",
"width": 120
},
{
"label": _("Owner"),
"fieldname": "owner",
"fieldtype": "Link",
"options": "user",
"width": 120
},
{
"label": _("Company"),
"fieldname": "company",
"fieldtype": "Link",
"options": "Company",
"width": 120
},
{
"fieldname":"address",
"label": _("Address"),
"fieldtype": "Data",
"width": 130
},
{
"fieldname":"state",
"label": _("State"),
"fieldtype": "Data",
"width": 100
},
{
"fieldname":"pincode",
"label": _("Postal Code"),
"fieldtype": "Data",
"width": 90
},
{
"fieldname":"country",
"label": _("Country"),
"fieldtype": "Link",
"options": "Country",
"width": 100
},
]
return columns
def get_data(filters):
return frappe.db.sql("""
SELECT
`tabLead`.name,
`tabLead`.lead_name,
`tabLead`.status,
`tabLead`.lead_owner,
`tabLead`.territory,
`tabLead`.source,
`tabLead`.email_id,
`tabLead`.mobile_no,
`tabLead`.phone,
`tabLead`.owner,
`tabLead`.company,
concat_ws(', ',
trim(',' from `tabAddress`.address_line1),
trim(',' from tabAddress.address_line2)
) AS address,
`tabAddress`.state,
`tabAddress`.pincode,
`tabAddress`.country
FROM
`tabLead` left join `tabDynamic Link` on (
`tabLead`.name = `tabDynamic Link`.link_name and
`tabDynamic Link`.parenttype = 'Address')
left join `tabAddress` on (
`tabAddress`.name=`tabDynamic Link`.parent)
WHERE
company = %(company)s
AND `tabLead`.creation BETWEEN %(from_date)s AND %(to_date)s
{conditions}
ORDER BY
`tabLead`.creation asc """.format(conditions=get_conditions(filters)), filters, as_dict=1)
def get_conditions(filters) :
conditions = []
if filters.get("territory"):
conditions.append(" and `tabLead`.territory=%(territory)s")
if filters.get("status"):
conditions.append(" and `tabLead`.status=%(status)s")
return " ".join(conditions) if conditions else ""

View File

@ -0,0 +1,67 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Lost Opportunity"] = {
"filters": [
{
"fieldname":"company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
"default": frappe.defaults.get_user_default("Company"),
"reqd": 1
},
{
"fieldname":"from_date",
"label": __("From Date"),
"fieldtype": "Date",
"default": frappe.datetime.add_months(frappe.datetime.get_today(), -12),
"reqd": 1
},
{
"fieldname":"to_date",
"label": __("To Date"),
"fieldtype": "Date",
"default": frappe.datetime.get_today(),
"reqd": 1
},
{
"fieldname":"lost_reason",
"label": __("Lost Reason"),
"fieldtype": "Link",
"options": "Opportunity Lost Reason"
},
{
"fieldname":"territory",
"label": __("Territory"),
"fieldtype": "Link",
"options": "Territory"
},
{
"fieldname":"opportunity_from",
"label": __("Opportunity From"),
"fieldtype": "Link",
"options": "DocType",
"get_query": function() {
return {
"filters": {
"name": ["in", ["Customer", "Lead"]],
}
}
}
},
{
"fieldname":"party_name",
"label": __("Party"),
"fieldtype": "Dynamic Link",
"options": "opportunity_from"
},
{
"fieldname":"contact_by",
"label": __("Next Contact By"),
"fieldtype": "Link",
"options": "User"
},
]
};

View File

@ -1,13 +1,14 @@
{ {
"add_total_row": 0, "add_total_row": 0,
"creation": "2018-12-31 16:30:57.188837", "creation": "2018-12-31 16:30:57.188837",
"disable_prepared_report": 0,
"disabled": 0, "disabled": 0,
"docstatus": 0, "docstatus": 0,
"doctype": "Report", "doctype": "Report",
"idx": 0, "idx": 0,
"is_standard": "Yes", "is_standard": "Yes",
"json": "{\"order_by\": \"`tabOpportunity`.`modified` desc\", \"filters\": [[\"Opportunity\", \"status\", \"=\", \"Lost\"]], \"fields\": [[\"name\", \"Opportunity\"], [\"opportunity_from\", \"Opportunity\"], [\"party_name\", \"Opportunity\"], [\"customer_name\", \"Opportunity\"], [\"opportunity_type\", \"Opportunity\"], [\"status\", \"Opportunity\"], [\"contact_by\", \"Opportunity\"], [\"docstatus\", \"Opportunity\"], [\"lost_reason\", \"Lost Reason Detail\"]], \"add_totals_row\": 0, \"add_total_row\": 0, \"page_length\": 20}", "json": "{\"order_by\": \"`tabOpportunity`.`modified` desc\", \"filters\": [[\"Opportunity\", \"status\", \"=\", \"Lost\"]], \"fields\": [[\"name\", \"Opportunity\"], [\"opportunity_from\", \"Opportunity\"], [\"party_name\", \"Opportunity\"], [\"customer_name\", \"Opportunity\"], [\"opportunity_type\", \"Opportunity\"], [\"status\", \"Opportunity\"], [\"contact_by\", \"Opportunity\"], [\"docstatus\", \"Opportunity\"], [\"lost_reason\", \"Lost Reason Detail\"]], \"add_totals_row\": 0, \"add_total_row\": 0, \"page_length\": 20}",
"modified": "2019-06-26 16:33:08.083618", "modified": "2020-07-29 15:49:02.848845",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Lost Opportunity", "name": "Lost Opportunity",
@ -15,7 +16,7 @@
"prepared_report": 0, "prepared_report": 0,
"ref_doctype": "Opportunity", "ref_doctype": "Opportunity",
"report_name": "Lost Opportunity", "report_name": "Lost Opportunity",
"report_type": "Report Builder", "report_type": "Script Report",
"roles": [ "roles": [
{ {
"role": "Sales User" "role": "Sales User"

View File

@ -0,0 +1,131 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
from frappe import _
import frappe
def execute(filters=None):
columns, data = get_columns(), get_data(filters)
return columns, data
def get_columns():
columns = [
{
"label": _("Opportunity"),
"fieldname": "name",
"fieldtype": "Link",
"options": "Opportunity",
"width": 170,
},
{
"label": _("Opportunity From"),
"fieldname": "opportunity_from",
"fieldtype": "Link",
"options": "DocType",
"width": 130
},
{
"label": _("Party"),
"fieldname":"party_name",
"fieldtype": "Dynamic Link",
"options": "opportunity_from",
"width": 160
},
{
"label": _("Customer/Lead Name"),
"fieldname":"customer_name",
"fieldtype": "Data",
"width": 150
},
{
"label": _("Opportunity Type"),
"fieldname": "opportunity_type",
"fieldtype": "Data",
"width": 130
},
{
"label": _("Lost Reasons"),
"fieldname": "lost_reason",
"fieldtype": "Data",
"width": 220
},
{
"label": _("Sales Stage"),
"fieldname": "sales_stage",
"fieldtype": "Link",
"options": "Sales Stage",
"width": 150
},
{
"label": _("Territory"),
"fieldname": "territory",
"fieldtype": "Link",
"options": "Territory",
"width": 150
},
{
"label": _("Next Contact By"),
"fieldname": "contact_by",
"fieldtype": "Link",
"options": "User",
"width": 150
}
]
return columns
def get_data(filters):
return frappe.db.sql("""
SELECT
`tabOpportunity`.name,
`tabOpportunity`.opportunity_from,
`tabOpportunity`.party_name,
`tabOpportunity`.customer_name,
`tabOpportunity`.opportunity_type,
`tabOpportunity`.contact_by,
GROUP_CONCAT(`tabOpportunity Lost Reason Detail`.lost_reason separator ', ') lost_reason,
`tabOpportunity`.sales_stage,
`tabOpportunity`.territory
FROM
`tabOpportunity`
{join}
WHERE
`tabOpportunity`.status = 'Lost' and `tabOpportunity`.company = %(company)s
AND `tabOpportunity`.modified BETWEEN %(from_date)s AND %(to_date)s
{conditions}
GROUP BY
`tabOpportunity`.name
ORDER BY
`tabOpportunity`.creation asc """.format(conditions=get_conditions(filters), join=get_join(filters)), filters, as_dict=1)
def get_conditions(filters):
conditions = []
if filters.get("territory"):
conditions.append(" and `tabOpportunity`.territory=%(territory)s")
if filters.get("opportunity_from"):
conditions.append(" and `tabOpportunity`.opportunity_from=%(opportunity_from)s")
if filters.get("party_name"):
conditions.append(" and `tabOpportunity`.party_name=%(party_name)s")
if filters.get("contact_by"):
conditions.append(" and `tabOpportunity`.contact_by=%(contact_by)s")
return " ".join(conditions) if conditions else ""
def get_join(filters):
join = """LEFT JOIN `tabOpportunity Lost Reason Detail`
ON `tabOpportunity Lost Reason Detail`.parenttype = 'Opportunity' and
`tabOpportunity Lost Reason Detail`.parent = `tabOpportunity`.name"""
if filters.get("lost_reason"):
join = """JOIN `tabOpportunity Lost Reason Detail`
ON `tabOpportunity Lost Reason Detail`.parenttype = 'Opportunity' and
`tabOpportunity Lost Reason Detail`.parent = `tabOpportunity`.name and
`tabOpportunity Lost Reason Detail`.lost_reason = '{0}'
""".format(filters.get("lost_reason"))
return join

View File

@ -7,6 +7,8 @@ import unittest
import frappe import frappe
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_healthcare_docs, create_clinical_procedure_template from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_healthcare_docs, create_clinical_procedure_template
test_dependencies = ['Item']
class TestClinicalProcedure(unittest.TestCase): class TestClinicalProcedure(unittest.TestCase):
def test_procedure_template_item(self): def test_procedure_template_item(self):
patient, medical_department, practitioner = create_healthcare_docs() patient, medical_department, practitioner = create_healthcare_docs()

View File

@ -3,6 +3,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.utils import cint
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe import _ from frappe import _
@ -24,8 +25,7 @@ class JobOffer(Document):
check_vacancies = frappe.get_single("HR Settings").check_vacancies check_vacancies = frappe.get_single("HR Settings").check_vacancies
if staffing_plan and check_vacancies: if staffing_plan and check_vacancies:
job_offers = self.get_job_offer(staffing_plan.from_date, staffing_plan.to_date) job_offers = self.get_job_offer(staffing_plan.from_date, staffing_plan.to_date)
if not staffing_plan.get("vacancies") or cint(staffing_plan.vacancies) - len(job_offers) <= 0:
if not staffing_plan.get("vacancies") or staffing_plan.vacancies - len(job_offers) <= 0:
error_variable = 'for ' + frappe.bold(self.designation) error_variable = 'for ' + frappe.bold(self.designation)
if staffing_plan.get("parent"): if staffing_plan.get("parent"):
error_variable = frappe.bold(get_link_to_form("Staffing Plan", staffing_plan.parent)) error_variable = frappe.bold(get_link_to_form("Staffing Plan", staffing_plan.parent))
@ -65,7 +65,7 @@ def get_staffing_plan_detail(designation, company, offer_date):
AND %s between sp.from_date and sp.to_date AND %s between sp.from_date and sp.to_date
""", (designation, company, offer_date), as_dict=1) """, (designation, company, offer_date), as_dict=1)
return frappe._dict(detail[0]) if detail else None return frappe._dict(detail[0]) if (detail and detail[0].parent) else None
@frappe.whitelist() @frappe.whitelist()
def make_employee(source_name, target_doc=None): def make_employee(source_name, target_doc=None):

View File

@ -7,7 +7,7 @@ frappe.ui.form.on("Communication", {
}, },
setup_custom_buttons: (frm) => { setup_custom_buttons: (frm) => {
let confirm_msg = "Are you sure you want to create {0} from this email"; let confirm_msg = "Are you sure you want to create {0} from this email?";
if(frm.doc.reference_doctype !== "Issue") { if(frm.doc.reference_doctype !== "Issue") {
frm.add_custom_button(__("Issue"), () => { frm.add_custom_button(__("Issue"), () => {
frappe.confirm(__(confirm_msg, [__("Issue")]), () => { frappe.confirm(__(confirm_msg, [__("Issue")]), () => {
@ -62,17 +62,36 @@ frappe.ui.form.on("Communication", {
}, },
make_opportunity_from_communication: (frm) => { make_opportunity_from_communication: (frm) => {
return frappe.call({ const fields = [{
method: "erpnext.crm.doctype.opportunity.opportunity.make_opportunity_from_communication", fieldtype: 'Link',
args: { label: __('Select a Company'),
communication: frm.doc.name fieldname: 'company',
}, options: 'Company',
freeze: true, reqd: 1,
callback: (r) => { default: frappe.defaults.get_user_default("Company")
if(r.message) { }];
frm.reload_doc()
frappe.prompt(fields, data => {
frappe.call({
method: "erpnext.crm.doctype.opportunity.opportunity.make_opportunity_from_communication",
args: {
communication: frm.doc.name,
company: data.company
},
freeze: true,
callback: (r) => {
if(r.message) {
frm.reload_doc();
frappe.show_alert({
message: __("Opportunity {0} created",
['<a href="#Form/Opportunity/'+r.message+'">' + r.message + '</a>']),
indicator: 'green'
});
}
} }
} });
}) },
'Create an Opportunity',
'Create');
} }
}); });

View File

@ -22,6 +22,8 @@
} }
.filter-options { .filter-options {
margin-left: -5px;
padding-left: 5px;
max-height: 300px; max-height: 300px;
overflow: auto; overflow: auto;
} }

View File

@ -674,6 +674,9 @@ def update_grand_total_for_rcm(doc, method):
if country != 'India': if country != 'India':
return return
if not doc.total_taxes_and_charges:
return
if doc.reverse_charge == 'Y': if doc.reverse_charge == 'Y':
gst_accounts = get_gst_accounts(doc.company) gst_accounts = get_gst_accounts(doc.company)
gst_account_list = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \ gst_account_list = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \
@ -721,7 +724,7 @@ def make_regional_gl_entries(gl_entries, doc):
country = frappe.get_cached_value('Company', doc.company, 'country') country = frappe.get_cached_value('Company', doc.company, 'country')
if country != 'India': if country != 'India':
return return gl_entries
if doc.reverse_charge == 'Y': if doc.reverse_charge == 'Y':
gst_accounts = get_gst_accounts(doc.company) gst_accounts = get_gst_accounts(doc.company)
@ -732,6 +735,7 @@ def make_regional_gl_entries(gl_entries, doc):
if tax.category not in ("Total", "Valuation and Total"): if tax.category not in ("Total", "Valuation and Total"):
continue continue
dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit"
if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in gst_account_list: if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in gst_account_list:
account_currency = get_account_currency(tax.account_head) account_currency = get_account_currency(tax.account_head)
@ -741,8 +745,8 @@ def make_regional_gl_entries(gl_entries, doc):
"cost_center": tax.cost_center, "cost_center": tax.cost_center,
"posting_date": doc.posting_date, "posting_date": doc.posting_date,
"against": doc.supplier, "against": doc.supplier,
"credit": tax.base_tax_amount_after_discount_amount, dr_or_cr: tax.base_tax_amount_after_discount_amount,
"credits_in_account_currency": tax.base_tax_amount_after_discount_amount \ dr_or_cr + "_in_account_currency": tax.base_tax_amount_after_discount_amount \
if account_currency==doc.company_currency \ if account_currency==doc.company_currency \
else tax.tax_amount_after_discount_amount else tax.tax_amount_after_discount_amount
}, account_currency, item=tax) }, account_currency, item=tax)

View File

@ -3,7 +3,7 @@
{ {
"hidden": 0, "hidden": 0,
"label": "Retail Operations", "label": "Retail Operations",
"links": "[\n {\n \"description\": \"Setup default values for POS Invoices\",\n \"label\": \"Point-of-Sale Profile\",\n \"name\": \"POS Profile\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"POS Profile\"\n ],\n \"description\": \"Point of Sale\",\n \"label\": \"POS\",\n \"name\": \"pos\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"description\": \"Cashier Closing\",\n \"label\": \"Cashier Closing\",\n \"name\": \"Cashier Closing\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Setup mode of POS (Online / Offline)\",\n \"label\": \"POS Settings\",\n \"name\": \"POS Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"To make Customer based incentive schemes.\",\n \"label\": \"Loyalty Program\",\n \"name\": \"Loyalty Program\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"To view logs of Loyalty Points assigned to a Customer.\",\n \"label\": \"Loyalty Point Entry\",\n \"name\": \"Loyalty Point Entry\",\n \"type\": \"doctype\"\n }\n]" "links": "[\n {\n \"description\": \"Setup default values for POS Invoices\",\n \"label\": \"Point of Sale Profile\",\n \"name\": \"POS Profile\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"POS Profile\"\n ],\n \"description\": \"Point of Sale\",\n \"label\": \"Point of Sale\",\n \"name\": \"point-of-sale\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"description\": \"Setup mode of POS (Online / Offline)\",\n \"label\": \"POS Settings\",\n \"name\": \"POS Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Cashier Closing\",\n \"label\": \"Cashier Closing\",\n \"name\": \"Cashier Closing\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"To make Customer based incentive schemes.\",\n \"label\": \"Loyalty Program\",\n \"name\": \"Loyalty Program\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"To view logs of Loyalty Points assigned to a Customer.\",\n \"label\": \"Loyalty Point Entry\",\n \"name\": \"Loyalty Point Entry\",\n \"type\": \"doctype\"\n }\n]"
} }
], ],
"category": "Domains", "category": "Domains",
@ -14,10 +14,11 @@
"docstatus": 0, "docstatus": 0,
"doctype": "Desk Page", "doctype": "Desk Page",
"extends_another_page": 0, "extends_another_page": 0,
"hide_custom": 0,
"idx": 0, "idx": 0,
"is_standard": 1, "is_standard": 1,
"label": "Retail", "label": "Retail",
"modified": "2020-04-26 22:42:39.346750", "modified": "2020-08-20 18:00:07.515691",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Retail", "name": "Retail",
@ -25,5 +26,27 @@
"pin_to_bottom": 0, "pin_to_bottom": 0,
"pin_to_top": 0, "pin_to_top": 0,
"restrict_to_domain": "Retail", "restrict_to_domain": "Retail",
"shortcuts": [] "shortcuts": [
{
"color": "#9deca2",
"doc_view": "",
"format": "{} Active",
"label": "Point of Sale Profile",
"link_to": "POS Profile",
"stats_filter": "{\n \"disabled\": 0\n}",
"type": "DocType"
},
{
"doc_view": "",
"label": "Point of Sale",
"link_to": "point-of-sale",
"type": "Page"
},
{
"doc_view": "",
"label": "POS Settings",
"link_to": "POS Settings",
"type": "DocType"
}
]
} }

View File

@ -33,7 +33,7 @@ def get_data():
}, },
{ {
'label': _('Support'), 'label': _('Support'),
'items': ['Issue'] 'items': ['Issue', 'Maintenance Visit']
}, },
{ {
'label': _('Projects'), 'label': _('Projects'),

View File

@ -10,6 +10,7 @@ def get_data():
'Payment Entry': 'reference_name', 'Payment Entry': 'reference_name',
'Payment Request': 'reference_name', 'Payment Request': 'reference_name',
'Auto Repeat': 'reference_document', 'Auto Repeat': 'reference_document',
'Maintenance Visit': 'prevdoc_docname'
}, },
'internal_links': { 'internal_links': {
'Quotation': ['items', 'prevdoc_docname'] 'Quotation': ['items', 'prevdoc_docname']
@ -17,7 +18,7 @@ def get_data():
'transactions': [ 'transactions': [
{ {
'label': _('Fulfillment'), 'label': _('Fulfillment'),
'items': ['Sales Invoice', 'Pick List', 'Delivery Note'] 'items': ['Sales Invoice', 'Pick List', 'Delivery Note', 'Maintenance Visit']
}, },
{ {
'label': _('Purchasing'), 'label': _('Purchasing'),

View File

@ -86,7 +86,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
this.$summary_container.append( this.$summary_container.append(
`<div class="summary-btns flex summary-btns justify-between w-full f-shrink-0"></div>` `<div class="summary-btns flex summary-btns justify-between w-full f-shrink-0"></div>`
) )
this.$summary_btns = this.$summary_container.find('.summary-btns'); this.$summary_btns = this.$summary_container.find('.summary-btns');
} }
@ -110,7 +110,10 @@ erpnext.PointOfSale.PastOrderSummary = class {
{fieldname:'print', fieldtype:'Data', label:'Print Preview'} {fieldname:'print', fieldtype:'Data', label:'Print Preview'}
], ],
primary_action: () => { primary_action: () => {
this.events.get_frm().print_preview.printit(true); const frm = this.events.get_frm();
frm.doc = this.doc;
frm.print_preview.lang_code = frm.doc.language;
frm.print_preview.printit(true);
}, },
primary_action_label: __('Print'), primary_action_label: __('Print'),
}); });
@ -174,7 +177,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
<div class="flex"> <div class="flex">
<div class="text-md-0 text-dark-grey text-bold w-fit">Tax Charges</div> <div class="text-md-0 text-dark-grey text-bold w-fit">Tax Charges</div>
<div class="flex ml-6 text-dark-grey"> <div class="flex ml-6 text-dark-grey">
${ ${
doc.taxes.map((t, i) => { doc.taxes.map((t, i) => {
let margin_left = ''; let margin_left = '';
if (i !== 0) margin_left = 'ml-2'; if (i !== 0) margin_left = 'ml-2';
@ -271,6 +274,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
// this.print_dialog.show(); // this.print_dialog.show();
const frm = this.events.get_frm(); const frm = this.events.get_frm();
frm.doc = this.doc; frm.doc = this.doc;
frm.print_preview.lang_code = frm.doc.language;
frm.print_preview.printit(true); frm.print_preview.printit(true);
}); });
} }
@ -284,9 +288,9 @@ erpnext.PointOfSale.PastOrderSummary = class {
this.$summary_container.find('.print-btn').click(); this.$summary_container.find('.print-btn').click();
}); });
} }
toggle_component(show) { toggle_component(show) {
show ? show ?
this.$component.removeClass('d-none') : this.$component.removeClass('d-none') :
this.$component.addClass('d-none'); this.$component.addClass('d-none');
} }
@ -372,9 +376,9 @@ erpnext.PointOfSale.PastOrderSummary = class {
} }
get_condition_btn_map(after_submission) { get_condition_btn_map(after_submission) {
if (after_submission) if (after_submission)
return [{ condition: true, visible_btns: ['Print Receipt', 'Email Receipt', 'New Order'] }]; return [{ condition: true, visible_btns: ['Print Receipt', 'Email Receipt', 'New Order'] }];
return [ return [
{ condition: this.doc.docstatus === 0, visible_btns: ['Edit Order'] }, { condition: this.doc.docstatus === 0, visible_btns: ['Edit Order'] },
{ condition: !this.doc.is_return && this.doc.docstatus === 1, visible_btns: ['Print Receipt', 'Email Receipt', 'Return']}, { condition: !this.doc.is_return && this.doc.docstatus === 1, visible_btns: ['Print Receipt', 'Email Receipt', 'Return']},
@ -384,7 +388,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
load_summary_of(doc, after_submission=false) { load_summary_of(doc, after_submission=false) {
this.$summary_wrapper.removeClass("d-none"); this.$summary_wrapper.removeClass("d-none");
after_submission ? after_submission ?
this.switch_to_post_submit_summary() : this.switch_to_recent_invoice_summary(); this.switch_to_post_submit_summary() : this.switch_to_recent_invoice_summary();

View File

@ -7,6 +7,7 @@ import frappe
from erpnext.accounts.doctype.cash_flow_mapper.default_cash_flow_mapper import DEFAULT_MAPPERS from erpnext.accounts.doctype.cash_flow_mapper.default_cash_flow_mapper import DEFAULT_MAPPERS
from .default_success_action import get_default_success_action from .default_success_action import get_default_success_action
from frappe import _ from frappe import _
from frappe.utils import cint
from frappe.desk.page.setup_wizard.setup_wizard import add_all_roles_to from frappe.desk.page.setup_wizard.setup_wizard import add_all_roles_to
from frappe.custom.doctype.custom_field.custom_field import create_custom_field from frappe.custom.doctype.custom_field.custom_field import create_custom_field
from erpnext.setup.default_energy_point_rules import get_default_energy_point_rules from erpnext.setup.default_energy_point_rules import get_default_energy_point_rules
@ -29,8 +30,8 @@ def after_install():
def check_setup_wizard_not_completed(): def check_setup_wizard_not_completed():
if frappe.db.get_default('desktop:home_page') != 'setup-wizard': if cint(frappe.db.get_single_value('System Settings', 'setup_complete') or 0):
message = """ERPNext can only be installed on a fresh site where the setup wizard is not completed. message = """ERPNext can only be installed on a fresh site where the setup wizard is not completed.
You can reinstall this site (after saving your data) using: bench --site [sitename] reinstall""" You can reinstall this site (after saving your data) using: bench --site [sitename] reinstall"""
frappe.throw(message) frappe.throw(message)

View File

@ -111,6 +111,7 @@ class Item(WebsiteGenerator):
self.synced_with_hub = 0 self.synced_with_hub = 0
self.validate_has_variants() self.validate_has_variants()
self.validate_attributes_in_variants()
self.validate_stock_exists_for_template_item() self.validate_stock_exists_for_template_item()
self.validate_attributes() self.validate_attributes()
self.validate_variant_attributes() self.validate_variant_attributes()
@ -806,6 +807,77 @@ class Item(WebsiteGenerator):
if frappe.db.exists("Item", {"variant_of": self.name}): if frappe.db.exists("Item", {"variant_of": self.name}):
frappe.throw(_("Item has variants.")) frappe.throw(_("Item has variants."))
def validate_attributes_in_variants(self):
if not self.has_variants or self.get("__islocal"):
return
old_doc = self.get_doc_before_save()
old_doc_attributes = set([attr.attribute for attr in old_doc.attributes])
own_attributes = [attr.attribute for attr in self.attributes]
# Check if old attributes were removed from the list
# Is old_attrs is a subset of new ones
# that means we need not check any changes
if old_doc_attributes.issubset(set(own_attributes)):
return
from collections import defaultdict
# get all item variants
items = [item["name"] for item in frappe.get_all("Item", {"variant_of": self.name})]
# get all deleted attributes
deleted_attribute = list(old_doc_attributes.difference(set(own_attributes)))
# fetch all attributes of these items
item_attributes = frappe.get_all(
"Item Variant Attribute",
filters={
"parent": ["in", items],
"attribute": ["in", deleted_attribute]
},
fields=["attribute", "parent"]
)
not_included = defaultdict(list)
for attr in item_attributes:
if attr["attribute"] not in own_attributes:
not_included[attr["parent"]].append(attr["attribute"])
if not len(not_included):
return
def body(docnames):
docnames.sort()
return "<br>".join(docnames)
def table_row(title, body):
return """<tr>
<td>{0}</td>
<td>{1}</td>
</tr>""".format(title, body)
rows = ''
for docname, attr_list in not_included.items():
link = "<a href='#Form/Item/{0}'>{0}</a>".format(frappe.bold(_(docname)))
rows += table_row(link, body(attr_list))
error_description = _('The following deleted attributes exist in Variants but not in the Template. You can either delete the Variants or keep the attribute(s) in template.')
message = """
<div>{0}</div><br>
<table class="table">
<thead>
<td>{1}</td>
<td>{2}</td>
</thead>
{3}
</table>
""".format(error_description, _('Variant Items'), _('Attributes'), rows)
frappe.throw(message, title=_("Variant Attribute Error"), is_minimizable=True, wide=True)
def validate_stock_exists_for_template_item(self): def validate_stock_exists_for_template_item(self):
if self.stock_ledger_created() and self._doc_before_save: if self.stock_ledger_created() and self._doc_before_save:
if (cint(self._doc_before_save.has_variants) != cint(self.has_variants) if (cint(self._doc_before_save.has_variants) != cint(self.has_variants)

View File

@ -227,6 +227,14 @@ class PurchaseReceipt(BuyingController):
if not stock_value_diff: if not stock_value_diff:
continue continue
# If PR is sub-contracted and fg item rate is zero
# in that case if account for shource and target warehouse are same,
# then GL entries should not be posted
if flt(stock_value_diff) == flt(d.rm_supp_cost) \
and warehouse_account.get(self.supplier_warehouse) \
and warehouse_account[d.warehouse]["account"] == warehouse_account[self.supplier_warehouse]["account"]:
continue
gl_entries.append(self.get_gl_dict({ gl_entries.append(self.get_gl_dict({
"account": warehouse_account[d.warehouse]["account"], "account": warehouse_account[d.warehouse]["account"],
"against": stock_rbnb, "against": stock_rbnb,
@ -242,16 +250,16 @@ class PurchaseReceipt(BuyingController):
credit_amount = flt(d.base_net_amount, d.precision("base_net_amount")) \ credit_amount = flt(d.base_net_amount, d.precision("base_net_amount")) \
if credit_currency == self.company_currency else flt(d.net_amount, d.precision("net_amount")) if credit_currency == self.company_currency else flt(d.net_amount, d.precision("net_amount"))
if credit_amount:
gl_entries.append(self.get_gl_dict({ gl_entries.append(self.get_gl_dict({
"account": warehouse_account[d.from_warehouse]['account'] \ "account": warehouse_account[d.from_warehouse]['account'] \
if d.from_warehouse else stock_rbnb, if d.from_warehouse else stock_rbnb,
"against": warehouse_account[d.warehouse]["account"], "against": warehouse_account[d.warehouse]["account"],
"cost_center": d.cost_center, "cost_center": d.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"), "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"debit": -1 * flt(d.base_net_amount, d.precision("base_net_amount")), "debit": -1 * flt(d.base_net_amount, d.precision("base_net_amount")),
"debit_in_account_currency": -1 * credit_amount "debit_in_account_currency": -1 * credit_amount
}, credit_currency, item=d)) }, credit_currency, item=d))
negative_expense_to_be_booked += flt(d.item_tax_amount) negative_expense_to_be_booked += flt(d.item_tax_amount)

View File

@ -18,6 +18,28 @@ class TestPurchaseReceipt(unittest.TestCase):
set_perpetual_inventory(0) set_perpetual_inventory(0)
frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1) frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1)
def test_reverse_purchase_receipt_sle(self):
frappe.db.set_value('UOM', '_Test UOM', 'must_be_whole_number', 0)
pr = make_purchase_receipt(qty=0.5)
sl_entry = frappe.db.get_all("Stock Ledger Entry", {"voucher_type": "Purchase Receipt",
"voucher_no": pr.name}, ['actual_qty'])
self.assertEqual(len(sl_entry), 1)
self.assertEqual(sl_entry[0].actual_qty, 0.5)
pr.cancel()
sl_entry_cancelled = frappe.db.get_all("Stock Ledger Entry", {"voucher_type": "Purchase Receipt",
"voucher_no": pr.name}, ['actual_qty'], order_by='creation')
self.assertEqual(len(sl_entry_cancelled), 2)
self.assertEqual(sl_entry_cancelled[1].actual_qty, -0.5)
frappe.db.set_value('UOM', '_Test UOM', 'must_be_whole_number', 1)
def test_make_purchase_invoice(self): def test_make_purchase_invoice(self):
pr = make_purchase_receipt(do_not_save=True) pr = make_purchase_receipt(do_not_save=True)
self.assertRaises(frappe.ValidationError, make_purchase_invoice, pr.name) self.assertRaises(frappe.ValidationError, make_purchase_invoice, pr.name)
@ -121,6 +143,22 @@ class TestPurchaseReceipt(unittest.TestCase):
rm_supp_cost = sum([d.amount for d in pr.get("supplied_items")]) rm_supp_cost = sum([d.amount for d in pr.get("supplied_items")])
self.assertEqual(pr.get("items")[0].rm_supp_cost, flt(rm_supp_cost, 2)) self.assertEqual(pr.get("items")[0].rm_supp_cost, flt(rm_supp_cost, 2))
def test_subcontracting_gle_fg_item_rate_zero(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
set_perpetual_inventory()
frappe.db.set_value("Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM")
make_stock_entry(item_code="_Test Item", target="Work In Progress - TCP1", qty=100, basic_rate=100, company="_Test Company with perpetual inventory")
make_stock_entry(item_code="_Test Item Home Desktop 100", target="Work In Progress - TCP1",
qty=100, basic_rate=100, company="_Test Company with perpetual inventory")
pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=0, is_subcontracted="Yes",
company="_Test Company with perpetual inventory", warehouse='Stores - TCP1', supplier_warehouse='Work In Progress - TCP1')
gl_entries = get_gl_entries("Purchase Receipt", pr.name)
self.assertFalse(gl_entries)
set_perpetual_inventory(0)
def test_serial_no_supplier(self): def test_serial_no_supplier(self):
pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1) pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1)
self.assertEqual(frappe.db.get_value("Serial No", pr.get("items")[0].serial_no, "supplier"), self.assertEqual(frappe.db.get_value("Serial No", pr.get("items")[0].serial_no, "supplier"),
@ -688,7 +726,7 @@ def make_purchase_receipt(**args):
"received_qty": received_qty, "received_qty": received_qty,
"rejected_qty": rejected_qty, "rejected_qty": rejected_qty,
"rejected_warehouse": args.rejected_warehouse or "_Test Rejected Warehouse - _TC" if rejected_qty != 0 else "", "rejected_warehouse": args.rejected_warehouse or "_Test Rejected Warehouse - _TC" if rejected_qty != 0 else "",
"rate": args.rate or 50, "rate": args.rate if args.rate != None else 50,
"conversion_factor": args.conversion_factor or 1.0, "conversion_factor": args.conversion_factor or 1.0,
"serial_no": args.serial_no, "serial_no": args.serial_no,
"stock_uom": args.stock_uom or "_Test UOM", "stock_uom": args.stock_uom or "_Test UOM",

View File

@ -31,7 +31,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
sle['posting_time'] = now_datetime().strftime('%H:%M:%S.%f') sle['posting_time'] = now_datetime().strftime('%H:%M:%S.%f')
if cancel: if cancel:
sle['actual_qty'] = -flt(sle.get('actual_qty'), 0) sle['actual_qty'] = -flt(sle.get('actual_qty'))
if sle['actual_qty'] < 0 and not sle.get('outgoing_rate'): if sle['actual_qty'] < 0 and not sle.get('outgoing_rate'):
sle['outgoing_rate'] = get_incoming_outgoing_rate_for_cancel(sle.item_code, sle['outgoing_rate'] = get_incoming_outgoing_rate_for_cancel(sle.item_code,

View File

@ -209,11 +209,11 @@ function set_time_to_resolve_and_response(frm) {
frm.dashboard.set_headline_alert( frm.dashboard.set_headline_alert(
'<div class="row">' + '<div class="row">' +
'<div class="col-xs-6">' + '<div class="col-xs-12 col-sm-6">' +
'<span class="indicator whitespace-nowrap '+ time_to_respond.indicator +'"><span class="hidden-xs">Time to Respond: '+ time_to_respond.diff_display +'</span></span> ' + '<span class="indicator whitespace-nowrap '+ time_to_respond.indicator +'"><span>Time to Respond: '+ time_to_respond.diff_display +'</span></span> ' +
'</div>' + '</div>' +
'<div class="col-xs-6">' + '<div class="col-xs-12 col-sm-6">' +
'<span class="indicator whitespace-nowrap '+ time_to_resolve.indicator +'"><span class="hidden-xs">Time to Resolve: '+ time_to_resolve.diff_display +'</span></span> ' + '<span class="indicator whitespace-nowrap '+ time_to_resolve.indicator +'"><span>Time to Resolve: '+ time_to_resolve.diff_display +'</span></span> ' +
'</div>' + '</div>' +
'</div>' '</div>'
); );