Merge branch 'version-13-hotfix' into mergify/bp/version-13-hotfix/pr-29426

This commit is contained in:
Deepesh Garg
2022-01-30 17:30:10 +05:30
committed by GitHub
20 changed files with 133 additions and 40 deletions

View File

@@ -28,6 +28,7 @@ ignore =
B007,
B950,
W191,
E124, # closing bracket, irritating while writing QB code
max-line-length = 200
exclude=.github/helper/semgrep_rules

View File

@@ -71,7 +71,8 @@ class ShippingRule(Document):
if doc.currency != doc.company_currency:
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):
for condition in self.get("conditions"):

View File

@@ -77,17 +77,17 @@ class StockController(AccountsController):
.format(d.idx, get_link_to_form("Batch", d.get("batch_no"))))
def clean_serial_nos(self):
from erpnext.stock.doctype.serial_no.serial_no import clean_serial_no_string
for row in self.get("items"):
if hasattr(row, "serial_no") and row.serial_no:
# replace commas by linefeed
row.serial_no = row.serial_no.replace(",", "\n")
# remove extra whitespace and store one serial no on each line
row.serial_no = clean_serial_no_string(row.serial_no)
# strip preceeding and succeeding spaces for each SN
# (SN could have valid spaces in between e.g. SN - 123 - 2021)
serial_no_list = row.serial_no.split("\n")
serial_no_list = [sn.strip() for sn in serial_no_list]
row.serial_no = "\n".join(serial_no_list)
for row in self.get('packed_items') or []:
if hasattr(row, "serial_no") and row.serial_no:
# remove extra whitespace and store one serial no on each line
row.serial_no = clean_serial_no_string(row.serial_no)
def get_gl_entries(self, warehouse_account=None, default_expense_account=None,
default_cost_center=None):

View File

@@ -99,7 +99,6 @@
"search_index": 1
},
{
"fetch_from": "inpatient_record.patient",
"fieldname": "patient",
"fieldtype": "Link",
"ignore_user_permissions": 1,
@@ -559,7 +558,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2020-11-30 11:04:17.195848",
"modified": "2022-01-20 12:37:07.943153",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Lab Test",

View File

@@ -337,9 +337,13 @@ let check_and_set_availability = function(frm) {
});
d.fields_dict['department'].df.onchange = () => {
d.set_values({
'practitioner': ''
});
if (d.get_value('department') == frm.doc.department) {
d.set_value('practitioner', frm.doc.practitioner);
} else {
d.set_value('practitioner', '');
d.fields_dict.available_slots.html('');
d.get_primary_btn().attr('disabled', true);
}
let department = d.get_value('department');
if (department) {
d.fields_dict.practitioner.get_query = function() {
@@ -426,7 +430,8 @@ let check_and_set_availability = function(frm) {
slot_details.forEach((slot_info) => {
slot_html += `<div class="slot-info">
<span> <b> ${__('Practitioner Schedule:')} </b> ${slot_info.slot_name} </span><br>
<span> <b> ${slot_info.practitioner_name} </b> </span><br>
<span> <b> ${__('Schedule:')} </b> ${slot_info.slot_name} </span><br>
<span> <b> ${__('Service Unit:')} </b> ${slot_info.service_unit} </span>`;
if (slot_info.service_unit_capacity) {

View File

@@ -388,7 +388,8 @@ def get_available_slots(practitioner_doc, date):
fields=['name', 'appointment_time', 'duration', 'status'])
slot_details.append({'slot_name': slot_name, 'service_unit': schedule_entry.service_unit, 'avail_slot': available_slots,
'appointments': appointments, 'allow_overlap': allow_overlap, 'service_unit_capacity': service_unit_capacity})
'appointments': appointments, 'allow_overlap': allow_overlap, 'service_unit_capacity': service_unit_capacity,
'practitioner_name': practitioner_doc.practitioner_name})
return slot_details

View File

@@ -66,7 +66,6 @@
"search_index": 1
},
{
"fetch_from": "inpatient_record.patient",
"fieldname": "patient",
"fieldtype": "Link",
"hide_days": 1,
@@ -224,7 +223,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2020-07-30 16:53:13.076104",
"modified": "2022-01-20 12:38:55.382621",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Sample Collection",

View File

@@ -51,7 +51,6 @@
"read_only": 1
},
{
"fetch_from": "inpatient_record.patient",
"fieldname": "patient",
"fieldtype": "Link",
"ignore_user_permissions": 1,
@@ -259,7 +258,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2020-05-17 22:23:24.632286",
"modified": "2022-01-20 12:30:07.515185",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Vital Signs",

View File

@@ -68,12 +68,18 @@ class Employee(NestedSet):
self.employee_name = ' '.join(filter(lambda x: x, [self.first_name, self.middle_name, self.last_name]))
def validate_user_details(self):
data = frappe.db.get_value('User',
self.user_id, ['enabled', 'user_image'], as_dict=1)
if data.get("user_image") and self.image == '':
self.image = data.get("user_image")
self.validate_for_enabled_user_id(data.get("enabled", 0))
self.validate_duplicate_user_id()
if self.user_id:
data = frappe.db.get_value("User",
self.user_id, ["enabled", "user_image"], as_dict=1)
if not data:
self.user_id = None
return
if data.get("user_image") and self.image == "":
self.image = data.get("user_image")
self.validate_for_enabled_user_id(data.get("enabled", 0))
self.validate_duplicate_user_id()
def update_nsm_model(self):
frappe.utils.nestedset.update_nsm(self)

View File

@@ -149,6 +149,7 @@ class BOM(WebsiteGenerator):
self.set_bom_material_details()
self.set_bom_scrap_items_detail()
self.validate_materials()
self.validate_transfer_against()
self.set_routing_operations()
self.validate_operations()
self.calculate_cost()
@@ -682,6 +683,12 @@ class BOM(WebsiteGenerator):
if act_pbom and act_pbom[0][0]:
frappe.throw(_("Cannot deactivate or cancel BOM as it is linked with other BOMs"))
def validate_transfer_against(self):
if not self.with_operations:
self.transfer_material_against = "Work Order"
if not self.transfer_material_against and not self.is_new():
frappe.throw(_("Setting {} is required").format(self.meta.get_label("transfer_material_against")), title=_("Missing value"))
def set_routing_operations(self):
if self.routing and self.with_operations and not self.operations:
self.get_routing()

View File

@@ -356,6 +356,36 @@ class TestBOM(ERPNextTestCase):
self.assertTrue(0 < len(filtered) <= 3, msg="Item filtering showing excessive results")
def test_valid_transfer_defaults(self):
bom_with_op = frappe.db.get_value("BOM", {"item": "_Test FG Item 2", "with_operations": 1, "is_active": 1})
bom = frappe.copy_doc(frappe.get_doc("BOM", bom_with_op), ignore_no_copy=False)
# test defaults
bom.docstatus = 0
bom.transfer_material_against = None
bom.insert()
self.assertEqual(bom.transfer_material_against, "Work Order")
bom.reload()
bom.transfer_material_against = None
with self.assertRaises(frappe.ValidationError):
bom.save()
bom.reload()
# test saner default
bom.transfer_material_against = "Job Card"
bom.with_operations = 0
bom.save()
self.assertEqual(bom.transfer_material_against, "Work Order")
# test no value on existing doc
bom.transfer_material_against = None
bom.with_operations = 0
bom.save()
self.assertEqual(bom.transfer_material_against, "Work Order")
bom.delete()
def get_default_bom(item_code="_Test FG Item 2"):
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})

View File

@@ -31,6 +31,7 @@ from erpnext.stock.doctype.batch.batch import make_batch
from erpnext.stock.doctype.item.item import get_item_defaults, validate_end_of_life
from erpnext.stock.doctype.serial_no.serial_no import (
auto_make_serial_nos,
clean_serial_no_string,
get_auto_serial_nos,
get_serial_nos,
)
@@ -65,6 +66,7 @@ class WorkOrder(Document):
self.validate_warehouse_belongs_to_company()
self.calculate_operating_cost()
self.validate_qty()
self.validate_transfer_against()
self.validate_operation_time()
self.status = self.get_status()
@@ -72,6 +74,7 @@ class WorkOrder(Document):
self.set_required_items(reset_only_qty = len(self.get("required_items")))
def validate_sales_order(self):
if self.sales_order:
self.check_sales_order_on_hold_or_close()
@@ -356,6 +359,7 @@ class WorkOrder(Document):
frappe.delete_doc("Batch", row.name)
def make_serial_nos(self, args):
self.serial_no = clean_serial_no_string(self.serial_no)
serial_no_series = frappe.get_cached_value("Item", self.production_item, "serial_no_series")
if serial_no_series:
self.serial_no = get_auto_serial_nos(serial_no_series, self.qty)
@@ -621,6 +625,16 @@ class WorkOrder(Document):
if not self.qty > 0:
frappe.throw(_("Quantity to Manufacture must be greater than 0."))
def validate_transfer_against(self):
if not self.docstatus == 1:
# let user configure operations until they're ready to submit
return
if not self.operations:
self.transfer_material_against = "Work Order"
if not self.transfer_material_against:
frappe.throw(_("Setting {} is required").format(self.meta.get_label("transfer_material_against")), title=_("Missing value"))
def validate_operation_time(self):
for d in self.operations:
if not d.time_in_mins > 0:

View File

@@ -345,3 +345,4 @@ erpnext.patches.v13_0.agriculture_deprecation_warning
erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit
erpnext.patches.v13_0.hospitality_deprecation_warning
erpnext.patches.v13_0.delete_bank_reconciliation_detail
erpnext.patches.v13_0.update_sane_transfer_against

View File

@@ -0,0 +1,11 @@
import frappe
def execute():
bom = frappe.qb.DocType("BOM")
(frappe.qb
.update(bom)
.set(bom.transfer_material_against, "Work Order")
.where(bom.with_operations == 0)
).run()

View File

@@ -590,6 +590,6 @@ function check_can_calculate_pending_qty(me) {
&& doc.fg_completed_qty
&& erpnext.stock.bom
&& erpnext.stock.bom.name === doc.bom_no;
const itemChecks = !!item;
const itemChecks = !!item && !item.allow_alternative_item;
return docChecks && itemChecks;
}

View File

@@ -80,7 +80,7 @@ def get_data(conditions, filters):
and so.docstatus = 1
{conditions}
GROUP BY soi.name
ORDER BY so.transaction_date ASC
ORDER BY so.transaction_date ASC, soi.item_code ASC
""".format(conditions=conditions), filters, as_dict=1)
return data

View File

@@ -219,18 +219,20 @@ class Item(Document):
self.item_code))
def add_default_uom_in_conversion_factor_table(self):
uom_conv_list = [d.uom for d in self.get("uoms")]
if self.stock_uom not in uom_conv_list:
ch = self.append('uoms', {})
ch.uom = self.stock_uom
ch.conversion_factor = 1
if not self.is_new() and self.has_value_changed("stock_uom"):
self.uoms = []
frappe.msgprint(
_("Successfully changed Stock UOM, please redefine conversion factors for new UOM."),
alert=True,
)
to_remove = []
for d in self.get("uoms"):
if d.conversion_factor == 1 and d.uom != self.stock_uom:
to_remove.append(d)
uoms_list = [d.uom for d in self.get("uoms")]
[self.remove(d) for d in to_remove]
if self.stock_uom not in uoms_list:
self.append("uoms", {
"uom": self.stock_uom,
"conversion_factor": 1
})
def update_website_item(self):
"""Update Website Item if change in Item impacts it."""

View File

@@ -573,6 +573,16 @@ class TestItem(ERPNextTestCase):
except frappe.ValidationError as e:
self.fail(f"UoM change not allowed even though no SLE / BIN with positive qty exists: {e}")
def test_erasure_of_old_conversions(self):
item = create_item("_item change uom")
item.stock_uom = "Gram"
item.append("uoms", frappe._dict(uom="Box", conversion_factor=2))
item.save()
item.reload()
item.stock_uom = "Nos"
item.save()
self.assertEqual(len(item.uoms), 1)
def test_validate_stock_item(self):
self.assertRaises(frappe.ValidationError, validate_is_stock_item, "_Test Non Stock Item")

View File

@@ -484,6 +484,13 @@ def get_serial_nos(serial_no):
return [s.strip() for s in cstr(serial_no).strip().upper().replace(',', '\n').split('\n')
if s.strip()]
def clean_serial_no_string(serial_no: str) -> str:
if not serial_no:
return ""
serial_no_list = get_serial_nos(serial_no)
return "\n".join(serial_no_list)
def update_args_for_serial_no(serial_no_doc, serial_no, args, is_new=False):
for field in ["item_code", "work_order", "company", "batch_no", "supplier", "location"]:
if args.get(field):

View File

@@ -86,10 +86,10 @@ frappe.query_reports["Stock Ledger"] = {
],
"formatter": function (value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
if (column.fieldname == "out_qty" && data.out_qty < 0) {
if (column.fieldname == "out_qty" && data && data.out_qty < 0) {
value = "<span style='color:red'>" + value + "</span>";
}
else if (column.fieldname == "in_qty" && data.in_qty > 0) {
else if (column.fieldname == "in_qty" && data && data.in_qty > 0) {
value = "<span style='color:green'>" + value + "</span>";
}