Merge branch 'version-13-hotfix' into crm-contact-duplication-v13

This commit is contained in:
Anupam Kumar
2022-01-20 17:30:21 +05:30
committed by GitHub
19 changed files with 279 additions and 112 deletions

View File

@@ -5,9 +5,14 @@ on:
paths-ignore:
- '**.js'
- '**.md'
types: [opened, unlabeled, synchronize, reopened]
workflow_dispatch:
concurrency:
group: patch-mariadb-v13-${{ github.event.number }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-18.04
@@ -25,6 +30,11 @@ jobs:
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps:
- name: Check for merge conficts label
if: ${{ contains(github.event.pull_request.labels.*.name, 'conflicts') }}
run: |
echo "Remove merge conflicts and remove conflict label to run CI"
exit 1
- name: Clone
uses: actions/checkout@v2

View File

@@ -5,6 +5,7 @@ on:
paths-ignore:
- '**.js'
- '**.md'
types: [opened, unlabeled, synchronize, reopened]
workflow_dispatch:
push:
branches: [ develop ]
@@ -12,6 +13,10 @@ on:
- '**.js'
- '**.md'
concurrency:
group: server-mariadb-v13-${{ github.event.number }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-18.04
@@ -35,6 +40,12 @@ jobs:
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps:
- name: Check for merge conficts label
if: ${{ contains(github.event.pull_request.labels.*.name, 'conflicts') }}
run: |
echo "Remove merge conflicts and remove conflict label to run CI"
exit 1
- name: Clone
uses: actions/checkout@v2
@@ -89,39 +100,8 @@ jobs:
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
- name: Run Tests
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --use-orchestrator --with-coverage
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --use-orchestrator
env:
TYPE: server
CI_BUILD_ID: ${{ github.run_id }}
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
- name: Upload Coverage Data
run: |
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
cd ${GITHUB_WORKSPACE}
pip3 install coverage==5.5
pip3 install coveralls==3.0.1
coveralls
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_FLAG_NAME: run-${{ matrix.container }}
COVERALLS_SERVICE_NAME: ${{ github.event_name == 'pull_request' && 'github' || 'github-actions' }}
COVERALLS_PARALLEL: true
coveralls:
name: Coverage Wrap Up
needs: test
container: python:3-slim
runs-on: ubuntu-18.04
steps:
- name: Clone
uses: actions/checkout@v2
- name: Coveralls Finished
run: |
cd ${GITHUB_WORKSPACE}
pip3 install coverage==5.5
pip3 install coveralls==3.0.1
coveralls --finish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -6,6 +6,10 @@ on:
- '**.md'
workflow_dispatch:
concurrency:
group: ui-v13-${{ github.event.number }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-18.04

View File

@@ -17,6 +17,7 @@ from openpyxl.styles import Font
from openpyxl.utils import get_column_letter
from six import string_types
INVALID_VALUES = ("", None)
class BankStatementImport(DataImport):
def __init__(self, *args, **kwargs):
@@ -96,6 +97,18 @@ def download_errored_template(data_import_name):
data_import = frappe.get_doc("Bank Statement Import", data_import_name)
data_import.export_errored_rows()
def parse_data_from_template(raw_data):
data = []
for i, row in enumerate(raw_data):
if all(v in INVALID_VALUES for v in row):
# empty row
continue
data.append(row)
return data
def start_import(data_import, bank_account, import_file_path, google_sheets_url, bank, template_options):
"""This method runs in background job"""
@@ -105,7 +118,8 @@ def start_import(data_import, bank_account, import_file_path, google_sheets_url,
file = import_file_path if import_file_path else google_sheets_url
import_file = ImportFile("Bank Transaction", file = file, import_type="Insert New Records")
data = import_file.raw_data
data = parse_data_from_template(import_file.raw_data)
if import_file_path:
add_bank_account(data, bank_account)

View File

@@ -201,8 +201,8 @@ def get_course_schedule_events(start, end, filters=None):
conditions = get_event_conditions("Course Schedule", filters)
data = frappe.db.sql("""select name, course, color,
timestamp(schedule_date, from_time) as from_datetime,
timestamp(schedule_date, to_time) as to_datetime,
timestamp(schedule_date, from_time) as from_time,
timestamp(schedule_date, to_time) as to_time,
room, student_group, 0 as 'allDay'
from `tabCourse Schedule`
where ( schedule_date between %(start)s and %(end)s )

View File

@@ -3,6 +3,8 @@
# For license information, please see license.txt
from datetime import datetime
import frappe
from frappe import _
from frappe.model.document import Document
@@ -30,6 +32,14 @@ class CourseSchedule(Document):
if self.from_time > self.to_time:
frappe.throw(_("From Time cannot be greater than To Time."))
"""Handles specicfic case to update schedule date in calendar """
if isinstance(self.from_time, str):
try:
datetime_obj = datetime.strptime(self.from_time, '%Y-%m-%d %H:%M:%S')
self.schedule_date = datetime_obj
except ValueError:
pass
def validate_overlap(self):
"""Validates overlap for Student Group, Instructor, Room"""
@@ -47,4 +57,4 @@ class CourseSchedule(Document):
validate_overlap_for(self, "Assessment Plan", "student_group")
validate_overlap_for(self, "Assessment Plan", "room")
validate_overlap_for(self, "Assessment Plan", "supervisor", self.instructor)
validate_overlap_for(self, "Assessment Plan", "supervisor", self.instructor)

View File

@@ -1,11 +1,10 @@
frappe.views.calendar["Course Schedule"] = {
field_map: {
// from_datetime and to_datetime don't exist as docfields but are used in onload
"start": "from_datetime",
"end": "to_datetime",
"start": "from_time",
"end": "to_time",
"id": "name",
"title": "course",
"allDay": "allDay"
"allDay": "allDay",
},
gantt: false,
order_by: "schedule_date",

View File

@@ -6,6 +6,7 @@ import unittest
import frappe
from frappe.utils import to_timedelta, today
from frappe.utils.data import add_to_date
from erpnext.education.utils import OverlapError
@@ -39,6 +40,11 @@ class TestCourseSchedule(unittest.TestCase):
make_course_schedule_test_record(from_time= cs1.from_time, to_time= cs1.to_time,
student_group="Course-TC102-2014-2015 (_Test Academic Term)", instructor="_Test Instructor 2", room=frappe.get_all("Room")[1].name)
def test_update_schedule_date(self):
doc = make_course_schedule_test_record(schedule_date= add_to_date(today(), days=1))
doc.schedule_date = add_to_date(doc.schedule_date, days=1)
doc.save()
def make_course_schedule_test_record(**args):
args = frappe._dict(args)

View File

@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from frappe import _, throw
from frappe.utils import add_days, cint, cstr, date_diff, formatdate, getdate
@@ -306,13 +305,18 @@ class MaintenanceSchedule(TransactionBase):
return schedule.name
@frappe.whitelist()
def update_serial_nos(s_id):
serial_nos = frappe.db.get_value('Maintenance Schedule Detail', s_id, 'serial_no')
def get_serial_nos_from_schedule(item_code, schedule=None):
serial_nos = []
if schedule:
serial_nos = frappe.db.get_value('Maintenance Schedule Item', {
'parent': schedule,
'item_code': item_code
}, 'serial_no')
if serial_nos:
serial_nos = get_serial_nos(serial_nos)
return serial_nos
else:
return False
return serial_nos
@frappe.whitelist()
def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=None):
@@ -320,12 +324,9 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No
def update_status_and_detail(source, target, parent):
target.maintenance_type = "Scheduled"
target.maintenance_schedule = source.name
target.maintenance_schedule_detail = s_id
def update_sales_and_serial(source, target, parent):
sales_person = frappe.db.get_value('Maintenance Schedule Detail', s_id, 'sales_person')
target.service_person = sales_person
def update_serial(source, target, parent):
serial_nos = get_serial_nos(target.serial_no)
if len(serial_nos) == 1:
target.serial_no = serial_nos[0]
@@ -346,7 +347,10 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No
"Maintenance Schedule Item": {
"doctype": "Maintenance Visit Purpose",
"condition": lambda doc: doc.item_name == item_name,
"postprocess": update_sales_and_serial
"field_map": {
"sales_person": "service_person"
},
"postprocess": update_serial
}
}, target_doc)

View File

@@ -7,8 +7,11 @@ import frappe
from frappe.utils.data import add_days, formatdate, today
from erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule import (
get_serial_nos_from_schedule,
make_maintenance_visit,
)
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
# test_records = frappe.get_test_records('Maintenance Schedule')
@@ -80,6 +83,42 @@ class TestMaintenanceSchedule(unittest.TestCase):
#checks if visit status is back updated in schedule
self.assertTrue(ms.schedules[1].completion_status, "Partially Completed")
def test_serial_no_filters(self):
# Without serial no. set in schedule -> returns None
item_code = "_Test Serial Item"
make_serial_item_with_serial(item_code)
ms = make_maintenance_schedule(item_code=item_code)
ms.submit()
s_item = ms.schedules[0]
mv = make_maintenance_visit(source_name=ms.name, item_name=item_code, s_id=s_item.name)
mvi = mv.purposes[0]
serial_nos = get_serial_nos_from_schedule(mvi.item_name, ms.name)
self.assertEqual(serial_nos, None)
# With serial no. set in schedule -> returns serial nos.
make_serial_item_with_serial(item_code)
ms = make_maintenance_schedule(item_code=item_code, serial_no="TEST001, TEST002")
ms.submit()
s_item = ms.schedules[0]
mv = make_maintenance_visit(source_name=ms.name, item_name=item_code, s_id=s_item.name)
mvi = mv.purposes[0]
serial_nos = get_serial_nos_from_schedule(mvi.item_name, ms.name)
self.assertEqual(serial_nos, ["TEST001", "TEST002"])
frappe.db.rollback()
def make_serial_item_with_serial(item_code):
serial_item_doc = create_item(item_code, is_stock_item=1)
if not serial_item_doc.has_serial_no or not serial_item_doc.serial_no_series:
serial_item_doc.has_serial_no = 1
serial_item_doc.serial_no_series = "TEST.###"
serial_item_doc.save(ignore_permissions=True)
active_serials = frappe.db.get_all('Serial No', {"status": "Active", "item_code": item_code})
if len(active_serials) < 2:
make_serialized_item(item_code=item_code)
def get_events(ms):
return frappe.get_all("Event Participants", filters={
"reference_doctype": ms.doctype,
@@ -87,17 +126,18 @@ def get_events(ms):
"parenttype": "Event"
})
def make_maintenance_schedule():
def make_maintenance_schedule(**args):
ms = frappe.new_doc("Maintenance Schedule")
ms.company = "_Test Company"
ms.customer = "_Test Customer"
ms.transaction_date = today()
ms.append("items", {
"item_code": "_Test Item",
"item_code": args.get("item_code") or "_Test Item",
"start_date": today(),
"periodicity": "Weekly",
"no_of_visits": 4,
"serial_no": args.get("serial_no"),
"sales_person": "Sales Team",
})
ms.insert(ignore_permissions=True)

View File

@@ -2,52 +2,54 @@
// License: GNU General Public License v3. See license.txt
frappe.provide("erpnext.maintenance");
var serial_nos = [];
frappe.ui.form.on('Maintenance Visit', {
refresh: function (frm) {
//filters for serial_no based on item_code
frm.set_query('serial_no', 'purposes', function (frm, cdt, cdn) {
let item = locals[cdt][cdn];
if (serial_nos) {
return {
filters: {
'item_code': item.item_code,
'name': ["in", serial_nos]
}
};
} else {
return {
filters: {
'item_code': item.item_code
}
};
}
});
},
setup: function (frm) {
frm.set_query('contact_person', erpnext.queries.contact_query);
frm.set_query('customer_address', erpnext.queries.address_query);
frm.set_query('customer', erpnext.queries.customer);
},
onload: function (frm, cdt, cdn) {
let item = locals[cdt][cdn];
onload: function (frm) {
// filters for serial no based on item code
if (frm.doc.maintenance_type === "Scheduled") {
const schedule_id = item.purposes[0].prevdoc_detail_docname || frm.doc.maintenance_schedule_detail;
let item_code = frm.doc.purposes[0].item_code;
frappe.call({
method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.update_serial_nos",
method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.get_serial_nos_from_schedule",
args: {
s_id: schedule_id
},
callback: function (r) {
serial_nos = r.message;
schedule: frm.doc.maintenance_schedule,
item_code: item_code
}
}).then((r) => {
let serial_nos = r.message;
frm.set_query('serial_no', 'purposes', () => {
if (serial_nos.length > 0) {
return {
filters: {
'item_code': item_code,
'name': ["in", serial_nos]
}
};
}
return {
filters: {
'item_code': item_code
}
};
});
});
} else {
frm.set_query('serial_no', 'purposes', (frm, cdt, cdn) => {
let row = locals[cdt][cdn];
return {
filters: {
'item_code': row.item_code
}
};
});
}
if (!frm.doc.status) {
frm.set_value({ status: 'Draft' });
}
if (frm.doc.__islocal) {
frm.doc.maintenance_type == 'Unscheduled' && frm.clear_table("purposes");
frm.set_value({ mntc_date: frappe.datetime.get_today() });
}
},
@@ -60,7 +62,6 @@ frappe.ui.form.on('Maintenance Visit', {
contact_person: function (frm) {
erpnext.utils.get_contact_details(frm);
}
})
// TODO commonify this code

View File

@@ -179,8 +179,7 @@
"label": "Purposes",
"oldfieldname": "maintenance_visit_details",
"oldfieldtype": "Table",
"options": "Maintenance Visit Purpose",
"reqd": 1
"options": "Maintenance Visit Purpose"
},
{
"fieldname": "more_info",
@@ -294,10 +293,11 @@
"idx": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-05-27 16:06:17.352572",
"modified": "2021-12-17 03:10:27.608112",
"modified_by": "Administrator",
"module": "Maintenance",
"name": "Maintenance Visit",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{

View File

@@ -18,6 +18,10 @@ class MaintenanceVisit(TransactionBase):
if d.serial_no and not frappe.db.exists("Serial No", d.serial_no):
frappe.throw(_("Serial No {0} does not exist").format(d.serial_no))
def validate_purpose_table(self):
if not self.purposes:
frappe.throw(_("Add Items in the Purpose Table"), title="Purposes Required")
def validate_maintenance_date(self):
if self.maintenance_type == "Scheduled" and self.maintenance_schedule_detail:
item_ref = frappe.db.get_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'item_reference')
@@ -29,6 +33,7 @@ class MaintenanceVisit(TransactionBase):
def validate(self):
self.validate_serial_no()
self.validate_maintenance_date()
self.validate_purpose_table()
def update_completion_status(self):
if self.maintenance_schedule_detail:

View File

@@ -949,6 +949,7 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company):
warehouses, item.get("quantity"), company, ignore_validation=True)
required_qty = item.get("quantity")
# get available material by transferring to production warehouse
for d in locations:
if required_qty <=0: return
@@ -959,12 +960,14 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company):
new_dict.update({
"quantity": quantity,
"material_request_type": "Material Transfer",
"uom": new_dict.get("stock_uom"), # internal transfer should be in stock UOM
"from_warehouse": d.get("warehouse")
})
required_qty -= quantity
new_mr_items.append(new_dict)
# raise purchase request for remaining qty
if required_qty:
stock_uom, purchase_uom = frappe.db.get_value(
'Item',

View File

@@ -4,6 +4,39 @@
frappe.query_reports["BOM Operations Time"] = {
"filters": [
{
"fieldname": "item_code",
"label": __("Item Code"),
"fieldtype": "Link",
"width": "100",
"options": "Item",
"get_query": () =>{
return {
filters: { "disabled": 0, "is_stock_item": 1 }
}
}
},
{
"fieldname": "bom_id",
"label": __("BOM ID"),
"fieldtype": "MultiSelectList",
"width": "100",
"options": "BOM",
"get_data": function(txt) {
return frappe.db.get_link_options("BOM", txt);
},
"get_query": () =>{
return {
filters: { "docstatus": 1, "is_active": 1, "with_operations": 1 }
}
}
},
{
"fieldname": "workstation",
"label": __("Workstation"),
"fieldtype": "Link",
"width": "100",
"options": "Workstation"
},
]
};

View File

@@ -1,14 +1,16 @@
{
"add_total_row": 0,
"add_total_row": 1,
"columns": [],
"creation": "2020-03-03 01:41:20.862521",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"letter_head": "",
"modified": "2020-03-03 01:41:20.862521",
"modified": "2022-01-20 14:21:47.771591",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Operations Time",

View File

@@ -12,19 +12,15 @@ def execute(filters=None):
return columns, data
def get_data(filters):
data = []
bom_wise_data = {}
bom_data, report_data = [], []
bom_data = []
for d in frappe.db.sql("""
SELECT
bom.name, bom.item, bom.item_name, bom.uom,
bomps.operation, bomps.workstation, bomps.time_in_mins
FROM `tabBOM` bom, `tabBOM Operation` bomps
WHERE
bom.docstatus = 1 and bom.is_active = 1 and bom.name = bomps.parent
""", as_dict=1):
bom_operation_data = get_filtered_data(filters)
for d in bom_operation_data:
row = get_args()
if d.name not in bom_data:
bom_wise_data[d.name] = []
bom_data.append(d.name)
row.update(d)
else:
@@ -34,14 +30,49 @@ def get_data(filters):
"time_in_mins": d.time_in_mins
})
data.append(row)
# maintain BOM wise data for grouping such as:
# {"BOM A": [{Row1}, {Row2}], "BOM B": ...}
bom_wise_data[d.name].append(row)
used_as_subassembly_items = get_bom_count(bom_data)
for d in data:
d.used_as_subassembly_items = used_as_subassembly_items.get(d.name, 0)
for d in bom_wise_data:
for row in bom_wise_data[d]:
row.used_as_subassembly_items = used_as_subassembly_items.get(row.name, 0)
report_data.append(row)
return data
return report_data
def get_filtered_data(filters):
bom = frappe.qb.DocType("BOM")
bom_ops = frappe.qb.DocType("BOM Operation")
bom_ops_query = (
frappe.qb.from_(bom)
.join(bom_ops).on(bom.name == bom_ops.parent)
.select(
bom.name, bom.item, bom.item_name, bom.uom,
bom_ops.operation, bom_ops.workstation, bom_ops.time_in_mins
).where(
(bom.docstatus == 1)
& (bom.is_active == 1)
)
)
if filters.get("item_code"):
bom_ops_query = bom_ops_query.where(bom.item == filters.get("item_code"))
if filters.get("bom_id"):
bom_ops_query = bom_ops_query.where(bom.name.isin(filters.get("bom_id")))
if filters.get("workstation"):
bom_ops_query = bom_ops_query.where(
bom_ops.workstation == filters.get("workstation")
)
bom_operation_data = bom_ops_query.run(as_dict=True)
return bom_operation_data
def get_bom_count(bom_data):
data = frappe.get_all("BOM Item",
@@ -68,13 +99,13 @@ def get_columns(filters):
"options": "BOM",
"fieldname": "name",
"fieldtype": "Link",
"width": 140
"width": 220
}, {
"label": _("BOM Item Code"),
"label": _("Item Code"),
"options": "Item",
"fieldname": "item",
"fieldtype": "Link",
"width": 140
"width": 150
}, {
"label": _("Item Name"),
"fieldname": "item_name",
@@ -85,13 +116,13 @@ def get_columns(filters):
"options": "UOM",
"fieldname": "uom",
"fieldtype": "Link",
"width": 140
"width": 100
}, {
"label": _("Operation"),
"options": "Operation",
"fieldname": "operation",
"fieldtype": "Link",
"width": 120
"width": 140
}, {
"label": _("Workstation"),
"options": "Workstation",
@@ -101,11 +132,11 @@ def get_columns(filters):
}, {
"label": _("Time (In Mins)"),
"fieldname": "time_in_mins",
"fieldtype": "Int",
"width": 140
"fieldtype": "Float",
"width": 120
}, {
"label": _("Sub-assembly BOM Count"),
"fieldname": "used_as_subassembly_items",
"fieldtype": "Int",
"width": 180
"width": 200
}]

View File

@@ -341,3 +341,4 @@ erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021
erpnext.patches.v13_0.update_tax_category_for_rcm
erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template
erpnext.patches.v13_0.agriculture_deprecation_warning
erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit

View File

@@ -0,0 +1,24 @@
import frappe
def execute():
frappe.reload_doc("maintenance", "doctype", "maintenance_visit")
# Updates the Maintenance Schedule link to fetch serial nos
from frappe.query_builder.functions import Coalesce
mvp = frappe.qb.DocType('Maintenance Visit Purpose')
mv = frappe.qb.DocType('Maintenance Visit')
frappe.qb.update(
mv
).join(
mvp
).on(mvp.parent == mv.name).set(
mv.maintenance_schedule,
Coalesce(mvp.prevdoc_docname, '')
).where(
(mv.maintenance_type == "Scheduled")
& (mvp.prevdoc_docname.notnull())
& (mv.docstatus < 2)
).run(as_dict=1)