Merge branch 'develop' into product-bundle-packing-list-logic
This commit is contained in:
commit
e9fc6c3249
1
.flake8
1
.flake8
@ -28,6 +28,7 @@ ignore =
|
|||||||
B007,
|
B007,
|
||||||
B950,
|
B950,
|
||||||
W191,
|
W191,
|
||||||
|
E124, # closing bracket, irritating while writing QB code
|
||||||
|
|
||||||
max-line-length = 200
|
max-line-length = 200
|
||||||
exclude=.github/helper/semgrep_rules
|
exclude=.github/helper/semgrep_rules
|
||||||
|
@ -2,9 +2,10 @@
|
|||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
|
||||||
|
from functools import reduce
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import flt
|
from frappe.utils import flt
|
||||||
from six.moves import reduce
|
|
||||||
|
|
||||||
from erpnext.controllers.status_updater import StatusUpdater
|
from erpnext.controllers.status_updater import StatusUpdater
|
||||||
|
|
||||||
|
@ -15,17 +15,6 @@ frappe.ui.form.on('Cost Center', {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
frm.set_query("cost_center", "distributed_cost_center", function() {
|
|
||||||
return {
|
|
||||||
filters: {
|
|
||||||
company: frm.doc.company,
|
|
||||||
is_group: 0,
|
|
||||||
enable_distributed_cost_center: 0,
|
|
||||||
name: ['!=', frm.doc.name]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
if (!frm.is_new()) {
|
if (!frm.is_new()) {
|
||||||
|
@ -16,9 +16,6 @@
|
|||||||
"cb0",
|
"cb0",
|
||||||
"is_group",
|
"is_group",
|
||||||
"disabled",
|
"disabled",
|
||||||
"section_break_9",
|
|
||||||
"enable_distributed_cost_center",
|
|
||||||
"distributed_cost_center",
|
|
||||||
"lft",
|
"lft",
|
||||||
"rgt",
|
"rgt",
|
||||||
"old_parent"
|
"old_parent"
|
||||||
@ -122,31 +119,13 @@
|
|||||||
"fieldname": "disabled",
|
"fieldname": "disabled",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Disabled"
|
"label": "Disabled"
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "enable_distributed_cost_center",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Enable Distributed Cost Center"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "eval:doc.is_group==0",
|
|
||||||
"fieldname": "section_break_9",
|
|
||||||
"fieldtype": "Section Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "enable_distributed_cost_center",
|
|
||||||
"fieldname": "distributed_cost_center",
|
|
||||||
"fieldtype": "Table",
|
|
||||||
"label": "Distributed Cost Center",
|
|
||||||
"options": "Distributed Cost Center"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-money",
|
"icon": "fa fa-money",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"is_tree": 1,
|
"is_tree": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-06-17 16:09:30.025214",
|
"modified": "2022-01-31 13:22:58.916273",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Cost Center",
|
"name": "Cost Center",
|
||||||
@ -189,5 +168,6 @@
|
|||||||
"search_fields": "parent_cost_center, is_group",
|
"search_fields": "parent_cost_center, is_group",
|
||||||
"show_name_in_global_search": 1,
|
"show_name_in_global_search": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "ASC"
|
"sort_order": "ASC",
|
||||||
|
"states": []
|
||||||
}
|
}
|
@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import cint
|
|
||||||
from frappe.utils.nestedset import NestedSet
|
from frappe.utils.nestedset import NestedSet
|
||||||
|
|
||||||
from erpnext.accounts.utils import validate_field_number
|
from erpnext.accounts.utils import validate_field_number
|
||||||
@ -20,24 +19,6 @@ class CostCenter(NestedSet):
|
|||||||
def validate(self):
|
def validate(self):
|
||||||
self.validate_mandatory()
|
self.validate_mandatory()
|
||||||
self.validate_parent_cost_center()
|
self.validate_parent_cost_center()
|
||||||
self.validate_distributed_cost_center()
|
|
||||||
|
|
||||||
def validate_distributed_cost_center(self):
|
|
||||||
if cint(self.enable_distributed_cost_center):
|
|
||||||
if not self.distributed_cost_center:
|
|
||||||
frappe.throw(_("Please enter distributed cost center"))
|
|
||||||
if sum(x.percentage_allocation for x in self.distributed_cost_center) != 100:
|
|
||||||
frappe.throw(_("Total percentage allocation for distributed cost center should be equal to 100"))
|
|
||||||
if not self.get('__islocal'):
|
|
||||||
if not cint(frappe.get_cached_value("Cost Center", {"name": self.name}, "enable_distributed_cost_center")) \
|
|
||||||
and self.check_if_part_of_distributed_cost_center():
|
|
||||||
frappe.throw(_("Cannot enable Distributed Cost Center for a Cost Center already allocated in another Distributed Cost Center"))
|
|
||||||
if next((True for x in self.distributed_cost_center if x.cost_center == x.parent), False):
|
|
||||||
frappe.throw(_("Parent Cost Center cannot be added in Distributed Cost Center"))
|
|
||||||
if check_if_distributed_cost_center_enabled(list(x.cost_center for x in self.distributed_cost_center)):
|
|
||||||
frappe.throw(_("A Distributed Cost Center cannot be added in the Distributed Cost Center allocation table."))
|
|
||||||
else:
|
|
||||||
self.distributed_cost_center = []
|
|
||||||
|
|
||||||
def validate_mandatory(self):
|
def validate_mandatory(self):
|
||||||
if self.cost_center_name != self.company and not self.parent_cost_center:
|
if self.cost_center_name != self.company and not self.parent_cost_center:
|
||||||
@ -64,10 +45,10 @@ class CostCenter(NestedSet):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def convert_ledger_to_group(self):
|
def convert_ledger_to_group(self):
|
||||||
if cint(self.enable_distributed_cost_center):
|
if self.if_allocation_exists_against_cost_center():
|
||||||
frappe.throw(_("Cost Center with enabled distributed cost center can not be converted to group"))
|
frappe.throw(_("Cost Center with Allocation records can not be converted to a group"))
|
||||||
if self.check_if_part_of_distributed_cost_center():
|
if self.check_if_part_of_cost_center_allocation():
|
||||||
frappe.throw(_("Cost Center Already Allocated in a Distributed Cost Center cannot be converted to group"))
|
frappe.throw(_("Cost Center is a part of Cost Center Allocation, hence cannot be converted to a group"))
|
||||||
if self.check_gle_exists():
|
if self.check_gle_exists():
|
||||||
frappe.throw(_("Cost Center with existing transactions can not be converted to group"))
|
frappe.throw(_("Cost Center with existing transactions can not be converted to group"))
|
||||||
self.is_group = 1
|
self.is_group = 1
|
||||||
@ -81,8 +62,17 @@ class CostCenter(NestedSet):
|
|||||||
return frappe.db.sql("select name from `tabCost Center` where \
|
return frappe.db.sql("select name from `tabCost Center` where \
|
||||||
parent_cost_center = %s and docstatus != 2", self.name)
|
parent_cost_center = %s and docstatus != 2", self.name)
|
||||||
|
|
||||||
def check_if_part_of_distributed_cost_center(self):
|
def if_allocation_exists_against_cost_center(self):
|
||||||
return frappe.db.get_value("Distributed Cost Center", {"cost_center": self.name})
|
return frappe.db.get_value("Cost Center Allocation", filters = {
|
||||||
|
"main_cost_center": self.name,
|
||||||
|
"docstatus": 1
|
||||||
|
})
|
||||||
|
|
||||||
|
def check_if_part_of_cost_center_allocation(self):
|
||||||
|
return frappe.db.get_value("Cost Center Allocation Percentage", filters = {
|
||||||
|
"cost_center": self.name,
|
||||||
|
"docstatus": 1
|
||||||
|
})
|
||||||
|
|
||||||
def before_rename(self, olddn, newdn, merge=False):
|
def before_rename(self, olddn, newdn, merge=False):
|
||||||
# Add company abbr if not provided
|
# Add company abbr if not provided
|
||||||
@ -126,8 +116,4 @@ def on_doctype_update():
|
|||||||
def get_name_with_number(new_account, account_number):
|
def get_name_with_number(new_account, account_number):
|
||||||
if account_number and not new_account[0].isdigit():
|
if account_number and not new_account[0].isdigit():
|
||||||
new_account = account_number + " - " + new_account
|
new_account = account_number + " - " + new_account
|
||||||
return new_account
|
return new_account
|
||||||
|
|
||||||
def check_if_distributed_cost_center_enabled(cost_center_list):
|
|
||||||
value_list = frappe.get_list("Cost Center", {"name": ["in", cost_center_list]}, "enable_distributed_cost_center", as_list=1)
|
|
||||||
return next((True for x in value_list if x[0]), False)
|
|
@ -23,33 +23,6 @@ class TestCostCenter(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertRaises(frappe.ValidationError, cost_center.save)
|
self.assertRaises(frappe.ValidationError, cost_center.save)
|
||||||
|
|
||||||
def test_validate_distributed_cost_center(self):
|
|
||||||
|
|
||||||
if not frappe.db.get_value('Cost Center', {'name': '_Test Cost Center - _TC'}):
|
|
||||||
frappe.get_doc(test_records[0]).insert()
|
|
||||||
|
|
||||||
if not frappe.db.get_value('Cost Center', {'name': '_Test Cost Center 2 - _TC'}):
|
|
||||||
frappe.get_doc(test_records[1]).insert()
|
|
||||||
|
|
||||||
invalid_distributed_cost_center = frappe.get_doc({
|
|
||||||
"company": "_Test Company",
|
|
||||||
"cost_center_name": "_Test Distributed Cost Center",
|
|
||||||
"doctype": "Cost Center",
|
|
||||||
"is_group": 0,
|
|
||||||
"parent_cost_center": "_Test Company - _TC",
|
|
||||||
"enable_distributed_cost_center": 1,
|
|
||||||
"distributed_cost_center": [{
|
|
||||||
"cost_center": "_Test Cost Center - _TC",
|
|
||||||
"percentage_allocation": 40
|
|
||||||
}, {
|
|
||||||
"cost_center": "_Test Cost Center 2 - _TC",
|
|
||||||
"percentage_allocation": 50
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
self.assertRaises(frappe.ValidationError, invalid_distributed_cost_center.save)
|
|
||||||
|
|
||||||
def create_cost_center(**args):
|
def create_cost_center(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
if args.cost_center_name:
|
if args.cost_center_name:
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.ui.form.on('Cost Center Allocation', {
|
||||||
|
setup: function(frm) {
|
||||||
|
let filters = {"is_group": 0};
|
||||||
|
if (frm.doc.company) {
|
||||||
|
$.extend(filters, {
|
||||||
|
"company": frm.doc.company
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
frm.set_query('main_cost_center', function() {
|
||||||
|
return {
|
||||||
|
filters: filters
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,128 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"autoname": "CC-ALLOC-.#####",
|
||||||
|
"creation": "2022-01-13 20:07:29.871109",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"main_cost_center",
|
||||||
|
"company",
|
||||||
|
"column_break_2",
|
||||||
|
"valid_from",
|
||||||
|
"section_break_5",
|
||||||
|
"allocation_percentages",
|
||||||
|
"amended_from"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "main_cost_center",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Main Cost Center",
|
||||||
|
"options": "Cost Center",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "Today",
|
||||||
|
"fieldname": "valid_from",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Valid From",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_2",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_5",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "main_cost_center.company",
|
||||||
|
"fieldname": "company",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Company",
|
||||||
|
"options": "Company",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "allocation_percentages",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Cost Center Allocation Percentages",
|
||||||
|
"options": "Cost Center Allocation Percentage",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "amended_from",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Amended From",
|
||||||
|
"no_copy": 1,
|
||||||
|
"options": "Cost Center Allocation",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"is_submittable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2022-01-31 11:47:12.086253",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "Cost Center Allocation",
|
||||||
|
"name_case": "UPPER CASE",
|
||||||
|
"naming_rule": "Expression (old style)",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"amend": 1,
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amend": 1,
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Accounts Manager",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amend": 1,
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Accounts User",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
@ -0,0 +1,90 @@
|
|||||||
|
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
from frappe.model.document import Document
|
||||||
|
from frappe.utils import add_days, format_date, getdate
|
||||||
|
|
||||||
|
|
||||||
|
class MainCostCenterCantBeChild(frappe.ValidationError):
|
||||||
|
pass
|
||||||
|
class InvalidMainCostCenter(frappe.ValidationError):
|
||||||
|
pass
|
||||||
|
class InvalidChildCostCenter(frappe.ValidationError):
|
||||||
|
pass
|
||||||
|
class WrongPercentageAllocation(frappe.ValidationError):
|
||||||
|
pass
|
||||||
|
class InvalidDateError(frappe.ValidationError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class CostCenterAllocation(Document):
|
||||||
|
def validate(self):
|
||||||
|
self.validate_total_allocation_percentage()
|
||||||
|
self.validate_from_date_based_on_existing_gle()
|
||||||
|
self.validate_backdated_allocation()
|
||||||
|
self.validate_main_cost_center()
|
||||||
|
self.validate_child_cost_centers()
|
||||||
|
|
||||||
|
def validate_total_allocation_percentage(self):
|
||||||
|
total_percentage = sum([d.percentage for d in self.get("allocation_percentages", [])])
|
||||||
|
|
||||||
|
if total_percentage != 100:
|
||||||
|
frappe.throw(_("Total percentage against cost centers should be 100"), WrongPercentageAllocation)
|
||||||
|
|
||||||
|
def validate_from_date_based_on_existing_gle(self):
|
||||||
|
# Check if GLE exists against the main cost center
|
||||||
|
# If exists ensure from date is set after posting date of last GLE
|
||||||
|
|
||||||
|
last_gle_date = frappe.db.get_value("GL Entry",
|
||||||
|
{"cost_center": self.main_cost_center, "is_cancelled": 0},
|
||||||
|
"posting_date", order_by="posting_date desc")
|
||||||
|
|
||||||
|
if last_gle_date:
|
||||||
|
if getdate(self.valid_from) <= getdate(last_gle_date):
|
||||||
|
frappe.throw(_("Valid From must be after {0} as last GL Entry against the cost center {1} posted on this date")
|
||||||
|
.format(last_gle_date, self.main_cost_center), InvalidDateError)
|
||||||
|
|
||||||
|
def validate_backdated_allocation(self):
|
||||||
|
# Check if there are any future existing allocation records against the main cost center
|
||||||
|
# If exists, warn the user about it
|
||||||
|
|
||||||
|
future_allocation = frappe.db.get_value("Cost Center Allocation", filters = {
|
||||||
|
"main_cost_center": self.main_cost_center,
|
||||||
|
"valid_from": (">=", self.valid_from),
|
||||||
|
"name": ("!=", self.name),
|
||||||
|
"docstatus": 1
|
||||||
|
}, fieldname=['valid_from', 'name'], order_by='valid_from', as_dict=1)
|
||||||
|
|
||||||
|
if future_allocation:
|
||||||
|
frappe.msgprint(_("Another Cost Center Allocation record {0} applicable from {1}, hence this allocation will be applicable upto {2}")
|
||||||
|
.format(frappe.bold(future_allocation.name), frappe.bold(format_date(future_allocation.valid_from)),
|
||||||
|
frappe.bold(format_date(add_days(future_allocation.valid_from, -1)))),
|
||||||
|
title=_("Warning!"), indicator="orange", alert=1
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate_main_cost_center(self):
|
||||||
|
# Main cost center itself cannot be entered in child table
|
||||||
|
if self.main_cost_center in [d.cost_center for d in self.allocation_percentages]:
|
||||||
|
frappe.throw(_("Main Cost Center {0} cannot be entered in the child table")
|
||||||
|
.format(self.main_cost_center), MainCostCenterCantBeChild)
|
||||||
|
|
||||||
|
# If main cost center is used for allocation under any other cost center,
|
||||||
|
# allocation cannot be done against it
|
||||||
|
parent = frappe.db.get_value("Cost Center Allocation Percentage", filters = {
|
||||||
|
"cost_center": self.main_cost_center,
|
||||||
|
"docstatus": 1
|
||||||
|
}, fieldname='parent')
|
||||||
|
if parent:
|
||||||
|
frappe.throw(_("{0} cannot be used as a Main Cost Center because it has been used as child in Cost Center Allocation {1}")
|
||||||
|
.format(self.main_cost_center, parent), InvalidMainCostCenter)
|
||||||
|
|
||||||
|
def validate_child_cost_centers(self):
|
||||||
|
# Check if child cost center is used as main cost center in any existing allocation
|
||||||
|
main_cost_centers = [d.main_cost_center for d in
|
||||||
|
frappe.get_all("Cost Center Allocation", {'docstatus': 1}, 'main_cost_center')]
|
||||||
|
|
||||||
|
for d in self.allocation_percentages:
|
||||||
|
if d.cost_center in main_cost_centers:
|
||||||
|
frappe.throw(_("Cost Center {0} cannot be used for allocation as it is used as main cost center in other allocation record.")
|
||||||
|
.format(d.cost_center), InvalidChildCostCenter)
|
@ -0,0 +1,156 @@
|
|||||||
|
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.utils import add_days, today
|
||||||
|
|
||||||
|
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||||
|
from erpnext.accounts.doctype.cost_center_allocation.cost_center_allocation import (
|
||||||
|
InvalidChildCostCenter,
|
||||||
|
InvalidDateError,
|
||||||
|
InvalidMainCostCenter,
|
||||||
|
MainCostCenterCantBeChild,
|
||||||
|
WrongPercentageAllocation,
|
||||||
|
)
|
||||||
|
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||||
|
|
||||||
|
|
||||||
|
class TestCostCenterAllocation(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
cost_centers = ["Main Cost Center 1", "Main Cost Center 2", "Sub Cost Center 1", "Sub Cost Center 2"]
|
||||||
|
for cc in cost_centers:
|
||||||
|
create_cost_center(cost_center_name=cc, company="_Test Company")
|
||||||
|
|
||||||
|
def test_gle_based_on_cost_center_allocation(self):
|
||||||
|
cca = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
|
||||||
|
{
|
||||||
|
"Sub Cost Center 1 - _TC": 60,
|
||||||
|
"Sub Cost Center 2 - _TC": 40
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
jv = make_journal_entry("_Test Cash - _TC", "Sales - _TC", 100,
|
||||||
|
cost_center = "Main Cost Center 1 - _TC", submit=True)
|
||||||
|
|
||||||
|
expected_values = [
|
||||||
|
["Sub Cost Center 1 - _TC", 0.0, 60],
|
||||||
|
["Sub Cost Center 2 - _TC", 0.0, 40]
|
||||||
|
]
|
||||||
|
|
||||||
|
gle = frappe.qb.DocType("GL Entry")
|
||||||
|
gl_entries = (
|
||||||
|
frappe.qb.from_(gle)
|
||||||
|
.select(gle.cost_center, gle.debit, gle.credit)
|
||||||
|
.where(gle.voucher_type == 'Journal Entry')
|
||||||
|
.where(gle.voucher_no == jv.name)
|
||||||
|
.where(gle.account == 'Sales - _TC')
|
||||||
|
.orderby(gle.cost_center)
|
||||||
|
).run(as_dict=1)
|
||||||
|
|
||||||
|
self.assertTrue(gl_entries)
|
||||||
|
|
||||||
|
for i, gle in enumerate(gl_entries):
|
||||||
|
self.assertEqual(expected_values[i][0], gle.cost_center)
|
||||||
|
self.assertEqual(expected_values[i][1], gle.debit)
|
||||||
|
self.assertEqual(expected_values[i][2], gle.credit)
|
||||||
|
|
||||||
|
cca.cancel()
|
||||||
|
jv.cancel()
|
||||||
|
|
||||||
|
def test_main_cost_center_cant_be_child(self):
|
||||||
|
# Main cost center itself cannot be entered in child table
|
||||||
|
cca = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
|
||||||
|
{
|
||||||
|
"Sub Cost Center 1 - _TC": 60,
|
||||||
|
"Main Cost Center 1 - _TC": 40
|
||||||
|
}, save=False
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertRaises(MainCostCenterCantBeChild, cca.save)
|
||||||
|
|
||||||
|
def test_invalid_main_cost_center(self):
|
||||||
|
# If main cost center is used for allocation under any other cost center,
|
||||||
|
# allocation cannot be done against it
|
||||||
|
cca1 = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
|
||||||
|
{
|
||||||
|
"Sub Cost Center 1 - _TC": 60,
|
||||||
|
"Sub Cost Center 2 - _TC": 40
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
cca2 = create_cost_center_allocation("_Test Company", "Sub Cost Center 1 - _TC",
|
||||||
|
{
|
||||||
|
"Sub Cost Center 2 - _TC": 100
|
||||||
|
}, save=False
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertRaises(InvalidMainCostCenter, cca2.save)
|
||||||
|
|
||||||
|
cca1.cancel()
|
||||||
|
|
||||||
|
def test_if_child_cost_center_has_any_allocation_record(self):
|
||||||
|
# Check if any child cost center is used as main cost center in any other existing allocation
|
||||||
|
cca1 = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
|
||||||
|
{
|
||||||
|
"Sub Cost Center 1 - _TC": 60,
|
||||||
|
"Sub Cost Center 2 - _TC": 40
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
cca2 = create_cost_center_allocation("_Test Company", "Main Cost Center 2 - _TC",
|
||||||
|
{
|
||||||
|
"Main Cost Center 1 - _TC": 60,
|
||||||
|
"Sub Cost Center 1 - _TC": 40
|
||||||
|
}, save=False
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertRaises(InvalidChildCostCenter, cca2.save)
|
||||||
|
|
||||||
|
cca1.cancel()
|
||||||
|
|
||||||
|
def test_total_percentage(self):
|
||||||
|
cca = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
|
||||||
|
{
|
||||||
|
"Sub Cost Center 1 - _TC": 40,
|
||||||
|
"Sub Cost Center 2 - _TC": 40
|
||||||
|
}, save=False
|
||||||
|
)
|
||||||
|
self.assertRaises(WrongPercentageAllocation, cca.save)
|
||||||
|
|
||||||
|
def test_valid_from_based_on_existing_gle(self):
|
||||||
|
# GLE posted against Sub Cost Center 1 on today
|
||||||
|
jv = make_journal_entry("_Test Cash - _TC", "Sales - _TC", 100,
|
||||||
|
cost_center = "Main Cost Center 1 - _TC", posting_date=today(), submit=True)
|
||||||
|
|
||||||
|
# try to set valid from as yesterday
|
||||||
|
cca = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
|
||||||
|
{
|
||||||
|
"Sub Cost Center 1 - _TC": 60,
|
||||||
|
"Sub Cost Center 2 - _TC": 40
|
||||||
|
}, valid_from=add_days(today(), -1), save=False
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertRaises(InvalidDateError, cca.save)
|
||||||
|
|
||||||
|
jv.cancel()
|
||||||
|
|
||||||
|
def create_cost_center_allocation(company, main_cost_center, allocation_percentages,
|
||||||
|
valid_from=None, valid_upto=None, save=True, submit=True):
|
||||||
|
doc = frappe.new_doc("Cost Center Allocation")
|
||||||
|
doc.main_cost_center = main_cost_center
|
||||||
|
doc.company = company
|
||||||
|
doc.valid_from = valid_from or today()
|
||||||
|
doc.valid_upto = valid_upto
|
||||||
|
for cc, percentage in allocation_percentages.items():
|
||||||
|
doc.append("allocation_percentages", {
|
||||||
|
"cost_center": cc,
|
||||||
|
"percentage": percentage
|
||||||
|
})
|
||||||
|
if save:
|
||||||
|
doc.save()
|
||||||
|
if submit:
|
||||||
|
doc.submit()
|
||||||
|
|
||||||
|
return doc
|
@ -1,12 +1,13 @@
|
|||||||
{
|
{
|
||||||
"actions": [],
|
"actions": [],
|
||||||
"creation": "2020-03-19 12:34:01.500390",
|
"allow_rename": 1,
|
||||||
|
"creation": "2022-01-13 20:07:30.096306",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"cost_center",
|
"cost_center",
|
||||||
"percentage_allocation"
|
"percentage"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@ -18,23 +19,23 @@
|
|||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "percentage_allocation",
|
"fieldname": "percentage",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Percent",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Percentage Allocation",
|
"label": "Percentage (%)",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-03-19 12:54:43.674655",
|
"modified": "2022-02-01 22:22:31.589523",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Distributed Cost Center",
|
"name": "Cost Center Allocation Percentage",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 1,
|
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"track_changes": 1
|
"states": []
|
||||||
}
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class CostCenterAllocationPercentage(Document):
|
||||||
|
pass
|
@ -39,9 +39,6 @@ def test_create_test_data():
|
|||||||
"selling_cost_center": "Main - _TC",
|
"selling_cost_center": "Main - _TC",
|
||||||
"income_account": "Sales - _TC"
|
"income_account": "Sales - _TC"
|
||||||
}],
|
}],
|
||||||
"show_in_website": 1,
|
|
||||||
"route":"-test-tesla-car",
|
|
||||||
"website_warehouse": "Stores - _TC"
|
|
||||||
})
|
})
|
||||||
item.insert()
|
item.insert()
|
||||||
# create test item price
|
# create test item price
|
||||||
|
@ -291,7 +291,7 @@ class PaymentRequest(Document):
|
|||||||
if not status:
|
if not status:
|
||||||
return
|
return
|
||||||
|
|
||||||
shopping_cart_settings = frappe.get_doc("Shopping Cart Settings")
|
shopping_cart_settings = frappe.get_doc("E Commerce Settings")
|
||||||
|
|
||||||
if status in ["Authorized", "Completed"]:
|
if status in ["Authorized", "Completed"]:
|
||||||
redirect_to = None
|
redirect_to = None
|
||||||
@ -435,13 +435,13 @@ def get_existing_payment_request_amount(ref_dt, ref_dn):
|
|||||||
""", (ref_dt, ref_dn))
|
""", (ref_dt, ref_dn))
|
||||||
return flt(existing_payment_request_amount[0][0]) if existing_payment_request_amount else 0
|
return flt(existing_payment_request_amount[0][0]) if existing_payment_request_amount else 0
|
||||||
|
|
||||||
def get_gateway_details(args):
|
def get_gateway_details(args): # nosemgrep
|
||||||
"""return gateway and payment account of default payment gateway"""
|
"""return gateway and payment account of default payment gateway"""
|
||||||
if args.get("payment_gateway_account"):
|
if args.get("payment_gateway_account"):
|
||||||
return get_payment_gateway_account(args.get("payment_gateway_account"))
|
return get_payment_gateway_account(args.get("payment_gateway_account"))
|
||||||
|
|
||||||
if args.order_type == "Shopping Cart":
|
if args.order_type == "Shopping Cart":
|
||||||
payment_gateway_account = frappe.get_doc("Shopping Cart Settings").payment_gateway_account
|
payment_gateway_account = frappe.get_doc("E Commerce Settings").payment_gateway_account
|
||||||
return get_payment_gateway_account(payment_gateway_account)
|
return get_payment_gateway_account(payment_gateway_account)
|
||||||
|
|
||||||
gateway_account = get_payment_gateway_account({"is_default": 1})
|
gateway_account = get_payment_gateway_account({"is_default": 1})
|
||||||
|
@ -42,7 +42,6 @@ class POSInvoice(SalesInvoice):
|
|||||||
self.validate_serialised_or_batched_item()
|
self.validate_serialised_or_batched_item()
|
||||||
self.validate_stock_availablility()
|
self.validate_stock_availablility()
|
||||||
self.validate_return_items_qty()
|
self.validate_return_items_qty()
|
||||||
self.validate_non_stock_items()
|
|
||||||
self.set_status()
|
self.set_status()
|
||||||
self.set_account_for_mode_of_payment()
|
self.set_account_for_mode_of_payment()
|
||||||
self.validate_pos()
|
self.validate_pos()
|
||||||
@ -158,22 +157,39 @@ class POSInvoice(SalesInvoice):
|
|||||||
frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another Sales Invoice. Please select valid serial no.")
|
frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another Sales Invoice. Please select valid serial no.")
|
||||||
.format(item.idx, bold_delivered_serial_nos), title=_("Item Unavailable"))
|
.format(item.idx, bold_delivered_serial_nos), title=_("Item Unavailable"))
|
||||||
|
|
||||||
|
def validate_invalid_serial_nos(self, item):
|
||||||
|
serial_nos = get_serial_nos(item.serial_no)
|
||||||
|
error_msg = []
|
||||||
|
invalid_serials, msg = "", ""
|
||||||
|
for serial_no in serial_nos:
|
||||||
|
if not frappe.db.exists('Serial No', serial_no):
|
||||||
|
invalid_serials = invalid_serials + (", " if invalid_serials else "") + serial_no
|
||||||
|
msg = (_("Row #{}: Following Serial numbers for item {} are <b>Invalid</b>: {}").format(item.idx, frappe.bold(item.get("item_code")), frappe.bold(invalid_serials)))
|
||||||
|
if invalid_serials:
|
||||||
|
error_msg.append(msg)
|
||||||
|
|
||||||
|
if error_msg:
|
||||||
|
frappe.throw(error_msg, title=_("Invalid Item"), as_list=True)
|
||||||
|
|
||||||
def validate_stock_availablility(self):
|
def validate_stock_availablility(self):
|
||||||
if self.is_return or self.docstatus != 1:
|
if self.is_return or self.docstatus != 1:
|
||||||
return
|
return
|
||||||
|
|
||||||
allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
|
allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
|
||||||
for d in self.get('items'):
|
for d in self.get('items'):
|
||||||
|
is_service_item = not (frappe.db.get_value('Item', d.get('item_code'), 'is_stock_item'))
|
||||||
|
if is_service_item:
|
||||||
|
return
|
||||||
if d.serial_no:
|
if d.serial_no:
|
||||||
self.validate_pos_reserved_serial_nos(d)
|
self.validate_pos_reserved_serial_nos(d)
|
||||||
self.validate_delivered_serial_nos(d)
|
self.validate_delivered_serial_nos(d)
|
||||||
|
self.validate_invalid_serial_nos(d)
|
||||||
elif d.batch_no:
|
elif d.batch_no:
|
||||||
self.validate_pos_reserved_batch_qty(d)
|
self.validate_pos_reserved_batch_qty(d)
|
||||||
else:
|
else:
|
||||||
if allow_negative_stock:
|
if allow_negative_stock:
|
||||||
return
|
return
|
||||||
|
|
||||||
available_stock = get_stock_availability(d.item_code, d.warehouse)
|
available_stock, is_stock_item = get_stock_availability(d.item_code, d.warehouse)
|
||||||
|
|
||||||
item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty)
|
item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty)
|
||||||
if flt(available_stock) <= 0:
|
if flt(available_stock) <= 0:
|
||||||
@ -244,14 +260,6 @@ class POSInvoice(SalesInvoice):
|
|||||||
.format(d.idx, bold_serial_no, bold_return_against)
|
.format(d.idx, bold_serial_no, bold_return_against)
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate_non_stock_items(self):
|
|
||||||
for d in self.get("items"):
|
|
||||||
is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item")
|
|
||||||
if not is_stock_item:
|
|
||||||
if not frappe.db.exists('Product Bundle', d.item_code):
|
|
||||||
frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice.")
|
|
||||||
.format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
|
|
||||||
|
|
||||||
def validate_mode_of_payment(self):
|
def validate_mode_of_payment(self):
|
||||||
if len(self.payments) == 0:
|
if len(self.payments) == 0:
|
||||||
frappe.throw(_("At least one mode of payment is required for POS invoice."))
|
frappe.throw(_("At least one mode of payment is required for POS invoice."))
|
||||||
@ -491,12 +499,18 @@ class POSInvoice(SalesInvoice):
|
|||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_stock_availability(item_code, warehouse):
|
def get_stock_availability(item_code, warehouse):
|
||||||
if frappe.db.get_value('Item', item_code, 'is_stock_item'):
|
if frappe.db.get_value('Item', item_code, 'is_stock_item'):
|
||||||
|
is_stock_item = True
|
||||||
bin_qty = get_bin_qty(item_code, warehouse)
|
bin_qty = get_bin_qty(item_code, warehouse)
|
||||||
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
|
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
|
||||||
return bin_qty - pos_sales_qty
|
return bin_qty - pos_sales_qty, is_stock_item
|
||||||
else:
|
else:
|
||||||
|
is_stock_item = False
|
||||||
if frappe.db.exists('Product Bundle', item_code):
|
if frappe.db.exists('Product Bundle', item_code):
|
||||||
return get_bundle_availability(item_code, warehouse)
|
return get_bundle_availability(item_code, warehouse), is_stock_item
|
||||||
|
else:
|
||||||
|
# Is a service item
|
||||||
|
return 0, is_stock_item
|
||||||
|
|
||||||
|
|
||||||
def get_bundle_availability(bundle_item_code, warehouse):
|
def get_bundle_availability(bundle_item_code, warehouse):
|
||||||
product_bundle = frappe.get_doc('Product Bundle', bundle_item_code)
|
product_bundle = frappe.get_doc('Product Bundle', bundle_item_code)
|
||||||
|
@ -354,6 +354,24 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
pos2.insert()
|
pos2.insert()
|
||||||
self.assertRaises(frappe.ValidationError, pos2.submit)
|
self.assertRaises(frappe.ValidationError, pos2.submit)
|
||||||
|
|
||||||
|
def test_invalid_serial_no_validation(self):
|
||||||
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
|
||||||
|
|
||||||
|
se = make_serialized_item(company='_Test Company',
|
||||||
|
target_warehouse="Stores - _TC", cost_center='Main - _TC', expense_account='Cost of Goods Sold - _TC')
|
||||||
|
serial_nos = se.get("items")[0].serial_no + 'wrong'
|
||||||
|
|
||||||
|
pos = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC',
|
||||||
|
account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC',
|
||||||
|
expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC',
|
||||||
|
item=se.get("items")[0].item_code, rate=1000, qty=2, do_not_save=1)
|
||||||
|
|
||||||
|
pos.get('items')[0].has_serial_no = 1
|
||||||
|
pos.get('items')[0].serial_no = serial_nos
|
||||||
|
pos.insert()
|
||||||
|
|
||||||
|
self.assertRaises(frappe.ValidationError, pos.submit)
|
||||||
|
|
||||||
def test_loyalty_points(self):
|
def test_loyalty_points(self):
|
||||||
from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
|
from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
|
||||||
get_loyalty_program_details_with_points,
|
get_loyalty_program_details_with_points,
|
||||||
|
@ -628,6 +628,26 @@ class TestPricingRule(unittest.TestCase):
|
|||||||
for doc in [si, si1]:
|
for doc in [si, si1]:
|
||||||
doc.delete()
|
doc.delete()
|
||||||
|
|
||||||
|
def test_multiple_pricing_rules_with_min_qty(self):
|
||||||
|
make_pricing_rule(discount_percentage=20, selling=1, priority=1, min_qty=4,
|
||||||
|
apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 1")
|
||||||
|
make_pricing_rule(discount_percentage=10, selling=1, priority=2, min_qty=4,
|
||||||
|
apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 2")
|
||||||
|
|
||||||
|
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1, currency="USD")
|
||||||
|
item = si.items[0]
|
||||||
|
item.stock_qty = 1
|
||||||
|
si.save()
|
||||||
|
self.assertFalse(item.discount_percentage)
|
||||||
|
item.qty = 5
|
||||||
|
item.stock_qty = 5
|
||||||
|
si.save()
|
||||||
|
self.assertEqual(item.discount_percentage, 30)
|
||||||
|
si.delete()
|
||||||
|
|
||||||
|
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 1")
|
||||||
|
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 2")
|
||||||
|
|
||||||
test_dependencies = ["Campaign"]
|
test_dependencies = ["Campaign"]
|
||||||
|
|
||||||
def make_pricing_rule(**args):
|
def make_pricing_rule(**args):
|
||||||
|
@ -73,7 +73,7 @@ def sorted_by_priority(pricing_rules, args, doc=None):
|
|||||||
for key in sorted(pricing_rule_dict):
|
for key in sorted(pricing_rule_dict):
|
||||||
pricing_rules_list.extend(pricing_rule_dict.get(key))
|
pricing_rules_list.extend(pricing_rule_dict.get(key))
|
||||||
|
|
||||||
return pricing_rules_list or pricing_rules
|
return pricing_rules_list
|
||||||
|
|
||||||
def filter_pricing_rule_based_on_condition(pricing_rules, doc=None):
|
def filter_pricing_rule_based_on_condition(pricing_rules, doc=None):
|
||||||
filtered_pricing_rules = []
|
filtered_pricing_rules = []
|
||||||
|
@ -548,6 +548,10 @@ class PurchaseInvoice(BuyingController):
|
|||||||
exchange_rate_map, net_rate_map = get_purchase_document_details(self)
|
exchange_rate_map, net_rate_map = get_purchase_document_details(self)
|
||||||
|
|
||||||
enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting'))
|
enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting'))
|
||||||
|
provisional_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company, \
|
||||||
|
'enable_provisional_accounting_for_non_stock_items'))
|
||||||
|
|
||||||
|
purchase_receipt_doc_map = {}
|
||||||
|
|
||||||
for item in self.get("items"):
|
for item in self.get("items"):
|
||||||
if flt(item.base_net_amount):
|
if flt(item.base_net_amount):
|
||||||
@ -643,19 +647,23 @@ class PurchaseInvoice(BuyingController):
|
|||||||
else:
|
else:
|
||||||
amount = flt(item.base_net_amount + item.item_tax_amount, item.precision("base_net_amount"))
|
amount = flt(item.base_net_amount + item.item_tax_amount, item.precision("base_net_amount"))
|
||||||
|
|
||||||
auto_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company, 'enable_perpetual_inventory_for_non_stock_items'))
|
if provisional_accounting_for_non_stock_items:
|
||||||
|
|
||||||
if auto_accounting_for_non_stock_items:
|
|
||||||
service_received_but_not_billed_account = self.get_company_default("service_received_but_not_billed")
|
|
||||||
|
|
||||||
if item.purchase_receipt:
|
if item.purchase_receipt:
|
||||||
|
provisional_account = self.get_company_default("default_provisional_account")
|
||||||
|
purchase_receipt_doc = purchase_receipt_doc_map.get(item.purchase_receipt)
|
||||||
|
|
||||||
|
if not purchase_receipt_doc:
|
||||||
|
purchase_receipt_doc = frappe.get_doc("Purchase Receipt", item.purchase_receipt)
|
||||||
|
purchase_receipt_doc_map[item.purchase_receipt] = purchase_receipt_doc
|
||||||
|
|
||||||
# Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt
|
# Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt
|
||||||
expense_booked_in_pr = frappe.db.get_value('GL Entry', {'is_cancelled': 0,
|
expense_booked_in_pr = frappe.db.get_value('GL Entry', {'is_cancelled': 0,
|
||||||
'voucher_type': 'Purchase Receipt', 'voucher_no': item.purchase_receipt, 'voucher_detail_no': item.pr_detail,
|
'voucher_type': 'Purchase Receipt', 'voucher_no': item.purchase_receipt, 'voucher_detail_no': item.pr_detail,
|
||||||
'account':service_received_but_not_billed_account}, ['name'])
|
'account':provisional_account}, ['name'])
|
||||||
|
|
||||||
if expense_booked_in_pr:
|
if expense_booked_in_pr:
|
||||||
expense_account = service_received_but_not_billed_account
|
# Intentionally passing purchase invoice item to handle partial billing
|
||||||
|
purchase_receipt_doc.add_provisional_gl_entry(item, gl_entries, self.posting_date, reverse=1)
|
||||||
|
|
||||||
if not self.is_internal_transfer():
|
if not self.is_internal_transfer():
|
||||||
gl_entries.append(self.get_gl_dict({
|
gl_entries.append(self.get_gl_dict({
|
||||||
|
@ -11,12 +11,17 @@ from frappe.utils import add_days, cint, flt, getdate, nowdate, today
|
|||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
|
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
|
||||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||||
|
from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice
|
||||||
|
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||||
from erpnext.controllers.accounts_controller import get_payment_terms
|
from erpnext.controllers.accounts_controller import get_payment_terms
|
||||||
from erpnext.controllers.buying_controller import QtyMismatchError
|
from erpnext.controllers.buying_controller import QtyMismatchError
|
||||||
from erpnext.exceptions import InvalidCurrency
|
from erpnext.exceptions import InvalidCurrency
|
||||||
from erpnext.projects.doctype.project.test_project import make_project
|
from erpnext.projects.doctype.project.test_project import make_project
|
||||||
from erpnext.stock.doctype.item.test_item import create_item
|
from erpnext.stock.doctype.item.test_item import create_item
|
||||||
|
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||||
|
make_purchase_invoice as create_purchase_invoice_from_receipt,
|
||||||
|
)
|
||||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
|
||||||
get_taxes,
|
get_taxes,
|
||||||
make_purchase_receipt,
|
make_purchase_receipt,
|
||||||
@ -1147,8 +1152,6 @@ class TestPurchaseInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
def test_purchase_invoice_advance_taxes(self):
|
def test_purchase_invoice_advance_taxes(self):
|
||||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||||
from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice
|
|
||||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
|
||||||
|
|
||||||
# create a new supplier to test
|
# create a new supplier to test
|
||||||
supplier = create_supplier(supplier_name = '_Test TDS Advance Supplier',
|
supplier = create_supplier(supplier_name = '_Test TDS Advance Supplier',
|
||||||
@ -1221,6 +1224,45 @@ class TestPurchaseInvoice(unittest.TestCase):
|
|||||||
payment_entry.load_from_db()
|
payment_entry.load_from_db()
|
||||||
self.assertEqual(payment_entry.taxes[0].allocated_amount, 0)
|
self.assertEqual(payment_entry.taxes[0].allocated_amount, 0)
|
||||||
|
|
||||||
|
def test_provisional_accounting_entry(self):
|
||||||
|
item = create_item("_Test Non Stock Item", is_stock_item=0)
|
||||||
|
provisional_account = create_account(account_name="Provision Account",
|
||||||
|
parent_account="Current Liabilities - _TC", company="_Test Company")
|
||||||
|
|
||||||
|
company = frappe.get_doc('Company', '_Test Company')
|
||||||
|
company.enable_provisional_accounting_for_non_stock_items = 1
|
||||||
|
company.default_provisional_account = provisional_account
|
||||||
|
company.save()
|
||||||
|
|
||||||
|
pr = make_purchase_receipt(item_code="_Test Non Stock Item", posting_date=add_days(nowdate(), -2))
|
||||||
|
|
||||||
|
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||||
|
pi.set_posting_time = 1
|
||||||
|
pi.posting_date = add_days(pr.posting_date, -1)
|
||||||
|
pi.items[0].expense_account = 'Cost of Goods Sold - _TC'
|
||||||
|
pi.save()
|
||||||
|
pi.submit()
|
||||||
|
|
||||||
|
# Check GLE for Purchase Invoice
|
||||||
|
expected_gle = [
|
||||||
|
['Cost of Goods Sold - _TC', 250, 0, add_days(pr.posting_date, -1)],
|
||||||
|
['Creditors - _TC', 0, 250, add_days(pr.posting_date, -1)]
|
||||||
|
]
|
||||||
|
|
||||||
|
check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
|
||||||
|
|
||||||
|
expected_gle_for_purchase_receipt = [
|
||||||
|
["Provision Account - _TC", 250, 0, pr.posting_date],
|
||||||
|
["_Test Account Cost for Goods Sold - _TC", 0, 250, pr.posting_date],
|
||||||
|
["Provision Account - _TC", 0, 250, pi.posting_date],
|
||||||
|
["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date]
|
||||||
|
]
|
||||||
|
|
||||||
|
check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
|
||||||
|
|
||||||
|
company.enable_provisional_accounting_for_non_stock_items = 0
|
||||||
|
company.save()
|
||||||
|
|
||||||
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||||
gl_entries = frappe.db.sql("""select account, debit, credit, posting_date
|
gl_entries = frappe.db.sql("""select account, debit, credit, posting_date
|
||||||
from `tabGL Entry`
|
from `tabGL Entry`
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
{% include "erpnext/regional/india/taxes.js" %}
|
{% include "erpnext/regional/india/taxes.js" %}
|
||||||
|
{% include "erpnext/regional/india/e_invoice/einvoice.js" %}
|
||||||
|
|
||||||
erpnext.setup_auto_gst_taxation('Sales Invoice');
|
erpnext.setup_auto_gst_taxation('Sales Invoice');
|
||||||
|
erpnext.setup_einvoice_actions('Sales Invoice')
|
||||||
|
|
||||||
frappe.ui.form.on("Sales Invoice", {
|
frappe.ui.form.on("Sales Invoice", {
|
||||||
setup: function(frm) {
|
setup: function(frm) {
|
||||||
|
@ -36,4 +36,139 @@ frappe.listview_settings['Sales Invoice'].onload = function (list_view) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
list_view.page.add_actions_menu_item(__('Generate E-Way Bill JSON'), action, false);
|
list_view.page.add_actions_menu_item(__('Generate E-Way Bill JSON'), action, false);
|
||||||
|
|
||||||
|
const generate_irns = () => {
|
||||||
|
const docnames = list_view.get_checked_items(true);
|
||||||
|
if (docnames && docnames.length) {
|
||||||
|
frappe.call({
|
||||||
|
method: 'erpnext.regional.india.e_invoice.utils.generate_einvoices',
|
||||||
|
args: { docnames },
|
||||||
|
freeze: true,
|
||||||
|
freeze_message: __('Generating E-Invoices...')
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
frappe.msgprint({
|
||||||
|
message: __('Please select at least one sales invoice to generate IRN'),
|
||||||
|
title: __('No Invoice Selected'),
|
||||||
|
indicator: 'red'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancel_irns = () => {
|
||||||
|
const docnames = list_view.get_checked_items(true);
|
||||||
|
|
||||||
|
const fields = [
|
||||||
|
{
|
||||||
|
"label": "Reason",
|
||||||
|
"fieldname": "reason",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"reqd": 1,
|
||||||
|
"default": "1-Duplicate",
|
||||||
|
"options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Remark",
|
||||||
|
"fieldname": "remark",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const d = new frappe.ui.Dialog({
|
||||||
|
title: __("Cancel IRN"),
|
||||||
|
fields: fields,
|
||||||
|
primary_action: function() {
|
||||||
|
const data = d.get_values();
|
||||||
|
frappe.call({
|
||||||
|
method: 'erpnext.regional.india.e_invoice.utils.cancel_irns',
|
||||||
|
args: {
|
||||||
|
doctype: list_view.doctype,
|
||||||
|
docnames,
|
||||||
|
reason: data.reason.split('-')[0],
|
||||||
|
remark: data.remark
|
||||||
|
},
|
||||||
|
freeze: true,
|
||||||
|
freeze_message: __('Cancelling E-Invoices...'),
|
||||||
|
});
|
||||||
|
d.hide();
|
||||||
|
},
|
||||||
|
primary_action_label: __('Submit')
|
||||||
|
});
|
||||||
|
d.show();
|
||||||
|
};
|
||||||
|
|
||||||
|
let einvoicing_enabled = false;
|
||||||
|
frappe.db.get_single_value("E Invoice Settings", "enable").then(enabled => {
|
||||||
|
einvoicing_enabled = enabled;
|
||||||
|
});
|
||||||
|
|
||||||
|
list_view.$result.on("change", "input[type=checkbox]", () => {
|
||||||
|
if (einvoicing_enabled) {
|
||||||
|
const docnames = list_view.get_checked_items(true);
|
||||||
|
// show/hide e-invoicing actions when no sales invoices are checked
|
||||||
|
if (docnames && docnames.length) {
|
||||||
|
// prevent adding actions twice if e-invoicing action group already exists
|
||||||
|
if (list_view.page.get_inner_group_button(__('E-Invoicing')).length == 0) {
|
||||||
|
list_view.page.add_inner_button(__('Generate IRNs'), generate_irns, __('E-Invoicing'));
|
||||||
|
list_view.page.add_inner_button(__('Cancel IRNs'), cancel_irns, __('E-Invoicing'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
list_view.page.remove_inner_button(__('Generate IRNs'), __('E-Invoicing'));
|
||||||
|
list_view.page.remove_inner_button(__('Cancel IRNs'), __('E-Invoicing'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
frappe.realtime.on("bulk_einvoice_generation_complete", (data) => {
|
||||||
|
const { failures, user, invoices } = data;
|
||||||
|
|
||||||
|
if (invoices.length != failures.length) {
|
||||||
|
frappe.msgprint({
|
||||||
|
message: __('{0} e-invoices generated successfully', [invoices.length]),
|
||||||
|
title: __('Bulk E-Invoice Generation Complete'),
|
||||||
|
indicator: 'orange'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failures && failures.length && user == frappe.session.user) {
|
||||||
|
let message = `
|
||||||
|
Failed to generate IRNs for following ${failures.length} sales invoices:
|
||||||
|
<ul style="padding-left: 20px; padding-top: 5px;">
|
||||||
|
${failures.map(d => `<li>${d.docname}</li>`).join('')}
|
||||||
|
</ul>
|
||||||
|
`;
|
||||||
|
frappe.msgprint({
|
||||||
|
message: message,
|
||||||
|
title: __('Bulk E-Invoice Generation Complete'),
|
||||||
|
indicator: 'orange'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
frappe.realtime.on("bulk_einvoice_cancellation_complete", (data) => {
|
||||||
|
const { failures, user, invoices } = data;
|
||||||
|
|
||||||
|
if (invoices.length != failures.length) {
|
||||||
|
frappe.msgprint({
|
||||||
|
message: __('{0} e-invoices cancelled successfully', [invoices.length]),
|
||||||
|
title: __('Bulk E-Invoice Cancellation Complete'),
|
||||||
|
indicator: 'orange'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failures && failures.length && user == frappe.session.user) {
|
||||||
|
let message = `
|
||||||
|
Failed to cancel IRNs for following ${failures.length} sales invoices:
|
||||||
|
<ul style="padding-left: 20px; padding-top: 5px;">
|
||||||
|
${failures.map(d => `<li>${d.docname}</li>`).join('')}
|
||||||
|
</ul>
|
||||||
|
`;
|
||||||
|
frappe.msgprint({
|
||||||
|
message: message,
|
||||||
|
title: __('Bulk E-Invoice Cancellation Complete'),
|
||||||
|
indicator: 'orange'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@ -469,7 +469,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
|||||||
let row = frappe.get_doc(d.doctype, d.name)
|
let row = frappe.get_doc(d.doctype, d.name)
|
||||||
set_timesheet_detail_rate(row.doctype, row.name, me.frm.doc.currency, row.timesheet_detail)
|
set_timesheet_detail_rate(row.doctype, row.name, me.frm.doc.currency, row.timesheet_detail)
|
||||||
});
|
});
|
||||||
frm.trigger("calculate_timesheet_totals");
|
this.frm.trigger("calculate_timesheet_totals");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -294,6 +294,8 @@ class SalesInvoice(SellingController):
|
|||||||
|
|
||||||
def before_cancel(self):
|
def before_cancel(self):
|
||||||
self.check_if_consolidated_invoice()
|
self.check_if_consolidated_invoice()
|
||||||
|
|
||||||
|
super(SalesInvoice, self).before_cancel()
|
||||||
self.update_time_sheet(None)
|
self.update_time_sheet(None)
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
|
@ -2100,6 +2100,54 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
self.assertEqual(data['billLists'][0]['actualFromStateCode'],7)
|
self.assertEqual(data['billLists'][0]['actualFromStateCode'],7)
|
||||||
self.assertEqual(data['billLists'][0]['fromStateCode'],27)
|
self.assertEqual(data['billLists'][0]['fromStateCode'],27)
|
||||||
|
|
||||||
|
def test_einvoice_submission_without_irn(self):
|
||||||
|
# init
|
||||||
|
einvoice_settings = frappe.get_doc('E Invoice Settings')
|
||||||
|
einvoice_settings.enable = 1
|
||||||
|
einvoice_settings.applicable_from = nowdate()
|
||||||
|
einvoice_settings.append('credentials', {
|
||||||
|
'company': '_Test Company',
|
||||||
|
'gstin': '27AAECE4835E1ZR',
|
||||||
|
'username': 'test',
|
||||||
|
'password': 'test'
|
||||||
|
})
|
||||||
|
einvoice_settings.save()
|
||||||
|
|
||||||
|
country = frappe.flags.country
|
||||||
|
frappe.flags.country = 'India'
|
||||||
|
|
||||||
|
si = make_sales_invoice_for_ewaybill()
|
||||||
|
self.assertRaises(frappe.ValidationError, si.submit)
|
||||||
|
|
||||||
|
si.irn = 'test_irn'
|
||||||
|
si.submit()
|
||||||
|
|
||||||
|
# reset
|
||||||
|
einvoice_settings = frappe.get_doc('E Invoice Settings')
|
||||||
|
einvoice_settings.enable = 0
|
||||||
|
frappe.flags.country = country
|
||||||
|
|
||||||
|
def test_einvoice_json(self):
|
||||||
|
from erpnext.regional.india.e_invoice.utils import make_einvoice, validate_totals
|
||||||
|
|
||||||
|
si = get_sales_invoice_for_e_invoice()
|
||||||
|
si.discount_amount = 100
|
||||||
|
si.save()
|
||||||
|
|
||||||
|
einvoice = make_einvoice(si)
|
||||||
|
self.assertTrue(einvoice['EwbDtls'])
|
||||||
|
validate_totals(einvoice)
|
||||||
|
|
||||||
|
si.apply_discount_on = 'Net Total'
|
||||||
|
si.save()
|
||||||
|
einvoice = make_einvoice(si)
|
||||||
|
validate_totals(einvoice)
|
||||||
|
|
||||||
|
[d.set('included_in_print_rate', 1) for d in si.taxes]
|
||||||
|
si.save()
|
||||||
|
einvoice = make_einvoice(si)
|
||||||
|
validate_totals(einvoice)
|
||||||
|
|
||||||
def test_item_tax_net_range(self):
|
def test_item_tax_net_range(self):
|
||||||
item = create_item("T Shirt")
|
item = create_item("T Shirt")
|
||||||
|
|
||||||
|
@ -71,7 +71,8 @@ class ShippingRule(Document):
|
|||||||
if doc.currency != doc.company_currency:
|
if doc.currency != doc.company_currency:
|
||||||
shipping_amount = flt(shipping_amount / doc.conversion_rate, 2)
|
shipping_amount = flt(shipping_amount / doc.conversion_rate, 2)
|
||||||
|
|
||||||
self.add_shipping_rule_to_tax_table(doc, shipping_amount)
|
if shipping_amount:
|
||||||
|
self.add_shipping_rule_to_tax_table(doc, shipping_amount)
|
||||||
|
|
||||||
def get_shipping_amount_from_rules(self, value):
|
def get_shipping_amount_from_rules(self, value):
|
||||||
for condition in self.get("conditions"):
|
for condition in self.get("conditions"):
|
||||||
|
@ -98,7 +98,7 @@ class TaxRule(Document):
|
|||||||
def validate_use_for_shopping_cart(self):
|
def validate_use_for_shopping_cart(self):
|
||||||
'''If shopping cart is enabled and no tax rule exists for shopping cart, enable this one'''
|
'''If shopping cart is enabled and no tax rule exists for shopping cart, enable this one'''
|
||||||
if (not self.use_for_shopping_cart
|
if (not self.use_for_shopping_cart
|
||||||
and cint(frappe.db.get_single_value('Shopping Cart Settings', 'enabled'))
|
and cint(frappe.db.get_single_value('E Commerce Settings', 'enabled'))
|
||||||
and not frappe.db.get_value('Tax Rule', {'use_for_shopping_cart': 1, 'name': ['!=', self.name]})):
|
and not frappe.db.get_value('Tax Rule', {'use_for_shopping_cart': 1, 'name': ['!=', self.name]})):
|
||||||
|
|
||||||
self.use_for_shopping_cart = 1
|
self.use_for_shopping_cart = 1
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
|
||||||
|
import copy
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.meta import get_field_precision
|
from frappe.model.meta import get_field_precision
|
||||||
@ -51,49 +53,57 @@ def validate_accounting_period(gl_map):
|
|||||||
.format(frappe.bold(accounting_periods[0].name)), ClosedAccountingPeriod)
|
.format(frappe.bold(accounting_periods[0].name)), ClosedAccountingPeriod)
|
||||||
|
|
||||||
def process_gl_map(gl_map, merge_entries=True, precision=None):
|
def process_gl_map(gl_map, merge_entries=True, precision=None):
|
||||||
|
if not gl_map:
|
||||||
|
return []
|
||||||
|
|
||||||
|
gl_map = distribute_gl_based_on_cost_center_allocation(gl_map, precision)
|
||||||
|
|
||||||
if merge_entries:
|
if merge_entries:
|
||||||
gl_map = merge_similar_entries(gl_map, precision)
|
gl_map = merge_similar_entries(gl_map, precision)
|
||||||
for entry in gl_map:
|
|
||||||
# toggle debit, credit if negative entry
|
|
||||||
if flt(entry.debit) < 0:
|
|
||||||
entry.credit = flt(entry.credit) - flt(entry.debit)
|
|
||||||
entry.debit = 0.0
|
|
||||||
|
|
||||||
if flt(entry.debit_in_account_currency) < 0:
|
gl_map = toggle_debit_credit_if_negative(gl_map)
|
||||||
entry.credit_in_account_currency = \
|
|
||||||
flt(entry.credit_in_account_currency) - flt(entry.debit_in_account_currency)
|
|
||||||
entry.debit_in_account_currency = 0.0
|
|
||||||
|
|
||||||
if flt(entry.credit) < 0:
|
|
||||||
entry.debit = flt(entry.debit) - flt(entry.credit)
|
|
||||||
entry.credit = 0.0
|
|
||||||
|
|
||||||
if flt(entry.credit_in_account_currency) < 0:
|
|
||||||
entry.debit_in_account_currency = \
|
|
||||||
flt(entry.debit_in_account_currency) - flt(entry.credit_in_account_currency)
|
|
||||||
entry.credit_in_account_currency = 0.0
|
|
||||||
|
|
||||||
update_net_values(entry)
|
|
||||||
|
|
||||||
return gl_map
|
return gl_map
|
||||||
|
|
||||||
def update_net_values(entry):
|
def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None):
|
||||||
# In some scenarios net value needs to be shown in the ledger
|
cost_center_allocation = get_cost_center_allocation_data(gl_map[0]["company"], gl_map[0]["posting_date"])
|
||||||
# This method updates net values as debit or credit
|
if not cost_center_allocation:
|
||||||
if entry.post_net_value and entry.debit and entry.credit:
|
return gl_map
|
||||||
if entry.debit > entry.credit:
|
|
||||||
entry.debit = entry.debit - entry.credit
|
|
||||||
entry.debit_in_account_currency = entry.debit_in_account_currency \
|
|
||||||
- entry.credit_in_account_currency
|
|
||||||
entry.credit = 0
|
|
||||||
entry.credit_in_account_currency = 0
|
|
||||||
else:
|
|
||||||
entry.credit = entry.credit - entry.debit
|
|
||||||
entry.credit_in_account_currency = entry.credit_in_account_currency \
|
|
||||||
- entry.debit_in_account_currency
|
|
||||||
|
|
||||||
entry.debit = 0
|
new_gl_map = []
|
||||||
entry.debit_in_account_currency = 0
|
for d in gl_map:
|
||||||
|
cost_center = d.get("cost_center")
|
||||||
|
if cost_center and cost_center_allocation.get(cost_center):
|
||||||
|
for sub_cost_center, percentage in cost_center_allocation.get(cost_center, {}).items():
|
||||||
|
gle = copy.deepcopy(d)
|
||||||
|
gle.cost_center = sub_cost_center
|
||||||
|
for field in ("debit", "credit", "debit_in_account_currency", "credit_in_company_currency"):
|
||||||
|
gle[field] = flt(flt(d.get(field)) * percentage / 100, precision)
|
||||||
|
new_gl_map.append(gle)
|
||||||
|
else:
|
||||||
|
new_gl_map.append(d)
|
||||||
|
|
||||||
|
return new_gl_map
|
||||||
|
|
||||||
|
def get_cost_center_allocation_data(company, posting_date):
|
||||||
|
par = frappe.qb.DocType("Cost Center Allocation")
|
||||||
|
child = frappe.qb.DocType("Cost Center Allocation Percentage")
|
||||||
|
|
||||||
|
records = (
|
||||||
|
frappe.qb.from_(par).inner_join(child).on(par.name == child.parent)
|
||||||
|
.select(par.main_cost_center, child.cost_center, child.percentage)
|
||||||
|
.where(par.docstatus == 1)
|
||||||
|
.where(par.company == company)
|
||||||
|
.where(par.valid_from <= posting_date)
|
||||||
|
.orderby(par.valid_from, order=frappe.qb.desc)
|
||||||
|
).run(as_dict=True)
|
||||||
|
|
||||||
|
cc_allocation = frappe._dict()
|
||||||
|
for d in records:
|
||||||
|
cc_allocation.setdefault(d.main_cost_center, frappe._dict())\
|
||||||
|
.setdefault(d.cost_center, d.percentage)
|
||||||
|
|
||||||
|
return cc_allocation
|
||||||
|
|
||||||
def merge_similar_entries(gl_map, precision=None):
|
def merge_similar_entries(gl_map, precision=None):
|
||||||
merged_gl_map = []
|
merged_gl_map = []
|
||||||
@ -145,6 +155,49 @@ def check_if_in_list(gle, gl_map, dimensions=None):
|
|||||||
if same_head:
|
if same_head:
|
||||||
return e
|
return e
|
||||||
|
|
||||||
|
def toggle_debit_credit_if_negative(gl_map):
|
||||||
|
for entry in gl_map:
|
||||||
|
# toggle debit, credit if negative entry
|
||||||
|
if flt(entry.debit) < 0:
|
||||||
|
entry.credit = flt(entry.credit) - flt(entry.debit)
|
||||||
|
entry.debit = 0.0
|
||||||
|
|
||||||
|
if flt(entry.debit_in_account_currency) < 0:
|
||||||
|
entry.credit_in_account_currency = \
|
||||||
|
flt(entry.credit_in_account_currency) - flt(entry.debit_in_account_currency)
|
||||||
|
entry.debit_in_account_currency = 0.0
|
||||||
|
|
||||||
|
if flt(entry.credit) < 0:
|
||||||
|
entry.debit = flt(entry.debit) - flt(entry.credit)
|
||||||
|
entry.credit = 0.0
|
||||||
|
|
||||||
|
if flt(entry.credit_in_account_currency) < 0:
|
||||||
|
entry.debit_in_account_currency = \
|
||||||
|
flt(entry.debit_in_account_currency) - flt(entry.credit_in_account_currency)
|
||||||
|
entry.credit_in_account_currency = 0.0
|
||||||
|
|
||||||
|
update_net_values(entry)
|
||||||
|
|
||||||
|
return gl_map
|
||||||
|
|
||||||
|
def update_net_values(entry):
|
||||||
|
# In some scenarios net value needs to be shown in the ledger
|
||||||
|
# This method updates net values as debit or credit
|
||||||
|
if entry.post_net_value and entry.debit and entry.credit:
|
||||||
|
if entry.debit > entry.credit:
|
||||||
|
entry.debit = entry.debit - entry.credit
|
||||||
|
entry.debit_in_account_currency = entry.debit_in_account_currency \
|
||||||
|
- entry.credit_in_account_currency
|
||||||
|
entry.credit = 0
|
||||||
|
entry.credit_in_account_currency = 0
|
||||||
|
else:
|
||||||
|
entry.credit = entry.credit - entry.debit
|
||||||
|
entry.credit_in_account_currency = entry.credit_in_account_currency \
|
||||||
|
- entry.debit_in_account_currency
|
||||||
|
|
||||||
|
entry.debit = 0
|
||||||
|
entry.debit_in_account_currency = 0
|
||||||
|
|
||||||
def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
|
def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
|
||||||
if not from_repost:
|
if not from_repost:
|
||||||
validate_cwip_accounts(gl_map)
|
validate_cwip_accounts(gl_map)
|
||||||
|
@ -58,7 +58,7 @@ def _get_party_details(party=None, account=None, party_type="Customer", company=
|
|||||||
frappe.throw(_("Not permitted for {0}").format(party), frappe.PermissionError)
|
frappe.throw(_("Not permitted for {0}").format(party), frappe.PermissionError)
|
||||||
|
|
||||||
party = frappe.get_doc(party_type, party)
|
party = frappe.get_doc(party_type, party)
|
||||||
currency = party.default_currency if party.get("default_currency") else get_company_currency(company)
|
currency = party.get("default_currency") or currency or get_company_currency(company)
|
||||||
|
|
||||||
party_address, shipping_address = set_address_details(party_details, party, party_type, doctype, company, party_address, company_address, shipping_address)
|
party_address, shipping_address = set_address_details(party_details, party, party_type, doctype, company, party_address, company_address, shipping_address)
|
||||||
set_contact_details(party_details, party, party_type)
|
set_contact_details(party_details, party, party_type)
|
||||||
|
173
erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html
Normal file
173
erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
{%- from "templates/print_formats/standard_macros.html" import add_header, render_field, print_value -%}
|
||||||
|
{%- set einvoice = json.loads(doc.signed_einvoice) -%}
|
||||||
|
|
||||||
|
<div class="page-break">
|
||||||
|
<div {% if print_settings.repeat_header_footer %} id="header-html" class="hidden-pdf" {% endif %}>
|
||||||
|
{% if letter_head and not no_letterhead %}
|
||||||
|
<div class="letter-head">{{ letter_head }}</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="print-heading">
|
||||||
|
<h2>E Invoice<br><small>{{ doc.name }}</small></h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if print_settings.repeat_header_footer %}
|
||||||
|
<div id="footer-html" class="visible-pdf">
|
||||||
|
{% if not no_letterhead and footer %}
|
||||||
|
<div class="letter-head-footer">
|
||||||
|
{{ footer }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<p class="text-center small page-number visible-pdf">
|
||||||
|
{{ _("Page {0} of {1}").format('<span class="page"></span>', '<span class="topage"></span>') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<h5 class="font-bold" style="margin-top: 0px;">1. Transaction Details</h5>
|
||||||
|
<div class="row section-break" style="border-bottom: 1px solid #d1d8dd; padding-bottom: 10px;">
|
||||||
|
<div class="col-xs-8 column-break">
|
||||||
|
<div class="row data-field">
|
||||||
|
<div class="col-xs-4"><label>IRN</label></div>
|
||||||
|
<div class="col-xs-8 value">{{ einvoice.Irn }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row data-field">
|
||||||
|
<div class="col-xs-4"><label>Ack. No</label></div>
|
||||||
|
<div class="col-xs-8 value">{{ einvoice.AckNo }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row data-field">
|
||||||
|
<div class="col-xs-4"><label>Ack. Date</label></div>
|
||||||
|
<div class="col-xs-8 value">{{ frappe.utils.format_datetime(einvoice.AckDt, "dd/MM/yyyy hh:mm:ss") }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row data-field">
|
||||||
|
<div class="col-xs-4"><label>Category</label></div>
|
||||||
|
<div class="col-xs-8 value">{{ einvoice.TranDtls.SupTyp }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row data-field">
|
||||||
|
<div class="col-xs-4"><label>Document Type</label></div>
|
||||||
|
<div class="col-xs-8 value">{{ einvoice.DocDtls.Typ }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row data-field">
|
||||||
|
<div class="col-xs-4"><label>Document No</label></div>
|
||||||
|
<div class="col-xs-8 value">{{ einvoice.DocDtls.No }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-4 column-break">
|
||||||
|
<img src="{{ doc.qrcode_image }}" width="175px" style="float: right;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h5 class="font-bold" style="margin-top: 15px; margin-bottom: 10px;">2. Party Details</h5>
|
||||||
|
<div class="row section-break" style="border-bottom: 1px solid #d1d8dd; padding-bottom: 10px;">
|
||||||
|
{%- set seller = einvoice.SellerDtls -%}
|
||||||
|
<div class="col-xs-6 column-break">
|
||||||
|
<h5 style="margin-bottom: 5px;">Seller</h5>
|
||||||
|
<p>{{ seller.Gstin }}</p>
|
||||||
|
<p>{{ seller.LglNm }}</p>
|
||||||
|
<p>{{ seller.Addr1 }}</p>
|
||||||
|
{%- if seller.Addr2 -%} <p>{{ seller.Addr2 }}</p> {% endif %}
|
||||||
|
<p>{{ seller.Loc }}</p>
|
||||||
|
<p>{{ frappe.db.get_value("Address", doc.company_address, "gst_state") }} - {{ seller.Pin }}</p>
|
||||||
|
|
||||||
|
{%- if einvoice.ShipDtls -%}
|
||||||
|
{%- set shipping = einvoice.ShipDtls -%}
|
||||||
|
<h5 style="margin-bottom: 5px;">Shipped From</h5>
|
||||||
|
<p>{{ shipping.Gstin }}</p>
|
||||||
|
<p>{{ shipping.LglNm }}</p>
|
||||||
|
<p>{{ shipping.Addr1 }}</p>
|
||||||
|
{%- if shipping.Addr2 -%} <p>{{ shipping.Addr2 }}</p> {% endif %}
|
||||||
|
<p>{{ shipping.Loc }}</p>
|
||||||
|
<p>{{ frappe.db.get_value("Address", doc.shipping_address_name, "gst_state") }} - {{ shipping.Pin }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{%- set buyer = einvoice.BuyerDtls -%}
|
||||||
|
<div class="col-xs-6 column-break">
|
||||||
|
<h5 style="margin-bottom: 5px;">Buyer</h5>
|
||||||
|
<p>{{ buyer.Gstin }}</p>
|
||||||
|
<p>{{ buyer.LglNm }}</p>
|
||||||
|
<p>{{ buyer.Addr1 }}</p>
|
||||||
|
{%- if buyer.Addr2 -%} <p>{{ buyer.Addr2 }}</p> {% endif %}
|
||||||
|
<p>{{ buyer.Loc }}</p>
|
||||||
|
<p>{{ frappe.db.get_value("Address", doc.customer_address, "gst_state") }} - {{ buyer.Pin }}</p>
|
||||||
|
|
||||||
|
{%- if einvoice.DispDtls -%}
|
||||||
|
{%- set dispatch = einvoice.DispDtls -%}
|
||||||
|
<h5 style="margin-bottom: 5px;">Dispatched From</h5>
|
||||||
|
{%- if dispatch.Gstin -%} <p>{{ dispatch.Gstin }}</p> {% endif %}
|
||||||
|
<p>{{ dispatch.LglNm }}</p>
|
||||||
|
<p>{{ dispatch.Addr1 }}</p>
|
||||||
|
{%- if dispatch.Addr2 -%} <p>{{ dispatch.Addr2 }}</p> {% endif %}
|
||||||
|
<p>{{ dispatch.Loc }}</p>
|
||||||
|
<p>{{ frappe.db.get_value("Address", doc.dispatch_address_name, "gst_state") }} - {{ dispatch.Pin }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="overflow-x: auto;">
|
||||||
|
<h5 class="font-bold" style="margin-top: 15px; margin-bottom: 10px;">3. Item Details</h5>
|
||||||
|
<table class="table table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-left" style="width: 3%;">Sr. No.</th>
|
||||||
|
<th class="text-left">Item</th>
|
||||||
|
<th class="text-left" style="width: 10%;">HSN Code</th>
|
||||||
|
<th class="text-left" style="width: 5%;">Qty</th>
|
||||||
|
<th class="text-left" style="width: 5%;">UOM</th>
|
||||||
|
<th class="text-left">Rate</th>
|
||||||
|
<th class="text-left" style="width: 5%;">Discount</th>
|
||||||
|
<th class="text-left">Taxable Amount</th>
|
||||||
|
<th class="text-left" style="width: 7%;">Tax Rate</th>
|
||||||
|
<th class="text-left" style="width: 5%;">Other Charges</th>
|
||||||
|
<th class="text-left">Total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item in einvoice.ItemList %}
|
||||||
|
<tr>
|
||||||
|
<td class="text-left" style="width: 3%;">{{ item.SlNo }}</td>
|
||||||
|
<td class="text-left">{{ item.PrdDesc }}</td>
|
||||||
|
<td class="text-left" style="width: 10%;">{{ item.HsnCd }}</td>
|
||||||
|
<td class="text-right" style="width: 5%;">{{ item.Qty }}</td>
|
||||||
|
<td class="text-left" style="width: 5%;">{{ item.Unit }}</td>
|
||||||
|
<td class="text-right">{{ frappe.utils.fmt_money(item.UnitPrice, None, "INR") }}</td>
|
||||||
|
<td class="text-right" style="width: 5%;">{{ frappe.utils.fmt_money(item.Discount, None, "INR") }}</td>
|
||||||
|
<td class="text-right">{{ frappe.utils.fmt_money(item.AssAmt, None, "INR") }}</td>
|
||||||
|
<td class="text-right" style="width: 7%;">{{ item.GstRt + item.CesRt }} %</td>
|
||||||
|
<td class="text-right" style="width: 5%;">{{ frappe.utils.fmt_money(0, None, "INR") }}</td>
|
||||||
|
<td class="text-right">{{ frappe.utils.fmt_money(item.TotItemVal, None, "INR") }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div style="overflow-x: auto;">
|
||||||
|
<h5 class="font-bold" style="margin-bottom: 0px;">4. Value Details</h5>
|
||||||
|
<table class="table table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-left">Taxable Amount</th>
|
||||||
|
<th class="text-left">CGST</th>
|
||||||
|
<th class="text-left"">SGST</th>
|
||||||
|
<th class="text-left">IGST</th>
|
||||||
|
<th class="text-left">CESS</th>
|
||||||
|
<th class="text-left" style="width: 10%;">State CESS</th>
|
||||||
|
<th class="text-left">Discount</th>
|
||||||
|
<th class="text-left" style="width: 10%;">Other Charges</th>
|
||||||
|
<th class="text-left" style="width: 10%;">Round Off</th>
|
||||||
|
<th class="text-left">Total Value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{%- set value_details = einvoice.ValDtls -%}
|
||||||
|
<tr>
|
||||||
|
<td class="text-right">{{ frappe.utils.fmt_money(value_details.AssVal, None, "INR") }}</td>
|
||||||
|
<td class="text-right">{{ frappe.utils.fmt_money(value_details.CgstVal, None, "INR") }}</td>
|
||||||
|
<td class="text-right">{{ frappe.utils.fmt_money(value_details.SgstVal, None, "INR") }}</td>
|
||||||
|
<td class="text-right">{{ frappe.utils.fmt_money(value_details.IgstVal, None, "INR") }}</td>
|
||||||
|
<td class="text-right">{{ frappe.utils.fmt_money(value_details.CesVal, None, "INR") }}</td>
|
||||||
|
<td class="text-right">{{ frappe.utils.fmt_money(0, None, "INR") }}</td>
|
||||||
|
<td class="text-right">{{ frappe.utils.fmt_money(value_details.Discount, None, "INR") }}</td>
|
||||||
|
<td class="text-right">{{ frappe.utils.fmt_money(value_details.OthChrg, None, "INR") }}</td>
|
||||||
|
<td class="text-right">{{ frappe.utils.fmt_money(value_details.RndOffAmt, None, "INR") }}</td>
|
||||||
|
<td class="text-right">{{ frappe.utils.fmt_money(value_details.TotInvVal, None, "INR") }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"align_labels_right": 1,
|
||||||
|
"creation": "2020-10-10 18:01:21.032914",
|
||||||
|
"custom_format": 0,
|
||||||
|
"default_print_language": "en-US",
|
||||||
|
"disabled": 1,
|
||||||
|
"doc_type": "Sales Invoice",
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Print Format",
|
||||||
|
"font": "Default",
|
||||||
|
"html": "",
|
||||||
|
"idx": 0,
|
||||||
|
"line_breaks": 1,
|
||||||
|
"modified": "2020-10-23 19:54:40.634936",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "GST E-Invoice",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"print_format_builder": 0,
|
||||||
|
"print_format_type": "Jinja",
|
||||||
|
"raw_printing": 0,
|
||||||
|
"show_section_headings": 1,
|
||||||
|
"standard": "Yes"
|
||||||
|
}
|
@ -5,7 +5,6 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _, scrub
|
from frappe import _, scrub
|
||||||
from frappe.utils import cint, flt
|
from frappe.utils import cint, flt
|
||||||
from six import iteritems
|
|
||||||
|
|
||||||
from erpnext.accounts.party import get_partywise_advanced_payment_amount
|
from erpnext.accounts.party import get_partywise_advanced_payment_amount
|
||||||
from erpnext.accounts.report.accounts_receivable.accounts_receivable import ReceivablePayableReport
|
from erpnext.accounts.report.accounts_receivable.accounts_receivable import ReceivablePayableReport
|
||||||
@ -40,7 +39,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
|||||||
if self.filters.show_gl_balance:
|
if self.filters.show_gl_balance:
|
||||||
gl_balance_map = get_gl_balance(self.filters.report_date)
|
gl_balance_map = get_gl_balance(self.filters.report_date)
|
||||||
|
|
||||||
for party, party_dict in iteritems(self.party_total):
|
for party, party_dict in self.party_total.items():
|
||||||
if party_dict.outstanding == 0:
|
if party_dict.outstanding == 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -29,18 +29,6 @@ def execute(filters=None):
|
|||||||
dimension_items = cam_map.get(dimension)
|
dimension_items = cam_map.get(dimension)
|
||||||
if dimension_items:
|
if dimension_items:
|
||||||
data = get_final_data(dimension, dimension_items, filters, period_month_ranges, data, 0)
|
data = get_final_data(dimension, dimension_items, filters, period_month_ranges, data, 0)
|
||||||
else:
|
|
||||||
DCC_allocation = frappe.db.sql('''SELECT parent, sum(percentage_allocation) as percentage_allocation
|
|
||||||
FROM `tabDistributed Cost Center`
|
|
||||||
WHERE cost_center IN %(dimension)s
|
|
||||||
AND parent NOT IN %(dimension)s
|
|
||||||
GROUP BY parent''',{'dimension':[dimension]})
|
|
||||||
if DCC_allocation:
|
|
||||||
filters['budget_against_filter'] = [DCC_allocation[0][0]]
|
|
||||||
ddc_cam_map = get_dimension_account_month_map(filters)
|
|
||||||
dimension_items = ddc_cam_map.get(DCC_allocation[0][0])
|
|
||||||
if dimension_items:
|
|
||||||
data = get_final_data(dimension, dimension_items, filters, period_month_ranges, data, DCC_allocation[0][1])
|
|
||||||
|
|
||||||
chart = get_chart_data(filters, columns, data)
|
chart = get_chart_data(filters, columns, data)
|
||||||
|
|
||||||
|
@ -387,42 +387,15 @@ def set_gl_entries_by_account(
|
|||||||
key: value
|
key: value
|
||||||
})
|
})
|
||||||
|
|
||||||
distributed_cost_center_query = ""
|
gl_entries = frappe.db.sql("""
|
||||||
if filters and filters.get('cost_center'):
|
select posting_date, account, debit, credit, is_opening, fiscal_year,
|
||||||
distributed_cost_center_query = """
|
debit_in_account_currency, credit_in_account_currency, account_currency from `tabGL Entry`
|
||||||
UNION ALL
|
|
||||||
SELECT posting_date,
|
|
||||||
account,
|
|
||||||
debit*(DCC_allocation.percentage_allocation/100) as debit,
|
|
||||||
credit*(DCC_allocation.percentage_allocation/100) as credit,
|
|
||||||
is_opening,
|
|
||||||
fiscal_year,
|
|
||||||
debit_in_account_currency*(DCC_allocation.percentage_allocation/100) as debit_in_account_currency,
|
|
||||||
credit_in_account_currency*(DCC_allocation.percentage_allocation/100) as credit_in_account_currency,
|
|
||||||
account_currency
|
|
||||||
FROM `tabGL Entry`,
|
|
||||||
(
|
|
||||||
SELECT parent, sum(percentage_allocation) as percentage_allocation
|
|
||||||
FROM `tabDistributed Cost Center`
|
|
||||||
WHERE cost_center IN %(cost_center)s
|
|
||||||
AND parent NOT IN %(cost_center)s
|
|
||||||
GROUP BY parent
|
|
||||||
) as DCC_allocation
|
|
||||||
WHERE company=%(company)s
|
|
||||||
{additional_conditions}
|
|
||||||
AND posting_date <= %(to_date)s
|
|
||||||
AND is_cancelled = 0
|
|
||||||
AND cost_center = DCC_allocation.parent
|
|
||||||
""".format(additional_conditions=additional_conditions.replace("and cost_center in %(cost_center)s ", ''))
|
|
||||||
|
|
||||||
gl_entries = frappe.db.sql("""select posting_date, account, debit, credit, is_opening, fiscal_year, debit_in_account_currency, credit_in_account_currency, account_currency from `tabGL Entry`
|
|
||||||
where company=%(company)s
|
where company=%(company)s
|
||||||
{additional_conditions}
|
{additional_conditions}
|
||||||
and posting_date <= %(to_date)s
|
and posting_date <= %(to_date)s
|
||||||
and is_cancelled = 0
|
and is_cancelled = 0""".format(
|
||||||
{distributed_cost_center_query}""".format(
|
additional_conditions=additional_conditions), gl_filters, as_dict=True
|
||||||
additional_conditions=additional_conditions,
|
)
|
||||||
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), filters.get('company'))
|
convert_to_presentation_currency(gl_entries, get_currency(filters), filters.get('company'))
|
||||||
|
@ -176,44 +176,7 @@ def get_gl_entries(filters, accounting_dimensions):
|
|||||||
if accounting_dimensions:
|
if accounting_dimensions:
|
||||||
dimension_fields = ', '.join(accounting_dimensions) + ','
|
dimension_fields = ', '.join(accounting_dimensions) + ','
|
||||||
|
|
||||||
distributed_cost_center_query = ""
|
gl_entries = frappe.db.sql("""
|
||||||
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,
|
|
||||||
credit_in_account_currency*(DCC_allocation.percentage_allocation/100) as credit_in_account_currency """
|
|
||||||
|
|
||||||
distributed_cost_center_query = """
|
|
||||||
UNION ALL
|
|
||||||
SELECT name as gl_entry,
|
|
||||||
posting_date,
|
|
||||||
account,
|
|
||||||
party_type,
|
|
||||||
party,
|
|
||||||
voucher_type,
|
|
||||||
voucher_no, {dimension_fields}
|
|
||||||
cost_center, project,
|
|
||||||
against_voucher_type,
|
|
||||||
against_voucher,
|
|
||||||
account_currency,
|
|
||||||
remarks, against,
|
|
||||||
is_opening, `tabGL Entry`.creation {select_fields_with_percentage}
|
|
||||||
FROM `tabGL Entry`,
|
|
||||||
(
|
|
||||||
SELECT parent, sum(percentage_allocation) as percentage_allocation
|
|
||||||
FROM `tabDistributed Cost Center`
|
|
||||||
WHERE cost_center IN %(cost_center)s
|
|
||||||
AND parent NOT IN %(cost_center)s
|
|
||||||
GROUP BY parent
|
|
||||||
) as DCC_allocation
|
|
||||||
WHERE company=%(company)s
|
|
||||||
{conditions}
|
|
||||||
AND posting_date <= %(to_date)s
|
|
||||||
AND cost_center = DCC_allocation.parent
|
|
||||||
""".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(
|
|
||||||
"""
|
|
||||||
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, {dimension_fields}
|
voucher_type, voucher_no, {dimension_fields}
|
||||||
@ -222,13 +185,11 @@ def get_gl_entries(filters, accounting_dimensions):
|
|||||||
remarks, against, is_opening, creation {select_fields}
|
remarks, against, is_opening, creation {select_fields}
|
||||||
from `tabGL Entry`
|
from `tabGL Entry`
|
||||||
where company=%(company)s {conditions}
|
where company=%(company)s {conditions}
|
||||||
{distributed_cost_center_query}
|
|
||||||
{order_by_statement}
|
{order_by_statement}
|
||||||
""".format(
|
""".format(
|
||||||
dimension_fields=dimension_fields, select_fields=select_fields, conditions=get_conditions(filters), distributed_cost_center_query=distributed_cost_center_query,
|
dimension_fields=dimension_fields, select_fields=select_fields,
|
||||||
order_by_statement=order_by_statement
|
conditions=get_conditions(filters), 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, filters.get('company'))
|
return convert_to_presentation_currency(gl_entries, currency_map, filters.get('company'))
|
||||||
|
@ -109,7 +109,6 @@ def accumulate_values_into_parents(accounts, accounts_by_name):
|
|||||||
|
|
||||||
def prepare_data(accounts, filters, total_row, parent_children_map, based_on):
|
def prepare_data(accounts, filters, total_row, parent_children_map, based_on):
|
||||||
data = []
|
data = []
|
||||||
new_accounts = accounts
|
|
||||||
company_currency = frappe.get_cached_value('Company', filters.get("company"), "default_currency")
|
company_currency = frappe.get_cached_value('Company', filters.get("company"), "default_currency")
|
||||||
|
|
||||||
for d in accounts:
|
for d in accounts:
|
||||||
@ -123,19 +122,6 @@ def prepare_data(accounts, filters, total_row, parent_children_map, based_on):
|
|||||||
"currency": company_currency,
|
"currency": company_currency,
|
||||||
"based_on": based_on
|
"based_on": based_on
|
||||||
}
|
}
|
||||||
if based_on == 'cost_center':
|
|
||||||
cost_center_doc = frappe.get_doc("Cost Center",d.name)
|
|
||||||
if not cost_center_doc.enable_distributed_cost_center:
|
|
||||||
DCC_allocation = frappe.db.sql("""SELECT parent, sum(percentage_allocation) as percentage_allocation
|
|
||||||
FROM `tabDistributed Cost Center`
|
|
||||||
WHERE cost_center IN %(cost_center)s
|
|
||||||
AND parent NOT IN %(cost_center)s
|
|
||||||
GROUP BY parent""",{'cost_center': [d.name]})
|
|
||||||
if DCC_allocation:
|
|
||||||
for account in new_accounts:
|
|
||||||
if account['name'] == DCC_allocation[0][0]:
|
|
||||||
for value in value_fields:
|
|
||||||
d[value] += account[value]*(DCC_allocation[0][1]/100)
|
|
||||||
|
|
||||||
for key in value_fields:
|
for key in value_fields:
|
||||||
row[key] = flt(d.get(key, 0.0), 3)
|
row[key] = flt(d.get(key, 0.0), 3)
|
||||||
|
@ -1023,6 +1023,17 @@
|
|||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"type": "Link"
|
"type": "Link"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"dependencies": "Cost Center",
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 0,
|
||||||
|
"label": "Cost Center Allocation",
|
||||||
|
"link_count": 0,
|
||||||
|
"link_to": "Cost Center Allocation",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"dependencies": "Cost Center",
|
"dependencies": "Cost Center",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
|
@ -108,6 +108,10 @@ frappe.ui.form.on('Asset', {
|
|||||||
frm.trigger("create_asset_repair");
|
frm.trigger("create_asset_repair");
|
||||||
}, __("Manage"));
|
}, __("Manage"));
|
||||||
|
|
||||||
|
frm.add_custom_button(__("Split Asset"), function() {
|
||||||
|
frm.trigger("split_asset");
|
||||||
|
}, __("Manage"));
|
||||||
|
|
||||||
if (frm.doc.status != 'Fully Depreciated') {
|
if (frm.doc.status != 'Fully Depreciated') {
|
||||||
frm.add_custom_button(__("Adjust Asset Value"), function() {
|
frm.add_custom_button(__("Adjust Asset Value"), function() {
|
||||||
frm.trigger("create_asset_value_adjustment");
|
frm.trigger("create_asset_value_adjustment");
|
||||||
@ -322,6 +326,43 @@ frappe.ui.form.on('Asset', {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
split_asset: function(frm) {
|
||||||
|
const title = __('Split Asset');
|
||||||
|
|
||||||
|
const fields = [
|
||||||
|
{
|
||||||
|
fieldname: 'split_qty',
|
||||||
|
fieldtype: 'Int',
|
||||||
|
label: __('Split Qty'),
|
||||||
|
reqd: 1
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let dialog = new frappe.ui.Dialog({
|
||||||
|
title: title,
|
||||||
|
fields: fields
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.set_primary_action(__('Split'), function() {
|
||||||
|
const dialog_data = dialog.get_values();
|
||||||
|
frappe.call({
|
||||||
|
args: {
|
||||||
|
"asset_name": frm.doc.name,
|
||||||
|
"split_qty": cint(dialog_data.split_qty)
|
||||||
|
},
|
||||||
|
method: "erpnext.assets.doctype.asset.asset.split_asset",
|
||||||
|
callback: function(r) {
|
||||||
|
let doclist = frappe.model.sync(r.message);
|
||||||
|
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.show();
|
||||||
|
},
|
||||||
|
|
||||||
create_asset_value_adjustment: function(frm) {
|
create_asset_value_adjustment: function(frm) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
args: {
|
args: {
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"allow_import": 1,
|
"allow_import": 1,
|
||||||
"allow_rename": 1,
|
"allow_rename": 1,
|
||||||
"autoname": "naming_series:",
|
"autoname": "naming_series:",
|
||||||
"creation": "2016-03-01 17:01:27.920130",
|
"creation": "2022-01-18 02:26:55.975005",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"document_type": "Document",
|
"document_type": "Document",
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
@ -23,6 +23,7 @@
|
|||||||
"asset_name",
|
"asset_name",
|
||||||
"asset_category",
|
"asset_category",
|
||||||
"location",
|
"location",
|
||||||
|
"split_from",
|
||||||
"custodian",
|
"custodian",
|
||||||
"department",
|
"department",
|
||||||
"disposal_date",
|
"disposal_date",
|
||||||
@ -142,6 +143,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
|
"fetch_from": "item_code.image",
|
||||||
"fieldname": "image",
|
"fieldname": "image",
|
||||||
"fieldtype": "Attach Image",
|
"fieldtype": "Attach Image",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
@ -482,6 +484,13 @@
|
|||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Finance Books"
|
"label": "Finance Books"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "split_from",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Split From",
|
||||||
|
"options": "Asset",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "asset_quantity",
|
"fieldname": "asset_quantity",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
@ -509,7 +518,7 @@
|
|||||||
"link_fieldname": "asset"
|
"link_fieldname": "asset"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2022-01-18 12:57:36.741192",
|
"modified": "2022-01-30 20:19:24.680027",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Assets",
|
"module": "Assets",
|
||||||
"name": "Asset",
|
"name": "Asset",
|
||||||
|
@ -38,7 +38,8 @@ class Asset(AccountsController):
|
|||||||
self.validate_item()
|
self.validate_item()
|
||||||
self.validate_cost_center()
|
self.validate_cost_center()
|
||||||
self.set_missing_values()
|
self.set_missing_values()
|
||||||
self.prepare_depreciation_data()
|
if not self.split_from:
|
||||||
|
self.prepare_depreciation_data()
|
||||||
self.validate_gross_and_purchase_amount()
|
self.validate_gross_and_purchase_amount()
|
||||||
if self.get("schedules"):
|
if self.get("schedules"):
|
||||||
self.validate_expected_value_after_useful_life()
|
self.validate_expected_value_after_useful_life()
|
||||||
@ -202,143 +203,143 @@ class Asset(AccountsController):
|
|||||||
start = self.clear_depreciation_schedule()
|
start = self.clear_depreciation_schedule()
|
||||||
|
|
||||||
for finance_book in self.get('finance_books'):
|
for finance_book in self.get('finance_books'):
|
||||||
self.validate_asset_finance_books(finance_book)
|
self._make_depreciation_schedule(finance_book, start, date_of_sale)
|
||||||
|
|
||||||
# value_after_depreciation - current Asset value
|
def _make_depreciation_schedule(self, finance_book, start, date_of_sale):
|
||||||
if self.docstatus == 1 and finance_book.value_after_depreciation:
|
self.validate_asset_finance_books(finance_book)
|
||||||
value_after_depreciation = flt(finance_book.value_after_depreciation)
|
|
||||||
else:
|
|
||||||
value_after_depreciation = (flt(self.gross_purchase_amount) -
|
|
||||||
flt(self.opening_accumulated_depreciation))
|
|
||||||
|
|
||||||
finance_book.value_after_depreciation = value_after_depreciation
|
value_after_depreciation = self._get_value_after_depreciation(finance_book)
|
||||||
|
finance_book.value_after_depreciation = value_after_depreciation
|
||||||
|
|
||||||
number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - \
|
number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - \
|
||||||
cint(self.number_of_depreciations_booked)
|
cint(self.number_of_depreciations_booked)
|
||||||
|
|
||||||
has_pro_rata = self.check_is_pro_rata(finance_book)
|
has_pro_rata = self.check_is_pro_rata(finance_book)
|
||||||
|
if has_pro_rata:
|
||||||
|
number_of_pending_depreciations += 1
|
||||||
|
|
||||||
if has_pro_rata:
|
skip_row = False
|
||||||
number_of_pending_depreciations += 1
|
|
||||||
|
|
||||||
skip_row = False
|
for n in range(start[finance_book.idx-1], number_of_pending_depreciations):
|
||||||
|
# If depreciation is already completed (for double declining balance)
|
||||||
|
if skip_row: continue
|
||||||
|
|
||||||
for n in range(start[finance_book.idx-1], number_of_pending_depreciations):
|
depreciation_amount = get_depreciation_amount(self, value_after_depreciation, finance_book)
|
||||||
# If depreciation is already completed (for double declining balance)
|
|
||||||
if skip_row: continue
|
|
||||||
|
|
||||||
depreciation_amount = get_depreciation_amount(self, value_after_depreciation, finance_book)
|
if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1:
|
||||||
|
schedule_date = add_months(finance_book.depreciation_start_date,
|
||||||
|
n * cint(finance_book.frequency_of_depreciation))
|
||||||
|
|
||||||
if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1:
|
# schedule date will be a year later from start date
|
||||||
schedule_date = add_months(finance_book.depreciation_start_date,
|
# so monthly schedule date is calculated by removing 11 months from it
|
||||||
n * cint(finance_book.frequency_of_depreciation))
|
monthly_schedule_date = add_months(schedule_date, - finance_book.frequency_of_depreciation + 1)
|
||||||
|
|
||||||
# schedule date will be a year later from start date
|
# if asset is being sold
|
||||||
# so monthly schedule date is calculated by removing 11 months from it
|
if date_of_sale:
|
||||||
monthly_schedule_date = add_months(schedule_date, - finance_book.frequency_of_depreciation + 1)
|
from_date = self.get_from_date(finance_book.finance_book)
|
||||||
|
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
|
||||||
# if asset is being sold
|
from_date, date_of_sale)
|
||||||
if date_of_sale:
|
|
||||||
from_date = self.get_from_date(finance_book.finance_book)
|
|
||||||
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
|
|
||||||
from_date, date_of_sale)
|
|
||||||
|
|
||||||
if depreciation_amount > 0:
|
|
||||||
self.append("schedules", {
|
|
||||||
"schedule_date": date_of_sale,
|
|
||||||
"depreciation_amount": depreciation_amount,
|
|
||||||
"depreciation_method": finance_book.depreciation_method,
|
|
||||||
"finance_book": finance_book.finance_book,
|
|
||||||
"finance_book_id": finance_book.idx
|
|
||||||
})
|
|
||||||
|
|
||||||
break
|
|
||||||
|
|
||||||
# For first row
|
|
||||||
if has_pro_rata and not self.opening_accumulated_depreciation and n==0:
|
|
||||||
from_date = add_days(self.available_for_use_date, -1) # needed to calc depr amount for available_for_use_date too
|
|
||||||
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
|
|
||||||
from_date, finance_book.depreciation_start_date)
|
|
||||||
|
|
||||||
# For first depr schedule date will be the start date
|
|
||||||
# so monthly schedule date is calculated by removing month difference between use date and start date
|
|
||||||
monthly_schedule_date = add_months(finance_book.depreciation_start_date, - months + 1)
|
|
||||||
|
|
||||||
# For last row
|
|
||||||
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
|
|
||||||
if not self.flags.increase_in_asset_life:
|
|
||||||
# In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission
|
|
||||||
self.to_date = add_months(self.available_for_use_date,
|
|
||||||
(n + self.number_of_depreciations_booked) * cint(finance_book.frequency_of_depreciation))
|
|
||||||
|
|
||||||
depreciation_amount_without_pro_rata = depreciation_amount
|
|
||||||
|
|
||||||
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book,
|
|
||||||
depreciation_amount, schedule_date, self.to_date)
|
|
||||||
|
|
||||||
depreciation_amount = self.get_adjusted_depreciation_amount(depreciation_amount_without_pro_rata,
|
|
||||||
depreciation_amount, finance_book.finance_book)
|
|
||||||
|
|
||||||
monthly_schedule_date = add_months(schedule_date, 1)
|
|
||||||
schedule_date = add_days(schedule_date, days)
|
|
||||||
last_schedule_date = schedule_date
|
|
||||||
|
|
||||||
if not depreciation_amount: continue
|
|
||||||
value_after_depreciation -= flt(depreciation_amount,
|
|
||||||
self.precision("gross_purchase_amount"))
|
|
||||||
|
|
||||||
# Adjust depreciation amount in the last period based on the expected value after useful life
|
|
||||||
if finance_book.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1
|
|
||||||
and value_after_depreciation != finance_book.expected_value_after_useful_life)
|
|
||||||
or value_after_depreciation < finance_book.expected_value_after_useful_life):
|
|
||||||
depreciation_amount += (value_after_depreciation - finance_book.expected_value_after_useful_life)
|
|
||||||
skip_row = True
|
|
||||||
|
|
||||||
if depreciation_amount > 0:
|
if depreciation_amount > 0:
|
||||||
# With monthly depreciation, each depreciation is divided by months remaining until next date
|
self._add_depreciation_row(date_of_sale, depreciation_amount, finance_book.depreciation_method,
|
||||||
if self.allow_monthly_depreciation:
|
finance_book.finance_book, finance_book.idx)
|
||||||
# month range is 1 to 12
|
|
||||||
# In pro rata case, for first and last depreciation, month range would be different
|
|
||||||
month_range = months \
|
|
||||||
if (has_pro_rata and n==0) or (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) \
|
|
||||||
else finance_book.frequency_of_depreciation
|
|
||||||
|
|
||||||
for r in range(month_range):
|
break
|
||||||
if (has_pro_rata and n == 0):
|
|
||||||
# For first entry of monthly depr
|
# For first row
|
||||||
if r == 0:
|
if has_pro_rata and not self.opening_accumulated_depreciation and n==0:
|
||||||
days_until_first_depr = date_diff(monthly_schedule_date, self.available_for_use_date)
|
from_date = add_days(self.available_for_use_date, -1) # needed to calc depr amount for available_for_use_date too
|
||||||
per_day_amt = depreciation_amount / days
|
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
|
||||||
depreciation_amount_for_current_month = per_day_amt * days_until_first_depr
|
from_date, finance_book.depreciation_start_date)
|
||||||
depreciation_amount -= depreciation_amount_for_current_month
|
|
||||||
date = monthly_schedule_date
|
# For first depr schedule date will be the start date
|
||||||
amount = depreciation_amount_for_current_month
|
# so monthly schedule date is calculated by removing month difference between use date and start date
|
||||||
else:
|
monthly_schedule_date = add_months(finance_book.depreciation_start_date, - months + 1)
|
||||||
date = add_months(monthly_schedule_date, r)
|
|
||||||
amount = depreciation_amount / (month_range - 1)
|
# For last row
|
||||||
elif (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) and r == cint(month_range) - 1:
|
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
|
||||||
# For last entry of monthly depr
|
if not self.flags.increase_in_asset_life:
|
||||||
date = last_schedule_date
|
# In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission
|
||||||
amount = depreciation_amount / month_range
|
self.to_date = add_months(self.available_for_use_date,
|
||||||
|
(n + self.number_of_depreciations_booked) * cint(finance_book.frequency_of_depreciation))
|
||||||
|
|
||||||
|
depreciation_amount_without_pro_rata = depreciation_amount
|
||||||
|
|
||||||
|
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book,
|
||||||
|
depreciation_amount, schedule_date, self.to_date)
|
||||||
|
|
||||||
|
depreciation_amount = self.get_adjusted_depreciation_amount(depreciation_amount_without_pro_rata,
|
||||||
|
depreciation_amount, finance_book.finance_book)
|
||||||
|
|
||||||
|
monthly_schedule_date = add_months(schedule_date, 1)
|
||||||
|
schedule_date = add_days(schedule_date, days)
|
||||||
|
last_schedule_date = schedule_date
|
||||||
|
|
||||||
|
if not depreciation_amount: continue
|
||||||
|
value_after_depreciation -= flt(depreciation_amount,
|
||||||
|
self.precision("gross_purchase_amount"))
|
||||||
|
|
||||||
|
# Adjust depreciation amount in the last period based on the expected value after useful life
|
||||||
|
if finance_book.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1
|
||||||
|
and value_after_depreciation != finance_book.expected_value_after_useful_life)
|
||||||
|
or value_after_depreciation < finance_book.expected_value_after_useful_life):
|
||||||
|
depreciation_amount += (value_after_depreciation - finance_book.expected_value_after_useful_life)
|
||||||
|
skip_row = True
|
||||||
|
|
||||||
|
if depreciation_amount > 0:
|
||||||
|
# With monthly depreciation, each depreciation is divided by months remaining until next date
|
||||||
|
if self.allow_monthly_depreciation:
|
||||||
|
# month range is 1 to 12
|
||||||
|
# In pro rata case, for first and last depreciation, month range would be different
|
||||||
|
month_range = months \
|
||||||
|
if (has_pro_rata and n==0) or (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) \
|
||||||
|
else finance_book.frequency_of_depreciation
|
||||||
|
|
||||||
|
for r in range(month_range):
|
||||||
|
if (has_pro_rata and n == 0):
|
||||||
|
# For first entry of monthly depr
|
||||||
|
if r == 0:
|
||||||
|
days_until_first_depr = date_diff(monthly_schedule_date, self.available_for_use_date)
|
||||||
|
per_day_amt = depreciation_amount / days
|
||||||
|
depreciation_amount_for_current_month = per_day_amt * days_until_first_depr
|
||||||
|
depreciation_amount -= depreciation_amount_for_current_month
|
||||||
|
date = monthly_schedule_date
|
||||||
|
amount = depreciation_amount_for_current_month
|
||||||
else:
|
else:
|
||||||
date = add_months(monthly_schedule_date, r)
|
date = add_months(monthly_schedule_date, r)
|
||||||
amount = depreciation_amount / month_range
|
amount = depreciation_amount / (month_range - 1)
|
||||||
|
elif (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) and r == cint(month_range) - 1:
|
||||||
|
# For last entry of monthly depr
|
||||||
|
date = last_schedule_date
|
||||||
|
amount = depreciation_amount / month_range
|
||||||
|
else:
|
||||||
|
date = add_months(monthly_schedule_date, r)
|
||||||
|
amount = depreciation_amount / month_range
|
||||||
|
|
||||||
self.append("schedules", {
|
self._add_depreciation_row(date, amount, finance_book.depreciation_method,
|
||||||
"schedule_date": date,
|
finance_book.finance_book, finance_book.idx)
|
||||||
"depreciation_amount": amount,
|
else:
|
||||||
"depreciation_method": finance_book.depreciation_method,
|
self._add_depreciation_row(schedule_date, depreciation_amount, finance_book.depreciation_method,
|
||||||
"finance_book": finance_book.finance_book,
|
finance_book.finance_book, finance_book.idx)
|
||||||
"finance_book_id": finance_book.idx
|
|
||||||
})
|
def _add_depreciation_row(self, schedule_date, depreciation_amount, depreciation_method, finance_book, finance_book_id):
|
||||||
else:
|
self.append("schedules", {
|
||||||
self.append("schedules", {
|
"schedule_date": schedule_date,
|
||||||
"schedule_date": schedule_date,
|
"depreciation_amount": depreciation_amount,
|
||||||
"depreciation_amount": depreciation_amount,
|
"depreciation_method": depreciation_method,
|
||||||
"depreciation_method": finance_book.depreciation_method,
|
"finance_book": finance_book,
|
||||||
"finance_book": finance_book.finance_book,
|
"finance_book_id": finance_book_id
|
||||||
"finance_book_id": finance_book.idx
|
})
|
||||||
})
|
|
||||||
|
def _get_value_after_depreciation(self, finance_book):
|
||||||
|
# value_after_depreciation - current Asset value
|
||||||
|
if self.docstatus == 1 and finance_book.value_after_depreciation:
|
||||||
|
value_after_depreciation = flt(finance_book.value_after_depreciation)
|
||||||
|
else:
|
||||||
|
value_after_depreciation = (flt(self.gross_purchase_amount) -
|
||||||
|
flt(self.opening_accumulated_depreciation))
|
||||||
|
|
||||||
|
return value_after_depreciation
|
||||||
|
|
||||||
# depreciation schedules need to be cleared before modification due to increase in asset life/asset sales
|
# depreciation schedules need to be cleared before modification due to increase in asset life/asset sales
|
||||||
# JE: Journal Entry, FB: Finance Book
|
# JE: Journal Entry, FB: Finance Book
|
||||||
@ -348,7 +349,6 @@ class Asset(AccountsController):
|
|||||||
depr_schedule = []
|
depr_schedule = []
|
||||||
|
|
||||||
for schedule in self.get('schedules'):
|
for schedule in self.get('schedules'):
|
||||||
|
|
||||||
# to update start when there are JEs linked with all the schedule rows corresponding to an FB
|
# to update start when there are JEs linked with all the schedule rows corresponding to an FB
|
||||||
if len(start) == (int(schedule.finance_book_id) - 2):
|
if len(start) == (int(schedule.finance_book_id) - 2):
|
||||||
start.append(num_of_depreciations_completed)
|
start.append(num_of_depreciations_completed)
|
||||||
@ -924,3 +924,113 @@ def get_depreciation_amount(asset, depreciable_value, row):
|
|||||||
depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100))
|
depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100))
|
||||||
|
|
||||||
return depreciation_amount
|
return depreciation_amount
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def split_asset(asset_name, split_qty):
|
||||||
|
asset = frappe.get_doc("Asset", asset_name)
|
||||||
|
split_qty = cint(split_qty)
|
||||||
|
|
||||||
|
if split_qty >= asset.asset_quantity:
|
||||||
|
frappe.throw(_("Split qty cannot be grater than or equal to asset qty"))
|
||||||
|
|
||||||
|
remaining_qty = asset.asset_quantity - split_qty
|
||||||
|
|
||||||
|
new_asset = create_new_asset_after_split(asset, split_qty)
|
||||||
|
update_existing_asset(asset, remaining_qty)
|
||||||
|
|
||||||
|
return new_asset
|
||||||
|
|
||||||
|
def update_existing_asset(asset, remaining_qty):
|
||||||
|
remaining_gross_purchase_amount = flt((asset.gross_purchase_amount * remaining_qty) / asset.asset_quantity)
|
||||||
|
opening_accumulated_depreciation = flt((asset.opening_accumulated_depreciation * remaining_qty) / asset.asset_quantity)
|
||||||
|
|
||||||
|
frappe.db.set_value("Asset", asset.name, {
|
||||||
|
'opening_accumulated_depreciation': opening_accumulated_depreciation,
|
||||||
|
'gross_purchase_amount': remaining_gross_purchase_amount,
|
||||||
|
'asset_quantity': remaining_qty
|
||||||
|
})
|
||||||
|
|
||||||
|
for finance_book in asset.get('finance_books'):
|
||||||
|
value_after_depreciation = flt((finance_book.value_after_depreciation * remaining_qty)/asset.asset_quantity)
|
||||||
|
expected_value_after_useful_life = flt((finance_book.expected_value_after_useful_life * remaining_qty)/asset.asset_quantity)
|
||||||
|
frappe.db.set_value('Asset Finance Book', finance_book.name, 'value_after_depreciation', value_after_depreciation)
|
||||||
|
frappe.db.set_value('Asset Finance Book', finance_book.name, 'expected_value_after_useful_life', expected_value_after_useful_life)
|
||||||
|
|
||||||
|
accumulated_depreciation = 0
|
||||||
|
|
||||||
|
for term in asset.get('schedules'):
|
||||||
|
depreciation_amount = flt((term.depreciation_amount * remaining_qty)/asset.asset_quantity)
|
||||||
|
frappe.db.set_value('Depreciation Schedule', term.name, 'depreciation_amount', depreciation_amount)
|
||||||
|
accumulated_depreciation += depreciation_amount
|
||||||
|
frappe.db.set_value('Depreciation Schedule', term.name, 'accumulated_depreciation_amount', accumulated_depreciation)
|
||||||
|
|
||||||
|
def create_new_asset_after_split(asset, split_qty):
|
||||||
|
new_asset = frappe.copy_doc(asset)
|
||||||
|
new_gross_purchase_amount = flt((asset.gross_purchase_amount * split_qty) / asset.asset_quantity)
|
||||||
|
opening_accumulated_depreciation = flt((asset.opening_accumulated_depreciation * split_qty) / asset.asset_quantity)
|
||||||
|
|
||||||
|
new_asset.gross_purchase_amount = new_gross_purchase_amount
|
||||||
|
new_asset.opening_accumulated_depreciation = opening_accumulated_depreciation
|
||||||
|
new_asset.asset_quantity = split_qty
|
||||||
|
new_asset.split_from = asset.name
|
||||||
|
accumulated_depreciation = 0
|
||||||
|
|
||||||
|
for finance_book in new_asset.get('finance_books'):
|
||||||
|
finance_book.value_after_depreciation = flt((finance_book.value_after_depreciation * split_qty)/asset.asset_quantity)
|
||||||
|
finance_book.expected_value_after_useful_life = flt((finance_book.expected_value_after_useful_life * split_qty)/asset.asset_quantity)
|
||||||
|
|
||||||
|
for term in new_asset.get('schedules'):
|
||||||
|
depreciation_amount = flt((term.depreciation_amount * split_qty)/asset.asset_quantity)
|
||||||
|
term.depreciation_amount = depreciation_amount
|
||||||
|
accumulated_depreciation += depreciation_amount
|
||||||
|
term.accumulated_depreciation_amount = accumulated_depreciation
|
||||||
|
|
||||||
|
new_asset.submit()
|
||||||
|
new_asset.set_status()
|
||||||
|
|
||||||
|
for term in new_asset.get('schedules'):
|
||||||
|
# Update references in JV
|
||||||
|
if term.journal_entry:
|
||||||
|
add_reference_in_jv_on_split(term.journal_entry, new_asset.name, asset.name, term.depreciation_amount)
|
||||||
|
|
||||||
|
return new_asset
|
||||||
|
|
||||||
|
def add_reference_in_jv_on_split(entry_name, new_asset_name, old_asset_name, depreciation_amount):
|
||||||
|
journal_entry = frappe.get_doc('Journal Entry', entry_name)
|
||||||
|
entries_to_add = []
|
||||||
|
idx = len(journal_entry.get('accounts')) + 1
|
||||||
|
|
||||||
|
for account in journal_entry.get('accounts'):
|
||||||
|
if account.reference_name == old_asset_name:
|
||||||
|
entries_to_add.append(frappe.copy_doc(account).as_dict())
|
||||||
|
if account.credit:
|
||||||
|
account.credit = account.credit - depreciation_amount
|
||||||
|
account.credit_in_account_currency = account.credit_in_account_currency - \
|
||||||
|
account.exchange_rate * depreciation_amount
|
||||||
|
elif account.debit:
|
||||||
|
account.debit = account.debit - depreciation_amount
|
||||||
|
account.debit_in_account_currency = account.debit_in_account_currency - \
|
||||||
|
account.exchange_rate * depreciation_amount
|
||||||
|
|
||||||
|
for entry in entries_to_add:
|
||||||
|
entry.reference_name = new_asset_name
|
||||||
|
if entry.credit:
|
||||||
|
entry.credit = depreciation_amount
|
||||||
|
entry.credit_in_account_currency = entry.exchange_rate * depreciation_amount
|
||||||
|
elif entry.debit:
|
||||||
|
entry.debit = depreciation_amount
|
||||||
|
entry.debit_in_account_currency = entry.exchange_rate * depreciation_amount
|
||||||
|
|
||||||
|
entry.idx = idx
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
journal_entry.append('accounts', entry)
|
||||||
|
|
||||||
|
journal_entry.flags.ignore_validate_update_after_submit = True
|
||||||
|
journal_entry.save()
|
||||||
|
|
||||||
|
# Repost GL Entries
|
||||||
|
journal_entry.docstatus = 2
|
||||||
|
journal_entry.make_gl_entries(1)
|
||||||
|
journal_entry.docstatus = 1
|
||||||
|
journal_entry.make_gl_entries()
|
@ -7,7 +7,7 @@ import frappe
|
|||||||
from frappe.utils import add_days, add_months, cstr, flt, get_last_day, getdate, nowdate
|
from frappe.utils import add_days, add_months, cstr, flt, get_last_day, getdate, nowdate
|
||||||
|
|
||||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||||
from erpnext.assets.doctype.asset.asset import make_sales_invoice
|
from erpnext.assets.doctype.asset.asset import make_sales_invoice, split_asset
|
||||||
from erpnext.assets.doctype.asset.depreciation import (
|
from erpnext.assets.doctype.asset.depreciation import (
|
||||||
post_depreciation_entries,
|
post_depreciation_entries,
|
||||||
restore_asset,
|
restore_asset,
|
||||||
@ -245,6 +245,57 @@ class TestAsset(AssetSetup):
|
|||||||
si.cancel()
|
si.cancel()
|
||||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated")
|
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated")
|
||||||
|
|
||||||
|
def test_asset_splitting(self):
|
||||||
|
asset = create_asset(
|
||||||
|
calculate_depreciation = 1,
|
||||||
|
asset_quantity=10,
|
||||||
|
available_for_use_date = '2020-01-01',
|
||||||
|
purchase_date = '2020-01-01',
|
||||||
|
expected_value_after_useful_life = 0,
|
||||||
|
total_number_of_depreciations = 6,
|
||||||
|
number_of_depreciations_booked = 1,
|
||||||
|
frequency_of_depreciation = 10,
|
||||||
|
depreciation_start_date = '2021-01-01',
|
||||||
|
opening_accumulated_depreciation=20000,
|
||||||
|
gross_purchase_amount=120000,
|
||||||
|
submit = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
post_depreciation_entries(date="2021-01-01")
|
||||||
|
|
||||||
|
self.assertEqual(asset.asset_quantity, 10)
|
||||||
|
self.assertEqual(asset.gross_purchase_amount, 120000)
|
||||||
|
self.assertEqual(asset.opening_accumulated_depreciation, 20000)
|
||||||
|
|
||||||
|
new_asset = split_asset(asset.name, 2)
|
||||||
|
asset.load_from_db()
|
||||||
|
|
||||||
|
self.assertEqual(new_asset.asset_quantity, 2)
|
||||||
|
self.assertEqual(new_asset.gross_purchase_amount, 24000)
|
||||||
|
self.assertEqual(new_asset.opening_accumulated_depreciation, 4000)
|
||||||
|
self.assertEqual(new_asset.split_from, asset.name)
|
||||||
|
self.assertEqual(new_asset.schedules[0].depreciation_amount, 4000)
|
||||||
|
self.assertEqual(new_asset.schedules[1].depreciation_amount, 4000)
|
||||||
|
|
||||||
|
self.assertEqual(asset.asset_quantity, 8)
|
||||||
|
self.assertEqual(asset.gross_purchase_amount, 96000)
|
||||||
|
self.assertEqual(asset.opening_accumulated_depreciation, 16000)
|
||||||
|
self.assertEqual(asset.schedules[0].depreciation_amount, 16000)
|
||||||
|
self.assertEqual(asset.schedules[1].depreciation_amount, 16000)
|
||||||
|
|
||||||
|
journal_entry = asset.schedules[0].journal_entry
|
||||||
|
|
||||||
|
jv = frappe.get_doc('Journal Entry', journal_entry)
|
||||||
|
self.assertEqual(jv.accounts[0].credit_in_account_currency, 16000)
|
||||||
|
self.assertEqual(jv.accounts[1].debit_in_account_currency, 16000)
|
||||||
|
self.assertEqual(jv.accounts[2].credit_in_account_currency, 4000)
|
||||||
|
self.assertEqual(jv.accounts[3].debit_in_account_currency, 4000)
|
||||||
|
|
||||||
|
self.assertEqual(jv.accounts[0].reference_name, asset.name)
|
||||||
|
self.assertEqual(jv.accounts[1].reference_name, asset.name)
|
||||||
|
self.assertEqual(jv.accounts[2].reference_name, new_asset.name)
|
||||||
|
self.assertEqual(jv.accounts[3].reference_name, new_asset.name)
|
||||||
|
|
||||||
def test_expense_head(self):
|
def test_expense_head(self):
|
||||||
pr = make_purchase_receipt(item_code="Macbook Pro",
|
pr = make_purchase_receipt(item_code="Macbook Pro",
|
||||||
qty=2, rate=200000.0, location="Test Location")
|
qty=2, rate=200000.0, location="Test Location")
|
||||||
@ -1197,7 +1248,8 @@ def create_asset(**args):
|
|||||||
"available_for_use_date": args.available_for_use_date or "2020-06-06",
|
"available_for_use_date": args.available_for_use_date or "2020-06-06",
|
||||||
"location": args.location or "Test Location",
|
"location": args.location or "Test Location",
|
||||||
"asset_owner": args.asset_owner or "Company",
|
"asset_owner": args.asset_owner or "Company",
|
||||||
"is_existing_asset": args.is_existing_asset or 1
|
"is_existing_asset": args.is_existing_asset or 1,
|
||||||
|
"asset_quantity": args.get("asset_quantity") or 1
|
||||||
})
|
})
|
||||||
|
|
||||||
if asset.calculate_depreciation:
|
if asset.calculate_depreciation:
|
||||||
|
@ -131,28 +131,6 @@ class Supplier(TransactionBase):
|
|||||||
if frappe.defaults.get_global_default('supp_master_name') == 'Supplier Name':
|
if frappe.defaults.get_global_default('supp_master_name') == 'Supplier Name':
|
||||||
frappe.db.set(self, "supplier_name", newdn)
|
frappe.db.set(self, "supplier_name", newdn)
|
||||||
|
|
||||||
def create_onboarding_docs(self, args):
|
|
||||||
company = frappe.defaults.get_defaults().get('company') or \
|
|
||||||
frappe.db.get_single_value('Global Defaults', 'default_company')
|
|
||||||
|
|
||||||
for i in range(1, args.get('max_count')):
|
|
||||||
supplier = args.get('supplier_name_' + str(i))
|
|
||||||
if supplier:
|
|
||||||
try:
|
|
||||||
doc = frappe.get_doc({
|
|
||||||
'doctype': self.doctype,
|
|
||||||
'supplier_name': supplier,
|
|
||||||
'supplier_group': _('Local'),
|
|
||||||
'company': company
|
|
||||||
}).insert()
|
|
||||||
|
|
||||||
if args.get('supplier_email_' + str(i)):
|
|
||||||
from erpnext.selling.doctype.customer.customer import create_contact
|
|
||||||
create_contact(supplier, 'Supplier',
|
|
||||||
doc.name, args.get('supplier_email_' + str(i)))
|
|
||||||
except frappe.NameError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@frappe.validate_and_sanitize_search_inputs
|
@frappe.validate_and_sanitize_search_inputs
|
||||||
def get_supplier_primary_contact(doctype, txt, searchfield, start, page_len, filters):
|
def get_supplier_primary_contact(doctype, txt, searchfield, start, page_len, filters):
|
||||||
|
@ -1,49 +0,0 @@
|
|||||||
{
|
|
||||||
"add_more_button": 1,
|
|
||||||
"app": "ERPNext",
|
|
||||||
"creation": "2019-11-15 14:45:32.626641",
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Onboarding Slide",
|
|
||||||
"domains": [],
|
|
||||||
"help_links": [
|
|
||||||
{
|
|
||||||
"label": "Learn More",
|
|
||||||
"video_id": "zsrrVDk6VBs"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"idx": 0,
|
|
||||||
"image_src": "",
|
|
||||||
"is_completed": 0,
|
|
||||||
"max_count": 3,
|
|
||||||
"modified": "2019-12-09 17:54:18.452038",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"name": "Add A Few Suppliers",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"ref_doctype": "Supplier",
|
|
||||||
"slide_desc": "",
|
|
||||||
"slide_fields": [
|
|
||||||
{
|
|
||||||
"align": "",
|
|
||||||
"fieldname": "supplier_name",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Supplier Name",
|
|
||||||
"placeholder": "",
|
|
||||||
"reqd": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"align": "",
|
|
||||||
"fieldtype": "Column Break",
|
|
||||||
"reqd": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"align": "",
|
|
||||||
"fieldname": "supplier_email",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Supplier Email",
|
|
||||||
"reqd": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"slide_order": 50,
|
|
||||||
"slide_title": "Add A Few Suppliers",
|
|
||||||
"slide_type": "Create"
|
|
||||||
}
|
|
@ -167,9 +167,14 @@ class AccountsController(TransactionBase):
|
|||||||
|
|
||||||
validate_regional(self)
|
validate_regional(self)
|
||||||
|
|
||||||
|
validate_einvoice_fields(self)
|
||||||
|
|
||||||
if self.doctype != 'Material Request':
|
if self.doctype != 'Material Request':
|
||||||
apply_pricing_rule_on_transaction(self)
|
apply_pricing_rule_on_transaction(self)
|
||||||
|
|
||||||
|
def before_cancel(self):
|
||||||
|
validate_einvoice_fields(self)
|
||||||
|
|
||||||
def on_trash(self):
|
def on_trash(self):
|
||||||
# delete sl and gl entries on deletion of transaction
|
# delete sl and gl entries on deletion of transaction
|
||||||
if frappe.db.get_single_value('Accounts Settings', 'delete_linked_ledger_entries'):
|
if frappe.db.get_single_value('Accounts Settings', 'delete_linked_ledger_entries'):
|
||||||
@ -2151,3 +2156,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
@erpnext.allow_regional
|
@erpnext.allow_regional
|
||||||
def validate_regional(doc):
|
def validate_regional(doc):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@erpnext.allow_regional
|
||||||
|
def validate_einvoice_fields(doc):
|
||||||
|
pass
|
||||||
|
@ -132,13 +132,17 @@ class EmployeeBoardingController(Document):
|
|||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
# delete task project
|
# delete task project
|
||||||
for task in frappe.get_all('Task', filters={'project': self.project}):
|
project = self.project
|
||||||
|
for task in frappe.get_all('Task', filters={'project': project}):
|
||||||
frappe.delete_doc('Task', task.name, force=1)
|
frappe.delete_doc('Task', task.name, force=1)
|
||||||
frappe.delete_doc('Project', self.project, force=1)
|
frappe.delete_doc('Project', project, force=1)
|
||||||
self.db_set('project', '')
|
self.db_set('project', '')
|
||||||
for activity in self.activities:
|
for activity in self.activities:
|
||||||
activity.db_set('task', '')
|
activity.db_set('task', '')
|
||||||
|
|
||||||
|
frappe.msgprint(_('Linked Project {} and Tasks deleted.').format(
|
||||||
|
project), alert=True, indicator='blue')
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_onboarding_details(parent, parenttype):
|
def get_onboarding_details(parent, parenttype):
|
||||||
|
@ -132,7 +132,7 @@ def find_variant(template, args, variant_item_code=None):
|
|||||||
|
|
||||||
conditions = " or ".join(conditions)
|
conditions = " or ".join(conditions)
|
||||||
|
|
||||||
from erpnext.portal.product_configurator.utils import get_item_codes_by_attributes
|
from erpnext.e_commerce.variant_selector.utils import get_item_codes_by_attributes
|
||||||
possible_variants = [i for i in get_item_codes_by_attributes(args, template) if i != variant_item_code]
|
possible_variants = [i for i in get_item_codes_by_attributes(args, template) if i != variant_item_code]
|
||||||
|
|
||||||
for variant in possible_variants:
|
for variant in possible_variants:
|
||||||
@ -262,9 +262,8 @@ def generate_keyed_value_combinations(args):
|
|||||||
def copy_attributes_to_variant(item, variant):
|
def copy_attributes_to_variant(item, variant):
|
||||||
# copy non no-copy fields
|
# copy non no-copy fields
|
||||||
|
|
||||||
exclude_fields = ["naming_series", "item_code", "item_name", "show_in_website",
|
exclude_fields = ["naming_series", "item_code", "item_name", "published_in_website",
|
||||||
"show_variant_in_website", "opening_stock", "variant_of", "valuation_rate",
|
"opening_stock", "variant_of", "valuation_rate"]
|
||||||
"has_variants", "attributes"]
|
|
||||||
|
|
||||||
if item.variant_based_on=='Manufacturer':
|
if item.variant_based_on=='Manufacturer':
|
||||||
# don't copy manufacturer values if based on part no
|
# don't copy manufacturer values if based on part no
|
||||||
|
@ -249,6 +249,9 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
|
|||||||
del filters['customer']
|
del filters['customer']
|
||||||
else:
|
else:
|
||||||
del filters['supplier']
|
del filters['supplier']
|
||||||
|
else:
|
||||||
|
filters.pop('customer', None)
|
||||||
|
filters.pop('supplier', None)
|
||||||
|
|
||||||
|
|
||||||
description_cond = ''
|
description_cond = ''
|
||||||
|
@ -204,7 +204,7 @@ class SellingController(StockController):
|
|||||||
valuation_rate_map = {}
|
valuation_rate_map = {}
|
||||||
|
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
if not item.item_code:
|
if not item.item_code or item.is_free_item:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
last_purchase_rate, is_stock_item = frappe.get_cached_value(
|
last_purchase_rate, is_stock_item = frappe.get_cached_value(
|
||||||
@ -251,7 +251,7 @@ class SellingController(StockController):
|
|||||||
valuation_rate_map[(rate.item_code, rate.warehouse)] = rate.valuation_rate
|
valuation_rate_map[(rate.item_code, rate.warehouse)] = rate.valuation_rate
|
||||||
|
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
if not item.item_code:
|
if not item.item_code or item.is_free_item:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
last_valuation_rate = valuation_rate_map.get(
|
last_valuation_rate = valuation_rate_map.get(
|
||||||
|
@ -40,7 +40,10 @@ class StockController(AccountsController):
|
|||||||
if self.docstatus == 2:
|
if self.docstatus == 2:
|
||||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||||
|
|
||||||
if cint(erpnext.is_perpetual_inventory_enabled(self.company)):
|
provisional_accounting_for_non_stock_items = \
|
||||||
|
cint(frappe.db.get_value('Company', self.company, 'enable_provisional_accounting_for_non_stock_items'))
|
||||||
|
|
||||||
|
if cint(erpnext.is_perpetual_inventory_enabled(self.company)) or provisional_accounting_for_non_stock_items:
|
||||||
warehouse_account = get_warehouse_account_map(self.company)
|
warehouse_account = get_warehouse_account_map(self.company)
|
||||||
|
|
||||||
if self.docstatus==1:
|
if self.docstatus==1:
|
||||||
@ -77,17 +80,17 @@ class StockController(AccountsController):
|
|||||||
.format(d.idx, get_link_to_form("Batch", d.get("batch_no"))))
|
.format(d.idx, get_link_to_form("Batch", d.get("batch_no"))))
|
||||||
|
|
||||||
def clean_serial_nos(self):
|
def clean_serial_nos(self):
|
||||||
|
from erpnext.stock.doctype.serial_no.serial_no import clean_serial_no_string
|
||||||
|
|
||||||
for row in self.get("items"):
|
for row in self.get("items"):
|
||||||
if hasattr(row, "serial_no") and row.serial_no:
|
if hasattr(row, "serial_no") and row.serial_no:
|
||||||
# replace commas by linefeed
|
# remove extra whitespace and store one serial no on each line
|
||||||
row.serial_no = row.serial_no.replace(",", "\n")
|
row.serial_no = clean_serial_no_string(row.serial_no)
|
||||||
|
|
||||||
# strip preceeding and succeeding spaces for each SN
|
for row in self.get('packed_items') or []:
|
||||||
# (SN could have valid spaces in between e.g. SN - 123 - 2021)
|
if hasattr(row, "serial_no") and row.serial_no:
|
||||||
serial_no_list = row.serial_no.split("\n")
|
# remove extra whitespace and store one serial no on each line
|
||||||
serial_no_list = [sn.strip() for sn in serial_no_list]
|
row.serial_no = clean_serial_no_string(row.serial_no)
|
||||||
|
|
||||||
row.serial_no = "\n".join(serial_no_list)
|
|
||||||
|
|
||||||
def get_gl_entries(self, warehouse_account=None, default_expense_account=None,
|
def get_gl_entries(self, warehouse_account=None, default_expense_account=None,
|
||||||
default_cost_center=None):
|
default_cost_center=None):
|
||||||
|
@ -56,6 +56,12 @@ class TestQueries(unittest.TestCase):
|
|||||||
bundled_stock_items = query(txt="_test product bundle item 5", filters={"is_stock_item": 1})
|
bundled_stock_items = query(txt="_test product bundle item 5", filters={"is_stock_item": 1})
|
||||||
self.assertEqual(len(bundled_stock_items), 0)
|
self.assertEqual(len(bundled_stock_items), 0)
|
||||||
|
|
||||||
|
# empty customer/supplier should be stripped of instead of failure
|
||||||
|
query(txt="", filters={"customer": None})
|
||||||
|
query(txt="", filters={"customer": ""})
|
||||||
|
query(txt="", filters={"supplier": None})
|
||||||
|
query(txt="", filters={"supplier": ""})
|
||||||
|
|
||||||
def test_bom_qury(self):
|
def test_bom_qury(self):
|
||||||
query = add_default_params(queries.bom, "BOM")
|
query = add_default_params(queries.bom, "BOM")
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ frappe.ui.form.on('Campaign', {
|
|||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
erpnext.toggle_naming_series();
|
erpnext.toggle_naming_series();
|
||||||
|
|
||||||
if (frm.doc.__islocal) {
|
if (frm.is_new()) {
|
||||||
frm.toggle_display("naming_series", frappe.boot.sysdefaults.campaign_naming_by=="Naming Series");
|
frm.toggle_display("naming_series", frappe.boot.sysdefaults.campaign_naming_by=="Naming Series");
|
||||||
} else {
|
} else {
|
||||||
cur_frm.add_custom_button(__("View Leads"), function() {
|
cur_frm.add_custom_button(__("View Leads"), function() {
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
# import frappe
|
import frappe
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
class CRMSettings(Document):
|
class CRMSettings(Document):
|
||||||
pass
|
def validate(self):
|
||||||
|
frappe.db.set_default("campaign_naming_by", self.get("campaign_naming_by", ""))
|
||||||
|
@ -2,13 +2,14 @@
|
|||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
import requests
|
import requests
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import get_url_to_form
|
from frappe.utils import get_url_to_form
|
||||||
from frappe.utils.file_manager import get_file_path
|
from frappe.utils.file_manager import get_file_path
|
||||||
from six.moves.urllib.parse import urlencode
|
|
||||||
|
|
||||||
|
|
||||||
class LinkedInSettings(Document):
|
class LinkedInSettings(Document):
|
||||||
|
@ -24,6 +24,14 @@ frappe.ui.form.on("Opportunity", {
|
|||||||
frm.trigger('set_contact_link');
|
frm.trigger('set_contact_link');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
validate: function(frm) {
|
||||||
|
if (frm.doc.status == "Lost" && !frm.doc.lost_reasons.length) {
|
||||||
|
frm.trigger('set_as_lost_dialog');
|
||||||
|
frappe.throw(__("Lost Reasons are required in case opportunity is Lost."));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
contact_date: function(frm) {
|
contact_date: function(frm) {
|
||||||
if(frm.doc.contact_date < frappe.datetime.now_datetime()){
|
if(frm.doc.contact_date < frappe.datetime.now_datetime()){
|
||||||
frm.set_value("contact_date", "");
|
frm.set_value("contact_date", "");
|
||||||
@ -82,7 +90,7 @@ frappe.ui.form.on("Opportunity", {
|
|||||||
frm.trigger('setup_opportunity_from');
|
frm.trigger('setup_opportunity_from');
|
||||||
erpnext.toggle_naming_series();
|
erpnext.toggle_naming_series();
|
||||||
|
|
||||||
if(!doc.__islocal && doc.status!=="Lost") {
|
if(!frm.is_new() && doc.status!=="Lost") {
|
||||||
if(doc.with_items){
|
if(doc.with_items){
|
||||||
frm.add_custom_button(__('Supplier Quotation'),
|
frm.add_custom_button(__('Supplier Quotation'),
|
||||||
function() {
|
function() {
|
||||||
@ -187,11 +195,11 @@ frappe.ui.form.on("Opportunity", {
|
|||||||
|
|
||||||
change_form_labels: function(frm) {
|
change_form_labels: function(frm) {
|
||||||
let company_currency = erpnext.get_currency(frm.doc.company);
|
let company_currency = erpnext.get_currency(frm.doc.company);
|
||||||
frm.set_currency_labels(["base_opportunity_amount", "base_total", "base_grand_total"], company_currency);
|
frm.set_currency_labels(["base_opportunity_amount", "base_total"], company_currency);
|
||||||
frm.set_currency_labels(["opportunity_amount", "total", "grand_total"], frm.doc.currency);
|
frm.set_currency_labels(["opportunity_amount", "total"], frm.doc.currency);
|
||||||
|
|
||||||
// toggle fields
|
// toggle fields
|
||||||
frm.toggle_display(["conversion_rate", "base_opportunity_amount", "base_total", "base_grand_total"],
|
frm.toggle_display(["conversion_rate", "base_opportunity_amount", "base_total"],
|
||||||
frm.doc.currency != company_currency);
|
frm.doc.currency != company_currency);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -209,20 +217,15 @@ frappe.ui.form.on("Opportunity", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
calculate_total: function(frm) {
|
calculate_total: function(frm) {
|
||||||
let total = 0, base_total = 0, grand_total = 0, base_grand_total = 0;
|
let total = 0, base_total = 0;
|
||||||
frm.doc.items.forEach(item => {
|
frm.doc.items.forEach(item => {
|
||||||
total += item.amount;
|
total += item.amount;
|
||||||
base_total += item.base_amount;
|
base_total += item.base_amount;
|
||||||
})
|
})
|
||||||
|
|
||||||
base_grand_total = base_total + frm.doc.base_opportunity_amount;
|
|
||||||
grand_total = total + frm.doc.opportunity_amount;
|
|
||||||
|
|
||||||
frm.set_value({
|
frm.set_value({
|
||||||
'total': flt(total),
|
'total': flt(total),
|
||||||
'base_total': flt(base_total),
|
'base_total': flt(base_total)
|
||||||
'grand_total': flt(grand_total),
|
|
||||||
'base_grand_total': flt(base_grand_total)
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,10 +42,8 @@
|
|||||||
"items",
|
"items",
|
||||||
"section_break_32",
|
"section_break_32",
|
||||||
"base_total",
|
"base_total",
|
||||||
"base_grand_total",
|
|
||||||
"column_break_33",
|
"column_break_33",
|
||||||
"total",
|
"total",
|
||||||
"grand_total",
|
|
||||||
"contact_info",
|
"contact_info",
|
||||||
"customer_address",
|
"customer_address",
|
||||||
"address_display",
|
"address_display",
|
||||||
@ -475,21 +473,6 @@
|
|||||||
"fieldname": "column_break_33",
|
"fieldname": "column_break_33",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "base_grand_total",
|
|
||||||
"fieldtype": "Currency",
|
|
||||||
"label": "Grand Total (Company Currency)",
|
|
||||||
"options": "Company:company:default_currency",
|
|
||||||
"print_hide": 1,
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "grand_total",
|
|
||||||
"fieldtype": "Currency",
|
|
||||||
"label": "Grand Total",
|
|
||||||
"options": "currency",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "lost_detail_section",
|
"fieldname": "lost_detail_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
@ -510,7 +493,7 @@
|
|||||||
"icon": "fa fa-info-sign",
|
"icon": "fa fa-info-sign",
|
||||||
"idx": 195,
|
"idx": 195,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-10-21 12:04:30.151379",
|
"modified": "2022-01-29 19:32:26.382896",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "CRM",
|
"module": "CRM",
|
||||||
"name": "Opportunity",
|
"name": "Opportunity",
|
||||||
@ -547,6 +530,7 @@
|
|||||||
"show_name_in_global_search": 1,
|
"show_name_in_global_search": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"subject_field": "title",
|
"subject_field": "title",
|
||||||
"timeline_field": "party_name",
|
"timeline_field": "party_name",
|
||||||
"title_field": "title",
|
"title_field": "title",
|
||||||
|
@ -69,8 +69,6 @@ class Opportunity(TransactionBase):
|
|||||||
|
|
||||||
self.total = flt(total)
|
self.total = flt(total)
|
||||||
self.base_total = flt(base_total)
|
self.base_total = flt(base_total)
|
||||||
self.grand_total = flt(self.total) + flt(self.opportunity_amount)
|
|
||||||
self.base_grand_total = flt(self.base_total) + flt(self.base_opportunity_amount)
|
|
||||||
|
|
||||||
def make_new_lead_if_required(self):
|
def make_new_lead_if_required(self):
|
||||||
"""Set lead against new opportunity"""
|
"""Set lead against new opportunity"""
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
frappe.ui.form.on('Prospect', {
|
frappe.ui.form.on('Prospect', {
|
||||||
refresh (frm) {
|
refresh (frm) {
|
||||||
|
frappe.dynamic_link = { doc: frm.doc, fieldname: "name", doctype: frm.doctype };
|
||||||
|
|
||||||
if (!frm.is_new() && frappe.boot.user.can_create.includes("Customer")) {
|
if (!frm.is_new() && frappe.boot.user.can_create.includes("Customer")) {
|
||||||
frm.add_custom_button(__("Customer"), function() {
|
frm.add_custom_button(__("Customer"), function() {
|
||||||
frappe.model.open_mapped_doc({
|
frappe.model.open_mapped_doc({
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/CRM",
|
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/CRM",
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"is_complete": 0,
|
"is_complete": 0,
|
||||||
"modified": "2020-07-08 14:05:42.644448",
|
"modified": "2022-01-29 20:14:29.502145",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "CRM",
|
"module": "CRM",
|
||||||
"name": "CRM",
|
"name": "CRM",
|
||||||
@ -33,6 +33,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"step": "Create and Send Quotation"
|
"step": "Create and Send Quotation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "CRM Settings"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"subtitle": "Lead, Opportunity, Customer, and more.",
|
"subtitle": "Lead, Opportunity, Customer, and more.",
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
"doctype": "Onboarding Step",
|
"doctype": "Onboarding Step",
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"is_complete": 0,
|
"is_complete": 0,
|
||||||
"is_mandatory": 0,
|
|
||||||
"is_single": 0,
|
"is_single": 0,
|
||||||
"is_skipped": 0,
|
"is_skipped": 0,
|
||||||
"modified": "2020-05-28 21:07:11.461172",
|
"modified": "2020-05-28 21:07:11.461172",
|
||||||
@ -13,6 +12,7 @@
|
|||||||
"name": "Create and Send Quotation",
|
"name": "Create and Send Quotation",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"reference_document": "Quotation",
|
"reference_document": "Quotation",
|
||||||
|
"show_form_tour": 0,
|
||||||
"show_full_form": 1,
|
"show_full_form": 1,
|
||||||
"title": "Create and Send Quotation",
|
"title": "Create and Send Quotation",
|
||||||
"validate_action": 1
|
"validate_action": 1
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
"doctype": "Onboarding Step",
|
"doctype": "Onboarding Step",
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"is_complete": 0,
|
"is_complete": 0,
|
||||||
"is_mandatory": 0,
|
|
||||||
"is_single": 0,
|
"is_single": 0,
|
||||||
"is_skipped": 0,
|
"is_skipped": 0,
|
||||||
"modified": "2020-05-28 21:07:01.373403",
|
"modified": "2020-05-28 21:07:01.373403",
|
||||||
@ -13,6 +12,7 @@
|
|||||||
"name": "Create Lead",
|
"name": "Create Lead",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"reference_document": "Lead",
|
"reference_document": "Lead",
|
||||||
|
"show_form_tour": 0,
|
||||||
"show_full_form": 1,
|
"show_full_form": 1,
|
||||||
"title": "Create Lead",
|
"title": "Create Lead",
|
||||||
"validate_action": 1
|
"validate_action": 1
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
"doctype": "Onboarding Step",
|
"doctype": "Onboarding Step",
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"is_complete": 0,
|
"is_complete": 0,
|
||||||
"is_mandatory": 0,
|
|
||||||
"is_single": 0,
|
"is_single": 0,
|
||||||
"is_skipped": 0,
|
"is_skipped": 0,
|
||||||
"modified": "2021-01-21 15:28:52.483839",
|
"modified": "2021-01-21 15:28:52.483839",
|
||||||
@ -13,6 +12,7 @@
|
|||||||
"name": "Create Opportunity",
|
"name": "Create Opportunity",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"reference_document": "Opportunity",
|
"reference_document": "Opportunity",
|
||||||
|
"show_form_tour": 0,
|
||||||
"show_full_form": 1,
|
"show_full_form": 1,
|
||||||
"title": "Create Opportunity",
|
"title": "Create Opportunity",
|
||||||
"validate_action": 1
|
"validate_action": 1
|
||||||
|
21
erpnext/crm/onboarding_step/crm_settings/crm_settings.json
Normal file
21
erpnext/crm/onboarding_step/crm_settings/crm_settings.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"action": "Go to Page",
|
||||||
|
"creation": "2022-01-29 20:14:24.803844",
|
||||||
|
"description": "# CRM Settings\n\nCRM module\u2019s features are configurable as per your business needs. CRM Settings is the place where you can set your preferences for:\n- Campaign\n- Lead\n- Opportunity\n- Quotation",
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Onboarding Step",
|
||||||
|
"idx": 0,
|
||||||
|
"is_complete": 0,
|
||||||
|
"is_single": 1,
|
||||||
|
"is_skipped": 0,
|
||||||
|
"modified": "2022-01-29 20:14:24.803844",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"name": "CRM Settings",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"path": "#crm-settings/CRM%20Settings",
|
||||||
|
"reference_document": "CRM Settings",
|
||||||
|
"show_form_tour": 0,
|
||||||
|
"show_full_form": 0,
|
||||||
|
"title": "CRM Settings",
|
||||||
|
"validate_action": 1
|
||||||
|
}
|
@ -5,13 +5,13 @@
|
|||||||
"doctype": "Onboarding Step",
|
"doctype": "Onboarding Step",
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"is_complete": 0,
|
"is_complete": 0,
|
||||||
"is_mandatory": 0,
|
|
||||||
"is_single": 0,
|
"is_single": 0,
|
||||||
"is_skipped": 0,
|
"is_skipped": 0,
|
||||||
"modified": "2020-05-14 17:28:16.448676",
|
"modified": "2020-05-14 17:28:16.448676",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"name": "Introduction to CRM",
|
"name": "Introduction to CRM",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
|
"show_form_tour": 0,
|
||||||
"show_full_form": 0,
|
"show_full_form": 0,
|
||||||
"title": "Introduction to CRM",
|
"title": "Introduction to CRM",
|
||||||
"validate_action": 1,
|
"validate_action": 1,
|
||||||
|
86
erpnext/e_commerce/api.py
Normal file
86
erpnext/e_commerce/api.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.utils import cint
|
||||||
|
|
||||||
|
from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder
|
||||||
|
from erpnext.e_commerce.product_data_engine.query import ProductQuery
|
||||||
|
from erpnext.setup.doctype.item_group.item_group import get_child_groups_for_website
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
def get_product_filter_data(query_args=None):
|
||||||
|
"""
|
||||||
|
Returns filtered products and discount filters.
|
||||||
|
:param query_args (dict): contains filters to get products list
|
||||||
|
|
||||||
|
Query Args filters:
|
||||||
|
search (str): Search Term.
|
||||||
|
field_filters (dict): Keys include item_group, brand, etc.
|
||||||
|
attribute_filters(dict): Keys include Color, Size, etc.
|
||||||
|
start (int): Offset items by
|
||||||
|
item_group (str): Valid Item Group
|
||||||
|
from_filters (bool): Set as True to jump to page 1
|
||||||
|
"""
|
||||||
|
if isinstance(query_args, str):
|
||||||
|
query_args = json.loads(query_args)
|
||||||
|
|
||||||
|
query_args = frappe._dict(query_args)
|
||||||
|
if query_args:
|
||||||
|
search = query_args.get("search")
|
||||||
|
field_filters = query_args.get("field_filters", {})
|
||||||
|
attribute_filters = query_args.get("attribute_filters", {})
|
||||||
|
start = cint(query_args.start) if query_args.get("start") else 0
|
||||||
|
item_group = query_args.get("item_group")
|
||||||
|
from_filters = query_args.get("from_filters")
|
||||||
|
else:
|
||||||
|
search, attribute_filters, item_group, from_filters = None, None, None, None
|
||||||
|
field_filters = {}
|
||||||
|
start = 0
|
||||||
|
|
||||||
|
# if new filter is checked, reset start to show filtered items from page 1
|
||||||
|
if from_filters:
|
||||||
|
start = 0
|
||||||
|
|
||||||
|
sub_categories = []
|
||||||
|
if item_group:
|
||||||
|
field_filters['item_group'] = item_group
|
||||||
|
sub_categories = get_child_groups_for_website(item_group, immediate=True)
|
||||||
|
|
||||||
|
engine = ProductQuery()
|
||||||
|
try:
|
||||||
|
result = engine.query(
|
||||||
|
attribute_filters,
|
||||||
|
field_filters,
|
||||||
|
search_term=search,
|
||||||
|
start=start,
|
||||||
|
item_group=item_group
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
traceback = frappe.get_traceback()
|
||||||
|
frappe.log_error(traceback, frappe._("Product Engine Error"))
|
||||||
|
return {"exc": "Something went wrong!"}
|
||||||
|
|
||||||
|
# discount filter data
|
||||||
|
filters = {}
|
||||||
|
discounts = result["discounts"]
|
||||||
|
|
||||||
|
if discounts:
|
||||||
|
filter_engine = ProductFiltersBuilder()
|
||||||
|
filters["discount_filters"] = filter_engine.get_discount_filters(discounts)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"items": result["items"] or [],
|
||||||
|
"filters": filters,
|
||||||
|
"settings": engine.settings,
|
||||||
|
"sub_categories": sub_categories,
|
||||||
|
"items_count": result["items_count"]
|
||||||
|
}
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
def get_guest_redirect_on_action():
|
||||||
|
return frappe.db.get_single_value("E Commerce Settings", "redirect_on_action")
|
@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
// License: GNU General Public License v3. See license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on("Shopping Cart Settings", {
|
frappe.ui.form.on("E Commerce Settings", {
|
||||||
onload: function(frm) {
|
onload: function(frm) {
|
||||||
if(frm.doc.__onload && frm.doc.__onload.quotation_series) {
|
if(frm.doc.__onload && frm.doc.__onload.quotation_series) {
|
||||||
frm.fields_dict.quotation_series.df.options = frm.doc.__onload.quotation_series;
|
frm.fields_dict.quotation_series.df.options = frm.doc.__onload.quotation_series;
|
||||||
@ -23,6 +23,21 @@ frappe.ui.form.on("Shopping Cart Settings", {
|
|||||||
</div>`
|
</div>`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
frappe.model.with_doctype("Item", () => {
|
||||||
|
const web_item_meta = frappe.get_meta('Website Item');
|
||||||
|
|
||||||
|
const valid_fields = web_item_meta.fields.filter(
|
||||||
|
df => ["Link", "Table MultiSelect"].includes(df.fieldtype) && !df.hidden
|
||||||
|
).map(df => ({ label: df.label, value: df.fieldname }));
|
||||||
|
|
||||||
|
frm.fields_dict.filter_fields.grid.update_docfield_property(
|
||||||
|
'fieldname', 'fieldtype', 'Select'
|
||||||
|
);
|
||||||
|
frm.fields_dict.filter_fields.grid.update_docfield_property(
|
||||||
|
'fieldname', 'options', valid_fields
|
||||||
|
);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
enabled: function(frm) {
|
enabled: function(frm) {
|
||||||
if (frm.doc.enabled === 1) {
|
if (frm.doc.enabled === 1) {
|
@ -0,0 +1,393 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2021-02-10 17:13:39.139103",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"products_per_page",
|
||||||
|
"filter_categories_section",
|
||||||
|
"enable_field_filters",
|
||||||
|
"filter_fields",
|
||||||
|
"enable_attribute_filters",
|
||||||
|
"filter_attributes",
|
||||||
|
"display_settings_section",
|
||||||
|
"hide_variants",
|
||||||
|
"enable_variants",
|
||||||
|
"show_price",
|
||||||
|
"column_break_9",
|
||||||
|
"show_stock_availability",
|
||||||
|
"show_quantity_in_website",
|
||||||
|
"allow_items_not_in_stock",
|
||||||
|
"column_break_13",
|
||||||
|
"show_apply_coupon_code_in_website",
|
||||||
|
"show_contact_us_button",
|
||||||
|
"show_attachments",
|
||||||
|
"section_break_18",
|
||||||
|
"company",
|
||||||
|
"price_list",
|
||||||
|
"enabled",
|
||||||
|
"store_page_docs",
|
||||||
|
"column_break_21",
|
||||||
|
"default_customer_group",
|
||||||
|
"quotation_series",
|
||||||
|
"checkout_settings_section",
|
||||||
|
"enable_checkout",
|
||||||
|
"show_price_in_quotation",
|
||||||
|
"column_break_27",
|
||||||
|
"save_quotations_as_draft",
|
||||||
|
"payment_gateway_account",
|
||||||
|
"payment_success_url",
|
||||||
|
"add_ons_section",
|
||||||
|
"enable_wishlist",
|
||||||
|
"column_break_22",
|
||||||
|
"enable_reviews",
|
||||||
|
"column_break_23",
|
||||||
|
"enable_recommendations",
|
||||||
|
"item_search_settings_section",
|
||||||
|
"redisearch_warning",
|
||||||
|
"search_index_fields",
|
||||||
|
"show_categories_in_search_autocomplete",
|
||||||
|
"is_redisearch_loaded",
|
||||||
|
"shop_by_category_section",
|
||||||
|
"slideshow",
|
||||||
|
"guest_display_settings_section",
|
||||||
|
"hide_price_for_guest",
|
||||||
|
"redirect_on_action"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"default": "6",
|
||||||
|
"fieldname": "products_per_page",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Products per Page"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"fieldname": "filter_categories_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Filters and Categories"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "hide_variants",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Hide Variants"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "The field filters will also work as categories in the <b>Shop by Category</b> page.",
|
||||||
|
"fieldname": "enable_field_filters",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Enable Field Filters (Categories)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "enable_attribute_filters",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Enable Attribute Filters"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "enable_field_filters",
|
||||||
|
"fieldname": "filter_fields",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Website Item Fields",
|
||||||
|
"options": "Website Filter Field"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "enable_attribute_filters",
|
||||||
|
"fieldname": "filter_attributes",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Attributes",
|
||||||
|
"options": "Website Attribute"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "enabled",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Enable Shopping Cart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "doc.enabled",
|
||||||
|
"fieldname": "store_page_docs",
|
||||||
|
"fieldtype": "HTML"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "display_settings_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Display Settings"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "show_attachments",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Show Public Attachments"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "show_price",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Show Price"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "show_stock_availability",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Show Stock Availability"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "enable_variants",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Enable Variant Selection"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_13",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "show_contact_us_button",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Show Contact Us Button"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"depends_on": "show_stock_availability",
|
||||||
|
"fieldname": "show_quantity_in_website",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Show Stock Quantity"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "show_apply_coupon_code_in_website",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Show Apply Coupon Code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "allow_items_not_in_stock",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Allow items not in stock to be added to cart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_18",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Shopping Cart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "enabled",
|
||||||
|
"fieldname": "company",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Company",
|
||||||
|
"mandatory_depends_on": "eval: doc.enabled === 1",
|
||||||
|
"options": "Company",
|
||||||
|
"remember_last_selected_value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "enabled",
|
||||||
|
"description": "Prices will not be shown if Price List is not set",
|
||||||
|
"fieldname": "price_list",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Price List",
|
||||||
|
"mandatory_depends_on": "eval: doc.enabled === 1",
|
||||||
|
"options": "Price List"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_21",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "enabled",
|
||||||
|
"fieldname": "default_customer_group",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"ignore_user_permissions": 1,
|
||||||
|
"label": "Default Customer Group",
|
||||||
|
"mandatory_depends_on": "eval: doc.enabled === 1",
|
||||||
|
"options": "Customer Group"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "enabled",
|
||||||
|
"fieldname": "quotation_series",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Quotation Series",
|
||||||
|
"mandatory_depends_on": "eval: doc.enabled === 1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"collapsible_depends_on": "eval:doc.enable_checkout",
|
||||||
|
"depends_on": "enabled",
|
||||||
|
"fieldname": "checkout_settings_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Checkout Settings"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "enable_checkout",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Enable Checkout"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "Orders",
|
||||||
|
"depends_on": "enable_checkout",
|
||||||
|
"description": "After payment completion redirect user to selected page.",
|
||||||
|
"fieldname": "payment_success_url",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Payment Success Url",
|
||||||
|
"mandatory_depends_on": "enable_checkout",
|
||||||
|
"options": "\nOrders\nInvoices\nMy Account"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_27",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"depends_on": "eval: doc.enable_checkout == 0",
|
||||||
|
"fieldname": "save_quotations_as_draft",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Save Quotations as Draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "enable_checkout",
|
||||||
|
"fieldname": "payment_gateway_account",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Payment Gateway Account",
|
||||||
|
"mandatory_depends_on": "enable_checkout",
|
||||||
|
"options": "Payment Gateway Account"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"depends_on": "enable_field_filters",
|
||||||
|
"fieldname": "shop_by_category_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Shop by Category"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "slideshow",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Slideshow",
|
||||||
|
"options": "Website Slideshow"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"fieldname": "add_ons_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Add-ons"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "enable_wishlist",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Enable Wishlist"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "enable_reviews",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Enable Reviews and Ratings"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "search_index_fields",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Search Index Fields",
|
||||||
|
"read_only_depends_on": "eval:!doc.is_redisearch_loaded"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"fieldname": "item_search_settings_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Item Search Settings"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "1",
|
||||||
|
"fieldname": "show_categories_in_search_autocomplete",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Show Categories in Search Autocomplete",
|
||||||
|
"read_only_depends_on": "eval:!doc.is_redisearch_loaded"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "is_redisearch_loaded",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Is Redisearch Loaded"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:!doc.is_redisearch_loaded",
|
||||||
|
"fieldname": "redisearch_warning",
|
||||||
|
"fieldtype": "HTML",
|
||||||
|
"label": "Redisearch Warning",
|
||||||
|
"options": "<p class=\"alert alert-warning\">Redisearch is not loaded. If you want to use the advanced product search feature, refer <a class=\"alert-link\" href=\"https://docs.erpnext.com/docs/v13/user/manual/en/setting-up/articles/installing-redisearch\" target=\"_blank\">here</a>.</p>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"depends_on": "eval:doc.show_price",
|
||||||
|
"fieldname": "hide_price_for_guest",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Hide Price for Guest"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_9",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"fieldname": "guest_display_settings_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Guest Display Settings"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Link to redirect Guest on actions that need login such as add to cart, wishlist, etc. <b>E.g.: /login</b>",
|
||||||
|
"fieldname": "redirect_on_action",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Redirect on Action"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_22",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_23",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "enable_recommendations",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Enable Recommendations"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"depends_on": "eval: doc.enable_checkout == 0",
|
||||||
|
"fieldname": "show_price_in_quotation",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Show Price in Quotation"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"issingle": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-09-02 14:02:44.785824",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "E-commerce",
|
||||||
|
"name": "E Commerce Settings",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
@ -1,25 +1,81 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
# -*- coding: utf-8 -*-
|
||||||
# License: GNU General Public License v3. See license.txt
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import flt
|
from frappe.utils import comma_and, flt, unique
|
||||||
|
|
||||||
|
from erpnext.e_commerce.redisearch_utils import (
|
||||||
|
create_website_items_index,
|
||||||
|
get_indexable_web_fields,
|
||||||
|
is_search_module_loaded,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ShoppingCartSetupError(frappe.ValidationError): pass
|
class ShoppingCartSetupError(frappe.ValidationError): pass
|
||||||
|
|
||||||
class ShoppingCartSettings(Document):
|
class ECommerceSettings(Document):
|
||||||
def onload(self):
|
def onload(self):
|
||||||
self.get("__onload").quotation_series = frappe.get_meta("Quotation").get_options("naming_series")
|
self.get("__onload").quotation_series = frappe.get_meta("Quotation").get_options("naming_series")
|
||||||
|
self.is_redisearch_loaded = is_search_module_loaded()
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
|
self.validate_field_filters()
|
||||||
|
self.validate_attribute_filters()
|
||||||
|
self.validate_checkout()
|
||||||
|
self.validate_search_index_fields()
|
||||||
|
|
||||||
if self.enabled:
|
if self.enabled:
|
||||||
self.validate_price_list_exchange_rate()
|
self.validate_price_list_exchange_rate()
|
||||||
|
|
||||||
|
frappe.clear_document_cache("E Commerce Settings", "E Commerce Settings")
|
||||||
|
|
||||||
|
def validate_field_filters(self):
|
||||||
|
if not (self.enable_field_filters and self.filter_fields):
|
||||||
|
return
|
||||||
|
|
||||||
|
item_meta = frappe.get_meta("Item")
|
||||||
|
valid_fields = [df.fieldname for df in item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"]]
|
||||||
|
|
||||||
|
for f in self.filter_fields:
|
||||||
|
if f.fieldname not in valid_fields:
|
||||||
|
frappe.throw(_("Filter Fields Row #{0}: Fieldname <b>{1}</b> must be of type 'Link' or 'Table MultiSelect'").format(f.idx, f.fieldname))
|
||||||
|
|
||||||
|
def validate_attribute_filters(self):
|
||||||
|
if not (self.enable_attribute_filters and self.filter_attributes):
|
||||||
|
return
|
||||||
|
|
||||||
|
# if attribute filters are enabled, hide_variants should be disabled
|
||||||
|
self.hide_variants = 0
|
||||||
|
|
||||||
|
def validate_checkout(self):
|
||||||
|
if self.enable_checkout and not self.payment_gateway_account:
|
||||||
|
self.enable_checkout = 0
|
||||||
|
|
||||||
|
def validate_search_index_fields(self):
|
||||||
|
if not self.search_index_fields:
|
||||||
|
return
|
||||||
|
|
||||||
|
fields = self.search_index_fields.replace(' ', '')
|
||||||
|
fields = unique(fields.strip(',').split(',')) # Remove extra ',' and remove duplicates
|
||||||
|
|
||||||
|
# All fields should be indexable
|
||||||
|
allowed_indexable_fields = get_indexable_web_fields()
|
||||||
|
|
||||||
|
if not (set(fields).issubset(allowed_indexable_fields)):
|
||||||
|
invalid_fields = list(set(fields).difference(allowed_indexable_fields))
|
||||||
|
num_invalid_fields = len(invalid_fields)
|
||||||
|
invalid_fields = comma_and(invalid_fields)
|
||||||
|
|
||||||
|
if num_invalid_fields > 1:
|
||||||
|
frappe.throw(_("{0} are not valid options for Search Index Field.").format(frappe.bold(invalid_fields)))
|
||||||
|
else:
|
||||||
|
frappe.throw(_("{0} is not a valid option for Search Index Field.").format(frappe.bold(invalid_fields)))
|
||||||
|
|
||||||
|
self.search_index_fields = ','.join(fields)
|
||||||
|
|
||||||
def validate_price_list_exchange_rate(self):
|
def validate_price_list_exchange_rate(self):
|
||||||
"Check if exchange rate exists for Price List currency (to Company's currency)."
|
"Check if exchange rate exists for Price List currency (to Company's currency)."
|
||||||
from erpnext.setup.utils import get_exchange_rate
|
from erpnext.setup.utils import get_exchange_rate
|
||||||
@ -60,12 +116,23 @@ class ShoppingCartSettings(Document):
|
|||||||
def get_shipping_rules(self, shipping_territory):
|
def get_shipping_rules(self, shipping_territory):
|
||||||
return self.get_name_from_territory(shipping_territory, "shipping_rules", "shipping_rule")
|
return self.get_name_from_territory(shipping_territory, "shipping_rules", "shipping_rule")
|
||||||
|
|
||||||
|
def on_change(self):
|
||||||
|
old_doc = self.get_doc_before_save()
|
||||||
|
|
||||||
|
if old_doc:
|
||||||
|
old_fields = old_doc.search_index_fields
|
||||||
|
new_fields = self.search_index_fields
|
||||||
|
|
||||||
|
# if search index fields get changed
|
||||||
|
if not (new_fields == old_fields):
|
||||||
|
create_website_items_index()
|
||||||
|
|
||||||
def validate_cart_settings(doc=None, method=None):
|
def validate_cart_settings(doc=None, method=None):
|
||||||
frappe.get_doc("Shopping Cart Settings", "Shopping Cart Settings").run_method("validate")
|
frappe.get_doc("E Commerce Settings", "E Commerce Settings").run_method("validate")
|
||||||
|
|
||||||
def get_shopping_cart_settings():
|
def get_shopping_cart_settings():
|
||||||
if not getattr(frappe.local, "shopping_cart_settings", None):
|
if not getattr(frappe.local, "shopping_cart_settings", None):
|
||||||
frappe.local.shopping_cart_settings = frappe.get_doc("Shopping Cart Settings", "Shopping Cart Settings")
|
frappe.local.shopping_cart_settings = frappe.get_doc("E Commerce Settings", "E Commerce Settings")
|
||||||
|
|
||||||
return frappe.local.shopping_cart_settings
|
return frappe.local.shopping_cart_settings
|
||||||
|
|
@ -1,24 +1,21 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
# -*- coding: utf-8 -*-
|
||||||
# License: GNU General Public License v3. See license.txt
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
# For license information, please see license.txt
|
|
||||||
|
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import (
|
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
|
||||||
ShoppingCartSetupError,
|
ShoppingCartSetupError,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestShoppingCartSettings(unittest.TestCase):
|
class TestECommerceSettings(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
frappe.db.sql("""delete from `tabSingles` where doctype="Shipping Cart Settings" """)
|
frappe.db.sql("""delete from `tabSingles` where doctype="Shipping Cart Settings" """)
|
||||||
|
|
||||||
def get_cart_settings(self):
|
def get_cart_settings(self):
|
||||||
return frappe.get_doc({"doctype": "Shopping Cart Settings",
|
return frappe.get_doc({"doctype": "E Commerce Settings",
|
||||||
"company": "_Test Company"})
|
"company": "_Test Company"})
|
||||||
|
|
||||||
# NOTE: Exchangrate API has all enabled currencies that ERPNext supports.
|
# NOTE: Exchangrate API has all enabled currencies that ERPNext supports.
|
||||||
@ -34,15 +31,17 @@ class TestShoppingCartSettings(unittest.TestCase):
|
|||||||
|
|
||||||
# cart_settings = self.get_cart_settings()
|
# cart_settings = self.get_cart_settings()
|
||||||
# cart_settings.price_list = "_Test Price List Rest of the World"
|
# cart_settings.price_list = "_Test Price List Rest of the World"
|
||||||
# self.assertRaises(ShoppingCartSetupError, cart_settings.validate_price_list_exchange_rate)
|
# self.assertRaises(ShoppingCartSetupError, cart_settings.validate_exchange_rates_exist)
|
||||||
|
|
||||||
# from erpnext.setup.doctype.currency_exchange.test_currency_exchange import test_records as \
|
# from erpnext.setup.doctype.currency_exchange.test_currency_exchange import (
|
||||||
# currency_exchange_records
|
# test_records as currency_exchange_records,
|
||||||
|
# )
|
||||||
# frappe.get_doc(currency_exchange_records[0]).insert()
|
# frappe.get_doc(currency_exchange_records[0]).insert()
|
||||||
# cart_settings.validate_price_list_exchange_rate()
|
# cart_settings.validate_exchange_rates_exist()
|
||||||
|
|
||||||
def test_tax_rule_validation(self):
|
def test_tax_rule_validation(self):
|
||||||
frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0")
|
frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0")
|
||||||
|
frappe.db.commit() # nosemgrep
|
||||||
|
|
||||||
cart_settings = self.get_cart_settings()
|
cart_settings = self.get_cart_settings()
|
||||||
cart_settings.enabled = 1
|
cart_settings.enabled = 1
|
||||||
@ -51,4 +50,13 @@ class TestShoppingCartSettings(unittest.TestCase):
|
|||||||
|
|
||||||
frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 1")
|
frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 1")
|
||||||
|
|
||||||
|
def setup_e_commerce_settings(values_dict):
|
||||||
|
"Accepts a dict of values that updates E Commerce Settings."
|
||||||
|
if not values_dict:
|
||||||
|
return
|
||||||
|
|
||||||
|
doc = frappe.get_doc("E Commerce Settings", "E Commerce Settings")
|
||||||
|
doc.update(values_dict)
|
||||||
|
doc.save()
|
||||||
|
|
||||||
test_dependencies = ["Tax Rule"]
|
test_dependencies = ["Tax Rule"]
|
8
erpnext/e_commerce/doctype/item_review/item_review.js
Normal file
8
erpnext/e_commerce/doctype/item_review/item_review.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.ui.form.on('Item Review', {
|
||||||
|
// refresh: function(frm) {
|
||||||
|
|
||||||
|
// }
|
||||||
|
});
|
134
erpnext/e_commerce/doctype/item_review/item_review.json
Normal file
134
erpnext/e_commerce/doctype/item_review/item_review.json
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"beta": 1,
|
||||||
|
"creation": "2021-03-23 16:47:26.542226",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"website_item",
|
||||||
|
"user",
|
||||||
|
"customer",
|
||||||
|
"column_break_3",
|
||||||
|
"item",
|
||||||
|
"published_on",
|
||||||
|
"reviews_section",
|
||||||
|
"review_title",
|
||||||
|
"rating",
|
||||||
|
"comment"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "website_item",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Website Item",
|
||||||
|
"options": "Website Item",
|
||||||
|
"read_only": 1,
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "user",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "User",
|
||||||
|
"options": "User",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_3",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "website_item.item_code",
|
||||||
|
"fieldname": "item",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Item",
|
||||||
|
"options": "Item",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "reviews_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Reviews"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "rating",
|
||||||
|
"fieldtype": "Rating",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Rating",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "comment",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Comment",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "review_title",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Review Title",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "customer",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Customer",
|
||||||
|
"options": "Customer",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "published_on",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Published on",
|
||||||
|
"read_only": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-08-10 12:08:58.119691",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "E-commerce",
|
||||||
|
"name": "Item Review",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Website Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Customer",
|
||||||
|
"share": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
147
erpnext/e_commerce/doctype/item_review/item_review.py
Normal file
147
erpnext/e_commerce/doctype/item_review/item_review.py
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
from frappe.contacts.doctype.contact.contact import get_contact_name
|
||||||
|
from frappe.model.document import Document
|
||||||
|
from frappe.utils import cint, flt
|
||||||
|
|
||||||
|
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
|
||||||
|
get_shopping_cart_settings,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UnverifiedReviewer(frappe.ValidationError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ItemReview(Document):
|
||||||
|
def after_insert(self):
|
||||||
|
# regenerate cache on review creation
|
||||||
|
reviews_dict = get_queried_reviews(self.website_item)
|
||||||
|
set_reviews_in_cache(self.website_item, reviews_dict)
|
||||||
|
|
||||||
|
def after_delete(self):
|
||||||
|
# regenerate cache on review deletion
|
||||||
|
reviews_dict = get_queried_reviews(self.website_item)
|
||||||
|
set_reviews_in_cache(self.website_item, reviews_dict)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_item_reviews(web_item, start=0, end=10, data=None):
|
||||||
|
"Get Website Item Review Data."
|
||||||
|
start, end = cint(start), cint(end)
|
||||||
|
settings = get_shopping_cart_settings()
|
||||||
|
|
||||||
|
# Get cached reviews for first page (start=0)
|
||||||
|
# avoid cache when page is different
|
||||||
|
from_cache = not bool(start)
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
data = frappe._dict()
|
||||||
|
|
||||||
|
if settings and settings.get("enable_reviews"):
|
||||||
|
reviews_cache = frappe.cache().hget("item_reviews", web_item)
|
||||||
|
if from_cache and reviews_cache:
|
||||||
|
data = reviews_cache
|
||||||
|
else:
|
||||||
|
data = get_queried_reviews(web_item, start, end, data)
|
||||||
|
if from_cache:
|
||||||
|
set_reviews_in_cache(web_item, data)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get_queried_reviews(web_item, start=0, end=10, data=None):
|
||||||
|
"""
|
||||||
|
Query Website Item wise reviews and cache if needed.
|
||||||
|
Cache stores only first page of reviews i.e. 10 reviews maximum.
|
||||||
|
Returns:
|
||||||
|
dict: Containing reviews, average ratings, % of reviews per rating and total reviews.
|
||||||
|
"""
|
||||||
|
if not data:
|
||||||
|
data = frappe._dict()
|
||||||
|
|
||||||
|
data.reviews = frappe.db.get_all(
|
||||||
|
"Item Review",
|
||||||
|
filters={"website_item": web_item},
|
||||||
|
fields=["*"],
|
||||||
|
limit_start=start,
|
||||||
|
limit_page_length=end
|
||||||
|
)
|
||||||
|
|
||||||
|
rating_data = frappe.db.get_all(
|
||||||
|
"Item Review",
|
||||||
|
filters={"website_item": web_item},
|
||||||
|
fields=["avg(rating) as average, count(*) as total"]
|
||||||
|
)[0]
|
||||||
|
|
||||||
|
data.average_rating = flt(rating_data.average, 1)
|
||||||
|
data.average_whole_rating = flt(data.average_rating, 0)
|
||||||
|
|
||||||
|
# get % of reviews per rating
|
||||||
|
reviews_per_rating = []
|
||||||
|
for i in range(1,6):
|
||||||
|
count = frappe.db.get_all(
|
||||||
|
"Item Review",
|
||||||
|
filters={"website_item": web_item, "rating": i},
|
||||||
|
fields=["count(*) as count"]
|
||||||
|
)[0].count
|
||||||
|
|
||||||
|
percent = flt((count / rating_data.total or 1) * 100, 0) if count else 0
|
||||||
|
reviews_per_rating.append(percent)
|
||||||
|
|
||||||
|
data.reviews_per_rating = reviews_per_rating
|
||||||
|
data.total_reviews = rating_data.total
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def set_reviews_in_cache(web_item, reviews_dict):
|
||||||
|
frappe.cache().hset("item_reviews", web_item, reviews_dict)
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def add_item_review(web_item, title, rating, comment=None):
|
||||||
|
""" Add an Item Review by a user if non-existent. """
|
||||||
|
if frappe.session.user == "Guest":
|
||||||
|
# guest user should not reach here ideally in the case they do via an API, throw error
|
||||||
|
frappe.throw(_("You are not verified to write a review yet."), exc=UnverifiedReviewer)
|
||||||
|
|
||||||
|
if not frappe.db.exists("Item Review", {"user": frappe.session.user, "website_item": web_item}):
|
||||||
|
doc = frappe.get_doc({
|
||||||
|
"doctype": "Item Review",
|
||||||
|
"user": frappe.session.user,
|
||||||
|
"customer": get_customer(),
|
||||||
|
"website_item": web_item,
|
||||||
|
"item": frappe.db.get_value("Website Item", web_item, "item_code"),
|
||||||
|
"review_title": title,
|
||||||
|
"rating": rating,
|
||||||
|
"comment": comment
|
||||||
|
})
|
||||||
|
doc.published_on = datetime.today().strftime("%d %B %Y")
|
||||||
|
doc.insert()
|
||||||
|
|
||||||
|
def get_customer(silent=False):
|
||||||
|
"""
|
||||||
|
silent: Return customer if exists else return nothing. Dont throw error.
|
||||||
|
"""
|
||||||
|
user = frappe.session.user
|
||||||
|
contact_name = get_contact_name(user)
|
||||||
|
customer = None
|
||||||
|
|
||||||
|
if contact_name:
|
||||||
|
contact = frappe.get_doc('Contact', contact_name)
|
||||||
|
for link in contact.links:
|
||||||
|
if link.link_doctype == "Customer":
|
||||||
|
customer = link.link_name
|
||||||
|
break
|
||||||
|
|
||||||
|
if customer:
|
||||||
|
return frappe.db.get_value("Customer", customer)
|
||||||
|
elif silent:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
# should not reach here unless via an API
|
||||||
|
frappe.throw(_("You are not a verified customer yet. Please contact us to proceed."),
|
||||||
|
exc=UnverifiedReviewer)
|
84
erpnext/e_commerce/doctype/item_review/test_item_review.py
Normal file
84
erpnext/e_commerce/doctype/item_review/test_item_review.py
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.core.doctype.user_permission.test_user_permission import create_user
|
||||||
|
|
||||||
|
from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import (
|
||||||
|
setup_e_commerce_settings,
|
||||||
|
)
|
||||||
|
from erpnext.e_commerce.doctype.item_review.item_review import (
|
||||||
|
UnverifiedReviewer,
|
||||||
|
add_item_review,
|
||||||
|
get_item_reviews,
|
||||||
|
)
|
||||||
|
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
|
||||||
|
from erpnext.e_commerce.shopping_cart.cart import get_party
|
||||||
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
|
||||||
|
|
||||||
|
class TestItemReview(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
item = make_item("Test Mobile Phone")
|
||||||
|
if not frappe.db.exists("Website Item", {"item_code": "Test Mobile Phone"}):
|
||||||
|
make_website_item(item, save=True)
|
||||||
|
|
||||||
|
setup_e_commerce_settings({"enable_reviews": 1})
|
||||||
|
frappe.local.shopping_cart_settings = None
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
frappe.get_cached_doc("Website Item", {"item_code": "Test Mobile Phone"}).delete()
|
||||||
|
setup_e_commerce_settings({"enable_reviews": 0})
|
||||||
|
|
||||||
|
def test_add_and_get_item_reviews_from_customer(self):
|
||||||
|
"Add / Get Reviews from a User that is a valid customer (has added to cart or purchased in the past)"
|
||||||
|
# create user
|
||||||
|
web_item = frappe.db.get_value("Website Item", {"item_code": "Test Mobile Phone"})
|
||||||
|
test_user = create_user("test_reviewer@example.com", "Customer")
|
||||||
|
frappe.set_user(test_user.name)
|
||||||
|
|
||||||
|
# create customer and contact against user
|
||||||
|
customer = get_party()
|
||||||
|
|
||||||
|
# post review on "Test Mobile Phone"
|
||||||
|
try:
|
||||||
|
add_item_review(web_item, "Great Product", 3, "Would recommend this product")
|
||||||
|
review_name = frappe.db.get_value("Item Review", {"website_item": web_item})
|
||||||
|
except Exception:
|
||||||
|
self.fail(f"Error while publishing review for {web_item}")
|
||||||
|
|
||||||
|
review_data = get_item_reviews(web_item, 0, 10)
|
||||||
|
|
||||||
|
self.assertEqual(len(review_data.reviews), 1)
|
||||||
|
self.assertEqual(review_data.average_rating, 3)
|
||||||
|
self.assertEqual(review_data.reviews_per_rating[2], 100)
|
||||||
|
|
||||||
|
# tear down
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
frappe.delete_doc("Item Review", review_name)
|
||||||
|
customer.delete()
|
||||||
|
|
||||||
|
def test_add_item_review_from_non_customer(self):
|
||||||
|
"Check if logged in user (who is not a customer yet) is blocked from posting reviews."
|
||||||
|
web_item = frappe.db.get_value("Website Item", {"item_code": "Test Mobile Phone"})
|
||||||
|
test_user = create_user("test_reviewer@example.com", "Customer")
|
||||||
|
frappe.set_user(test_user.name)
|
||||||
|
|
||||||
|
with self.assertRaises(UnverifiedReviewer):
|
||||||
|
add_item_review(web_item, "Great Product", 3, "Would recommend this product")
|
||||||
|
|
||||||
|
# tear down
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
|
||||||
|
def test_add_item_reviews_from_guest_user(self):
|
||||||
|
"Check if Guest user is blocked from posting reviews."
|
||||||
|
web_item = frappe.db.get_value("Website Item", {"item_code": "Test Mobile Phone"})
|
||||||
|
frappe.set_user("Guest")
|
||||||
|
|
||||||
|
with self.assertRaises(UnverifiedReviewer):
|
||||||
|
add_item_review(web_item, "Great Product", 3, "Would recommend this product")
|
||||||
|
|
||||||
|
# tear down
|
||||||
|
frappe.set_user("Administrator")
|
@ -0,0 +1,87 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2021-07-12 20:52:12.503470",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"website_item",
|
||||||
|
"website_item_name",
|
||||||
|
"column_break_2",
|
||||||
|
"item_code",
|
||||||
|
"more_information_section",
|
||||||
|
"route",
|
||||||
|
"column_break_6",
|
||||||
|
"website_item_image",
|
||||||
|
"website_item_thumbnail"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "website_item",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Website Item",
|
||||||
|
"options": "Website Item"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "website_item.web_item_name",
|
||||||
|
"fieldname": "website_item_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Website Item Name",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_2",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "more_information_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "More Information"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "website_item.route",
|
||||||
|
"fieldname": "route",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Route",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "website_item.image",
|
||||||
|
"fieldname": "website_item_image",
|
||||||
|
"fieldtype": "Attach",
|
||||||
|
"label": "Website Item Image",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_6",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "website_item.thumbnail",
|
||||||
|
"fieldname": "website_item_thumbnail",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Website Item Thumbnail",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "website_item.item_code",
|
||||||
|
"fieldname": "item_code",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Item Code"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-07-13 21:02:19.031652",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "E-commerce",
|
||||||
|
"name": "Recommended Items",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class RecommendedItems(Document):
|
||||||
|
pass
|
@ -0,0 +1,7 @@
|
|||||||
|
{% extends "templates/web.html" %}
|
||||||
|
|
||||||
|
{% block page_content %}
|
||||||
|
<h1>{{ title }}</h1>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
<!-- this is a sample default web page template -->
|
@ -0,0 +1,4 @@
|
|||||||
|
<div>
|
||||||
|
<a href="{{ doc.route }}">{{ doc.title or doc.name }}</a>
|
||||||
|
</div>
|
||||||
|
<!-- this is a sample default list template -->
|
538
erpnext/e_commerce/doctype/website_item/test_website_item.py
Normal file
538
erpnext/e_commerce/doctype/website_item/test_website_item.py
Normal file
@ -0,0 +1,538 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
from erpnext.controllers.item_variant import create_variant
|
||||||
|
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
|
||||||
|
get_shopping_cart_settings,
|
||||||
|
)
|
||||||
|
from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import (
|
||||||
|
setup_e_commerce_settings,
|
||||||
|
)
|
||||||
|
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
|
||||||
|
from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website
|
||||||
|
from erpnext.stock.doctype.item.item import DataValidationError
|
||||||
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
|
||||||
|
WEBITEM_DESK_TESTS = ("test_website_item_desk_item_sync", "test_publish_variant_and_template")
|
||||||
|
WEBITEM_PRICE_TESTS = ('test_website_item_price_for_logged_in_user', 'test_website_item_price_for_guest_user')
|
||||||
|
|
||||||
|
class TestWebsiteItem(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
setup_e_commerce_settings({
|
||||||
|
"company": "_Test Company",
|
||||||
|
"enabled": 1,
|
||||||
|
"default_customer_group": "_Test Customer Group",
|
||||||
|
"price_list": "_Test Price List India"
|
||||||
|
})
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
frappe.db.rollback()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
if self._testMethodName in WEBITEM_DESK_TESTS:
|
||||||
|
make_item("Test Web Item", {
|
||||||
|
"has_variant": 1,
|
||||||
|
"variant_based_on": "Item Attribute",
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"attribute": "Test Size"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
elif self._testMethodName in WEBITEM_PRICE_TESTS:
|
||||||
|
create_user_and_customer_if_not_exists("test_contact_customer@example.com", "_Test Contact For _Test Customer")
|
||||||
|
create_regular_web_item()
|
||||||
|
make_web_item_price(item_code="Test Mobile Phone")
|
||||||
|
|
||||||
|
# Note: When testing web item pricing rule logged-in user pricing rule must differ from guest pricing rule or test will falsely pass.
|
||||||
|
# This is because make_web_pricing_rule creates a pricing rule "selling": 1, without specifying "applicable_for". Therefor,
|
||||||
|
# when testing for logged-in user the test will get the previous pricing rule because "selling" is still true.
|
||||||
|
#
|
||||||
|
# I've attempted to mitigate this by setting applicable_for=Customer, and customer=Guest however, this only results in PermissionError failing the test.
|
||||||
|
make_web_pricing_rule(
|
||||||
|
title="Test Pricing Rule for Test Mobile Phone",
|
||||||
|
item_code="Test Mobile Phone",
|
||||||
|
selling=1)
|
||||||
|
make_web_pricing_rule(
|
||||||
|
title="Test Pricing Rule for Test Mobile Phone (Customer)",
|
||||||
|
item_code="Test Mobile Phone",
|
||||||
|
selling=1,
|
||||||
|
discount_percentage="25",
|
||||||
|
applicable_for="Customer",
|
||||||
|
customer="_Test Customer")
|
||||||
|
|
||||||
|
def test_index_creation(self):
|
||||||
|
"Check if index is getting created in db."
|
||||||
|
from erpnext.e_commerce.doctype.website_item.website_item import on_doctype_update
|
||||||
|
on_doctype_update()
|
||||||
|
|
||||||
|
indices = frappe.db.sql("show index from `tabWebsite Item`", as_dict=1)
|
||||||
|
expected_columns = {"route", "item_group", "brand"}
|
||||||
|
for index in indices:
|
||||||
|
expected_columns.discard(index.get("Column_name"))
|
||||||
|
|
||||||
|
if expected_columns:
|
||||||
|
self.fail(f"Expected db index on these columns: {', '.join(expected_columns)}")
|
||||||
|
|
||||||
|
def test_website_item_desk_item_sync(self):
|
||||||
|
"Check creation/updation/deletion of Website Item and its impact on Item master."
|
||||||
|
web_item = None
|
||||||
|
item = make_item("Test Web Item") # will return item if exists
|
||||||
|
try:
|
||||||
|
web_item = make_website_item(item, save=False)
|
||||||
|
web_item.save()
|
||||||
|
except Exception:
|
||||||
|
self.fail(f"Error while creating website item for {item}")
|
||||||
|
|
||||||
|
# check if website item was created
|
||||||
|
self.assertTrue(bool(web_item))
|
||||||
|
self.assertTrue(bool(web_item.route))
|
||||||
|
|
||||||
|
item.reload()
|
||||||
|
self.assertEqual(web_item.published, 1)
|
||||||
|
self.assertEqual(item.published_in_website, 1) # check if item was back updated
|
||||||
|
self.assertEqual(web_item.item_group, item.item_group)
|
||||||
|
|
||||||
|
# check if changing item data changes it in website item
|
||||||
|
item.item_name = "Test Web Item 1"
|
||||||
|
item.stock_uom = "Unit"
|
||||||
|
item.save()
|
||||||
|
web_item.reload()
|
||||||
|
self.assertEqual(web_item.item_name, item.item_name)
|
||||||
|
self.assertEqual(web_item.stock_uom, item.stock_uom)
|
||||||
|
|
||||||
|
# check if disabling item unpublished website item
|
||||||
|
item.disabled = 1
|
||||||
|
item.save()
|
||||||
|
web_item.reload()
|
||||||
|
self.assertEqual(web_item.published, 0)
|
||||||
|
|
||||||
|
# check if website item deletion, unpublishes desk item
|
||||||
|
web_item.delete()
|
||||||
|
item.reload()
|
||||||
|
self.assertEqual(item.published_in_website, 0)
|
||||||
|
|
||||||
|
item.delete()
|
||||||
|
|
||||||
|
def test_publish_variant_and_template(self):
|
||||||
|
"Check if template is published on publishing variant."
|
||||||
|
# template "Test Web Item" created on setUp
|
||||||
|
variant = create_variant("Test Web Item", {"Test Size": "Large"})
|
||||||
|
variant.save()
|
||||||
|
|
||||||
|
# check if template is not published
|
||||||
|
self.assertIsNone(frappe.db.exists("Website Item", {"item_code": variant.variant_of}))
|
||||||
|
|
||||||
|
variant_web_item = make_website_item(variant, save=False)
|
||||||
|
variant_web_item.save()
|
||||||
|
|
||||||
|
# check if template is published
|
||||||
|
try:
|
||||||
|
template_web_item = frappe.get_doc("Website Item", {"item_code": variant.variant_of})
|
||||||
|
except frappe.DoesNotExistError:
|
||||||
|
self.fail(f"Template of {variant.item_code}, {variant.variant_of} not published")
|
||||||
|
|
||||||
|
# teardown
|
||||||
|
variant_web_item.delete()
|
||||||
|
template_web_item.delete()
|
||||||
|
variant.delete()
|
||||||
|
|
||||||
|
def test_impact_on_merging_items(self):
|
||||||
|
"Check if merging items is blocked if old and new items both have website items"
|
||||||
|
first_item = make_item("Test First Item")
|
||||||
|
second_item = make_item("Test Second Item")
|
||||||
|
|
||||||
|
first_web_item = make_website_item(first_item, save=False)
|
||||||
|
first_web_item.save()
|
||||||
|
second_web_item = make_website_item(second_item, save=False)
|
||||||
|
second_web_item.save()
|
||||||
|
|
||||||
|
with self.assertRaises(DataValidationError):
|
||||||
|
frappe.rename_doc("Item", "Test First Item", "Test Second Item", merge=True)
|
||||||
|
|
||||||
|
# tear down
|
||||||
|
second_web_item.delete()
|
||||||
|
first_web_item.delete()
|
||||||
|
second_item.delete()
|
||||||
|
first_item.delete()
|
||||||
|
|
||||||
|
# Website Item Portal Tests Begin
|
||||||
|
|
||||||
|
def test_website_item_breadcrumbs(self):
|
||||||
|
"Check if breadcrumbs include homepage, product listing navigation page, parent item group(s) and item group."
|
||||||
|
from erpnext.setup.doctype.item_group.item_group import get_parent_item_groups
|
||||||
|
|
||||||
|
item_code = "Test Breadcrumb Item"
|
||||||
|
item = make_item(item_code, {
|
||||||
|
"item_group": "_Test Item Group B - 1",
|
||||||
|
})
|
||||||
|
|
||||||
|
if not frappe.db.exists("Website Item", {"item_code": item_code}):
|
||||||
|
web_item = make_website_item(item, save=False)
|
||||||
|
web_item.save()
|
||||||
|
else:
|
||||||
|
web_item = frappe.get_cached_doc("Website Item", {"item_code": item_code})
|
||||||
|
|
||||||
|
frappe.db.set_value("Item Group", "_Test Item Group B - 1", "show_in_website", 1)
|
||||||
|
frappe.db.set_value("Item Group", "_Test Item Group B", "show_in_website", 1)
|
||||||
|
|
||||||
|
breadcrumbs = get_parent_item_groups(item.item_group)
|
||||||
|
|
||||||
|
self.assertEqual(breadcrumbs[0]["name"], "Home")
|
||||||
|
self.assertEqual(breadcrumbs[1]["name"], "Shop by Category")
|
||||||
|
self.assertEqual(breadcrumbs[2]["name"], "_Test Item Group B") # parent item group
|
||||||
|
self.assertEqual(breadcrumbs[3]["name"], "_Test Item Group B - 1")
|
||||||
|
|
||||||
|
# tear down
|
||||||
|
web_item.delete()
|
||||||
|
item.delete()
|
||||||
|
|
||||||
|
def test_website_item_price_for_logged_in_user(self):
|
||||||
|
"Check if price details are fetched correctly while logged in."
|
||||||
|
item_code = "Test Mobile Phone"
|
||||||
|
|
||||||
|
# show price in e commerce settings
|
||||||
|
setup_e_commerce_settings({"show_price": 1})
|
||||||
|
|
||||||
|
# price and pricing rule added via setUp
|
||||||
|
|
||||||
|
# login as customer with pricing rule
|
||||||
|
frappe.set_user("test_contact_customer@example.com")
|
||||||
|
|
||||||
|
# check if price and slashed price is fetched correctly
|
||||||
|
frappe.local.shopping_cart_settings = None
|
||||||
|
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
|
||||||
|
self.assertTrue(bool(data.product_info["price"]))
|
||||||
|
|
||||||
|
price_object = data.product_info["price"]
|
||||||
|
self.assertEqual(price_object.get("discount_percent"), 25)
|
||||||
|
self.assertEqual(price_object.get("price_list_rate"), 750)
|
||||||
|
self.assertEqual(price_object.get("formatted_mrp"), "₹ 1,000.00")
|
||||||
|
self.assertEqual(price_object.get("formatted_price"), "₹ 750.00")
|
||||||
|
self.assertEqual(price_object.get("formatted_discount_percent"), "25%")
|
||||||
|
|
||||||
|
# switch to admin and disable show price
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
setup_e_commerce_settings({"show_price": 0})
|
||||||
|
|
||||||
|
# price should not be fetched for logged in user.
|
||||||
|
frappe.set_user("test_contact_customer@example.com")
|
||||||
|
frappe.local.shopping_cart_settings = None
|
||||||
|
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
|
||||||
|
self.assertFalse(bool(data.product_info["price"]))
|
||||||
|
|
||||||
|
# tear down
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
|
||||||
|
def test_website_item_price_for_guest_user(self):
|
||||||
|
"Check if price details are fetched correctly for guest user."
|
||||||
|
item_code = "Test Mobile Phone"
|
||||||
|
|
||||||
|
# show price for guest user in e commerce settings
|
||||||
|
setup_e_commerce_settings({
|
||||||
|
"show_price": 1,
|
||||||
|
"hide_price_for_guest": 0
|
||||||
|
})
|
||||||
|
|
||||||
|
# price and pricing rule added via setUp
|
||||||
|
|
||||||
|
# switch to guest user
|
||||||
|
frappe.set_user("Guest")
|
||||||
|
|
||||||
|
# price should be fetched
|
||||||
|
frappe.local.shopping_cart_settings = None
|
||||||
|
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
|
||||||
|
self.assertTrue(bool(data.product_info["price"]))
|
||||||
|
|
||||||
|
price_object = data.product_info["price"]
|
||||||
|
self.assertEqual(price_object.get("discount_percent"), 10)
|
||||||
|
self.assertEqual(price_object.get("price_list_rate"), 900)
|
||||||
|
|
||||||
|
# hide price for guest user
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
setup_e_commerce_settings({"hide_price_for_guest": 1})
|
||||||
|
frappe.set_user("Guest")
|
||||||
|
|
||||||
|
# price should not be fetched
|
||||||
|
frappe.local.shopping_cart_settings = None
|
||||||
|
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
|
||||||
|
self.assertFalse(bool(data.product_info["price"]))
|
||||||
|
|
||||||
|
# tear down
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
|
||||||
|
def test_website_item_stock_when_out_of_stock(self):
|
||||||
|
"""
|
||||||
|
Check if stock details are fetched correctly for empty inventory when:
|
||||||
|
1) Showing stock availability enabled:
|
||||||
|
- Warehouse unset
|
||||||
|
- Warehouse set
|
||||||
|
2) Showing stock availability disabled
|
||||||
|
"""
|
||||||
|
item_code = "Test Mobile Phone"
|
||||||
|
create_regular_web_item()
|
||||||
|
setup_e_commerce_settings({"show_stock_availability": 1})
|
||||||
|
|
||||||
|
frappe.local.shopping_cart_settings = None
|
||||||
|
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
|
||||||
|
|
||||||
|
# check if stock details are fetched and item not in stock without warehouse set
|
||||||
|
self.assertFalse(bool(data.product_info["in_stock"]))
|
||||||
|
self.assertFalse(bool(data.product_info["stock_qty"]))
|
||||||
|
|
||||||
|
# set warehouse
|
||||||
|
frappe.db.set_value("Website Item", {"item_code": item_code}, "website_warehouse", "_Test Warehouse - _TC")
|
||||||
|
|
||||||
|
# check if stock details are fetched and item not in stock with warehouse set
|
||||||
|
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
|
||||||
|
self.assertFalse(bool(data.product_info["in_stock"]))
|
||||||
|
self.assertEqual(data.product_info["stock_qty"][0][0], 0)
|
||||||
|
|
||||||
|
# disable show stock availability
|
||||||
|
setup_e_commerce_settings({"show_stock_availability": 0})
|
||||||
|
frappe.local.shopping_cart_settings = None
|
||||||
|
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
|
||||||
|
|
||||||
|
# check if stock detail attributes are not fetched if stock availability is hidden
|
||||||
|
self.assertIsNone(data.product_info.get("in_stock"))
|
||||||
|
self.assertIsNone(data.product_info.get("stock_qty"))
|
||||||
|
self.assertIsNone(data.product_info.get("show_stock_qty"))
|
||||||
|
|
||||||
|
# tear down
|
||||||
|
frappe.get_cached_doc("Website Item", {"item_code": "Test Mobile Phone"}).delete()
|
||||||
|
|
||||||
|
def test_website_item_stock_when_in_stock(self):
|
||||||
|
"""
|
||||||
|
Check if stock details are fetched correctly for available inventory when:
|
||||||
|
1) Showing stock availability enabled:
|
||||||
|
- Warehouse set
|
||||||
|
- Warehouse unset
|
||||||
|
2) Showing stock availability disabled
|
||||||
|
"""
|
||||||
|
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||||
|
|
||||||
|
item_code = "Test Mobile Phone"
|
||||||
|
create_regular_web_item()
|
||||||
|
setup_e_commerce_settings({"show_stock_availability": 1})
|
||||||
|
frappe.local.shopping_cart_settings = None
|
||||||
|
|
||||||
|
# set warehouse
|
||||||
|
frappe.db.set_value("Website Item", {"item_code": item_code}, "website_warehouse", "_Test Warehouse - _TC")
|
||||||
|
|
||||||
|
# stock up item
|
||||||
|
stock_entry = make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=2, rate=100)
|
||||||
|
|
||||||
|
# check if stock details are fetched and item is in stock with warehouse set
|
||||||
|
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
|
||||||
|
self.assertTrue(bool(data.product_info["in_stock"]))
|
||||||
|
self.assertEqual(data.product_info["stock_qty"][0][0], 2)
|
||||||
|
|
||||||
|
# unset warehouse
|
||||||
|
frappe.db.set_value("Website Item", {"item_code": item_code}, "website_warehouse", "")
|
||||||
|
|
||||||
|
# check if stock details are fetched and item not in stock without warehouse set
|
||||||
|
# (even though it has stock in some warehouse)
|
||||||
|
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
|
||||||
|
self.assertFalse(bool(data.product_info["in_stock"]))
|
||||||
|
self.assertFalse(bool(data.product_info["stock_qty"]))
|
||||||
|
|
||||||
|
# disable show stock availability
|
||||||
|
setup_e_commerce_settings({"show_stock_availability": 0})
|
||||||
|
frappe.local.shopping_cart_settings = None
|
||||||
|
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
|
||||||
|
|
||||||
|
# check if stock detail attributes are not fetched if stock availability is hidden
|
||||||
|
self.assertIsNone(data.product_info.get("in_stock"))
|
||||||
|
self.assertIsNone(data.product_info.get("stock_qty"))
|
||||||
|
self.assertIsNone(data.product_info.get("show_stock_qty"))
|
||||||
|
|
||||||
|
# tear down
|
||||||
|
stock_entry.cancel()
|
||||||
|
frappe.get_cached_doc("Website Item", {"item_code": "Test Mobile Phone"}).delete()
|
||||||
|
|
||||||
|
def test_recommended_item(self):
|
||||||
|
"Check if added recommended items are fetched correctly."
|
||||||
|
item_code = "Test Mobile Phone"
|
||||||
|
web_item = create_regular_web_item(item_code)
|
||||||
|
|
||||||
|
setup_e_commerce_settings({
|
||||||
|
"enable_recommendations": 1,
|
||||||
|
"show_price": 1
|
||||||
|
})
|
||||||
|
|
||||||
|
# create recommended web item and price for it
|
||||||
|
recommended_web_item = create_regular_web_item("Test Mobile Phone 1")
|
||||||
|
make_web_item_price(item_code="Test Mobile Phone 1")
|
||||||
|
|
||||||
|
# add recommended item to first web item
|
||||||
|
web_item.append("recommended_items", {"website_item": recommended_web_item.name})
|
||||||
|
web_item.save()
|
||||||
|
|
||||||
|
frappe.local.shopping_cart_settings = None
|
||||||
|
e_commerce_settings = get_shopping_cart_settings()
|
||||||
|
recommended_items = web_item.get_recommended_items(e_commerce_settings)
|
||||||
|
|
||||||
|
# test results if show price is enabled
|
||||||
|
self.assertEqual(len(recommended_items), 1)
|
||||||
|
recomm_item = recommended_items[0]
|
||||||
|
self.assertEqual(recomm_item.get("website_item_name"), "Test Mobile Phone 1")
|
||||||
|
self.assertTrue(bool(recomm_item.get("price_info"))) # price fetched
|
||||||
|
|
||||||
|
price_info = recomm_item.get("price_info")
|
||||||
|
self.assertEqual(price_info.get("price_list_rate"), 1000)
|
||||||
|
self.assertEqual(price_info.get("formatted_price"), "₹ 1,000.00")
|
||||||
|
|
||||||
|
# test results if show price is disabled
|
||||||
|
setup_e_commerce_settings({"show_price": 0})
|
||||||
|
|
||||||
|
frappe.local.shopping_cart_settings = None
|
||||||
|
e_commerce_settings = get_shopping_cart_settings()
|
||||||
|
recommended_items = web_item.get_recommended_items(e_commerce_settings)
|
||||||
|
|
||||||
|
self.assertEqual(len(recommended_items), 1)
|
||||||
|
self.assertFalse(bool(recommended_items[0].get("price_info"))) # price not fetched
|
||||||
|
|
||||||
|
# tear down
|
||||||
|
web_item.delete()
|
||||||
|
recommended_web_item.delete()
|
||||||
|
frappe.get_cached_doc("Item", "Test Mobile Phone 1").delete()
|
||||||
|
|
||||||
|
def test_recommended_item_for_guest_user(self):
|
||||||
|
"Check if added recommended items are fetched correctly for guest user."
|
||||||
|
item_code = "Test Mobile Phone"
|
||||||
|
web_item = create_regular_web_item(item_code)
|
||||||
|
|
||||||
|
# price visible to guests
|
||||||
|
setup_e_commerce_settings({
|
||||||
|
"enable_recommendations": 1,
|
||||||
|
"show_price": 1,
|
||||||
|
"hide_price_for_guest": 0
|
||||||
|
})
|
||||||
|
|
||||||
|
# create recommended web item and price for it
|
||||||
|
recommended_web_item = create_regular_web_item("Test Mobile Phone 1")
|
||||||
|
make_web_item_price(item_code="Test Mobile Phone 1")
|
||||||
|
|
||||||
|
# add recommended item to first web item
|
||||||
|
web_item.append("recommended_items", {"website_item": recommended_web_item.name})
|
||||||
|
web_item.save()
|
||||||
|
|
||||||
|
frappe.set_user("Guest")
|
||||||
|
|
||||||
|
frappe.local.shopping_cart_settings = None
|
||||||
|
e_commerce_settings = get_shopping_cart_settings()
|
||||||
|
recommended_items = web_item.get_recommended_items(e_commerce_settings)
|
||||||
|
|
||||||
|
# test results if show price is enabled
|
||||||
|
self.assertEqual(len(recommended_items), 1)
|
||||||
|
self.assertTrue(bool(recommended_items[0].get("price_info"))) # price fetched
|
||||||
|
|
||||||
|
# price hidden from guests
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
setup_e_commerce_settings({"hide_price_for_guest": 1})
|
||||||
|
frappe.set_user("Guest")
|
||||||
|
|
||||||
|
frappe.local.shopping_cart_settings = None
|
||||||
|
e_commerce_settings = get_shopping_cart_settings()
|
||||||
|
recommended_items = web_item.get_recommended_items(e_commerce_settings)
|
||||||
|
|
||||||
|
# test results if show price is enabled
|
||||||
|
self.assertEqual(len(recommended_items), 1)
|
||||||
|
self.assertFalse(bool(recommended_items[0].get("price_info"))) # price fetched
|
||||||
|
|
||||||
|
# tear down
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
web_item.delete()
|
||||||
|
recommended_web_item.delete()
|
||||||
|
frappe.get_cached_doc("Item", "Test Mobile Phone 1").delete()
|
||||||
|
|
||||||
|
def create_regular_web_item(item_code=None, item_args=None, web_args=None):
|
||||||
|
"Create Regular Item and Website Item."
|
||||||
|
item_code = item_code or "Test Mobile Phone"
|
||||||
|
item = make_item(item_code, properties=item_args)
|
||||||
|
|
||||||
|
if not frappe.db.exists("Website Item", {"item_code": item_code}):
|
||||||
|
web_item = make_website_item(item, save=False)
|
||||||
|
if web_args:
|
||||||
|
web_item.update(web_args)
|
||||||
|
web_item.save()
|
||||||
|
else:
|
||||||
|
web_item = frappe.get_cached_doc("Website Item", {"item_code": item_code})
|
||||||
|
|
||||||
|
return web_item
|
||||||
|
|
||||||
|
def make_web_item_price(**kwargs):
|
||||||
|
item_code = kwargs.get("item_code")
|
||||||
|
if not item_code:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not frappe.db.exists("Item Price", {"item_code": item_code}):
|
||||||
|
item_price = frappe.get_doc({
|
||||||
|
"doctype": "Item Price",
|
||||||
|
"item_code": item_code,
|
||||||
|
"price_list": kwargs.get("price_list") or "_Test Price List India",
|
||||||
|
"price_list_rate": kwargs.get("price_list_rate") or 1000
|
||||||
|
})
|
||||||
|
item_price.insert()
|
||||||
|
else:
|
||||||
|
item_price = frappe.get_cached_doc("Item Price", {"item_code": item_code})
|
||||||
|
|
||||||
|
return item_price
|
||||||
|
|
||||||
|
def make_web_pricing_rule(**kwargs):
|
||||||
|
title = kwargs.get("title")
|
||||||
|
if not title:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not frappe.db.exists("Pricing Rule", title):
|
||||||
|
pricing_rule = frappe.get_doc({
|
||||||
|
"doctype": "Pricing Rule",
|
||||||
|
"title": title,
|
||||||
|
"apply_on": kwargs.get("apply_on") or "Item Code",
|
||||||
|
"items": [{
|
||||||
|
"item_code": kwargs.get("item_code")
|
||||||
|
}],
|
||||||
|
"selling": kwargs.get("selling") or 0,
|
||||||
|
"buying": kwargs.get("buying") or 0,
|
||||||
|
"rate_or_discount": kwargs.get("rate_or_discount") or "Discount Percentage",
|
||||||
|
"discount_percentage": kwargs.get("discount_percentage") or 10,
|
||||||
|
"company": kwargs.get("company") or "_Test Company",
|
||||||
|
"currency": kwargs.get("currency") or "INR",
|
||||||
|
"for_price_list": kwargs.get("price_list") or "_Test Price List India",
|
||||||
|
"applicable_for": kwargs.get("applicable_for") or "",
|
||||||
|
"customer": kwargs.get("customer") or "",
|
||||||
|
})
|
||||||
|
pricing_rule.insert()
|
||||||
|
else:
|
||||||
|
pricing_rule = frappe.get_doc("Pricing Rule", {"title": title})
|
||||||
|
|
||||||
|
return pricing_rule
|
||||||
|
|
||||||
|
|
||||||
|
def create_user_and_customer_if_not_exists(email, first_name = None):
|
||||||
|
if frappe.db.exists("User", email):
|
||||||
|
return
|
||||||
|
|
||||||
|
frappe.get_doc({
|
||||||
|
"doctype": "User",
|
||||||
|
"user_type": "Website User",
|
||||||
|
"email": email,
|
||||||
|
"send_welcome_email": 0,
|
||||||
|
"first_name": first_name or email.split("@")[0]
|
||||||
|
}).insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
contact = frappe.get_last_doc("Contact", filters={"email_id": email})
|
||||||
|
link = contact.append('links', {})
|
||||||
|
link.link_doctype = "Customer"
|
||||||
|
link.link_name = "_Test Customer"
|
||||||
|
link.link_title = "_Test Customer"
|
||||||
|
contact.save()
|
||||||
|
|
||||||
|
test_dependencies = ["Price List", "Item Price", "Customer", "Contact", "Item"]
|
24
erpnext/e_commerce/doctype/website_item/website_item.js
Normal file
24
erpnext/e_commerce/doctype/website_item/website_item.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.ui.form.on('Website Item', {
|
||||||
|
onload: function(frm) {
|
||||||
|
// should never check Private
|
||||||
|
frm.fields_dict["website_image"].df.is_private = 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
image: function() {
|
||||||
|
refresh_field("image_view");
|
||||||
|
},
|
||||||
|
|
||||||
|
copy_from_item_group: function(frm) {
|
||||||
|
return frm.call({
|
||||||
|
doc: frm.doc,
|
||||||
|
method: "copy_specification_from_item_group"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
set_meta_tags(frm) {
|
||||||
|
frappe.utils.set_meta_tag(frm.doc.route);
|
||||||
|
}
|
||||||
|
});
|
415
erpnext/e_commerce/doctype/website_item/website_item.json
Normal file
415
erpnext/e_commerce/doctype/website_item/website_item.json
Normal file
@ -0,0 +1,415 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_guest_to_view": 1,
|
||||||
|
"allow_import": 1,
|
||||||
|
"autoname": "naming_series",
|
||||||
|
"creation": "2021-02-09 21:06:14.441698",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"naming_series",
|
||||||
|
"web_item_name",
|
||||||
|
"route",
|
||||||
|
"has_variants",
|
||||||
|
"variant_of",
|
||||||
|
"published",
|
||||||
|
"column_break_3",
|
||||||
|
"item_code",
|
||||||
|
"item_name",
|
||||||
|
"item_group",
|
||||||
|
"stock_uom",
|
||||||
|
"column_break_11",
|
||||||
|
"description",
|
||||||
|
"brand",
|
||||||
|
"image",
|
||||||
|
"display_section",
|
||||||
|
"website_image",
|
||||||
|
"website_image_alt",
|
||||||
|
"column_break_13",
|
||||||
|
"slideshow",
|
||||||
|
"thumbnail",
|
||||||
|
"stock_information_section",
|
||||||
|
"website_warehouse",
|
||||||
|
"column_break_24",
|
||||||
|
"on_backorder",
|
||||||
|
"section_break_17",
|
||||||
|
"short_description",
|
||||||
|
"web_long_description",
|
||||||
|
"column_break_27",
|
||||||
|
"website_specifications",
|
||||||
|
"copy_from_item_group",
|
||||||
|
"display_additional_information_section",
|
||||||
|
"show_tabbed_section",
|
||||||
|
"tabs",
|
||||||
|
"recommended_items_section",
|
||||||
|
"recommended_items",
|
||||||
|
"offers_section",
|
||||||
|
"offers",
|
||||||
|
"section_break_6",
|
||||||
|
"ranking",
|
||||||
|
"set_meta_tags",
|
||||||
|
"column_break_22",
|
||||||
|
"website_item_groups",
|
||||||
|
"advanced_display_section",
|
||||||
|
"website_content"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"description": "Website display name",
|
||||||
|
"fetch_from": "item_code.item_name",
|
||||||
|
"fetch_if_empty": 1,
|
||||||
|
"fieldname": "web_item_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Website Item Name",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_3",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "item_code",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Item Code",
|
||||||
|
"options": "Item",
|
||||||
|
"read_only_depends_on": "eval:!doc.__islocal",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "item_code.item_name",
|
||||||
|
"fieldname": "item_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Item Name",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"fieldname": "section_break_6",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Search and SEO"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "route",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Route",
|
||||||
|
"no_copy": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Items with higher ranking will be shown higher",
|
||||||
|
"fieldname": "ranking",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Ranking"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Show a slideshow at the top of the page",
|
||||||
|
"fieldname": "slideshow",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Slideshow",
|
||||||
|
"options": "Website Slideshow"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Item Image (if not slideshow)",
|
||||||
|
"fieldname": "website_image",
|
||||||
|
"fieldtype": "Attach",
|
||||||
|
"label": "Website Image"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Image Alternative Text",
|
||||||
|
"fieldname": "website_image_alt",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Image Description"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "thumbnail",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Thumbnail",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_13",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Show Stock availability based on this warehouse.",
|
||||||
|
"fieldname": "website_warehouse",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"ignore_user_permissions": 1,
|
||||||
|
"label": "Website Warehouse",
|
||||||
|
"options": "Warehouse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "List this Item in multiple groups on the website.",
|
||||||
|
"fieldname": "website_item_groups",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Website Item Groups",
|
||||||
|
"options": "Website Item Group"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "set_meta_tags",
|
||||||
|
"fieldtype": "Button",
|
||||||
|
"label": "Set Meta Tags"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_17",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Display Information"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "copy_from_item_group",
|
||||||
|
"fieldtype": "Button",
|
||||||
|
"label": "Copy From Item Group"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "website_specifications",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Website Specifications",
|
||||||
|
"options": "Item Website Specification"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "web_long_description",
|
||||||
|
"fieldtype": "Text Editor",
|
||||||
|
"label": "Website Description"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "You can use any valid Bootstrap 4 markup in this field. It will be shown on your Item Page.",
|
||||||
|
"fieldname": "website_content",
|
||||||
|
"fieldtype": "HTML Editor",
|
||||||
|
"label": "Website Content"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "item_code.item_group",
|
||||||
|
"fieldname": "item_group",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Item Group",
|
||||||
|
"options": "Item Group",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "image",
|
||||||
|
"fieldtype": "Attach Image",
|
||||||
|
"hidden": 1,
|
||||||
|
"in_preview": 1,
|
||||||
|
"label": "Image",
|
||||||
|
"print_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "1",
|
||||||
|
"fieldname": "published",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Published"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"depends_on": "has_variants",
|
||||||
|
"fetch_from": "item_code.has_variants",
|
||||||
|
"fieldname": "has_variants",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"in_standard_filter": 1,
|
||||||
|
"label": "Has Variants",
|
||||||
|
"no_copy": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "variant_of",
|
||||||
|
"fetch_from": "item_code.variant_of",
|
||||||
|
"fieldname": "variant_of",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"ignore_user_permissions": 1,
|
||||||
|
"in_standard_filter": 1,
|
||||||
|
"label": "Variant Of",
|
||||||
|
"options": "Item",
|
||||||
|
"read_only": 1,
|
||||||
|
"search_index": 1,
|
||||||
|
"set_only_once": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "item_code.stock_uom",
|
||||||
|
"fieldname": "stock_uom",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Stock UOM",
|
||||||
|
"options": "UOM",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "brand",
|
||||||
|
"fetch_from": "item_code.brand",
|
||||||
|
"fieldname": "brand",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Brand",
|
||||||
|
"options": "Brand"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"fieldname": "advanced_display_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Advanced Display Content"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "display_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Display Images"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_27",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_22",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "item_code.description",
|
||||||
|
"fieldname": "description",
|
||||||
|
"fieldtype": "Text Editor",
|
||||||
|
"label": "Item Description",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "WEB-ITM-.####",
|
||||||
|
"fieldname": "naming_series",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Naming Series",
|
||||||
|
"no_copy": 1,
|
||||||
|
"options": "WEB-ITM-.####",
|
||||||
|
"print_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "display_additional_information_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Display Additional Information"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "show_tabbed_section",
|
||||||
|
"fieldname": "tabs",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Tabs",
|
||||||
|
"options": "Website Item Tabbed Section"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "show_tabbed_section",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Add Section with Tabs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"fieldname": "offers_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Offers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "offers",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Offers to Display",
|
||||||
|
"options": "Website Offer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_11",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Short Description for List View",
|
||||||
|
"fieldname": "short_description",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Short Website Description"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"fieldname": "recommended_items_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Recommended Items"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "recommended_items",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Recommended/Similar Items",
|
||||||
|
"options": "Recommended Items"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "stock_information_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Stock Information"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_24",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "Indicate that Item is available on backorder and not usually pre-stocked",
|
||||||
|
"fieldname": "on_backorder",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "On Backorder"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"has_web_view": 1,
|
||||||
|
"image_field": "image",
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-09-02 13:08:41.942726",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "E-commerce",
|
||||||
|
"name": "Website Item",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Website Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Stock User",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Stock Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"search_fields": "web_item_name, item_code, item_group",
|
||||||
|
"show_name_in_global_search": 1,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"title_field": "web_item_name",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
441
erpnext/e_commerce/doctype/website_item/website_item.py
Normal file
441
erpnext/e_commerce/doctype/website_item/website_item.py
Normal file
@ -0,0 +1,441 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
from frappe.utils import cint, cstr, flt, random_string
|
||||||
|
from frappe.website.doctype.website_slideshow.website_slideshow import get_slideshow
|
||||||
|
from frappe.website.website_generator import WebsiteGenerator
|
||||||
|
|
||||||
|
from erpnext.e_commerce.doctype.item_review.item_review import get_item_reviews
|
||||||
|
from erpnext.e_commerce.redisearch_utils import (
|
||||||
|
delete_item_from_index,
|
||||||
|
insert_item_to_index,
|
||||||
|
update_index_for_item,
|
||||||
|
)
|
||||||
|
from erpnext.e_commerce.shopping_cart.cart import _set_price_list
|
||||||
|
from erpnext.setup.doctype.item_group.item_group import (
|
||||||
|
get_parent_item_groups,
|
||||||
|
invalidate_cache_for,
|
||||||
|
)
|
||||||
|
from erpnext.utilities.product import get_price
|
||||||
|
|
||||||
|
|
||||||
|
class WebsiteItem(WebsiteGenerator):
|
||||||
|
website = frappe._dict(
|
||||||
|
page_title_field="web_item_name",
|
||||||
|
condition_field="published",
|
||||||
|
template="templates/generators/item/item.html",
|
||||||
|
no_cache=1
|
||||||
|
)
|
||||||
|
|
||||||
|
def autoname(self):
|
||||||
|
# use naming series to accomodate items with same name (different item code)
|
||||||
|
from frappe.model.naming import make_autoname
|
||||||
|
|
||||||
|
from erpnext.setup.doctype.naming_series.naming_series import get_default_naming_series
|
||||||
|
|
||||||
|
naming_series = get_default_naming_series("Website Item")
|
||||||
|
if not self.name and naming_series:
|
||||||
|
self.name = make_autoname(naming_series, doc=self)
|
||||||
|
|
||||||
|
def onload(self):
|
||||||
|
super(WebsiteItem, self).onload()
|
||||||
|
|
||||||
|
def validate(self):
|
||||||
|
super(WebsiteItem, self).validate()
|
||||||
|
|
||||||
|
if not self.item_code:
|
||||||
|
frappe.throw(_("Item Code is required"), title=_("Mandatory"))
|
||||||
|
|
||||||
|
self.validate_duplicate_website_item()
|
||||||
|
self.validate_website_image()
|
||||||
|
self.make_thumbnail()
|
||||||
|
self.publish_unpublish_desk_item(publish=True)
|
||||||
|
|
||||||
|
if not self.get("__islocal"):
|
||||||
|
wig = frappe.qb.DocType("Website Item Group")
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(wig)
|
||||||
|
.select(wig.item_group)
|
||||||
|
.where(
|
||||||
|
(wig.parentfield == "website_item_groups")
|
||||||
|
& (wig.parenttype == "Website Item")
|
||||||
|
& (wig.parent == self.name)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
result = query.run(as_list=True)
|
||||||
|
|
||||||
|
self.old_website_item_groups = [x[0] for x in result]
|
||||||
|
|
||||||
|
def on_update(self):
|
||||||
|
invalidate_cache_for_web_item(self)
|
||||||
|
self.update_template_item()
|
||||||
|
|
||||||
|
def on_trash(self):
|
||||||
|
super(WebsiteItem, self).on_trash()
|
||||||
|
delete_item_from_index(self)
|
||||||
|
self.publish_unpublish_desk_item(publish=False)
|
||||||
|
|
||||||
|
def validate_duplicate_website_item(self):
|
||||||
|
existing_web_item = frappe.db.exists("Website Item", {"item_code": self.item_code})
|
||||||
|
if existing_web_item and existing_web_item != self.name:
|
||||||
|
message = _("Website Item already exists against Item {0}").format(frappe.bold(self.item_code))
|
||||||
|
frappe.throw(message, title=_("Already Published"))
|
||||||
|
|
||||||
|
def publish_unpublish_desk_item(self, publish=True):
|
||||||
|
if frappe.db.get_value("Item", self.item_code, "published_in_website") and publish:
|
||||||
|
return # if already published don't publish again
|
||||||
|
frappe.db.set_value("Item", self.item_code, "published_in_website", publish)
|
||||||
|
|
||||||
|
def make_route(self):
|
||||||
|
"""Called from set_route in WebsiteGenerator."""
|
||||||
|
if not self.route:
|
||||||
|
return cstr(frappe.db.get_value('Item Group', self.item_group,
|
||||||
|
'route')) + '/' + self.scrub((self.item_name if self.item_name else self.item_code) + '-' + random_string(5))
|
||||||
|
|
||||||
|
def update_template_item(self):
|
||||||
|
"""Publish Template Item if Variant is published."""
|
||||||
|
if self.variant_of:
|
||||||
|
if self.published:
|
||||||
|
# show template
|
||||||
|
template_item = frappe.get_doc("Item", self.variant_of)
|
||||||
|
|
||||||
|
if not template_item.published_in_website:
|
||||||
|
template_item.flags.ignore_permissions = True
|
||||||
|
make_website_item(template_item)
|
||||||
|
|
||||||
|
def validate_website_image(self):
|
||||||
|
if frappe.flags.in_import:
|
||||||
|
return
|
||||||
|
|
||||||
|
"""Validate if the website image is a public file"""
|
||||||
|
auto_set_website_image = False
|
||||||
|
if not self.website_image and self.image:
|
||||||
|
auto_set_website_image = True
|
||||||
|
self.website_image = self.image
|
||||||
|
|
||||||
|
if not self.website_image:
|
||||||
|
return
|
||||||
|
|
||||||
|
# find if website image url exists as public
|
||||||
|
file_doc = frappe.get_all(
|
||||||
|
"File",
|
||||||
|
filters={
|
||||||
|
"file_url": self.website_image
|
||||||
|
},
|
||||||
|
fields=["name", "is_private"],
|
||||||
|
order_by="is_private asc",
|
||||||
|
limit_page_length=1
|
||||||
|
)
|
||||||
|
|
||||||
|
if file_doc:
|
||||||
|
file_doc = file_doc[0]
|
||||||
|
|
||||||
|
if not file_doc:
|
||||||
|
if not auto_set_website_image:
|
||||||
|
frappe.msgprint(_("Website Image {0} attached to Item {1} cannot be found").format(self.website_image, self.name))
|
||||||
|
|
||||||
|
self.website_image = None
|
||||||
|
|
||||||
|
elif file_doc.is_private:
|
||||||
|
if not auto_set_website_image:
|
||||||
|
frappe.msgprint(_("Website Image should be a public file or website URL"))
|
||||||
|
|
||||||
|
self.website_image = None
|
||||||
|
|
||||||
|
def make_thumbnail(self):
|
||||||
|
"""Make a thumbnail of `website_image`"""
|
||||||
|
if frappe.flags.in_import or frappe.flags.in_migrate:
|
||||||
|
return
|
||||||
|
|
||||||
|
import requests.exceptions
|
||||||
|
|
||||||
|
if not self.is_new() and self.website_image != frappe.db.get_value(self.doctype, self.name, "website_image"):
|
||||||
|
self.thumbnail = None
|
||||||
|
|
||||||
|
if self.website_image and not self.thumbnail:
|
||||||
|
file_doc = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
file_doc = frappe.get_doc("File", {
|
||||||
|
"file_url": self.website_image,
|
||||||
|
"attached_to_doctype": "Website Item",
|
||||||
|
"attached_to_name": self.name
|
||||||
|
})
|
||||||
|
except frappe.DoesNotExistError:
|
||||||
|
pass
|
||||||
|
# cleanup
|
||||||
|
frappe.local.message_log.pop()
|
||||||
|
|
||||||
|
except requests.exceptions.HTTPError:
|
||||||
|
frappe.msgprint(_("Warning: Invalid attachment {0}").format(self.website_image))
|
||||||
|
self.website_image = None
|
||||||
|
|
||||||
|
except requests.exceptions.SSLError:
|
||||||
|
frappe.msgprint(
|
||||||
|
_("Warning: Invalid SSL certificate on attachment {0}").format(self.website_image))
|
||||||
|
self.website_image = None
|
||||||
|
|
||||||
|
# for CSV import
|
||||||
|
if self.website_image and not file_doc:
|
||||||
|
try:
|
||||||
|
file_doc = frappe.get_doc({
|
||||||
|
"doctype": "File",
|
||||||
|
"file_url": self.website_image,
|
||||||
|
"attached_to_doctype": "Website Item",
|
||||||
|
"attached_to_name": self.name
|
||||||
|
}).save()
|
||||||
|
|
||||||
|
except IOError:
|
||||||
|
self.website_image = None
|
||||||
|
|
||||||
|
if file_doc:
|
||||||
|
if not file_doc.thumbnail_url:
|
||||||
|
file_doc.make_thumbnail()
|
||||||
|
|
||||||
|
self.thumbnail = file_doc.thumbnail_url
|
||||||
|
|
||||||
|
def get_context(self, context):
|
||||||
|
context.show_search = True
|
||||||
|
context.search_link = "/search"
|
||||||
|
context.body_class = "product-page"
|
||||||
|
|
||||||
|
context.parents = get_parent_item_groups(self.item_group, from_item=True) # breadcumbs
|
||||||
|
self.attributes = frappe.get_all(
|
||||||
|
"Item Variant Attribute",
|
||||||
|
fields=["attribute", "attribute_value"],
|
||||||
|
filters={"parent": self.item_code}
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.slideshow:
|
||||||
|
context.update(get_slideshow(self))
|
||||||
|
|
||||||
|
self.set_metatags(context)
|
||||||
|
self.set_shopping_cart_data(context)
|
||||||
|
|
||||||
|
settings = context.shopping_cart.cart_settings
|
||||||
|
|
||||||
|
self.get_product_details_section(context)
|
||||||
|
|
||||||
|
if settings.get("enable_reviews"):
|
||||||
|
reviews_data = get_item_reviews(self.name)
|
||||||
|
context.update(reviews_data)
|
||||||
|
context.reviews = context.reviews[:4]
|
||||||
|
|
||||||
|
context.wished = False
|
||||||
|
if frappe.db.exists("Wishlist Item", {"item_code": self.item_code, "parent": frappe.session.user}):
|
||||||
|
context.wished = True
|
||||||
|
|
||||||
|
context.user_is_customer = check_if_user_is_customer()
|
||||||
|
|
||||||
|
context.recommended_items = None
|
||||||
|
if settings and settings.enable_recommendations:
|
||||||
|
context.recommended_items = self.get_recommended_items(settings)
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
def set_selected_attributes(self, variants, context, attribute_values_available):
|
||||||
|
for variant in variants:
|
||||||
|
variant.attributes = frappe.get_all(
|
||||||
|
"Item Variant Attribute",
|
||||||
|
filters={"parent": variant.name},
|
||||||
|
fields=["attribute", "attribute_value as value"])
|
||||||
|
|
||||||
|
# make an attribute-value map for easier access in templates
|
||||||
|
variant.attribute_map = frappe._dict(
|
||||||
|
{attr.attribute : attr.value for attr in variant.attributes}
|
||||||
|
)
|
||||||
|
|
||||||
|
for attr in variant.attributes:
|
||||||
|
values = attribute_values_available.setdefault(attr.attribute, [])
|
||||||
|
if attr.value not in values:
|
||||||
|
values.append(attr.value)
|
||||||
|
|
||||||
|
if variant.name == context.variant.name:
|
||||||
|
context.selected_attributes[attr.attribute] = attr.value
|
||||||
|
|
||||||
|
def set_attribute_values(self, attributes, context, attribute_values_available):
|
||||||
|
for attr in attributes:
|
||||||
|
values = context.attribute_values.setdefault(attr.attribute, [])
|
||||||
|
|
||||||
|
if cint(frappe.db.get_value("Item Attribute", attr.attribute, "numeric_values")):
|
||||||
|
for val in sorted(attribute_values_available.get(attr.attribute, []), key=flt):
|
||||||
|
values.append(val)
|
||||||
|
else:
|
||||||
|
# get list of values defined (for sequence)
|
||||||
|
for attr_value in frappe.db.get_all("Item Attribute Value",
|
||||||
|
fields=["attribute_value"],
|
||||||
|
filters={"parent": attr.attribute}, order_by="idx asc"):
|
||||||
|
|
||||||
|
if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []):
|
||||||
|
values.append(attr_value.attribute_value)
|
||||||
|
|
||||||
|
def set_metatags(self, context):
|
||||||
|
context.metatags = frappe._dict({})
|
||||||
|
|
||||||
|
safe_description = frappe.utils.to_markdown(self.description)
|
||||||
|
|
||||||
|
context.metatags.url = frappe.utils.get_url() + '/' + context.route
|
||||||
|
|
||||||
|
if context.website_image:
|
||||||
|
if context.website_image.startswith('http'):
|
||||||
|
url = context.website_image
|
||||||
|
else:
|
||||||
|
url = frappe.utils.get_url() + context.website_image
|
||||||
|
context.metatags.image = url
|
||||||
|
|
||||||
|
context.metatags.description = safe_description[:300]
|
||||||
|
|
||||||
|
context.metatags.title = self.web_item_name or self.item_name or self.item_code
|
||||||
|
|
||||||
|
context.metatags['og:type'] = 'product'
|
||||||
|
context.metatags['og:site_name'] = 'ERPNext'
|
||||||
|
|
||||||
|
def set_shopping_cart_data(self, context):
|
||||||
|
from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website
|
||||||
|
context.shopping_cart = get_product_info_for_website(self.item_code, skip_quotation_creation=True)
|
||||||
|
|
||||||
|
def copy_specification_from_item_group(self):
|
||||||
|
self.set("website_specifications", [])
|
||||||
|
if self.item_group:
|
||||||
|
for label, desc in frappe.db.get_values("Item Website Specification",
|
||||||
|
{"parent": self.item_group}, ["label", "description"]):
|
||||||
|
row = self.append("website_specifications")
|
||||||
|
row.label = label
|
||||||
|
row.description = desc
|
||||||
|
|
||||||
|
def get_product_details_section(self, context):
|
||||||
|
""" Get section with tabs or website specifications. """
|
||||||
|
context.show_tabs = self.show_tabbed_section
|
||||||
|
if self.show_tabbed_section and (self.tabs or self.website_specifications):
|
||||||
|
context.tabs = self.get_tabs()
|
||||||
|
else:
|
||||||
|
context.website_specifications = self.website_specifications
|
||||||
|
|
||||||
|
def get_tabs(self):
|
||||||
|
tab_values = {}
|
||||||
|
tab_values["tab_1_title"] = "Product Details"
|
||||||
|
tab_values["tab_1_content"] = frappe.render_template(
|
||||||
|
"templates/generators/item/item_specifications.html",
|
||||||
|
{
|
||||||
|
"website_specifications": self.website_specifications,
|
||||||
|
"show_tabs": self.show_tabbed_section
|
||||||
|
})
|
||||||
|
|
||||||
|
for row in self.tabs:
|
||||||
|
tab_values[f"tab_{row.idx + 1}_title"] = _(row.label)
|
||||||
|
tab_values[f"tab_{row.idx + 1}_content"] = row.content
|
||||||
|
|
||||||
|
return tab_values
|
||||||
|
|
||||||
|
def get_recommended_items(self, settings):
|
||||||
|
ri = frappe.qb.DocType("Recommended Items")
|
||||||
|
wi = frappe.qb.DocType("Website Item")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(ri)
|
||||||
|
.join(wi).on(ri.item_code == wi.item_code)
|
||||||
|
.select(
|
||||||
|
ri.item_code, ri.route,
|
||||||
|
ri.website_item_name,
|
||||||
|
ri.website_item_thumbnail
|
||||||
|
).where(
|
||||||
|
(ri.parent == self.name)
|
||||||
|
& (wi.published == 1)
|
||||||
|
).orderby(ri.idx)
|
||||||
|
)
|
||||||
|
items = query.run(as_dict=True)
|
||||||
|
|
||||||
|
if settings.show_price:
|
||||||
|
is_guest = frappe.session.user == "Guest"
|
||||||
|
# Show Price if logged in.
|
||||||
|
# If not logged in and price is hidden for guest, skip price fetch.
|
||||||
|
if is_guest and settings.hide_price_for_guest:
|
||||||
|
return items
|
||||||
|
|
||||||
|
selling_price_list = _set_price_list(settings, None)
|
||||||
|
for item in items:
|
||||||
|
item.price_info = get_price(
|
||||||
|
item.item_code,
|
||||||
|
selling_price_list,
|
||||||
|
settings.default_customer_group,
|
||||||
|
settings.company
|
||||||
|
)
|
||||||
|
|
||||||
|
return items
|
||||||
|
|
||||||
|
def invalidate_cache_for_web_item(doc):
|
||||||
|
"""Invalidate Website Item Group cache and rebuild ItemVariantsCacheManager."""
|
||||||
|
from erpnext.stock.doctype.item.item import invalidate_item_variants_cache_for_website
|
||||||
|
|
||||||
|
invalidate_cache_for(doc, doc.item_group)
|
||||||
|
|
||||||
|
website_item_groups = list(set((doc.get("old_website_item_groups") or [])
|
||||||
|
+ [d.item_group for d in doc.get({"doctype": "Website Item Group"}) if d.item_group]))
|
||||||
|
|
||||||
|
for item_group in website_item_groups:
|
||||||
|
invalidate_cache_for(doc, item_group)
|
||||||
|
|
||||||
|
# Update Search Cache
|
||||||
|
update_index_for_item(doc)
|
||||||
|
|
||||||
|
invalidate_item_variants_cache_for_website(doc)
|
||||||
|
|
||||||
|
def on_doctype_update():
|
||||||
|
# since route is a Text column, it needs a length for indexing
|
||||||
|
frappe.db.add_index("Website Item", ["route(500)"])
|
||||||
|
|
||||||
|
frappe.db.add_index("Website Item", ["item_group"])
|
||||||
|
frappe.db.add_index("Website Item", ["brand"])
|
||||||
|
|
||||||
|
def check_if_user_is_customer(user=None):
|
||||||
|
from frappe.contacts.doctype.contact.contact import get_contact_name
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
user = frappe.session.user
|
||||||
|
|
||||||
|
contact_name = get_contact_name(user)
|
||||||
|
customer = None
|
||||||
|
|
||||||
|
if contact_name:
|
||||||
|
contact = frappe.get_doc('Contact', contact_name)
|
||||||
|
for link in contact.links:
|
||||||
|
if link.link_doctype == "Customer":
|
||||||
|
customer = link.link_name
|
||||||
|
break
|
||||||
|
|
||||||
|
return True if customer else False
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def make_website_item(doc, save=True):
|
||||||
|
if not doc:
|
||||||
|
return
|
||||||
|
|
||||||
|
if isinstance(doc, str):
|
||||||
|
doc = json.loads(doc)
|
||||||
|
|
||||||
|
if frappe.db.exists("Website Item", {"item_code": doc.get("item_code")}):
|
||||||
|
message = _("Website Item already exists against {0}").format(frappe.bold(doc.get("item_code")))
|
||||||
|
frappe.throw(message, title=_("Already Published"))
|
||||||
|
|
||||||
|
website_item = frappe.new_doc("Website Item")
|
||||||
|
website_item.web_item_name = doc.get("item_name")
|
||||||
|
|
||||||
|
fields_to_map = ["item_code", "item_name", "item_group", "stock_uom", "brand", "image",
|
||||||
|
"has_variants", "variant_of", "description"]
|
||||||
|
for field in fields_to_map:
|
||||||
|
website_item.update({field: doc.get(field)})
|
||||||
|
|
||||||
|
if not save:
|
||||||
|
return website_item
|
||||||
|
|
||||||
|
website_item.save()
|
||||||
|
|
||||||
|
# Add to search cache
|
||||||
|
insert_item_to_index(website_item)
|
||||||
|
|
||||||
|
return [website_item.name, website_item.web_item_name]
|
20
erpnext/e_commerce/doctype/website_item/website_item_list.js
Normal file
20
erpnext/e_commerce/doctype/website_item/website_item_list.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
frappe.listview_settings['Website Item'] = {
|
||||||
|
add_fields: ["item_name", "web_item_name", "published", "image", "has_variants", "variant_of"],
|
||||||
|
filters: [["published", "=", "1"]],
|
||||||
|
|
||||||
|
get_indicator: function(doc) {
|
||||||
|
if (doc.has_variants && doc.published) {
|
||||||
|
return [__("Template"), "orange", "has_variants,=,Yes|published,=,1"];
|
||||||
|
} else if (doc.has_variants && !doc.published) {
|
||||||
|
return [__("Template"), "grey", "has_variants,=,Yes|published,=,0"];
|
||||||
|
} else if (doc.variant_of && doc.published) {
|
||||||
|
return [__("Variant"), "blue", "published,=,1|variant_of,=," + doc.variant_of];
|
||||||
|
} else if (doc.variant_of && !doc.published) {
|
||||||
|
return [__("Variant"), "grey", "published,=,0|variant_of,=," + doc.variant_of];
|
||||||
|
} else if (doc.published) {
|
||||||
|
return [__("Published"), "green", "published,=,1"];
|
||||||
|
} else {
|
||||||
|
return [__("Not Published"), "grey", "published,=,0"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2021-03-18 20:32:15.321402",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"label",
|
||||||
|
"content"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "label",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Label"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "content",
|
||||||
|
"fieldtype": "HTML Editor",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Content"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-03-18 20:35:26.991192",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "E-commerce",
|
||||||
|
"name": "Website Item Tabbed Section",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class WebsiteItemTabbedSection(Document):
|
||||||
|
pass
|
43
erpnext/e_commerce/doctype/website_offer/website_offer.json
Normal file
43
erpnext/e_commerce/doctype/website_offer/website_offer.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2021-04-21 13:37:14.162162",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"offer_title",
|
||||||
|
"offer_subtitle",
|
||||||
|
"offer_details"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "offer_title",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Offer Title"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "offer_subtitle",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Offer Subtitle"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "offer_details",
|
||||||
|
"fieldtype": "Text Editor",
|
||||||
|
"label": "Offer Details"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-04-21 13:56:04.660331",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "E-commerce",
|
||||||
|
"name": "Website Offer",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
14
erpnext/e_commerce/doctype/website_offer/website_offer.py
Normal file
14
erpnext/e_commerce/doctype/website_offer/website_offer.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class WebsiteOffer(Document):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
def get_offer_details(offer_id):
|
||||||
|
return frappe.db.get_value('Website Offer', {'name': offer_id}, ['offer_details'])
|
0
erpnext/e_commerce/doctype/wishlist/__init__.py
Normal file
0
erpnext/e_commerce/doctype/wishlist/__init__.py
Normal file
102
erpnext/e_commerce/doctype/wishlist/test_wishlist.py
Normal file
102
erpnext/e_commerce/doctype/wishlist/test_wishlist.py
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.core.doctype.user_permission.test_user_permission import create_user
|
||||||
|
|
||||||
|
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
|
||||||
|
from erpnext.e_commerce.doctype.wishlist.wishlist import add_to_wishlist, remove_from_wishlist
|
||||||
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
|
||||||
|
|
||||||
|
class TestWishlist(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
item = make_item("Test Phone Series X")
|
||||||
|
if not frappe.db.exists("Website Item", {"item_code": "Test Phone Series X"}):
|
||||||
|
make_website_item(item, save=True)
|
||||||
|
|
||||||
|
item = make_item("Test Phone Series Y")
|
||||||
|
if not frappe.db.exists("Website Item", {"item_code": "Test Phone Series Y"}):
|
||||||
|
make_website_item(item, save=True)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
frappe.get_cached_doc("Website Item", {"item_code": "Test Phone Series X"}).delete()
|
||||||
|
frappe.get_cached_doc("Website Item", {"item_code": "Test Phone Series Y"}).delete()
|
||||||
|
frappe.get_cached_doc("Item", "Test Phone Series X").delete()
|
||||||
|
frappe.get_cached_doc("Item", "Test Phone Series Y").delete()
|
||||||
|
|
||||||
|
def test_add_remove_items_in_wishlist(self):
|
||||||
|
"Check if items are added and removed from user's wishlist."
|
||||||
|
# add first item
|
||||||
|
add_to_wishlist("Test Phone Series X")
|
||||||
|
|
||||||
|
# check if wishlist was created and item was added
|
||||||
|
self.assertTrue(frappe.db.exists("Wishlist", {"user": frappe.session.user}))
|
||||||
|
self.assertTrue(frappe.db.exists("Wishlist Item", {"item_code": "Test Phone Series X", "parent": frappe.session.user}))
|
||||||
|
|
||||||
|
# add second item to wishlist
|
||||||
|
add_to_wishlist("Test Phone Series Y")
|
||||||
|
wishlist_length = frappe.db.get_value(
|
||||||
|
"Wishlist Item",
|
||||||
|
{"parent": frappe.session.user},
|
||||||
|
"count(*)"
|
||||||
|
)
|
||||||
|
self.assertEqual(wishlist_length, 2)
|
||||||
|
|
||||||
|
remove_from_wishlist("Test Phone Series X")
|
||||||
|
remove_from_wishlist("Test Phone Series Y")
|
||||||
|
|
||||||
|
wishlist_length = frappe.db.get_value(
|
||||||
|
"Wishlist Item",
|
||||||
|
{"parent": frappe.session.user},
|
||||||
|
"count(*)"
|
||||||
|
)
|
||||||
|
self.assertIsNone(frappe.db.exists("Wishlist Item", {"parent": frappe.session.user}))
|
||||||
|
self.assertEqual(wishlist_length, 0)
|
||||||
|
|
||||||
|
# tear down
|
||||||
|
frappe.get_doc("Wishlist", {"user": frappe.session.user}).delete()
|
||||||
|
|
||||||
|
def test_add_remove_in_wishlist_multiple_users(self):
|
||||||
|
"Check if items are added and removed from the correct user's wishlist."
|
||||||
|
test_user = create_user("test_reviewer@example.com", "Customer")
|
||||||
|
test_user_1 = create_user("test_reviewer_1@example.com", "Customer")
|
||||||
|
|
||||||
|
# add to wishlist for first user
|
||||||
|
frappe.set_user(test_user.name)
|
||||||
|
add_to_wishlist("Test Phone Series X")
|
||||||
|
|
||||||
|
# add to wishlist for second user
|
||||||
|
frappe.set_user(test_user_1.name)
|
||||||
|
add_to_wishlist("Test Phone Series X")
|
||||||
|
|
||||||
|
# check wishlist and its content for users
|
||||||
|
self.assertTrue(frappe.db.exists("Wishlist", {"user": test_user.name}))
|
||||||
|
self.assertTrue(frappe.db.exists("Wishlist Item",
|
||||||
|
{"item_code": "Test Phone Series X", "parent": test_user.name}))
|
||||||
|
|
||||||
|
self.assertTrue(frappe.db.exists("Wishlist", {"user": test_user_1.name}))
|
||||||
|
self.assertTrue(frappe.db.exists("Wishlist Item",
|
||||||
|
{"item_code": "Test Phone Series X", "parent": test_user_1.name}))
|
||||||
|
|
||||||
|
# remove item for second user
|
||||||
|
remove_from_wishlist("Test Phone Series X")
|
||||||
|
|
||||||
|
# make sure item was removed for second user and not first
|
||||||
|
self.assertFalse(frappe.db.exists("Wishlist Item",
|
||||||
|
{"item_code": "Test Phone Series X", "parent": test_user_1.name}))
|
||||||
|
self.assertTrue(frappe.db.exists("Wishlist Item",
|
||||||
|
{"item_code": "Test Phone Series X", "parent": test_user.name}))
|
||||||
|
|
||||||
|
# remove item for first user
|
||||||
|
frappe.set_user(test_user.name)
|
||||||
|
remove_from_wishlist("Test Phone Series X")
|
||||||
|
self.assertFalse(frappe.db.exists("Wishlist Item",
|
||||||
|
{"item_code": "Test Phone Series X", "parent": test_user.name}))
|
||||||
|
|
||||||
|
# tear down
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
frappe.get_doc("Wishlist", {"user": test_user.name}).delete()
|
||||||
|
frappe.get_doc("Wishlist", {"user": test_user_1.name}).delete()
|
8
erpnext/e_commerce/doctype/wishlist/wishlist.js
Normal file
8
erpnext/e_commerce/doctype/wishlist/wishlist.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.ui.form.on('Wishlist', {
|
||||||
|
// refresh: function(frm) {
|
||||||
|
|
||||||
|
// }
|
||||||
|
});
|
65
erpnext/e_commerce/doctype/wishlist/wishlist.json
Normal file
65
erpnext/e_commerce/doctype/wishlist/wishlist.json
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"autoname": "field:user",
|
||||||
|
"creation": "2021-03-10 18:52:28.769126",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"user",
|
||||||
|
"section_break_2",
|
||||||
|
"items"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "user",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "User",
|
||||||
|
"options": "User",
|
||||||
|
"reqd": 1,
|
||||||
|
"unique": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_2",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "items",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Items",
|
||||||
|
"options": "Wishlist Item"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"in_create": 1,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-07-08 13:11:21.693956",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "E-commerce",
|
||||||
|
"name": "Wishlist",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Website Manager",
|
||||||
|
"share": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user