Merge branch 'develop' into gross-profit-percentage
diff --git a/README.md b/README.md
index 1105a97..9609353 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,7 @@
[](https://github.com/frappe/erpnext/actions/workflows/server-tests.yml)
[](https://www.codetriage.com/frappe/erpnext)
[](https://codecov.io/gh/frappe/erpnext)
+[](https://hub.docker.com/r/frappe/erpnext-worker)
[https://erpnext.com](https://erpnext.com)
diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js
index 9dd882a..750e129 100644
--- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js
+++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js
@@ -8,7 +8,7 @@
}
let help_content =
- `<table class="table table-bordered" style="background-color: #f9f9f9;">
+ `<table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
<tr><td>
<p>
<i class="fa fa-hand-right"></i>
diff --git a/erpnext/accounts/doctype/loyalty_program/loyalty_program.js b/erpnext/accounts/doctype/loyalty_program/loyalty_program.js
index f90f867..6951b2a 100644
--- a/erpnext/accounts/doctype/loyalty_program/loyalty_program.js
+++ b/erpnext/accounts/doctype/loyalty_program/loyalty_program.js
@@ -6,7 +6,7 @@
frappe.ui.form.on('Loyalty Program', {
setup: function(frm) {
var help_content =
- `<table class="table table-bordered" style="background-color: #f9f9f9;">
+ `<table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
<tr><td>
<h4>
<i class="fa fa-hand-right"></i>
diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.js b/erpnext/accounts/doctype/pricing_rule/pricing_rule.js
index d79ad5f..8267582 100644
--- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.js
+++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.js
@@ -38,7 +38,7 @@
refresh: function(frm) {
var help_content =
- `<table class="table table-bordered" style="background-color: #f9f9f9;">
+ `<table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
<tr><td>
<h4>
<i class="fa fa-hand-right"></i>
diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py
index e3fac07..5fbe93e 100644
--- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py
+++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py
@@ -20,6 +20,9 @@
product_discount_fields = ['free_item', 'free_qty', 'free_item_uom',
'free_item_rate', 'same_item', 'is_recursive', 'apply_multiple_pricing_rules']
+class TransactionExists(frappe.ValidationError):
+ pass
+
class PromotionalScheme(Document):
def validate(self):
if not self.selling and not self.buying:
@@ -28,6 +31,40 @@
or self.product_discount_slabs):
frappe.throw(_("Price or product discount slabs are required"))
+ self.validate_applicable_for()
+ self.validate_pricing_rules()
+
+ def validate_applicable_for(self):
+ if self.applicable_for:
+ applicable_for = frappe.scrub(self.applicable_for)
+
+ if not self.get(applicable_for):
+ msg = (f'The field {frappe.bold(self.applicable_for)} is required')
+ frappe.throw(_(msg))
+
+ def validate_pricing_rules(self):
+ if self.is_new():
+ return
+
+ transaction_exists = False
+ docnames = []
+
+ # If user has changed applicable for
+ if self._doc_before_save.applicable_for == self.applicable_for:
+ return
+
+ docnames = frappe.get_all('Pricing Rule',
+ filters= {'promotional_scheme': self.name})
+
+ for docname in docnames:
+ if frappe.db.exists('Pricing Rule Detail',
+ {'pricing_rule': docname.name, 'docstatus': ('<', 2)}):
+ raise_for_transaction_exists(self.name)
+
+ if docnames and not transaction_exists:
+ for docname in docnames:
+ frappe.delete_doc('Pricing Rule', docname.name)
+
def on_update(self):
pricing_rules = frappe.get_all(
'Pricing Rule',
@@ -67,6 +104,13 @@
{'promotional_scheme': self.name}):
frappe.delete_doc('Pricing Rule', rule.name)
+def raise_for_transaction_exists(name):
+ msg = (f"""You can't change the {frappe.bold(_('Applicable For'))}
+ because transactions are present against the Promotional Scheme {frappe.bold(name)}. """)
+ msg += 'Kindly disable this Promotional Scheme and create new for new Applicable For.'
+
+ frappe.throw(_(msg), TransactionExists)
+
def get_pricing_rules(doc, rules=None):
if rules is None:
rules = {}
@@ -84,45 +128,59 @@
new_doc = []
args = get_args_for_pricing_rule(doc)
applicable_for = frappe.scrub(doc.get('applicable_for'))
+
for idx, d in enumerate(doc.get(child_doc)):
if d.name in rules:
- for applicable_for_value in args.get(applicable_for):
- temp_args = args.copy()
- docname = frappe.get_all(
- 'Pricing Rule',
- fields = ["promotional_scheme_id", "name", applicable_for],
- filters = {
- 'promotional_scheme_id': d.name,
- applicable_for: applicable_for_value
- }
- )
-
- if docname:
- pr = frappe.get_doc('Pricing Rule', docname[0].get('name'))
- temp_args[applicable_for] = applicable_for_value
- pr = set_args(temp_args, pr, doc, child_doc, discount_fields, d)
- else:
- pr = frappe.new_doc("Pricing Rule")
- pr.title = doc.name
- temp_args[applicable_for] = applicable_for_value
- pr = set_args(temp_args, pr, doc, child_doc, discount_fields, d)
-
+ if not args.get(applicable_for):
+ docname = get_pricing_rule_docname(d)
+ pr = prepare_pricing_rule(args, doc, child_doc, discount_fields, d, docname)
new_doc.append(pr)
+ else:
+ for applicable_for_value in args.get(applicable_for):
+ docname = get_pricing_rule_docname(d, applicable_for, applicable_for_value)
+ pr = prepare_pricing_rule(args, doc, child_doc, discount_fields,
+ d, docname, applicable_for, applicable_for_value)
+ new_doc.append(pr)
- else:
+ elif args.get(applicable_for):
applicable_for_values = args.get(applicable_for) or []
for applicable_for_value in applicable_for_values:
- pr = frappe.new_doc("Pricing Rule")
- pr.title = doc.name
- temp_args = args.copy()
- temp_args[applicable_for] = applicable_for_value
- pr = set_args(temp_args, pr, doc, child_doc, discount_fields, d)
+ pr = prepare_pricing_rule(args, doc, child_doc, discount_fields,
+ d, applicable_for=applicable_for, value= applicable_for_value)
+
new_doc.append(pr)
+ else:
+ pr = prepare_pricing_rule(args, doc, child_doc, discount_fields, d)
+ new_doc.append(pr)
return new_doc
+def get_pricing_rule_docname(row: dict, applicable_for: str = None, applicable_for_value: str = None) -> str:
+ fields = ['promotional_scheme_id', 'name']
+ filters = {
+ 'promotional_scheme_id': row.name
+ }
+ if applicable_for:
+ fields.append(applicable_for)
+ filters[applicable_for] = applicable_for_value
+ docname = frappe.get_all('Pricing Rule', fields = fields, filters = filters)
+ return docname[0].name if docname else ''
+
+def prepare_pricing_rule(args, doc, child_doc, discount_fields, d, docname=None, applicable_for=None, value=None):
+ if docname:
+ pr = frappe.get_doc("Pricing Rule", docname)
+ else:
+ pr = frappe.new_doc("Pricing Rule")
+
+ pr.title = doc.name
+ temp_args = args.copy()
+
+ if value:
+ temp_args[applicable_for] = value
+
+ return set_args(temp_args, pr, doc, child_doc, discount_fields, d)
def set_args(args, pr, doc, child_doc, discount_fields, child_doc_fields):
pr.update(args)
@@ -145,6 +203,7 @@
apply_on: d.get(apply_on),
'uom': d.uom
})
+
return pr
def get_args_for_pricing_rule(doc):
diff --git a/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py
index e1852ae..49192a4 100644
--- a/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py
+++ b/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py
@@ -5,10 +5,17 @@
import frappe
+from erpnext.accounts.doctype.promotional_scheme.promotional_scheme import TransactionExists
+from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
+
class TestPromotionalScheme(unittest.TestCase):
+ def setUp(self):
+ if frappe.db.exists('Promotional Scheme', '_Test Scheme'):
+ frappe.delete_doc('Promotional Scheme', '_Test Scheme')
+
def test_promotional_scheme(self):
- ps = make_promotional_scheme()
+ ps = make_promotional_scheme(applicable_for='Customer', customer='_Test Customer')
price_rules = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name", "creation"],
filters = {'promotional_scheme': ps.name})
self.assertTrue(len(price_rules),1)
@@ -39,22 +46,62 @@
filters = {'promotional_scheme': ps.name})
self.assertEqual(price_rules, [])
-def make_promotional_scheme():
+ def test_promotional_scheme_without_applicable_for(self):
+ ps = make_promotional_scheme()
+ price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name})
+
+ self.assertTrue(len(price_rules), 1)
+ frappe.delete_doc('Promotional Scheme', ps.name)
+
+ price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name})
+ self.assertEqual(price_rules, [])
+
+ def test_change_applicable_for_in_promotional_scheme(self):
+ ps = make_promotional_scheme()
+ price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name})
+ self.assertTrue(len(price_rules), 1)
+
+ so = make_sales_order(qty=5, currency='USD', do_not_save=True)
+ so.set_missing_values()
+ so.save()
+ self.assertEqual(price_rules[0].name, so.pricing_rules[0].pricing_rule)
+
+ ps.applicable_for = 'Customer'
+ ps.append('customer', {
+ 'customer': '_Test Customer'
+ })
+
+ self.assertRaises(TransactionExists, ps.save)
+
+ frappe.delete_doc('Sales Order', so.name)
+ frappe.delete_doc('Promotional Scheme', ps.name)
+ price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name})
+ self.assertEqual(price_rules, [])
+
+def make_promotional_scheme(**args):
+ args = frappe._dict(args)
+
ps = frappe.new_doc('Promotional Scheme')
ps.name = '_Test Scheme'
ps.append('items',{
'item_code': '_Test Item'
})
+
ps.selling = 1
ps.append('price_discount_slabs',{
'min_qty': 4,
+ 'validate_applied_rule': 0,
'discount_percentage': 20,
'rule_description': 'Test'
})
- ps.applicable_for = 'Customer'
- ps.append('customer',{
- 'customer': "_Test Customer"
- })
+
+ ps.company = '_Test Company'
+ if args.applicable_for:
+ ps.applicable_for = args.applicable_for
+ ps.append(frappe.scrub(args.applicable_for), {
+ frappe.scrub(args.applicable_for): args.get(frappe.scrub(args.applicable_for))
+ })
+
ps.save()
return ps
diff --git a/erpnext/accounts/doctype/promotional_scheme_price_discount/promotional_scheme_price_discount.json b/erpnext/accounts/doctype/promotional_scheme_price_discount/promotional_scheme_price_discount.json
index a70d5c9..aa3696d 100644
--- a/erpnext/accounts/doctype/promotional_scheme_price_discount/promotional_scheme_price_discount.json
+++ b/erpnext/accounts/doctype/promotional_scheme_price_discount/promotional_scheme_price_discount.json
@@ -136,7 +136,7 @@
"label": "Threshold for Suggestion"
},
{
- "default": "1",
+ "default": "0",
"fieldname": "validate_applied_rule",
"fieldtype": "Check",
"label": "Validate Applied Rule"
@@ -169,7 +169,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-08-19 15:49:29.598727",
+ "modified": "2021-11-16 00:25:33.843996",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Promotional Scheme Price Discount",
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js
index 2bd02da..b171086 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.js
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js
@@ -105,7 +105,7 @@
}
frm.trigger("material_requirement");
- const projected_qty_formula = ` <table class="table table-bordered" style="background-color: #f9f9f9;">
+ const projected_qty_formula = ` <table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
<tr><td style="padding-left:25px">
<div>
<h3 style="text-decoration: underline;">
diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js
index 433f78a..9c1a809 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js
+++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js
@@ -35,7 +35,7 @@
refresh() {
var help_content =
`<br><br>
- <table class="table table-bordered" style="background-color: #f9f9f9;">
+ <table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
<tr><td>
<h4>
<i class="fa fa-hand-right"></i>
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index d9d1957..38291d1 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -342,7 +342,7 @@
is_stock_reco = sle.voucher_type == "Stock Reconciliation"
msg = None
- if sr and (actual_qty < 0 or is_stock_reco) and sr.warehouse != sle.warehouse:
+ if sr and (actual_qty < 0 or is_stock_reco) and (sr.warehouse and sr.warehouse != sle.warehouse):
# receipt(inward) is being cancelled
msg = _("Cannot cancel {0} {1} as Serial No {2} does not belong to the warehouse {3}").format(
sle.voucher_type, doc_link, sr_link, frappe.bold(sle.warehouse))
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index de89b2b..48e339a 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -399,6 +399,34 @@
, do_not_submit=True)
self.assertRaises(frappe.ValidationError, sr.submit)
+ def test_serial_no_cancellation(self):
+
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+ item = create_item("Stock-Reco-Serial-Item-9", is_stock_item=1)
+ if not item.has_serial_no:
+ item.has_serial_no = 1
+ item.serial_no_series = "SRS9.####"
+ item.save()
+
+ item_code = item.name
+ warehouse = "_Test Warehouse - _TC"
+
+ se1 = make_stock_entry(item_code=item_code, target=warehouse, qty=10, basic_rate=700)
+
+ serial_nos = get_serial_nos(se1.items[0].serial_no)
+ # reduce 1 item
+ serial_nos.pop()
+ new_serial_nos = "\n".join(serial_nos)
+
+ sr = create_stock_reconciliation(item_code=item.name, warehouse=warehouse, serial_no=new_serial_nos, qty=9)
+ sr.cancel()
+
+ active_sr_no = frappe.get_all("Serial No",
+ filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"})
+
+ self.assertEqual(len(active_sr_no), 10)
+
+
def create_batch_item_with_batch(item_name, batch_id):
batch_item_doc = create_item(item_name, is_stock_item=1)
if not batch_item_doc.has_batch_no: