Merge branch 'version-13-hotfix' of https://github.com/frappe/erpnext into e-commerce-refactor

This commit is contained in:
marination
2021-07-08 11:12:14 +05:30
81 changed files with 2300 additions and 1629 deletions

View File

@@ -301,17 +301,21 @@ def process_deferred_accounting(posting_date=None):
start_date = add_months(today(), -1)
end_date = add_days(today(), -1)
for record_type in ('Income', 'Expense'):
doc = frappe.get_doc(dict(
doctype='Process Deferred Accounting',
posting_date=posting_date,
start_date=start_date,
end_date=end_date,
type=record_type
))
companies = frappe.get_all('Company')
doc.insert()
doc.submit()
for company in companies:
for record_type in ('Income', 'Expense'):
doc = frappe.get_doc(dict(
doctype='Process Deferred Accounting',
company=company.name,
posting_date=posting_date,
start_date=start_date,
end_date=end_date,
type=record_type
))
doc.insert()
doc.submit()
def make_gl_entries(doc, credit_account, debit_account, against,
amount, base_amount, posting_date, project, account_currency, cost_center, item, deferred_process=None):

View File

@@ -51,7 +51,7 @@ class BankStatementImport(DataImport):
self.import_file, self.google_sheets_url
)
if 'Bank Account' not in json.dumps(preview):
if 'Bank Account' not in json.dumps(preview['columns']):
frappe.throw(_("Please add the Bank Account column"))
from frappe.core.page.background_jobs.background_jobs import get_info

View File

@@ -1010,21 +1010,21 @@ class TestPurchaseInvoice(unittest.TestCase):
# Check GLE for Purchase Invoice
# Zero net effect on final TDS Payable on invoice
expected_gle = [
['_Test Account Cost for Goods Sold - _TC', 30000, 0],
['_Test Account Excise Duty - _TC', 0, 3000],
['Creditors - _TC', 0, 27000],
['TDS Payable - _TC', 3000, 3000]
['_Test Account Cost for Goods Sold - _TC', 30000],
['_Test Account Excise Duty - _TC', -3000],
['Creditors - _TC', -27000],
['TDS Payable - _TC', 0]
]
gl_entries = frappe.db.sql("""select account, debit, credit
gl_entries = frappe.db.sql("""select account, sum(debit - credit) as amount
from `tabGL Entry`
where voucher_type='Purchase Invoice' and voucher_no=%s
group by account
order by account asc""", (purchase_invoice.name), as_dict=1)
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_gle[i][0], gle.account)
self.assertEqual(expected_gle[i][1], gle.debit)
self.assertEqual(expected_gle[i][2], gle.credit)
self.assertEqual(expected_gle[i][1], gle.amount)
def update_tax_witholding_category(company, account, date):
from erpnext.accounts.utils import get_fiscal_year

View File

@@ -1957,6 +1957,33 @@ class TestSalesInvoice(unittest.TestCase):
einvoice = make_einvoice(si)
validate_totals(einvoice)
def test_item_tax_net_range(self):
item = create_item("T Shirt")
item.set('taxes', [])
item.append("taxes", {
"item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
"minimum_net_rate": 0,
"maximum_net_rate": 500
})
item.append("taxes", {
"item_tax_template": "_Test Account Excise Duty @ 12 - _TC",
"minimum_net_rate": 501,
"maximum_net_rate": 1000
})
item.save()
sales_invoice = create_sales_invoice(item = "T Shirt", rate=700, do_not_submit=True)
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC")
# Apply discount
sales_invoice.apply_discount_on = 'Net Total'
sales_invoice.discount_amount = 300
sales_invoice.save()
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill()
si.naming_series = 'INV-2020-.#####'
@@ -1985,32 +2012,6 @@ def get_sales_invoice_for_e_invoice():
return si
def test_item_tax_net_range(self):
item = create_item("T Shirt")
item.set('taxes', [])
item.append("taxes", {
"item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
"minimum_net_rate": 0,
"maximum_net_rate": 500
})
item.append("taxes", {
"item_tax_template": "_Test Account Excise Duty @ 12 - _TC",
"minimum_net_rate": 501,
"maximum_net_rate": 1000
})
item.save()
sales_invoice = create_sales_invoice(item = "T Shirt", rate=700, do_not_submit=True)
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC")
# Apply discount
sales_invoice.apply_discount_on = 'Net Total'
sales_invoice.discount_amount = 300
sales_invoice.save()
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
def make_test_address_for_ewaybill():
if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'):
@@ -2087,9 +2088,9 @@ def make_sales_invoice_for_ewaybill():
if not gst_account:
gst_settings.append("gst_accounts", {
"company": "_Test Company",
"cgst_account": "CGST - _TC",
"sgst_account": "SGST - _TC",
"igst_account": "IGST - _TC",
"cgst_account": "Output Tax CGST - _TC",
"sgst_account": "Output Tax SGST - _TC",
"igst_account": "Output Tax IGST - _TC",
})
gst_settings.save()
@@ -2106,7 +2107,7 @@ def make_sales_invoice_for_ewaybill():
si.append("taxes", {
"charge_type": "On Net Total",
"account_head": "CGST - _TC",
"account_head": "Output Tax CGST - _TC",
"cost_center": "Main - _TC",
"description": "CGST @ 9.0",
"rate": 9
@@ -2114,7 +2115,7 @@ def make_sales_invoice_for_ewaybill():
si.append("taxes", {
"charge_type": "On Net Total",
"account_head": "SGST - _TC",
"account_head": "Output Tax SGST - _TC",
"cost_center": "Main - _TC",
"description": "SGST @ 9.0",
"rate": 9

View File

@@ -1,24 +1,6 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
cur_frm.add_fetch("customer", "customer_group", "customer_group" );
cur_frm.add_fetch("supplier", "supplier_group_name", "supplier_group" );
frappe.ui.form.on("Tax Rule", "tax_type", function(frm) {
frm.toggle_reqd("sales_tax_template", frm.doc.tax_type=="Sales");
frm.toggle_reqd("purchase_tax_template", frm.doc.tax_type=="Purchase");
})
frappe.ui.form.on("Tax Rule", "onload", function(frm) {
if(frm.doc.__islocal) {
frm.set_value("use_for_shopping_cart", 1);
}
})
frappe.ui.form.on("Tax Rule", "refresh", function(frm) {
frappe.ui.form.trigger("Tax Rule", "tax_type");
})
frappe.ui.form.on("Tax Rule", "customer", function(frm) {
if(frm.doc.customer) {
frappe.call({

File diff suppressed because it is too large Load Diff

View File

@@ -50,7 +50,7 @@ class TestTaxRule(unittest.TestCase):
tax_rule1 = make_tax_rule(customer_group= "All Customer Groups",
sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority = 1, from_date = "2015-01-01")
tax_rule1.save()
self.assertEqual(get_tax_template("2015-01-01", {"customer_group" : "Commercial", "use_for_shopping_cart":0}),
self.assertEqual(get_tax_template("2015-01-01", {"customer_group" : "Commercial", "use_for_shopping_cart":1}),
"_Test Sales Taxes and Charges Template - _TC")
def test_conflict_with_overlapping_dates(self):

View File

@@ -542,6 +542,7 @@ def get_dashboard_info(party_type, party, loyalty_program=None):
select company, sum(debit_in_account_currency) - sum(credit_in_account_currency)
from `tabGL Entry`
where party_type = %s and party=%s
and is_cancelled = 0
group by company""", (party_type, party)))
for d in companies:

View File

@@ -380,7 +380,7 @@ def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, g
gl_entries = frappe.db.sql("""select gl.posting_date, gl.account, gl.debit, gl.credit, gl.is_opening, gl.company,
gl.fiscal_year, gl.debit_in_account_currency, gl.credit_in_account_currency, gl.account_currency,
acc.account_name, acc.account_number
from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s
from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s and gl.is_cancelled = 0
{additional_conditions} and gl.posting_date <= %(to_date)s and acc.lft >= %(lft)s and acc.rgt <= %(rgt)s
order by gl.account, gl.posting_date""".format(additional_conditions=additional_conditions),
{

View File

@@ -784,7 +784,7 @@ def get_children(doctype, parent, company, is_root=False):
return acc
def create_payment_gateway_account(gateway, payment_channel="Email"):
from erpnext.setup.setup_wizard.operations.company_setup import create_bank_account
from erpnext.setup.setup_wizard.operations.install_fixtures import create_bank_account
company = frappe.db.get_value("Global Defaults", None, "default_company")
if not company:

View File

@@ -97,6 +97,9 @@
"is_fixed_asset",
"item_tax_rate",
"section_break_72",
"production_plan",
"production_plan_item",
"production_plan_sub_assembly_item",
"page_break"
],
"fields": [
@@ -803,13 +806,37 @@
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "production_plan",
"fieldtype": "Link",
"label": "Production Plan",
"options": "Production Plan",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "production_plan_item",
"fieldtype": "Data",
"hidden": 1,
"label": "Production Plan Item",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "production_plan_sub_assembly_item",
"fieldtype": "Data",
"hidden": 1,
"label": "Production Plan Sub Assembly Item",
"no_copy": 1,
"read_only": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-03-22 11:46:12.357435",
"modified": "2021-06-28 19:22:22.715365",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",

View File

@@ -102,7 +102,7 @@
}
],
"links": [],
"modified": "2020-01-28 16:16:45.447213",
"modified": "2021-06-29 18:27:02.832979",
"modified_by": "Administrator",
"module": "CRM",
"name": "Appointment",
@@ -153,6 +153,18 @@
"role": "Sales User",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Employee",
"share": 1,
"write": 1
}
],
"quick_entry": 1,

View File

@@ -168,12 +168,13 @@ class Lead(SellingController):
if self.phone:
contact.append("phone_nos", {
"phone": self.phone,
"is_primary": 1
"is_primary_phone": 1
})
if self.mobile_no:
contact.append("phone_nos", {
"phone": self.mobile_no
"phone": self.mobile_no,
"is_primary_mobile_no":1
})
contact.insert(ignore_permissions=True)

View File

@@ -355,11 +355,11 @@ def get_or_create_course_enrollment(course, program):
student = get_current_student()
course_enrollment = get_enrollment("course", course, student.name)
if not course_enrollment:
program_enrollment = get_enrollment('program', program, student.name)
program_enrollment = get_enrollment('program', program.name, student.name)
if not program_enrollment:
frappe.throw(_("You are not enrolled in program {0}").format(program))
return
return student.enroll_in_course(course_name=course, program_enrollment=get_enrollment('program', program, student.name))
return student.enroll_in_course(course_name=course, program_enrollment=get_enrollment('program', program.name, student.name))
else:
return frappe.get_doc('Course Enrollment', course_enrollment)

View File

@@ -7,16 +7,21 @@ import frappe
import unittest
from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings import process_balance_info, verify_transaction
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
from erpnext.erpnext_integrations.utils import create_mode_of_payment
class TestMpesaSettings(unittest.TestCase):
def setUp(self):
# create payment gateway in setup
create_mpesa_settings(payment_gateway_name="_Test")
create_mpesa_settings(payment_gateway_name="_Account Balance")
create_mpesa_settings(payment_gateway_name="Payment")
def tearDown(self):
frappe.db.sql('delete from `tabMpesa Settings`')
frappe.db.sql('delete from `tabIntegration Request` where integration_request_service = "Mpesa"')
def test_creation_of_payment_gateway(self):
create_mpesa_settings(payment_gateway_name="_Test")
mode_of_payment = frappe.get_doc("Mode of Payment", "Mpesa-_Test")
mode_of_payment = create_mode_of_payment('Mpesa-_Test', payment_type="Phone")
self.assertTrue(frappe.db.exists("Payment Gateway Account", {'payment_gateway': "Mpesa-_Test"}))
self.assertTrue(mode_of_payment.name)
self.assertEqual(mode_of_payment.type, "Phone")
@@ -47,7 +52,6 @@ class TestMpesaSettings(unittest.TestCase):
integration_request.delete()
def test_processing_of_callback_payload(self):
create_mpesa_settings(payment_gateway_name="Payment")
mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES")
@@ -90,7 +94,6 @@ class TestMpesaSettings(unittest.TestCase):
pos_invoice.delete()
def test_processing_of_multiple_callback_payload(self):
create_mpesa_settings(payment_gateway_name="Payment")
mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500")
@@ -141,7 +144,6 @@ class TestMpesaSettings(unittest.TestCase):
pos_invoice.delete()
def test_processing_of_only_one_succes_callback_payload(self):
create_mpesa_settings(payment_gateway_name="Payment")
mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500")
@@ -202,6 +204,7 @@ def create_mpesa_settings(payment_gateway_name="Express"):
doc = frappe.get_doc(dict( #nosec
doctype="Mpesa Settings",
sandbox=1,
payment_gateway_name=payment_gateway_name,
consumer_key="5sMu9LVI1oS3oBGPJfh3JyvLHwZOdTKn",
consumer_secret="VI1oS3oBGPJfh3JyvLHw",

View File

@@ -52,7 +52,8 @@ def create_mode_of_payment(gateway, payment_type="General"):
"payment_gateway": gateway
}, ['payment_account'])
if not frappe.db.exists("Mode of Payment", gateway) and payment_gateway_account:
mode_of_payment = frappe.db.exists("Mode of Payment", gateway)
if not mode_of_payment and payment_gateway_account:
mode_of_payment = frappe.get_doc({
"doctype": "Mode of Payment",
"mode_of_payment": gateway,
@@ -66,6 +67,10 @@ def create_mode_of_payment(gateway, payment_type="General"):
})
mode_of_payment.insert(ignore_permissions=True)
return mode_of_payment
elif mode_of_payment:
return frappe.get_doc("Mode of Payment", mode_of_payment)
def get_tracking_url(carrier, tracking_number):
# Return the formatted Tracking URL.
tracking_url = ''

View File

@@ -157,6 +157,7 @@ website_route_rules = [
"parents": [{"label": _("Material Request"), "route": "material-requests"}]
}
},
{"from_route": "/project", "to_route": "Project"}
]
standard_portal_menu_items = [

View File

@@ -11,5 +11,5 @@ cur_frm.cscript.onload = function(doc, cdt, cdn) {
cur_frm.fields_dict.employee.get_query = function(doc,cdt,cdn) {
return{
query: "erpnext.controllers.queries.employee_query"
}
}
}

View File

@@ -15,6 +15,7 @@ class Attendance(Document):
validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"])
self.validate_attendance_date()
self.validate_duplicate_record()
self.validate_employee_status()
self.check_leave_record()
def validate_attendance_date(self):
@@ -38,6 +39,10 @@ class Attendance(Document):
frappe.throw(_("Attendance for employee {0} is already marked for the date {1}").format(
frappe.bold(self.employee), frappe.bold(self.attendance_date)))
def validate_employee_status(self):
if frappe.db.get_value("Employee", self.employee, "status") == "Inactive":
frappe.throw(_("Cannot mark attendance for an Inactive employee {0}").format(self.employee))
def check_leave_record(self):
leave_record = frappe.db.sql("""
select leave_type, half_day, half_day_date

View File

@@ -21,6 +21,9 @@ frappe.listview_settings['Attendance'] = {
label: __('For Employee'),
fieldtype: 'Link',
options: 'Employee',
get_query: () => {
return {query: "erpnext.controllers.queries.employee_query"}
},
reqd: 1,
onchange: function() {
dialog.set_df_property("unmarked_days", "hidden", 1);

View File

@@ -72,7 +72,8 @@ class TestExpenseClaim(unittest.TestCase):
def test_expense_claim_gl_entry(self):
payable_account = get_payable_account(company_name)
taxes = generate_taxes()
expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4", do_not_submit=True, taxes=taxes)
expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4",
do_not_submit=True, taxes=taxes)
expense_claim.submit()
gl_entries = frappe.db.sql("""select account, debit, credit
@@ -82,7 +83,7 @@ class TestExpenseClaim(unittest.TestCase):
self.assertTrue(gl_entries)
expected_values = dict((d[0], d) for d in [
['CGST - _TC4',18.0, 0.0],
['Output Tax CGST - _TC4',18.0, 0.0],
[payable_account, 0.0, 218.0],
["Travel Expenses - _TC4", 200.0, 0.0]
])
@@ -145,7 +146,7 @@ def generate_taxes():
parent_account = frappe.db.get_value('Account',
{'company': company_name, 'is_group':1, 'account_type': 'Tax'},
'name')
account = create_account(company=company_name, account_name="CGST", account_type="Tax", parent_account=parent_account)
account = create_account(company=company_name, account_name="Output Tax CGST", account_type="Tax", parent_account=parent_account)
return {'taxes':[{
"account_head": account,
"rate": 0,

View File

@@ -20,11 +20,10 @@ frappe.ui.form.on('Training Event', {
frappe.set_route("List", "Training Feedback");
});
}
}
});
frm.events.set_employee_query(frm);
},
frappe.ui.form.on("Training Event Employee", {
employee: function (frm) {
set_employee_query: function(frm) {
let emp = [];
for (let d in frm.doc.employees) {
if (frm.doc.employees[d].employee) {
@@ -40,3 +39,10 @@ frappe.ui.form.on("Training Event Employee", {
});
}
});
frappe.ui.form.on("Training Event Employee", {
employee: function(frm) {
frm.events.set_employee_query(frm);
}
});

View File

@@ -19,6 +19,7 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Employee",
"no_copy": 1,
"options": "Employee"
},
{
@@ -68,7 +69,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-05-21 12:41:59.336237",
"modified": "2021-07-02 17:20:27.630176",
"modified_by": "Administrator",
"module": "HR",
"name": "Training Event Employee",

View File

@@ -35,7 +35,9 @@
"no_copy": 1,
"options": "Loan Security Pledge",
"print_hide": 1,
"read_only": 1
"read_only": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fetch_from": "loan_application.applicant",
@@ -45,47 +47,63 @@
"in_standard_filter": 1,
"label": "Applicant",
"options": "applicant_type",
"reqd": 1
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "loan_security_details_section",
"fieldtype": "Section Break",
"label": "Loan Security Details"
"label": "Loan Security Details",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "loan",
"fieldtype": "Link",
"label": "Loan",
"options": "Loan"
"options": "Loan",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "loan_application",
"fieldtype": "Link",
"label": "Loan Application",
"options": "Loan Application"
"options": "Loan Application",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "total_security_value",
"fieldtype": "Currency",
"label": "Total Security Value",
"options": "Company:company:default_currency",
"read_only": 1
"read_only": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "maximum_loan_value",
"fieldtype": "Currency",
"label": "Maximum Loan Value",
"options": "Company:company:default_currency",
"read_only": 1
"read_only": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "loan_details_section",
"fieldtype": "Section Break",
"label": "Loan Details"
"label": "Loan Details",
"show_days": 1,
"show_seconds": 1
},
{
"default": "Requested",
@@ -94,37 +112,49 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Status",
"options": "Requested\nUnpledged\nPledged\nPartially Pledged",
"read_only": 1
"options": "Requested\nUnpledged\nPledged\nPartially Pledged\nCancelled",
"read_only": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "pledge_time",
"fieldtype": "Datetime",
"label": "Pledge Time",
"read_only": 1
"read_only": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "securities",
"fieldtype": "Table",
"label": "Securities",
"options": "Pledge",
"reqd": 1
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "section_break_10",
"fieldtype": "Section Break",
"label": "Totals"
"label": "Totals",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"reqd": 1
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fetch_from": "loan.applicant_type",
@@ -132,35 +162,45 @@
"fieldtype": "Select",
"label": "Applicant Type",
"options": "Employee\nMember\nCustomer",
"reqd": 1
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"collapsible": 1,
"fieldname": "more_information_section",
"fieldtype": "Section Break",
"label": "More Information"
"label": "More Information",
"show_days": 1,
"show_seconds": 1
},
{
"allow_on_submit": 1,
"fieldname": "reference_no",
"fieldtype": "Data",
"label": "Reference No"
"label": "Reference No",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "column_break_18",
"fieldtype": "Column Break"
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
},
{
"allow_on_submit": 1,
"fieldname": "description",
"fieldtype": "Text",
"label": "Description"
"label": "Description",
"show_days": 1,
"show_seconds": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-04-19 18:23:16.953305",
"modified": "2021-06-29 17:15:16.082256",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Security Pledge",

View File

@@ -23,6 +23,12 @@ class LoanSecurityPledge(Document):
update_shortfall_status(self.loan, self.total_security_value)
update_loan(self.loan, self.maximum_loan_value)
def on_cancel(self):
if self.loan:
self.db_set("status", "Cancelled")
self.db_set("pledge_time", None)
update_loan(self.loan, self.maximum_loan_value, cancel=1)
def validate_duplicate_securities(self):
security_list = []
for security in self.securities:
@@ -36,7 +42,7 @@ class LoanSecurityPledge(Document):
existing_pledge = ''
if self.loan:
existing_pledge = frappe.db.get_value('Loan Security Pledge', {'loan': self.loan}, ['name'])
existing_pledge = frappe.db.get_value('Loan Security Pledge', {'loan': self.loan, 'docstatus': 1}, ['name'])
if existing_pledge:
loan_security_type = frappe.db.get_value('Pledge', {'parent': existing_pledge}, ['loan_security_type'])
@@ -77,8 +83,12 @@ class LoanSecurityPledge(Document):
self.total_security_value = total_security_value
self.maximum_loan_value = maximum_loan_value
def update_loan(loan, maximum_value_against_pledge):
def update_loan(loan, maximum_value_against_pledge, cancel=0):
maximum_loan_value = frappe.db.get_value('Loan', {'name': loan}, ['maximum_loan_amount'])
frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_amount=%s, is_secured_loan=1
WHERE name=%s""", (maximum_loan_value + maximum_value_against_pledge, loan))
if cancel:
frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_amount=%s
WHERE name=%s""", (maximum_loan_value - maximum_value_against_pledge, loan))
else:
frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_amount=%s, is_secured_loan=1
WHERE name=%s""", (maximum_loan_value + maximum_value_against_pledge, loan))

View File

@@ -13,7 +13,7 @@ frappe.ui.form.on('Blanket Order', {
refresh: function(frm) {
erpnext.hide_company();
if (frm.doc.customer && frm.doc.docstatus === 1) {
if (frm.doc.customer && frm.doc.docstatus === 1 && frm.doc.to_date > frappe.datetime.get_today()) {
frm.add_custom_button(__("Sales Order"), function() {
frappe.model.open_mapped_doc({
method: "erpnext.manufacturing.doctype.blanket_order.blanket_order.make_order",

View File

@@ -1,4 +1,5 @@
{
"actions": [],
"autoname": "naming_series:",
"creation": "2018-05-24 07:18:08.256060",
"doctype": "DocType",
@@ -79,6 +80,7 @@
"reqd": 1
},
{
"allow_on_submit": 1,
"fieldname": "to_date",
"fieldtype": "Date",
"label": "To Date",
@@ -129,8 +131,10 @@
"label": "Terms and Conditions Details"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"modified": "2019-11-18 19:37:37.151686",
"links": [],
"modified": "2021-06-29 00:30:30.621636",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Blanket Order",

View File

@@ -325,8 +325,7 @@ frappe.ui.form.on("BOM", {
freeze: true,
args: {
update_parent: true,
from_child_bom:false,
save: frm.doc.docstatus === 1 ? true : false
from_child_bom:false
},
callback: function(r) {
refresh_field("items");

View File

@@ -36,6 +36,9 @@
"materials_section",
"inspection_required",
"quality_inspection_template",
"column_break_31",
"bom_level",
"section_break_33",
"items",
"scrap_section",
"scrap_items",
@@ -513,6 +516,22 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "column_break_31",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "bom_level",
"fieldtype": "Int",
"label": "BOM Level",
"read_only": 1
},
{
"fieldname": "section_break_33",
"fieldtype": "Section Break",
"hide_border": 1
}
],
"icon": "fa fa-sitemap",
@@ -520,7 +539,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
"modified": "2021-03-16 12:25:09.081968",
"modified": "2021-05-16 12:25:09.081968",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM",

View File

@@ -154,6 +154,7 @@ class BOM(WebsiteGenerator):
self.calculate_cost()
self.update_stock_qty()
self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False)
self.set_bom_level()
def get_context(self, context):
context.parents = [{'name': 'boms', 'title': _('All BOMs') }]
@@ -676,6 +677,19 @@ class BOM(WebsiteGenerator):
"""Get a complete tree representation preserving order of child items."""
return BOMTree(self.name)
def set_bom_level(self, update=False):
levels = []
self.bom_level = 0
for row in self.items:
if row.bom_no:
levels.append(frappe.get_cached_value("BOM", row.bom_no, "bom_level") or 0)
if levels:
self.bom_level = max(levels) + 1
if update:
self.db_set("bom_level", self.bom_level)
def get_bom_item_rate(args, bom_doc):
if bom_doc.rm_cost_as_per == 'Valuation Rate':
@@ -860,7 +874,7 @@ def get_children(doctype, parent=None, is_root=False, **filters):
frappe.form_dict.parent = parent
if frappe.form_dict.parent:
bom_doc = frappe.get_doc("BOM", frappe.form_dict.parent)
bom_doc = frappe.get_cached_doc("BOM", frappe.form_dict.parent)
frappe.has_permission("BOM", doc=bom_doc, throw=True)
bom_items = frappe.get_all('BOM Item',
@@ -871,7 +885,7 @@ def get_children(doctype, parent=None, is_root=False, **filters):
item_names = tuple(d.get('item_code') for d in bom_items)
items = frappe.get_list('Item',
fields=['image', 'description', 'name', 'stock_uom', 'item_name'],
fields=['image', 'description', 'name', 'stock_uom', 'item_name', 'is_sub_contracted_item'],
filters=[['name', 'in', item_names]]) # to get only required item dicts
for bom_item in bom_items:
@@ -884,6 +898,7 @@ def get_children(doctype, parent=None, is_root=False, **filters):
bom_item.parent_bom_qty = bom_doc.quantity
bom_item.expandable = 0 if bom_item.value in ('', None) else 1
bom_item.image = frappe.db.escape(bom_item.image)
return bom_items
@@ -1100,6 +1115,8 @@ def make_variant_bom(source_name, bom_no, item, variant_items, target_doc=None):
},
'BOM Item': {
'doctype': 'BOM Item',
# stop get_mapped_doc copying parent bom_no to children
'field_no_map': ['bom_no'],
'condition': lambda doc: doc.has_variants == 0
},
}, target_doc, postprocess)

View File

@@ -1,13 +1,31 @@
<div style="padding: 15px;">
{% if data.image %}
<img class="responsive" src={{ data.image }}>
<hr style="margin: 15px -15px;">
{% endif %}
<h4>
{{ __("Description") }}
</h4>
<div style="padding-top: 10px;">
{{ data.description }}
<div class="row mb-5">
<div class="col-md-5" style="max-height: 500px">
{% if data.image %}
<div class="border image-field " style="overflow: hidden;border-color:#e6e6e6">
<img class="responsive" src={{ data.image }}>
</div>
{% endif %}
</div>
<div class="col-md-7 h-500">
<h4>
{{ __("Description") }}
</h4>
<div style="padding-top: 10px;">
{{ data.description }}
</div>
<hr style="margin: 15px -15px;">
<p>
{% if data.value %}
<a style="margin-right: 7px; margin-bottom: 7px" class="btn btn-default btn-xs" href="#Form/BOM/{{ data.value }}">
{{ __("Open BOM {0}", [data.value.bold()]) }}</a>
{% endif %}
{% if data.item_code %}
<a class="btn btn-default btn-xs" href="#Form/Item/{{ data.item_code }}">
{{ __("Open Item {0}", [data.item_code.bold()]) }}</a>
{% endif %}
</p>
</div>
</div>
<hr style="margin: 15px -15px;">
<p>

View File

@@ -64,7 +64,7 @@ frappe.treeview_settings["BOM"] = {
if(node.is_root && node.data.value!="BOM") {
frappe.model.with_doc("BOM", node.data.value, function() {
var bom = frappe.model.get_doc("BOM", node.data.value);
node.data.image = bom.image || "";
node.data.image = escape(bom.image) || "";
node.data.description = bom.description || "";
});
}

View File

@@ -192,15 +192,20 @@ class JobCard(Document):
"completed_qty": args.get("completed_qty") or 0.0
})
elif args.get("start_time"):
for name in employees:
self.append("time_logs", {
"from_time": get_datetime(args.get("start_time")),
"employee": name.get('employee'),
"operation": args.get("sub_operation"),
"completed_qty": 0.0
})
new_args = {
"from_time": get_datetime(args.get("start_time")),
"operation": args.get("sub_operation"),
"completed_qty": 0.0
}
if not self.employee:
if employees:
for name in employees:
new_args.employee = name.get('employee')
self.add_start_time_log(new_args)
else:
self.add_start_time_log(new_args)
if not self.employee and employees:
self.set_employees(employees)
if self.status == "On Hold":
@@ -208,6 +213,9 @@ class JobCard(Document):
self.save()
def add_start_time_log(self, args):
self.append("time_logs", args)
def set_employees(self, employees):
for name in employees:
self.append('employee', {

View File

@@ -4,7 +4,7 @@
frappe.ui.form.on('Production Plan', {
setup: function(frm) {
frm.custom_make_buttons = {
'Work Order': 'Work Order',
'Work Order': 'Work Order / Subcontract PO',
'Material Request': 'Material Request',
};
@@ -68,17 +68,13 @@ frappe.ui.form.on('Production Plan', {
frm.trigger("show_progress");
if (frm.doc.status !== "Completed") {
if (frm.doc.po_items && frm.doc.status !== "Closed") {
frm.add_custom_button(__("Work Order"), ()=> {
frm.trigger("make_work_order");
}, __('Create'));
}
frm.add_custom_button(__("Work Order Tree"), ()=> {
frappe.set_route('Tree', 'Work Order', {production_plan: frm.doc.name});
}, __('View'));
if (frm.doc.mr_items && !in_list(['Material Requested', 'Closed'], frm.doc.status)) {
frm.add_custom_button(__("Material Request"), ()=> {
frm.trigger("make_material_request");
}, __('Create'));
}
frm.add_custom_button(__("Production Plan Summary"), ()=> {
frappe.set_route('query-report', 'Production Plan Summary', {production_plan: frm.doc.name});
}, __('View'));
if (frm.doc.status === "Closed") {
frm.add_custom_button(__("Re-open"), function() {
@@ -89,6 +85,18 @@ frappe.ui.form.on('Production Plan', {
frm.events.close_open_production_plan(frm, true);
}, __("Status"));
}
if (frm.doc.po_items && frm.doc.status !== "Closed") {
frm.add_custom_button(__("Work Order / Subcontract PO"), ()=> {
frm.trigger("make_work_order");
}, __('Create'));
}
if (frm.doc.mr_items && !in_list(['Material Requested', 'Closed'], frm.doc.status)) {
frm.add_custom_button(__("Material Request"), ()=> {
frm.trigger("make_material_request");
}, __('Create'));
}
}
}
@@ -233,6 +241,17 @@ frappe.ui.form.on('Production Plan', {
});
},
get_sub_assembly_items: function(frm) {
frappe.call({
method: "get_sub_assembly_items",
freeze: true,
doc: frm.doc,
callback: function() {
refresh_field("sub_assembly_items");
}
});
},
get_items_for_mr: function(frm) {
if (!frm.doc.for_warehouse) {
frappe.throw(__("Select warehouse for material requests"));

View File

@@ -32,6 +32,9 @@
"po_items",
"section_break_25",
"prod_plan_references",
"section_break_24",
"get_sub_assembly_items",
"sub_assembly_items",
"material_request_planning",
"include_non_stock_items",
"include_subcontracted_items",
@@ -187,7 +190,7 @@
"depends_on": "get_items_from",
"fieldname": "get_items",
"fieldtype": "Button",
"label": "Get Items For Work Order"
"label": "Get Finished Goods for Manufacture"
},
{
"fieldname": "po_items",
@@ -199,7 +202,7 @@
{
"fieldname": "material_request_planning",
"fieldtype": "Section Break",
"label": "Material Request Planning"
"label": "Material Requirement Planning"
},
{
"default": "1",
@@ -237,12 +240,13 @@
},
{
"fieldname": "section_break_27",
"fieldtype": "Section Break"
"fieldtype": "Section Break",
"hide_border": 1
},
{
"fieldname": "mr_items",
"fieldtype": "Table",
"label": "Material Request Plan Item",
"label": "Raw Materials",
"no_copy": 1,
"options": "Material Request Plan Item"
},
@@ -337,13 +341,30 @@
"hidden": 1,
"label": "Production Plan Item Reference",
"options": "Production Plan Item Reference"
},
{
"fieldname": "section_break_24",
"fieldtype": "Section Break",
"hide_border": 1
},
{
"fieldname": "sub_assembly_items",
"fieldtype": "Table",
"label": "Sub Assembly Items",
"no_copy": 1,
"options": "Production Plan Sub Assembly Item"
},
{
"fieldname": "get_sub_assembly_items",
"fieldtype": "Button",
"label": "Get Sub Assembly Items"
}
],
"icon": "fa fa-calendar",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-05-24 16:59:03.643211",
"modified": "2021-06-28 20:00:33.905114",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan",

View File

@@ -5,10 +5,11 @@
from __future__ import unicode_literals
import frappe, json, copy
from frappe import msgprint, _
from six import string_types, iteritems
from six import iteritems
from frappe.model.document import Document
from frappe.utils import cstr, flt, cint, nowdate, add_days, comma_and, now_datetime, ceil
from frappe.utils import (flt, cint, nowdate, add_days, comma_and, now_datetime,
ceil, get_link_to_form, getdate)
from frappe.utils.csvutils import build_csv_response
from erpnext.manufacturing.doctype.bom.bom import validate_bom_no, get_children
from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
@@ -349,49 +350,88 @@ class ProductionPlan(Document):
@frappe.whitelist()
def make_work_order(self):
wo_list = []
wo_list, po_list = [], []
subcontracted_po = {}
self.validate_data()
self.make_work_order_for_finished_goods(wo_list)
self.make_work_order_for_subassembly_items(wo_list, subcontracted_po)
self.make_subcontracted_purchase_order(subcontracted_po, po_list)
self.show_list_created_message('Work Order', wo_list)
self.show_list_created_message('Purchase Order', po_list)
def make_work_order_for_finished_goods(self, wo_list):
items_data = self.get_production_items()
for key, item in items_data.items():
if self.sub_assembly_items:
item['use_multi_level_bom'] = 0
work_order = self.create_work_order(item)
if work_order:
wo_list.append(work_order)
if item.get("make_work_order_for_sub_assembly_items"):
work_orders = self.make_work_order_for_sub_assembly_items(item)
wo_list.extend(work_orders)
def make_work_order_for_subassembly_items(self, wo_list, subcontracted_po):
for row in self.sub_assembly_items:
if row.type_of_manufacturing == 'Subcontract':
subcontracted_po.setdefault(row.supplier, []).append(row)
continue
args = {}
self.prepare_args_for_sub_assembly_items(row, args)
work_order = self.create_work_order(args)
if work_order:
wo_list.append(work_order)
def make_subcontracted_purchase_order(self, subcontracted_po, purchase_orders):
if not subcontracted_po:
return
for supplier, po_list in subcontracted_po.items():
po = frappe.new_doc('Purchase Order')
po.supplier = supplier
po.schedule_date = getdate(po_list[0].schedule_date) if po_list[0].schedule_date else nowdate()
po.is_subcontracted_item = 'Yes'
for row in po_list:
args = {
'item_code': row.production_item,
'warehouse': row.fg_warehouse,
'production_plan_sub_assembly_item': row.name,
'bom': row.bom_no,
'production_plan': self.name
}
for field in ['schedule_date', 'qty', 'uom', 'stock_uom', 'item_name',
'description', 'production_plan_item']:
args[field] = row.get(field)
po.append('items', args)
po.set_missing_values()
po.flags.ignore_mandatory = True
po.flags.ignore_validate = True
po.insert()
purchase_orders.append(po.name)
def show_list_created_message(self, doctype, doc_list=None):
if not doc_list:
return
frappe.flags.mute_messages = False
if doc_list:
doc_list = [get_link_to_form(doctype, p) for p in doc_list]
msgprint(_("{0} created").format(comma_and(doc_list)))
if wo_list:
wo_list = ["""<a href="/app/Form/Work Order/%s" target="_blank">%s</a>""" % \
(p, p) for p in wo_list]
msgprint(_("{0} created").format(comma_and(wo_list)))
else :
msgprint(_("No Work Orders created"))
def prepare_args_for_sub_assembly_items(self, row, args):
for field in ["production_item", "item_name", "qty", "fg_warehouse",
"description", "bom_no", "stock_uom", "bom_level", "production_plan_item"]:
args[field] = row.get(field)
def make_work_order_for_sub_assembly_items(self, item):
work_orders = []
bom_data = {}
get_sub_assembly_items(item.get("bom_no"), bom_data, item.get("qty"))
for key, data in bom_data.items():
data.update({
'qty': data.get("stock_qty"),
'production_plan': self.name,
'use_multi_level_bom': item.get("use_multi_level_bom"),
'company': self.company,
'fg_warehouse': item.get("fg_warehouse"),
'update_consumed_material_cost_in_project': 0
})
work_order = self.create_work_order(data)
if work_order:
work_orders.append(work_order)
return work_orders
args.update({
"use_multi_level_bom": 0,
"production_plan": self.name,
"production_plan_sub_assembly_item": row.name
})
def create_work_order(self, item):
from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError, get_default_warehouse
@@ -476,9 +516,32 @@ class ProductionPlan(Document):
else :
msgprint(_("No material request created"))
@frappe.whitelist()
def get_sub_assembly_items(self, manufacturing_type=None):
self.sub_assembly_items = []
for row in self.po_items:
bom_data = []
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty)
self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
self.save()
def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None):
bom_data = sorted(bom_data, key = lambda i: i.bom_level)
for data in bom_data:
data.qty = data.stock_qty
data.production_plan_item = row.name
data.fg_warehouse = row.warehouse
data.schedule_date = row.planned_start_date
data.type_of_manufacturing = manufacturing_type or ("Subcontract" if data.is_sub_contracted_item
else "In House")
self.append("sub_assembly_items", data)
@frappe.whitelist()
def download_raw_materials(doc, warehouses=None):
if isinstance(doc, string_types):
if isinstance(doc, str):
doc = frappe._dict(json.loads(doc))
item_list = [['Item Code', 'Description', 'Stock UOM', 'Warehouse', 'Required Qty as per BOM',
@@ -660,7 +723,7 @@ def get_sales_orders(self):
@frappe.whitelist()
def get_bin_details(row, company, for_warehouse=None, all_warehouse=False):
if isinstance(row, string_types):
if isinstance(row, str):
row = frappe._dict(json.loads(row))
company = frappe.db.escape(company)
@@ -684,8 +747,11 @@ def get_bin_details(row, company, for_warehouse=None, all_warehouse=False):
group by item_code, warehouse
""".format(conditions=conditions), { "item_code": row['item_code'] }, as_dict=1)
def get_warehouse_list(warehouses, warehouse_list=[]):
if isinstance(warehouses, string_types):
def get_warehouse_list(warehouses, warehouse_list=None):
if not warehouse_list:
warehouse_list = []
if isinstance(warehouses, str):
warehouses = json.loads(warehouses)
for row in warehouses:
@@ -697,7 +763,7 @@ def get_warehouse_list(warehouses, warehouse_list=[]):
@frappe.whitelist()
def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_data=None):
if isinstance(doc, string_types):
if isinstance(doc, str):
doc = frappe._dict(json.loads(doc))
warehouse_list = []
@@ -726,6 +792,9 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
so_item_details = frappe._dict()
for data in po_items:
if not data.get("include_exploded_items") and doc.get("sub_assembly_items"):
data["include_exploded_items"] = 1
planned_qty = data.get('required_qty') or data.get('planned_qty')
ignore_existing_ordered_qty = data.get('ignore_existing_ordered_qty') or ignore_existing_ordered_qty
warehouse = doc.get('for_warehouse')
@@ -857,23 +926,28 @@ def get_item_data(item_code):
# "description": item_details.get("description")
}
def get_sub_assembly_items(bom_no, bom_data, to_produce_qty):
def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0):
data = get_children('BOM', parent = bom_no)
for d in data:
if d.expandable:
key = (d.name, d.value)
if key not in bom_data:
bom_data.setdefault(key, {
'stock_qty': 0,
'description': d.description,
'production_item': d.item_code,
'item_name': d.item_name,
'stock_uom': d.stock_uom,
'uom': d.stock_uom,
'bom_no': d.value
})
parent_item_code = frappe.get_cached_value("BOM", bom_no, "item")
bom_level = (frappe.get_cached_value("BOM", d.value, "bom_level")
if d.value else 0)
bom_item = bom_data.get(key)
bom_item["stock_qty"] += (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty)
stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty)
bom_data.append(frappe._dict({
'parent_item_code': parent_item_code,
'description': d.description,
'production_item': d.item_code,
'item_name': d.item_name,
'stock_uom': d.stock_uom,
'uom': d.stock_uom,
'bom_no': d.value,
'is_sub_contracted_item': d.is_sub_contracted_item,
'bom_level': bom_level,
'indent': indent,
'stock_qty': stock_qty
}))
get_sub_assembly_items(bom_item.get("bom_no"), bom_data, bom_item["stock_qty"])
if d.value:
get_sub_assembly_items(d.value, bom_data, stock_qty, indent=indent+1)

View File

@@ -9,5 +9,9 @@ def get_data():
'label': _('Transactions'),
'items': ['Work Order', 'Material Request']
},
{
'label': _('Subcontract'),
'items': ['Purchase Order']
},
]
}

View File

@@ -169,7 +169,7 @@ class TestProductionPlan(unittest.TestCase):
pln.get_items()
pln.submit()
self.assertTrue(pln.po_items[0].planned_qty, 3)
self.assertTrue(pln.po_items[0].planned_qty, 3)
pln.make_work_order()
work_order = frappe.db.get_value('Work Order', {
@@ -193,10 +193,10 @@ class TestProductionPlan(unittest.TestCase):
for so_item in so_items:
so_wo_qty = frappe.db.get_value('Sales Order Item', so_item, 'work_order_qty')
self.assertEqual(so_wo_qty, 0.0)
latest_plan = frappe.get_doc('Production Plan', pln.name)
latest_plan.cancel()
def test_pp_to_mr_customer_provided(self):
#Material Request from Production Plan for Customer Provided
create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)
@@ -236,10 +236,10 @@ class TestProductionPlan(unittest.TestCase):
pln.append("po_items", {
"item_code": item_code,
"bom_no": frappe.db.get_value('BOM', {'item': "Test BOM 1"}),
"planned_qty": 3,
"make_work_order_for_sub_assembly_items": 1
"planned_qty": 3
})
pln.get_sub_assembly_items('In House')
pln.submit()
pln.make_work_order()

View File

@@ -9,18 +9,17 @@
"include_exploded_items",
"item_code",
"bom_no",
"planned_qty",
"column_break_6",
"make_work_order_for_sub_assembly_items",
"planned_qty",
"warehouse",
"planned_start_date",
"section_break_9",
"pending_qty",
"ordered_qty",
"produced_qty",
"column_break_17",
"description",
"stock_uom",
"produced_qty",
"reference_section",
"sales_order",
"sales_order_item",
@@ -32,11 +31,10 @@
],
"fields": [
{
"columns": 2,
"default": "0",
"columns": 1,
"default": "1",
"fieldname": "include_exploded_items",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Include Exploded Items"
},
{
@@ -80,13 +78,6 @@
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "If enabled, system will create the work order for the exploded items against which BOM is available.",
"fieldname": "make_work_order_for_sub_assembly_items",
"fieldtype": "Check",
"label": "Make Work Order for Sub Assembly Items"
},
{
"fieldname": "warehouse",
"fieldtype": "Link",
@@ -218,7 +209,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-04-28 19:14:57.772123",
"modified": "2021-06-28 18:31:06.822168",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan Item",

View File

@@ -0,0 +1,202 @@
{
"actions": [],
"creation": "2020-12-27 16:08:36.127199",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"production_item",
"item_name",
"fg_warehouse",
"parent_item_code",
"schedule_date",
"column_break_3",
"qty",
"bom_no",
"bom_level",
"type_of_manufacturing",
"supplier",
"work_order_details_section",
"work_order",
"purchase_order",
"production_plan_item",
"column_break_7",
"produced_qty",
"received_qty",
"indent",
"section_break_19",
"uom",
"stock_uom",
"column_break_22",
"description"
],
"fields": [
{
"fetch_from": "sub_assembly_item_code.item_name",
"fieldname": "item_name",
"fieldtype": "Data",
"label": "Item Name",
"read_only": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.type_of_manufacturing == \"In House\"",
"fieldname": "work_order_details_section",
"fieldtype": "Section Break",
"label": "Reference"
},
{
"fieldname": "work_order",
"fieldtype": "Link",
"label": "Work Order",
"options": "Work Order",
"read_only": 1
},
{
"fieldname": "column_break_7",
"fieldtype": "Column Break"
},
{
"columns": 1,
"fieldname": "qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Required Qty",
"read_only": 1
},
{
"fieldname": "purchase_order",
"fieldtype": "Link",
"label": "Purchase Order",
"options": "Purchase Order",
"read_only": 1
},
{
"fieldname": "received_qty",
"fieldtype": "Float",
"label": "Received Qty"
},
{
"fieldname": "bom_no",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Bom No",
"options": "BOM"
},
{
"fieldname": "production_plan_item",
"fieldtype": "Data",
"hidden": 1,
"label": "Production Plan Item",
"read_only": 1
},
{
"fieldname": "parent_item_code",
"fieldtype": "Link",
"label": "Finished Good",
"options": "Item",
"read_only": 1
},
{
"columns": 1,
"fetch_from": "bom_no.bom_level",
"fieldname": "bom_level",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Level (BOM)",
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "section_break_19",
"fieldtype": "Section Break",
"label": "Item Details"
},
{
"fieldname": "uom",
"fieldtype": "Link",
"label": "UOM",
"options": "UOM",
"read_only": 1
},
{
"fieldname": "stock_uom",
"fieldtype": "Link",
"label": "Stock UOM",
"options": "UOM",
"read_only": 1
},
{
"fieldname": "column_break_22",
"fieldtype": "Column Break"
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"label": "description",
"read_only": 1
},
{
"fieldname": "production_item",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Sub Assembly Item Code",
"options": "Item",
"read_only": 1
},
{
"fieldname": "indent",
"fieldtype": "Int",
"label": "Indent"
},
{
"fieldname": "fg_warehouse",
"fieldtype": "Link",
"label": "Target Warehouse",
"options": "Warehouse"
},
{
"fieldname": "produced_qty",
"fieldtype": "Data",
"label": "Produced Quantity",
"read_only": 1
},
{
"default": "In House",
"fieldname": "type_of_manufacturing",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Manufacturing Type",
"options": "In House\nSubcontract"
},
{
"fieldname": "supplier",
"fieldtype": "Link",
"label": "Supplier",
"mandatory_depends_on": "eval:doc.type_of_manufacturing == 'Subcontract'",
"options": "Supplier"
},
{
"fieldname": "schedule_date",
"fieldtype": "Datetime",
"in_list_view": 1,
"label": "Schedule Date"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-06-28 20:10:56.296410",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan Sub Assembly Item",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class ProductionPlanSubAssemblyItem(Document):
pass

View File

@@ -64,11 +64,16 @@
"description",
"stock_uom",
"column_break2",
"references_section",
"material_request",
"material_request_item",
"sales_order_item",
"column_break_61",
"production_plan",
"production_plan_item",
"production_plan_sub_assembly_item",
"parent_work_order",
"bom_level",
"product_bundle_item",
"amended_from"
],
@@ -546,17 +551,26 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
}
},
{
"fieldname": "production_plan_sub_assembly_item",
"fieldtype": "Data",
"label": "Production Plan Sub-assembly Item",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
}
],
"icon": "fa fa-cogs",
"idx": 1,
"image_field": "image",
"is_submittable": 1,
"links": [],
"modified": "2021-06-20 15:19:14.902699",
"modified": "2021-06-28 16:19:14.902699",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order",
"nsm_parent_field": "parent_work_order",
"owner": "Administrator",
"permissions": [
{

View File

@@ -483,7 +483,7 @@ class WorkOrder(Document):
self.set('operations', [])
if not self.bom_no:
if not self.bom_no or not frappe.get_cached_value('BOM', self.bom_no, 'with_operations'):
return
operations = []
@@ -590,6 +590,7 @@ class WorkOrder(Document):
def validate_operation_time(self):
for d in self.operations:
if not d.time_in_mins > 0:
print(self.bom_no, self.production_item)
frappe.throw(_("Operation Time must be greater than 0 for Operation {0}").format(d.operation))
def update_required_items(self):

View File

@@ -20,17 +20,20 @@ def get_exploded_items(bom, data, indent=0, qty=1):
fields= ['qty','bom_no','qty','scrap','item_code','item_name','description','uom'])
for item in exploded_items:
print(item.bom_no, indent)
item["indent"] = indent
data.append({
'item_code': item.item_code,
'item_name': item.item_name,
'indent': indent,
'bom_level': (frappe.get_cached_value("BOM", item.bom_no, "bom_level")
if item.bom_no else ""),
'bom': item.bom_no,
'qty': item.qty * qty,
'uom': item.uom,
'description': item.description,
'scrap': item.scrap
})
})
if item.bom_no:
get_exploded_items(item.bom_no, data, indent=indent+1, qty=item.qty)
@@ -68,6 +71,12 @@ def get_columns():
"fieldname": "uom",
"width": 100
},
{
"label": "BOM Level",
"fieldtype": "Data",
"fieldname": "bom_level",
"width": 100
},
{
"label": "Standard Description",
"fieldtype": "data",

View File

@@ -70,12 +70,12 @@ def get_bom_stock(filters):
ON bom_item.item_code = ledger.item_code
{conditions}
WHERE
bom_item.parent = '{bom}' and bom_item.parenttype='BOM'
bom_item.parent = {bom} and bom_item.parenttype='BOM'
GROUP BY bom_item.item_code""".format(
qty_field=qty_field,
table=table,
conditions=conditions,
bom=bom,
bom=frappe.db.escape(bom),
qty_to_produce=qty_to_produce or 1)
)

View File

@@ -68,6 +68,18 @@ frappe.query_reports["Job Card Summary"] = {
get_data: function(txt) {
return frappe.db.get_link_options('Item', txt);
}
},
{
label: __("Workstation"),
fieldname: "workstation",
fieldtype: "Link",
options: "Workstation"
},
{
label: __("Operation"),
fieldname: "operation",
fieldtype: "Link",
options: "Operation"
}
]
};

View File

@@ -1,14 +1,16 @@
{
"add_total_row": 0,
"add_total_row": 1,
"columns": [],
"creation": "2020-04-20 12:00:21.436619",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"letter_head": "Gadgets International",
"modified": "2020-04-20 12:00:21.436619",
"letter_head": "",
"modified": "2020-12-30 11:49:21.713561",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card Summary",

View File

@@ -0,0 +1,32 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Production Plan Summary"] = {
"filters": [
{
fieldname: "production_plan",
label: __("Production Plan"),
fieldtype: "Link",
options: "Production Plan",
reqd: 1,
get_query: function() {
return {
filters: {
"docstatus": 1
}
};
}
}
],
"formatter": function(value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
if (column.fieldname == "document_name") {
var color = data.pending_qty > 0 ? 'red': 'green';
value = `<a style='color:${color}' href="#Form/${data['document_type']}/${data['document_name']}" data-doctype="${data['document_type']}">${data['document_name']}</a>`;
}
return value;
},
};

View File

@@ -0,0 +1,26 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2020-12-27 11:43:39.781793",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2020-12-27 11:43:42.677584",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan Summary",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Production Plan",
"report_name": "Production Plan Summary",
"report_type": "Script Report",
"roles": [
{
"role": "Manufacturing User"
}
]
}

View File

@@ -0,0 +1,136 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.utils import flt
def execute(filters=None):
columns, data = [], []
data = get_data(filters)
columns = get_column(filters)
return columns, data
def get_data(filters):
data = []
order_details = {}
get_work_order_details(filters, order_details)
get_purchase_order_details(filters, order_details)
get_production_plan_item_details(filters, data, order_details)
return data
def get_production_plan_item_details(filters, data, order_details):
itemwise_indent = {}
production_plan_doc = frappe.get_cached_doc("Production Plan", filters.get("production_plan"))
for row in production_plan_doc.po_items:
work_order = frappe.get_cached_value("Work Order", {"production_plan_item": row.name,
"bom_no": row.bom_no, "production_item": row.item_code}, "name")
if row.item_code not in itemwise_indent:
itemwise_indent.setdefault(row.item_code, {})
data.append({
"indent": 0,
"item_code": row.item_code,
"item_name": frappe.get_cached_value("Item", row.item_code, "item_name"),
"qty": row.planned_qty,
"document_type": "Work Order",
"document_name": work_order,
"bom_level": frappe.get_cached_value("BOM", row.bom_no, "bom_level"),
"produced_qty": order_details.get((work_order, row.item_code)).get("produced_qty"),
"pending_qty": flt(row.planned_qty) - flt(order_details.get((work_order, row.item_code)).get("produced_qty"))
})
get_production_plan_sub_assembly_item_details(filters, row, production_plan_doc, data, order_details)
def get_production_plan_sub_assembly_item_details(filters, row, production_plan_doc, data, order_details):
for item in production_plan_doc.sub_assembly_items:
if row.name == item.production_plan_item:
subcontracted_item = (item.type_of_manufacturing == 'Subcontract')
if subcontracted_item:
docname = frappe.get_cached_value("Purchase Order Item",
{"production_plan_sub_assembly_item": item.name, "docstatus": ("<", 2)}, "parent")
else:
docname = frappe.get_cached_value("Work Order",
{"production_plan_sub_assembly_item": item.name, "docstatus": ("<", 2)}, "name")
data.append({
"indent": 1,
"item_code": item.production_item,
"item_name": item.item_name,
"qty": item.qty,
"document_type": "Work Order" if not subcontracted_item else "Purchase Order",
"document_name": docname,
"bom_level": item.bom_level,
"produced_qty": order_details.get((docname, item.production_item)).get("produced_qty"),
"pending_qty": flt(item.qty) - flt(order_details.get((docname, item.production_item)).get("produced_qty"))
})
def get_work_order_details(filters, order_details):
for row in frappe.get_all("Work Order", filters = {"production_plan": filters.get("production_plan")},
fields=["name", "produced_qty", "production_plan", "production_item"]):
order_details.setdefault((row.name, row.production_item), row)
def get_purchase_order_details(filters, order_details):
for row in frappe.get_all("Purchase Order Item", filters = {"production_plan": filters.get("production_plan")},
fields=["parent", "received_qty as produced_qty", "item_code"]):
order_details.setdefault((row.parent, row.item_code), row)
def get_column(filters):
return [
{
"label": "Finished Good",
"fieldtype": "Link",
"fieldname": "item_code",
"width": 300,
"options": "Item"
},
{
"label": "Item Name",
"fieldtype": "data",
"fieldname": "item_name",
"width": 100
},
{
"label": "Document Type",
"fieldtype": "Link",
"fieldname": "document_type",
"width": 150,
"options": "DocType"
},
{
"label": "Document Name",
"fieldtype": "Dynamic Link",
"fieldname": "document_name",
"width": 150
},
{
"label": "BOM Level",
"fieldtype": "Int",
"fieldname": "bom_level",
"width": 100
},
{
"label": "Order Qty",
"fieldtype": "Float",
"fieldname": "qty",
"width": 120
},
{
"label": "Received Qty",
"fieldtype": "Float",
"fieldname": "produced_qty",
"width": 160
},
{
"label": "Pending Qty",
"fieldtype": "Float",
"fieldname": "pending_qty",
"width": 110
}
]

View File

@@ -19,7 +19,7 @@ def execute(filters=None):
return columns, data, None, chart_data
def get_data(filters):
query_filters = {"docstatus": 1}
query_filters = {"docstatus": ("<", 2)}
fields = ["name", "status", "sales_order", "production_item", "qty", "produced_qty",
"planned_start_date", "planned_end_date", "actual_start_date", "actual_end_date", "lead_time"]
@@ -62,7 +62,8 @@ def get_chart_based_on_status(data):
"Not Started": 0,
"In Process": 0,
"Stopped": 0,
"Completed": 0
"Completed": 0,
"Draft": 0
}
for d in data:

View File

@@ -290,6 +290,7 @@ erpnext.patches.v13_0.set_training_event_attendance
erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold
erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
erpnext.patches.v13_0.update_job_card_details
erpnext.patches.v13_0.update_level_in_bom #1234sswef
erpnext.patches.v13_0.create_website_items
erpnext.patches.v13_0.populate_e_commerce_settings
erpnext.patches.v13_0.make_homepage_products_website_items
erpnext.patches.v13_0.make_homepage_products_website_items

View File

@@ -0,0 +1,30 @@
# Copyright (c) 2020, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
def execute():
for document in ["bom", "bom_item", "bom_explosion_item"]:
frappe.reload_doc('manufacturing', 'doctype', document)
frappe.db.sql(" update `tabBOM` set bom_level = 0 where docstatus = 1")
bom_list = frappe.db.sql_list("""select name from `tabBOM` bom
where docstatus=1 and is_active=1 and not exists(select bom_no from `tabBOM Item`
where parent=bom.name and ifnull(bom_no, '')!='')""")
count = 0
while(count < len(bom_list)):
for parent_bom in get_parent_boms(bom_list[count]):
bom_doc = frappe.get_cached_doc("BOM", parent_bom)
bom_doc.set_bom_level(update=True)
bom_list.append(parent_bom)
count += 1
def get_parent_boms(bom_no):
return frappe.db.sql_list("""
select distinct bom_item.parent from `tabBOM Item` bom_item
where bom_item.bom_no = %s and bom_item.docstatus=1 and bom_item.parenttype='BOM'
and exists(select bom.name from `tabBOM` bom where bom.name=bom_item.parent and bom.is_active=1)
""", bom_no)

View File

@@ -135,10 +135,26 @@ frappe.ui.form.on('Payroll Entry', {
});
frm.set_query('employee', 'employees', () => {
if (!frm.doc.company) {
frappe.msgprint(__("Please set a Company"));
return [];
let error_fields = [];
let mandatory_fields = ['company', 'payroll_frequency', 'start_date', 'end_date'];
let message = __('Mandatory fields required in {0}', [__(frm.doc.doctype)]);
mandatory_fields.forEach(field => {
if (!frm.doc[field]) {
error_fields.push(frappe.unscrub(field));
}
});
if (error_fields && error_fields.length) {
message = message + '<br><br><ul><li>' + error_fields.join('</li><li>') + "</ul>";
frappe.throw({
message: message,
indicator: 'red',
title: __('Missing Fields')
});
}
return {
query: "erpnext.payroll.doctype.payroll_entry.payroll_entry.employee_query",
filters: frm.events.get_employee_filters(frm)
@@ -148,25 +164,22 @@ frappe.ui.form.on('Payroll Entry', {
get_employee_filters: function (frm) {
let filters = {};
filters['company'] = frm.doc.company;
filters['start_date'] = frm.doc.start_date;
filters['end_date'] = frm.doc.end_date;
filters['salary_slip_based_on_timesheet'] = frm.doc.salary_slip_based_on_timesheet;
filters['payroll_frequency'] = frm.doc.payroll_frequency;
filters['payroll_payable_account'] = frm.doc.payroll_payable_account;
filters['currency'] = frm.doc.currency;
if (frm.doc.department) {
filters['department'] = frm.doc.department;
}
if (frm.doc.branch) {
filters['branch'] = frm.doc.branch;
}
if (frm.doc.designation) {
filters['designation'] = frm.doc.designation;
}
let fields = ['company', 'start_date', 'end_date', 'payroll_frequency', 'payroll_payable_account',
'currency', 'department', 'branch', 'designation'];
fields.forEach(field => {
if (frm.doc[field]) {
filters[field] = frm.doc[field];
}
});
if (frm.doc.employees) {
filters['employees'] = frm.doc.employees.filter(d => d.employee).map(d => d.employee);
let employees = frm.doc.employees.filter(d => d.employee).map(d => d.employee);
if (employees && employees.length) {
filters['employees'] = employees;
}
}
return filters;
},

View File

@@ -459,6 +459,7 @@ def get_emp_list(sal_struct, cond, end_date, payroll_payable_account):
where
t1.name = t2.employee
and t2.docstatus = 1
and t1.status != 'Inactive'
%s order by t2.from_date desc
""" % cond, {"sal_struct": tuple(sal_struct), "from_date": end_date, "payroll_payable_account": payroll_payable_account}, as_dict=True)
@@ -679,6 +680,10 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
conditions = []
include_employees = []
emp_cond = ''
if not filters.payroll_frequency:
frappe.throw(_('Select Payroll Frequency.'))
if filters.start_date and filters.end_date:
employee_list = get_employee_list(filters)
emp = filters.get('employees')

View File

@@ -147,7 +147,7 @@ erpnext.setup.slides_settings = [
}
// Validate bank name
if(me.values.bank_account){
if(me.values.bank_account) {
frappe.call({
async: false,
method: "erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts.validate_bank_account",

View File

@@ -35,6 +35,7 @@ frappe.ui.form.on('GST Settings', {
return {
filters: {
company: row.company,
account_type: "Tax",
is_group: 0
}
};

View File

@@ -19,6 +19,21 @@ class GSTSettings(Document):
from tabAddress where country = "India" and ifnull(gstin, '')!='' ''')
self.set_onload('data', data)
def validate(self):
# Validate duplicate accounts
self.validate_duplicate_accounts()
def validate_duplicate_accounts(self):
account_list = []
for account in self.get('gst_accounts'):
for fieldname in ['cgst_account', 'sgst_account', 'igst_account', 'cess_account']:
if account.get(fieldname) in account_list:
frappe.throw(_("Account {0} appears multiple times").format(
frappe.bold(account.get(fieldname))))
if account.get(fieldname):
account_list.append(account.get(fieldname))
@frappe.whitelist()
def send_reminder():
frappe.has_permission('GST Settings', throw=True)

View File

@@ -46,14 +46,14 @@ class TestGSTR3BReport(unittest.TestCase):
make_sales_invoice()
create_purchase_invoices()
if frappe.db.exists("GSTR 3B Report", "GSTR3B-March-2019-_Test Address-Billing"):
report = frappe.get_doc("GSTR 3B Report", "GSTR3B-March-2019-_Test Address-Billing")
if frappe.db.exists("GSTR 3B Report", "GSTR3B-March-2019-_Test Address GST-Billing"):
report = frappe.get_doc("GSTR 3B Report", "GSTR3B-March-2019-_Test Address GST-Billing")
report.save()
else:
report = frappe.get_doc({
"doctype": "GSTR 3B Report",
"company": "_Test Company GST",
"company_address": "_Test Address-Billing",
"company_address": "_Test Address GST-Billing",
"year": getdate().year,
"month": month_number_mapping.get(getdate().month)
}).insert()
@@ -89,7 +89,7 @@ class TestGSTR3BReport(unittest.TestCase):
si.append("taxes", {
"charge_type": "On Net Total",
"account_head": "IGST - _GST",
"account_head": "Output Tax IGST - _GST",
"cost_center": "Main - _GST",
"description": "IGST @ 18.0",
"rate": 18
@@ -117,7 +117,7 @@ def make_sales_invoice():
si.append("taxes", {
"charge_type": "On Net Total",
"account_head": "IGST - _GST",
"account_head": "Output Tax IGST - _GST",
"cost_center": "Main - _GST",
"description": "IGST @ 18.0",
"rate": 18
@@ -138,7 +138,7 @@ def make_sales_invoice():
si1.append("taxes", {
"charge_type": "On Net Total",
"account_head": "IGST - _GST",
"account_head": "Output Tax IGST - _GST",
"cost_center": "Main - _GST",
"description": "IGST @ 18.0",
"rate": 18
@@ -159,7 +159,7 @@ def make_sales_invoice():
si2.append("taxes", {
"charge_type": "On Net Total",
"account_head": "IGST - _GST",
"account_head": "Output Tax IGST - _GST",
"cost_center": "Main - _GST",
"description": "IGST @ 18.0",
"rate": 18
@@ -195,7 +195,7 @@ def create_purchase_invoices():
pi.append("taxes", {
"charge_type": "On Net Total",
"account_head": "CGST - _GST",
"account_head": "Input Tax CGST - _GST",
"cost_center": "Main - _GST",
"description": "CGST @ 9.0",
"rate": 9
@@ -203,7 +203,7 @@ def create_purchase_invoices():
pi.append("taxes", {
"charge_type": "On Net Total",
"account_head": "SGST - _GST",
"account_head": "Input Tax SGST - _GST",
"cost_center": "Main - _GST",
"description": "SGST @ 9.0",
"rate": 9
@@ -410,10 +410,10 @@ def make_company():
company.country = "India"
company.insert()
if not frappe.db.exists('Address', '_Test Address-Billing'):
if not frappe.db.exists('Address', '_Test Address GST-Billing'):
address = frappe.get_doc({
"address_title": "_Test Address GST",
"address_line1": "_Test Address Line 1",
"address_title": "_Test Address",
"address_type": "Billing",
"city": "_Test City",
"state": "Test State",
@@ -444,9 +444,9 @@ def set_account_heads():
if not gst_account:
gst_settings.append("gst_accounts", {
"company": "_Test Company GST",
"cgst_account": "CGST - _GST",
"sgst_account": "SGST - _GST",
"igst_account": "IGST - _GST",
"cgst_account": "Output Tax CGST - _GST",
"sgst_account": "Output Tax SGST - _GST",
"igst_account": "Output Tax IGST - _GST"
})
gst_settings.save()

View File

@@ -25,6 +25,7 @@ def setup_company_independent_fixtures(patch=False):
frappe.enqueue('erpnext.regional.india.setup.add_hsn_sac_codes', now=frappe.flags.in_test)
create_gratuity_rule()
add_print_formats()
update_accounts_settings_for_taxes()
def add_hsn_sac_codes():
if frappe.flags.in_test and frappe.flags.created_hsn_codes:
@@ -680,7 +681,7 @@ def make_custom_fields(update=True):
def make_fixtures(company=None):
docs = []
company = company.name if company else frappe.db.get_value("Global Defaults", None, "default_company")
company = company or frappe.db.get_value("Global Defaults", None, "default_company")
set_salary_components(docs)
set_tds_account(docs, company)
@@ -698,6 +699,53 @@ def make_fixtures(company=None):
# create records for Tax Withholding Category
set_tax_withholding_category(company)
def update_regional_tax_settings(country, company):
# Will only add default GST accounts if present
input_account_names = ['Input Tax CGST', 'Input Tax SGST', 'Input Tax IGST']
output_account_names = ['Output Tax CGST', 'Output Tax SGST', 'Output Tax IGST']
rcm_accounts = ['Input Tax CGST RCM', 'Input Tax SGST RCM', 'Input Tax IGST RCM']
gst_settings = frappe.get_single('GST Settings')
existing_account_list = []
for account in gst_settings.get('gst_accounts'):
for key in ['cgst_account', 'sgst_account', 'igst_account']:
existing_account_list.append(account.get(key))
gst_accounts = frappe._dict(frappe.get_all("Account",
{'company': company, 'account_name': ('in', input_account_names +
output_account_names + rcm_accounts)}, ['account_name', 'name'], as_list=1))
add_accounts_in_gst_settings(company, input_account_names, gst_accounts,
existing_account_list, gst_settings)
add_accounts_in_gst_settings(company, output_account_names, gst_accounts,
existing_account_list, gst_settings)
add_accounts_in_gst_settings(company, rcm_accounts, gst_accounts,
existing_account_list, gst_settings, is_reverse_charge=1)
gst_settings.save()
def add_accounts_in_gst_settings(company, account_names, gst_accounts,
existing_account_list, gst_settings, is_reverse_charge=0):
accounts_not_added = 1
for account in account_names:
# Default Account Added does not exists
if not gst_accounts.get(account):
accounts_not_added = 0
# Check if already added in GST Settings
if gst_accounts.get(account) in existing_account_list:
accounts_not_added = 0
if accounts_not_added:
gst_settings.append('gst_accounts', {
'company': company,
'cgst_account': gst_accounts.get(account_names[0]),
'sgst_account': gst_accounts.get(account_names[1]),
'igst_account': gst_accounts.get(account_names[2]),
'is_reverse_charge_account': is_reverse_charge
})
def set_salary_components(docs):
docs.extend([
{'doctype': 'Salary Component', 'salary_component': 'Professional Tax',
@@ -731,12 +779,13 @@ def set_tax_withholding_category(company):
docs = get_tds_details(accounts, fiscal_year)
for d in docs:
try:
if not frappe.db.exists("Tax Withholding Category", d.get("name")):
doc = frappe.get_doc(d)
doc.flags.ignore_validate = True
doc.flags.ignore_permissions = True
doc.flags.ignore_mandatory = True
doc.insert()
except frappe.DuplicateEntryError:
else:
doc = frappe.get_doc("Tax Withholding Category", d.get("name"))
if accounts:
@@ -749,11 +798,12 @@ def set_tax_withholding_category(company):
doc.append("rates", d.get('rates')[0])
doc.flags.ignore_permissions = True
doc.flags.ignore_validate = True
doc.flags.ignore_mandatory = True
doc.flags.ignore_links = True
doc.save()
def set_tds_account(docs, company):
abbr = frappe.get_value("Company", company, "abbr")
parent_account = frappe.db.get_value("Account", filters = {"account_name": "Duties and Taxes", "company": company})
if parent_account:
docs.extend([
@@ -912,7 +962,6 @@ def get_tds_details(accounts, fiscal_year):
]
def create_gratuity_rule():
# Standard Indain Gratuity Rule
if not frappe.db.exists("Gratuity Rule", "Indian Standard Gratuity Rule"):
rule = frappe.new_doc("Gratuity Rule")
@@ -930,3 +979,7 @@ def create_gratuity_rule():
rule.flags.ignore_mandatory = True
rule.save()
def update_accounts_settings_for_taxes():
if frappe.db.count('Company') == 1:
frappe.db.set_value('Accounts Settings', None, "add_taxes_from_item_tax_template", 0)

View File

@@ -11,7 +11,7 @@
"is_standard": "Yes",
"json": "{}",
"letter_head": "Logo",
"modified": "2021-03-12 12:36:48.689413",
"modified": "2021-03-13 12:36:48.689413",
"modified_by": "Administrator",
"module": "Regional",
"name": "E-Invoice Summary",

View File

@@ -110,7 +110,7 @@ class Company(NestedSet):
self.create_default_warehouses()
if frappe.flags.country_change:
install_country_fixtures(self.name)
install_country_fixtures(self.name, self.country)
self.create_default_tax_template()
if not frappe.db.get_value("Department", {"company": self.name}):
@@ -440,16 +440,15 @@ def get_name_with_abbr(name, company):
return " - ".join(parts)
def install_country_fixtures(company):
company_doc = frappe.get_doc("Company", company)
path = frappe.get_app_path('erpnext', 'regional', frappe.scrub(company_doc.country))
def install_country_fixtures(company, country):
path = frappe.get_app_path('erpnext', 'regional', frappe.scrub(country))
if os.path.exists(path.encode("utf-8")):
try:
module_name = "erpnext.regional.{0}.setup.setup".format(frappe.scrub(company_doc.country))
frappe.get_attr(module_name)(company_doc, False)
module_name = "erpnext.regional.{0}.setup.setup".format(frappe.scrub(country))
frappe.get_attr(module_name)(company, False)
except Exception as e:
frappe.log_error()
frappe.throw(_("Failed to setup defaults for country {0}. Please contact support@erpnext.com").format(frappe.bold(company_doc.country)))
frappe.throw(_("Failed to setup defaults for country {0}. Please contact support@erpnext.com").format(frappe.bold(country)))
def update_company_current_month_sales(company):

View File

@@ -1164,33 +1164,292 @@
},
"India": {
"tax_categories": [
{
"title": "In-State",
"is_inter_state": 0,
"gst_state": ""
},
{
"title": "Out-State",
"is_inter_state": 1,
"gst_state": ""
},
{
"title": "Reverse Charge In-State",
"is_inter_state": 0,
"gst_state": ""
},
{
"title": "Reverse Charge Out-State",
"is_inter_state": 1,
"gst_state": ""
},
{
"title": "Registered Composition",
"is_inter_state": 0,
"gst_state": ""
}
],
"chart_of_accounts": {
"*": {
"item_tax_templates": [
{
"title": "In State GST",
"title": "GST 9%",
"taxes": [
{
"tax_type": {
"account_name": "SGST",
"account_name": "Output Tax SGST",
"tax_rate": 9.00
}
},
{
"tax_type": {
"account_name": "CGST",
"account_name": "Output Tax CGST",
"tax_rate": 9.00
}
},
{
"tax_type": {
"account_name": "Output Tax IGST",
"tax_rate": 18.00
}
},
{
"tax_type": {
"account_name": "Input Tax SGST",
"tax_rate": 9.00,
"root_type": "Asset"
}
},
{
"tax_type": {
"account_name": "Input Tax CGST",
"tax_rate": 9.00,
"root_type": "Asset"
}
},
{
"tax_type": {
"account_name": "Input Tax IGST",
"tax_rate": 18.00,
"root_type": "Asset"
}
},
{
"tax_type": {
"account_name": "Input Tax SGST RCM",
"tax_rate": 9.00,
"root_type": "Asset"
}
},
{
"tax_type": {
"account_name": "Input Tax CGST RCM",
"tax_rate": 9.00,
"root_type": "Asset"
}
},
{
"tax_type": {
"account_name": "Input Tax IGST RCM",
"tax_rate": 18.00,
"root_type": "Asset"
}
}
]
},
{
"title": "Out of State GST",
"title": "GST 5%",
"taxes": [
{
"tax_type": {
"account_name": "IGST",
"tax_rate": 18.00
"account_name": "Output Tax SGST",
"tax_rate": 2.5
}
},
{
"tax_type": {
"account_name": "Output Tax CGST",
"tax_rate": 2.5
}
},
{
"tax_type": {
"account_name": "Output Tax IGST",
"tax_rate": 5.0
}
},
{
"tax_type": {
"account_name": "Input Tax SGST",
"tax_rate": 2.5,
"root_type": "Asset"
}
},
{
"tax_type": {
"account_name": "Input Tax CGST",
"tax_rate": 2.5,
"root_type": "Asset"
}
},
{
"tax_type": {
"account_name": "Input Tax IGST",
"tax_rate": 5.0,
"root_type": "Asset"
}
},
{
"tax_type": {
"account_name": "Input Tax SGST RCM",
"tax_rate": 2.50,
"root_type": "Asset"
}
},
{
"tax_type": {
"account_name": "Input Tax CGST RCM",
"tax_rate": 2.50,
"root_type": "Asset"
}
},
{
"tax_type": {
"account_name": "Input Tax IGST RCM",
"tax_rate": 5.00,
"root_type": "Asset"
}
}
]
},
{
"title": "GST 12%",
"taxes": [
{
"tax_type": {
"account_name": "Output Tax SGST",
"tax_rate": 6.0
}
},
{
"tax_type": {
"account_name": "Output Tax CGST",
"tax_rate": 6.0
}
},
{
"tax_type": {
"account_name": "Output Tax IGST",
"tax_rate": 12.0
}
},
{
"tax_type": {
"account_name": "Input Tax SGST",
"tax_rate": 6.0,
"root_type": "Asset"
}
},
{
"tax_type": {
"account_name": "Input Tax CGST",
"tax_rate": 6.0,
"root_type": "Asset"
}
},
{
"tax_type": {
"account_name": "Input Tax IGST",
"tax_rate": 12.0,
"root_type": "Asset"
}
},
{
"tax_type": {
"account_name": "Input Tax SGST RCM",
"tax_rate": 6.00,
"root_type": "Asset"
}
},
{
"tax_type": {
"account_name": "Input Tax CGST RCM",
"tax_rate": 6.00,
"root_type": "Asset"
}
},
{
"tax_type": {
"account_name": "Input Tax IGST RCM",
"tax_rate": 12.00,
"root_type": "Asset"
}
}
]
},
{
"title": "GST 28%",
"taxes": [
{
"tax_type": {
"account_name": "Output Tax SGST",
"tax_rate": 14.0
}
},
{
"tax_type": {
"account_name": "Output Tax CGST",
"tax_rate": 14.0
}
},
{
"tax_type": {
"account_name": "Output Tax IGST",
"tax_rate": 28.0
}
},
{
"tax_type": {
"account_name": "Input Tax SGST",
"tax_rate": 14.0,
"root_type": "Asset"
}
},
{
"tax_type": {
"account_name": "Input Tax CGST",
"tax_rate": 14.0,
"root_type": "Asset"
}
},
{
"tax_type": {
"account_name": "Input Tax IGST",
"tax_rate": 28.0,
"root_type": "Asset"
}
},
{
"tax_type": {
"account_name": "Input Tax SGST RCM",
"tax_rate": 14.00,
"root_type": "Asset"
}
},
{
"tax_type": {
"account_name": "Input Tax CGST RCM",
"tax_rate": 14.00,
"root_type": "Asset"
}
},
{
"tax_type": {
"account_name": "Input Tax IGST RCM",
"tax_rate": 28.00,
"root_type": "Asset"
}
}
]
@@ -1229,35 +1488,116 @@
]
}
],
"*": [
"sales_tax_templates": [
{
"title": "In State GST",
"title": "Output GST In-state",
"taxes": [
{
"account_head": {
"account_name": "SGST",
"tax_rate": 9.00
"account_name": "Output Tax SGST",
"tax_rate": 9.00,
"account_type": "Tax"
}
},
{
"account_head": {
"account_name": "CGST",
"tax_rate": 9.00
"account_name": "Output Tax CGST",
"tax_rate": 9.00,
"account_type": "Tax"
}
}
]
],
"tax_category": "In-State"
},
{
"title": "Out of State GST",
"title": "Output GST Out-state",
"taxes": [
{
"account_head": {
"account_name": "IGST",
"tax_rate": 18.00
"account_name": "Output Tax IGST",
"tax_rate": 18.00,
"account_type": "Tax"
}
}
]
],
"tax_category": "Out-State"
}
],
"purchase_tax_templates": [
{
"title": "Input GST In-state",
"taxes": [
{
"account_head": {
"account_name": "Input Tax SGST",
"tax_rate": 9.00,
"root_type": "Asset",
"account_type": "Tax"
}
},
{
"account_head": {
"account_name": "Input Tax CGST",
"tax_rate": 9.00,
"root_type": "Asset",
"account_type": "Tax"
}
}
],
"tax_category": "In-State"
},
{
"title": "Input GST Out-state",
"taxes": [
{
"account_head": {
"account_name": "Input Tax IGST",
"tax_rate": 18.00,
"root_type": "Asset",
"account_type": "Tax"
}
}
],
"tax_category": "Out-State"
},
{
"title": "Input GST RCM In-state",
"taxes": [
{
"account_head": {
"account_name": "Input Tax SGST RCM",
"tax_rate": 9.00,
"root_type": "Asset",
"account_type": "Tax"
}
},
{
"account_head": {
"account_name": "Input Tax CGST RCM",
"tax_rate": 9.00,
"root_type": "Asset",
"account_type": "Tax"
}
}
],
"tax_category": "Reverse Charge In-State"
},
{
"title": "Input GST RCM Out-state",
"taxes": [
{
"account_head": {
"account_name": "Input Tax IGST RCM",
"tax_rate": 18.00,
"root_type": "Asset",
"account_type": "Tax"
}
}
],
"tax_category": "Reverse Charge Out-State"
}
],
"*": [
{
"title": "VAT 5%",
"taxes": [
@@ -1349,7 +1689,7 @@
"Italy VAT 4%":{
"account_name": "IVA 4%",
"tax_rate": 4.00
}
}
},
"Ivory Coast": {

View File

@@ -42,29 +42,6 @@ def enable_shopping_cart(args):
'quotation_series': "QTN-",
}).insert()
def create_bank_account(args):
if args.get("bank_account"):
company_name = args.get('company_name')
bank_account_group = frappe.db.get_value("Account",
{"account_type": "Bank", "is_group": 1, "root_type": "Asset",
"company": company_name})
if bank_account_group:
bank_account = frappe.get_doc({
"doctype": "Account",
'account_name': args.get("bank_account"),
'parent_account': bank_account_group,
'is_group':0,
'company': company_name,
"account_type": "Bank",
})
try:
return bank_account.insert()
except RootNotEditable:
frappe.throw(_("Bank account cannot be named as {0}").format(args.get("bank_account")))
except frappe.DuplicateEntryError:
# bank account same as a CoA entry
pass
def create_email_digest():
from frappe.utils.user import get_system_managers
system_managers = get_system_managers(only_name=True)

View File

@@ -449,6 +449,8 @@ def install_defaults(args=None):
set_active_domains(args)
update_stock_settings()
update_shopping_cart_settings(args)
args.update({"set_default": 1})
create_bank_account(args)
def set_global_defaults(args):
@@ -480,17 +482,17 @@ def update_stock_settings():
stock_settings.save()
def create_bank_account(args):
if not args.bank_account:
if not args.get('bank_account'):
return
company_name = args.company_name
company_name = args.get('company_name')
bank_account_group = frappe.db.get_value("Account",
{"account_type": "Bank", "is_group": 1, "root_type": "Asset",
"company": company_name})
if bank_account_group:
bank_account = frappe.get_doc({
"doctype": "Account",
'account_name': args.bank_account,
'account_name': args.get('bank_account'),
'parent_account': bank_account_group,
'is_group':0,
'company': company_name,
@@ -499,10 +501,13 @@ def create_bank_account(args):
try:
doc = bank_account.insert()
frappe.db.set_value("Company", args.company_name, "default_bank_account", bank_account.name, update_modified=False)
if args.get('set_default'):
frappe.db.set_value("Company", args.get('company_name'), "default_bank_account", bank_account.name, update_modified=False)
return doc
except RootNotEditable:
frappe.throw(_("Bank account cannot be named as {0}").format(args.bank_account))
frappe.throw(_("Bank account cannot be named as {0}").format(args.get('bank_account')))
except frappe.DuplicateEntryError:
# bank account same as a CoA entry
pass

View File

@@ -27,6 +27,7 @@ def setup_taxes_and_charges(company_name: str, country: str):
country_wise_tax = simple_to_detailed(country_wise_tax)
from_detailed_data(company_name, country_wise_tax)
update_regional_tax_settings(country, company_name)
def simple_to_detailed(templates):
@@ -101,6 +102,17 @@ def from_detailed_data(company_name, data):
make_item_tax_template(company_name, template)
def update_regional_tax_settings(country, company):
path = frappe.get_app_path('erpnext', 'regional', frappe.scrub(country))
if os.path.exists(path.encode("utf-8")):
try:
module_name = "erpnext.regional.{0}.setup.update_regional_tax_settings".format(frappe.scrub(country))
frappe.get_attr(module_name)(country, company)
except Exception as e:
# Log error and ignore if failed to setup regional tax settings
frappe.log_error()
pass
def make_taxes_and_charges_template(company_name, doctype, template):
template['company'] = company_name
template['doctype'] = doctype
@@ -130,8 +142,14 @@ def make_taxes_and_charges_template(company_name, doctype, template):
if fieldname not in tax_row:
tax_row[fieldname] = default_value
return frappe.get_doc(template).insert(ignore_permissions=True)
doc = frappe.get_doc(template)
# Data in country wise json is already pre validated, hence validations can be ignored
# Ingone validations to make doctypes faster
doc.flags.ignore_links = True
doc.flags.ignore_validate = True
doc.insert(ignore_permissions=True)
return doc
def make_item_tax_template(company_name, template):
"""Create an Item Tax Template.
@@ -156,8 +174,24 @@ def make_item_tax_template(company_name, template):
if 'tax_rate' not in tax_row:
tax_row['tax_rate'] = account_data.get('tax_rate')
return frappe.get_doc(template).insert(ignore_permissions=True)
doc = frappe.get_doc(template)
# Data in country wise json is already pre validated, hence validations can be ignored
# Ingone validations to make doctypes faster
doc.flags.ignore_links = True
doc.flags.ignore_validate = True
doc.insert(ignore_permissions=True)
return doc
def make_tax_category(tax_category):
""" Make tax category based on title if not already created """
doctype = 'Tax Category'
if not frappe.db.exists(doctype, tax_category['title']):
tax_category['doctype'] = doctype
doc = frappe.get_doc(tax_category)
doc.flags.ignore_links = True
doc.flags.ignore_validate = True
doc.insert(ignore_permissions=True)
def get_or_create_account(company_name, account):
"""
@@ -175,8 +209,7 @@ def get_or_create_account(company_name, account):
or_filters={
'account_name': account.get('account_name'),
'account_number': account.get('account_number')
}
)
})
if existing_accounts:
return frappe.get_doc('Account', existing_accounts[0].name)
@@ -191,8 +224,11 @@ def get_or_create_account(company_name, account):
account['root_type'] = root_type
account['is_group'] = 0
return frappe.get_doc(account).insert(ignore_permissions=True, ignore_mandatory=True)
doc = frappe.get_doc(account)
doc.flags.ignore_links = True
doc.flags.ignore_validate = True
doc.insert(ignore_permissions=True, ignore_mandatory=True)
return doc
def get_or_create_tax_group(company_name, root_type):
# Look for a group account of type 'Tax'
@@ -237,7 +273,11 @@ def get_or_create_tax_group(company_name, root_type):
'account_type': 'Tax',
'account_name': account_name,
'parent_account': root_account.name
}).insert(ignore_permissions=True)
})
tax_group_account.flags.ignore_links = True
tax_group_account.flags.ignore_validate = True
tax_group_account.insert(ignore_permissions=True)
tax_group_name = tax_group_account.name

View File

@@ -11,10 +11,11 @@
"hide_custom": 0,
"icon": "settings",
"idx": 0,
"is_default": 0,
"is_standard": 1,
"label": "ERPNext Settings",
"links": [],
"modified": "2020-12-01 13:38:37.759596",
"modified": "2021-06-12 01:58:11.399566",
"modified_by": "Administrator",
"module": "Setup",
"name": "ERPNext Settings",
@@ -109,6 +110,13 @@
"label": "Domain Settings",
"link_to": "Domain Settings",
"type": "DocType"
},
{
"doc_view": "",
"icon": "retail",
"label": "Products Settings",
"link_to": "Products Settings",
"type": "DocType"
}
]
}
}

View File

@@ -386,6 +386,7 @@ class PurchaseReceipt(BuyingController):
against_account = ", ".join([d.account for d in gl_entries if flt(d.debit) > 0])
total_valuation_amount = sum(valuation_tax.values())
amount_including_divisional_loss = negative_expense_to_be_booked
stock_rbnb = self.get_company_default("stock_received_but_not_billed")
i = 1
for tax in self.get("taxes"):
if valuation_tax.get(tax.name):

View File

@@ -97,7 +97,7 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None):
at_capacity, rules = get_ordered_putaway_rules(item_code, company, source_warehouse=source_warehouse)
if not rules:
warehouse = source_warehouse or item.warehouse
warehouse = source_warehouse or item.get('warehouse')
if at_capacity:
# rules available, but no free space
items_not_accomodated.append([item_code, pending_qty])

View File

@@ -54,7 +54,7 @@ class TestStockLedgerEntry(unittest.TestCase):
)
# _Test Item for Reposting transferred from Stores to FG warehouse on 30-04-2020
make_stock_entry(
se = make_stock_entry(
item_code="_Test Item for Reposting",
source="Stores - _TC",
target="Finished Goods - _TC",
@@ -64,29 +64,29 @@ class TestStockLedgerEntry(unittest.TestCase):
posting_date='2020-04-30',
posting_time='14:00'
)
target_wh_sle = get_previous_sle({
target_wh_sle = frappe.db.get_value('Stock Ledger Entry', {
"item_code": "_Test Item for Reposting",
"warehouse": "Finished Goods - _TC",
"posting_date": '2020-04-30',
"posting_time": '14:00'
})
"voucher_type": "Stock Entry",
"voucher_no": se.name
}, ["valuation_rate"], as_dict=1)
self.assertEqual(target_wh_sle.get("valuation_rate"), 150)
# Repack entry on 5-5-2020
repack = create_repack_entry(company=company, posting_date='2020-05-05', posting_time='14:00')
finished_item_sle = get_previous_sle({
finished_item_sle = frappe.db.get_value('Stock Ledger Entry', {
"item_code": "_Test Finished Item for Reposting",
"warehouse": "Finished Goods - _TC",
"posting_date": '2020-05-05',
"posting_time": '14:00'
})
"voucher_type": "Stock Entry",
"voucher_no": repack.name
}, ["incoming_rate", "valuation_rate"], as_dict=1)
self.assertEqual(finished_item_sle.get("incoming_rate"), 540)
self.assertEqual(finished_item_sle.get("valuation_rate"), 540)
# Reconciliation for _Test Item for Reposting at Stores on 12-04-2020: Qty = 50, Rate = 150
create_stock_reconciliation(
sr = create_stock_reconciliation(
item_code="_Test Item for Reposting",
warehouse="Stores - _TC",
qty=50,
@@ -109,12 +109,12 @@ class TestStockLedgerEntry(unittest.TestCase):
self.assertEqual(target_wh_sle.get("valuation_rate"), 175)
# Check valuation rate of repacked item after back-dated entry at Stores
finished_item_sle = get_previous_sle({
finished_item_sle = frappe.db.get_value('Stock Ledger Entry', {
"item_code": "_Test Finished Item for Reposting",
"warehouse": "Finished Goods - _TC",
"posting_date": '2020-05-05',
"posting_time": '14:00'
})
"voucher_type": "Stock Entry",
"voucher_no": repack.name
}, ["incoming_rate", "valuation_rate"], as_dict=1)
self.assertEqual(finished_item_sle.get("incoming_rate"), 790)
self.assertEqual(finished_item_sle.get("valuation_rate"), 790)

View File

@@ -357,6 +357,7 @@ class StockReconciliation(StockController):
if row.current_qty:
data.actual_qty = -1 * row.current_qty
data.qty_after_transaction = flt(row.current_qty)
data.previous_qty_after_transaction = flt(row.qty)
data.valuation_rate = flt(row.current_valuation_rate)
data.stock_value = data.qty_after_transaction * data.valuation_rate
data.stock_value_difference = -1 * flt(row.amount_difference)
@@ -404,17 +405,18 @@ class StockReconciliation(StockController):
key = (d.item_code, d.warehouse)
if key not in merge_similar_entries:
d.total_amount = (d.actual_qty * d.valuation_rate)
merge_similar_entries[key] = d
elif d.serial_no:
data = merge_similar_entries[key]
data.actual_qty += d.actual_qty
data.qty_after_transaction += d.qty_after_transaction
data.valuation_rate = (data.valuation_rate + d.valuation_rate) / data.actual_qty
data.total_amount += (d.actual_qty * d.valuation_rate)
data.valuation_rate = (data.total_amount) / data.actual_qty
data.serial_no += '\n' + d.serial_no
if data.incoming_rate:
data.incoming_rate = (data.incoming_rate + d.incoming_rate) / data.actual_qty
data.incoming_rate = (data.total_amount) / data.actual_qty
for key, value in merge_similar_entries.items():
new_sl_entries.append(value)

View File

@@ -6,7 +6,7 @@
from __future__ import unicode_literals
import frappe, unittest
from frappe.utils import flt, nowdate, nowtime
from frappe.utils import flt, nowdate, nowtime, random_string, add_days
from erpnext.accounts.utils import get_stock_and_account_balance
from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import EmptyStockReconciliationItemsError, get_items
@@ -14,6 +14,7 @@ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.utils import get_incoming_rate, get_stock_value_on, get_valuation_method
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
class TestStockReconciliation(unittest.TestCase):
@classmethod
@@ -150,6 +151,42 @@ class TestStockReconciliation(unittest.TestCase):
stock_doc = frappe.get_doc("Stock Reconciliation", d)
stock_doc.cancel()
def test_stock_reco_for_merge_serialized_item(self):
to_delete_records = []
# Add new serial nos
serial_item_code = "Stock-Reco-Serial-Item-2"
serial_warehouse = "_Test Warehouse for Stock Reco1 - _TC"
sr = create_stock_reconciliation(item_code=serial_item_code, serial_no=random_string(6),
warehouse = serial_warehouse, qty=1, rate=100, do_not_submit=True, purpose='Opening Stock')
for i in range(3):
sr.append('items', {
'item_code': serial_item_code,
'warehouse': serial_warehouse,
'qty': 1,
'valuation_rate': 100,
'serial_no': random_string(6)
})
sr.save()
sr.submit()
sle_entries = frappe.get_all('Stock Ledger Entry', filters= {'voucher_no': sr.name},
fields = ['name', 'incoming_rate'])
self.assertEqual(len(sle_entries), 1)
self.assertEqual(sle_entries[0].incoming_rate, 100)
to_delete_records.append(sr.name)
to_delete_records.reverse()
for d in to_delete_records:
stock_doc = frappe.get_doc("Stock Reconciliation", d)
stock_doc.cancel()
def test_stock_reco_for_batch_item(self):
to_delete_records = []
to_delete_serial_nos = []
@@ -204,6 +241,117 @@ class TestStockReconciliation(unittest.TestCase):
self.assertEqual(sr.get("items")[0].valuation_rate, 0)
self.assertEqual(sr.get("items")[0].amount, 0)
def test_backdated_stock_reco_qty_reposting(self):
"""
Test if a backdated stock reco recalculates future qty until next reco.
-------------------------------------------
Var | Doc | Qty | Balance
-------------------------------------------
SR5 | Reco | 0 | 8 (posting date: today-4) [backdated]
PR1 | PR | 10 | 18 (posting date: today-3)
PR2 | PR | 1 | 19 (posting date: today-2)
SR4 | Reco | 0 | 6 (posting date: today-1) [backdated]
PR3 | PR | 1 | 7 (posting date: today) # can't post future PR
"""
item_code = "Backdated-Reco-Item"
warehouse = "_Test Warehouse - _TC"
create_item(item_code)
pr1 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=10, rate=100,
posting_date=add_days(nowdate(), -3))
pr2 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=1, rate=100,
posting_date=add_days(nowdate(), -2))
pr3 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=1, rate=100,
posting_date=nowdate())
pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0},
"qty_after_transaction")
pr3_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0},
"qty_after_transaction")
self.assertEqual(pr1_balance, 10)
self.assertEqual(pr3_balance, 12)
# post backdated stock reco in between
sr4 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=6, rate=100,
posting_date=add_days(nowdate(), -1))
pr3_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0},
"qty_after_transaction")
self.assertEqual(pr3_balance, 7)
# post backdated stock reco at the start
sr5 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=8, rate=100,
posting_date=add_days(nowdate(), -4))
pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0},
"qty_after_transaction")
pr2_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0},
"qty_after_transaction")
sr4_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0},
"qty_after_transaction")
self.assertEqual(pr1_balance, 18)
self.assertEqual(pr2_balance, 19)
self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected
# cancel backdated stock reco and check future impact
sr5.cancel()
pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0},
"qty_after_transaction")
pr2_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0},
"qty_after_transaction")
sr4_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0},
"qty_after_transaction")
self.assertEqual(pr1_balance, 10)
self.assertEqual(pr2_balance, 11)
self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected
# teardown
sr4.cancel()
pr3.cancel()
pr2.cancel()
pr1.cancel()
def test_backdated_stock_reco_future_negative_stock(self):
"""
Test if a backdated stock reco causes future negative stock and is blocked.
-------------------------------------------
Var | Doc | Qty | Balance
-------------------------------------------
PR1 | PR | 10 | 10 (posting date: today-2)
SR3 | Reco | 0 | 1 (posting date: today-1) [backdated & blocked]
DN2 | DN | -2 | 8(-1) (posting date: today)
"""
from erpnext.stock.stock_ledger import NegativeStockError
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
item_code = "Backdated-Reco-Item"
warehouse = "_Test Warehouse - _TC"
create_item(item_code)
negative_stock_setting = frappe.db.get_single_value("Stock Settings", "allow_negative_stock")
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 0)
pr1 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=10, rate=100,
posting_date=add_days(nowdate(), -2))
dn2 = create_delivery_note(item_code=item_code, warehouse=warehouse, qty=2, rate=120,
posting_date=nowdate())
pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0},
"qty_after_transaction")
dn2_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": dn2.name, "is_cancelled": 0},
"qty_after_transaction")
self.assertEqual(pr1_balance, 10)
self.assertEqual(dn2_balance, 8)
# check if stock reco is blocked
sr3 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=1, rate=100,
posting_date=add_days(nowdate(), -1), do_not_submit=True)
self.assertRaises(NegativeStockError, sr3.submit)
# teardown
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", negative_stock_setting)
sr3.cancel()
dn2.cancel()
pr1.cancel()
def insert_existing_sle(warehouse):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
@@ -231,6 +379,12 @@ def create_batch_or_serial_no_items():
serial_item_doc.serial_no_series = "SRSI.####"
serial_item_doc.save(ignore_permissions=True)
serial_item_doc = create_item("Stock-Reco-Serial-Item-2", is_stock_item=1)
if not serial_item_doc.has_serial_no:
serial_item_doc.has_serial_no = 1
serial_item_doc.serial_no_series = "SRSII.####"
serial_item_doc.save(ignore_permissions=True)
batch_item_doc = create_item("Stock-Reco-batch-Item-1", is_stock_item=1)
if not batch_item_doc.has_batch_no:
batch_item_doc.has_batch_no = 1

View File

@@ -55,6 +55,11 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
args = sle_doc.as_dict()
if sle.get("voucher_type") == "Stock Reconciliation":
# preserve previous_qty_after_transaction for qty reposting
args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction")
update_bin(args, allow_negative_stock, via_landed_cost_voucher)
def get_args_for_future_sle(row):
@@ -215,7 +220,7 @@ class update_entries_after(object):
"""
self.data.setdefault(args.warehouse, frappe._dict())
warehouse_dict = self.data[args.warehouse]
previous_sle = self.get_previous_sle_of_current_voucher(args)
previous_sle = get_previous_sle_of_current_voucher(args)
warehouse_dict.previous_sle = previous_sle
for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
@@ -227,29 +232,6 @@ class update_entries_after(object):
"stock_value_difference": 0.0
})
def get_previous_sle_of_current_voucher(self, args):
"""get stock ledger entries filtered by specific posting datetime conditions"""
args['time_format'] = '%H:%i:%s'
if not args.get("posting_date"):
args["posting_date"] = "1900-01-01"
if not args.get("posting_time"):
args["posting_time"] = "00:00"
sle = frappe.db.sql("""
select *, timestamp(posting_date, posting_time) as "timestamp"
from `tabStock Ledger Entry`
where item_code = %(item_code)s
and warehouse = %(warehouse)s
and is_cancelled = 0
and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
order by timestamp(posting_date, posting_time) desc, creation desc
limit 1
for update""", args, as_dict=1)
return sle[0] if sle else frappe._dict()
def build(self):
from erpnext.controllers.stock_controller import future_sle_exists
@@ -734,6 +716,35 @@ class update_entries_after(object):
bin_doc.flags.via_stock_ledger_entry = True
bin_doc.save(ignore_permissions=True)
def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False):
"""get stock ledger entries filtered by specific posting datetime conditions"""
args['time_format'] = '%H:%i:%s'
if not args.get("posting_date"):
args["posting_date"] = "1900-01-01"
if not args.get("posting_time"):
args["posting_time"] = "00:00"
voucher_condition = ""
if exclude_current_voucher:
voucher_no = args.get("voucher_no")
voucher_condition = f"and voucher_no != '{voucher_no}'"
sle = frappe.db.sql("""
select *, timestamp(posting_date, posting_time) as "timestamp"
from `tabStock Ledger Entry`
where item_code = %(item_code)s
and warehouse = %(warehouse)s
and is_cancelled = 0
{voucher_condition}
and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
order by timestamp(posting_date, posting_time) desc, creation desc
limit 1
for update""".format(voucher_condition=voucher_condition), args, as_dict=1)
return sle[0] if sle else frappe._dict()
def get_previous_sle(args, for_update=False):
"""
get the last sle on or before the current time-bucket,
@@ -862,9 +873,24 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
return valuation_rate
def update_qty_in_future_sle(args, allow_negative_stock=None):
"""Recalculate Qty after Transaction in future SLEs based on current SLE."""
datetime_limit_condition = ""
qty_shift = args.actual_qty
# find difference/shift in qty caused by stock reconciliation
if args.voucher_type == "Stock Reconciliation":
qty_shift = get_stock_reco_qty_shift(args)
# find the next nearest stock reco so that we only recalculate SLEs till that point
next_stock_reco_detail = get_next_stock_reco(args)
if next_stock_reco_detail:
detail = next_stock_reco_detail[0]
# add condition to update SLEs before this date & time
datetime_limit_condition = get_datetime_limit_condition(detail)
frappe.db.sql("""
update `tabStock Ledger Entry`
set qty_after_transaction = qty_after_transaction + {qty}
set qty_after_transaction = qty_after_transaction + {qty_shift}
where
item_code = %(item_code)s
and warehouse = %(warehouse)s
@@ -876,15 +902,70 @@ def update_qty_in_future_sle(args, allow_negative_stock=None):
and creation > %(creation)s
)
)
""".format(qty=args.actual_qty), args)
{datetime_limit_condition}
""".format(qty_shift=qty_shift, datetime_limit_condition=datetime_limit_condition), args)
validate_negative_qty_in_future_sle(args, allow_negative_stock)
def get_stock_reco_qty_shift(args):
stock_reco_qty_shift = 0
if args.get("is_cancelled"):
if args.get("previous_qty_after_transaction"):
# get qty (balance) that was set at submission
last_balance = args.get("previous_qty_after_transaction")
stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
else:
stock_reco_qty_shift = flt(args.actual_qty)
else:
# reco is being submitted
last_balance = get_previous_sle_of_current_voucher(args,
exclude_current_voucher=True).get("qty_after_transaction")
if last_balance is not None:
stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
else:
stock_reco_qty_shift = args.qty_after_transaction
return stock_reco_qty_shift
def get_next_stock_reco(args):
"""Returns next nearest stock reconciliaton's details."""
return frappe.db.sql("""
select
name, posting_date, posting_time, creation, voucher_no
from
`tabStock Ledger Entry`
where
item_code = %(item_code)s
and warehouse = %(warehouse)s
and voucher_type = 'Stock Reconciliation'
and voucher_no != %(voucher_no)s
and is_cancelled = 0
and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
or (
timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
and creation > %(creation)s
)
)
limit 1
""", args, as_dict=1)
def get_datetime_limit_condition(detail):
return f"""
and
(timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
or (
timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
and creation < '{detail.creation}'
)
)"""
def validate_negative_qty_in_future_sle(args, allow_negative_stock=None):
allow_negative_stock = allow_negative_stock \
or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
if args.actual_qty < 0 and not allow_negative_stock:
if (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation") and not allow_negative_stock:
sle = get_future_sle_with_negative_qty(args)
if sle:
message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(

View File

@@ -1,28 +1,54 @@
{% if doc.status=="Open" %}
<div class="web-list-item">
<a class="no-decoration" href="/projects?project={{ doc.name | urlencode }}">
<div class="row">
<div class="col-xs-6">
{{ doc.name }}
</div>
<div class="col-xs-3">
{% if doc.percent_complete %}
<div class="progress" style="margin-bottom: 0!important; margin-top: 10px!important; height:5px;">
<div class="progress-bar progress-bar-{{ "warning" if doc.percent_complete|round < 100 else "success"}}" role="progressbar"
aria-valuenow="{{ doc.percent_complete|round|int }}"
aria-valuemin="0" aria-valuemax="100" style="width:{{ doc.percent_complete|round|int }}%;">
</div>
</div>
{% else %}
<span class="indicator {{ "red" if doc.status=="Open" else "gray" }}">
{{ doc.status }}</span>
{% endif %}
</div>
<div class="col-xs-3 text-right small text-muted">
{{ frappe.utils.pretty_date(doc.modified) }}
</div>
</div>
</a>
</div>
{% if doc.status == "Open" %}
<div class="web-list-item transaction-list-item">
<div class="row">
<div class="col-xs-2">
<a class="transaction-item-link" href="/projects?project={{ doc.name | urlencode }}">Link</a>
{{ doc.name }}
</div>
<div class="col-xs-2">
{{ doc.project_name }}
</div>
<div class="col-xs-3 text-center">
{% if doc.percent_complete %}
{% set pill_class = "green" if doc.percent_complete | round == 100 else
"orange" %}
<div class="ellipsis">
<span class="indicator-pill {{ pill_class }} filterable ellipsis">
<span>{{ frappe.utils.cint(doc.percent_complete) }}
%</span>
</span>
</div>
{% else %}
<span class="indicator-pill {{ " red" if doc.status=="Open" else " darkgrey" }}">
{{ doc.status }}</span>
{% endif %}
</div>
{% if doc["_assign"] %}
{% set assigned_users = json.loads(doc["_assign"])%}
<div class="col-xs-2">
{% for user in assigned_users %}
{% set user_details = frappe
.db
.get_value("User", user, [
"full_name", "user_image"
], as_dict = True) %}
{% if user_details.user_image %}
<span class="avatar avatar-small" style="width:32px; height:32px;" title="{{ user_details.full_name }}">
<img src="{{ user_details.user_image }}">
</span>
{% else %}
<span class="avatar avatar-small" style="width:32px; height:32px;" title="{{ user_details.full_name }}">
<div class='standard-image' style="background-color: #F5F4F4; color: #000;">
{{ frappe.utils.get_abbr(user_details.full_name) }}
</div>
</span>
{% endif %}
{% endfor %}
</div>
{% endif %}
<div class="col-xs-3 text-right small text-muted">
{{ frappe.utils.pretty_date(doc.modified) }}
</div>
</div>
</div>
{% endif %}

View File

@@ -1,32 +1,5 @@
{% for task in doc.tasks %}
<div class='task'>
<a class="no-decoration task-link {{ task.css_seen }}" href="/tasks?name={{ task.name }}">
<div class='row project-item'>
<div class='col-xs-9'>
<span class="indicator {{ "red" if task.status=="Open" else "green" if task.status=="Closed" else "gray" }}" title="{{ task.status }}" > {{ task.subject }}</span>
<div class="small text-muted item-timestamp"
title="{{ frappe.utils.pretty_date(task.modified) }}">
{{ _("modified") }} {{ frappe.utils.pretty_date(task.modified) }}
</div>
</div>
<div class='col-xs-1'>{% if task.todo %}
{% if task.todo.user_image %}
<span class="avatar avatar-small" title="{{ task.todo.owner }}">
<img src="{{ task.todo.user_image }}">
</span>
{% else %}
<span class="avatar avatar-small standard-image" title="Assigned to {{ task.todo.owner }}">
</span>
{% endif %}
{% endif %} </div>
<div class='col-xs-2'>
<span class="pull-right list-comment-count small {{ "text-extra-muted" if task.comment_count==0 else "text-muted" }}">
<i class="octicon octicon-comment-discussion"></i>
{{ task.comment_count }}
</span>
</div>
</div>
</a>
</div>
<div class="web-list-item transaction-list-item">
{{ task_row(task, 0) }}
</div>
{% endfor %}

View File

@@ -1,23 +1,33 @@
{% for timesheet in doc.timesheets %}
<div class='timesheet'>
<a class="no-decoration timesheet-link {{ timesheet.css_seen }}" href="/timesheet/{{ timesheet.info.name}}">
<div class='row project-item'>
<div class='col-xs-10'>
<span class="indicator {{ "blue" if timesheet.info.status=="Submitted" else "red" if timesheet.info.status=="Draft" else "gray" }}" title="{{ timesheet.info.status }}" > {{ timesheet.info.name }} </span>
<div class="small text-muted item-timestamp">
{{ _("From") }} {{ frappe.format_date(timesheet.from_time) }} {{ _("to") }} {{ frappe.format_date(timesheet.to_time) }}
</div>
</div>
<div class='col-xs-1' style="margin-right:-30px;">
<span class="avatar avatar-small" title="{{ timesheet.info.modified_by }}"> <img src="{{ timesheet.info.user_image }}" style="display:flex;"></span>
</div>
<div class='col-xs-1'>
<span class="pull-right list-comment-count small {{ "text-extra-muted" if timesheet.comment_count==0 else "text-muted" }}">
<i class="octicon octicon-comment-discussion"></i>
{{ timesheet.info.comment_count }}
</span>
</div>
</div>
</a>
</div>
{% endfor %}
<div class="web-list-item transaction-list-item">
<div class="row">
<div class="col-xs-2">{{ timesheet.name }}</div>
<a class="transaction-item-link" href="/timesheet/{{ timesheet.name}}">Link</a>
<div class="col-xs-2">{{ timesheet.status }}</div>
<div class="col-xs-2">{{ frappe.utils.format_date(timesheet.from_time, "medium") }}</div>
<div class="col-xs-2">{{ frappe.utils.format_date(timesheet.to_time, "medium") }}</div>
<div class="col-xs-2">
{% set user_details = frappe
.db
.get_value("User", timesheet.modified_by, [
"full_name", "user_image"
], as_dict = True)
%}
{% if user_details.user_image %}
<span class="avatar avatar-small" style="width:32px; height:32px;" title="{{ user_details.full_name }}">
<img src="{{ user_details.user_image }}">
</span>
{% else %}
<span class="avatar avatar-small" style="width:32px; height:32px;" title="{{ user_details.full_name }}">
<div class='standard-image' style='background-color: #F5F4F4; color: #000;'>
{{ frappe.utils.get_abbr(user_details.full_name) }}
</div>
</span>
{% endif %}
</div>
<div class="col-xs-2 text-right">
{{ frappe.utils.pretty_date(timesheet.modified) }}
</div>
</div>
</div>
{% endfor %}

View File

@@ -1,90 +1,173 @@
{% extends "templates/web.html" %}
{% block title %}{{ doc.project_name }}{% endblock %}
{% block title %}
{{ doc.project_name }}
{% endblock %}
{% block head_include %}
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
{% endblock %}
{% block header %}
<h1>{{ doc.project_name }}</h1>
<h1>{{ doc.project_name }}</h1>
{% endblock %}
{% block style %}
<style>
{% include "templates/includes/projects.css" %}
</style>
<style>
{
% include "templates/includes/projects.css"%
}
</style>
{% endblock %}
{% block page_content %}
{% if doc.percent_complete %}
<div class="progress progress-hg">
<div class="progress-bar progress-bar-{{ "warning" if doc.percent_complete|round < 100 else "success" }} active" role="progressbar" aria-valuenow="{{ doc.percent_complete|round|int }}"
aria-valuemin="0" aria-valuemax="100" style="width:{{ doc.percent_complete|round|int }}%;">
</div>
</div>
{% endif %}
<div class="clearfix">
<h4 style="float: left;">{{ _("Tasks") }}</h4>
<a class="btn btn-secondary btn-light btn-sm" style="float: right; position: relative; top: 10px;" href='/tasks?new=1&project={{ doc.project_name }}'>{{ _("New task") }}</a>
</div>
{{ progress_bar(doc.percent_complete) }}
<p>
<!-- <a class='small underline task-status-switch' data-status='Open'>{{ _("Show closed") }}</a> -->
</p>
<div class="d-flex mt-5 mb-5 justify-content-between">
<h4>Status:</h4>
<h4>Progress:
<span>{{ doc.percent_complete }}
%</span>
</h4>
<h4>Hours Spent:
<span>{{ doc.actual_time }}</span>
</h4>
</div>
{% if doc.tasks %}
<div class='project-task-section'>
<div class='project-task'>
{% include "erpnext/templates/includes/projects/project_tasks.html" %}
</div>
<p><a id= 'more-task' style='display: none;' class='more-tasks small underline'>{{ _("More") }}</a><p>
</div>
{% else %}
<p class="text-muted">{{ _("No tasks") }}</p>
{% endif %}
{{ progress_bar(doc.percent_complete) }}
{% if doc.tasks %}
<div class="website-list">
<div class="result">
<div class="web-list-item transaction-list-item">
<div class="row">
<h3 class="col-xs-4">Tasks</h3>
<h3 class="col-xs-2">Status</h3>
<h3 class="col-xs-2">End Date</h3>
<h3 class="col-xs-2">Assigned To</h3>
<div class="col-xs-2 text-right">
<a class="btn btn-secondary btn-light btn-sm" href='/tasks?new=1&project={{ doc.project_name }}'>{{ _("New task") }}</a>
</div>
</div>
</div>
{% include "erpnext/templates/includes/projects/project_tasks.html" %}
</div>
</div>
{% else %}
<p class="font-weight-bold">{{ _("No Tasks") }}</p>
{% endif %}
<div class='padding'></div>
{% if doc.timesheets %}
<div class="website-list">
<div class="result">
<div class="web-list-item transaction-list-item">
<div class="row">
<h3 class="col-xs-2">Timesheets</h3>
<h3 class="col-xs-2">Status</h3>
<h3 class="col-xs-2">From</h3>
<h3 class="col-xs-2">To</h3>
<h3 class="col-xs-2">Modified By</h3>
<h3 class="col-xs-2 text-right">Modified On</h3>
</div>
</div>
{% include "erpnext/templates/includes/projects/project_timesheets.html" %}
</div>
</div>
{% else %}
<p class="font-weight-bold mt-5">{{ _("No Timesheets") }}</p>
{% endif %}
<h4>{{ _("Timesheets") }}</h4>
{% if doc.attachments %}
<div class='padding'></div>
{% if doc.timesheets %}
<div class='project-timelogs'>
{% include "erpnext/templates/includes/projects/project_timesheets.html" %}
</div>
{% if doc.timesheets|length > 9 %}
<p><a class='more-timelogs small underline'>{{ _("More") }}</a><p>
{% endif %}
{% else %}
<p class="text-muted">{{ _("No time sheets") }}</p>
{% endif %}
{% if doc.attachments %}
<div class='padding'></div>
<h4>{{ _("Attachments") }}</h4>
<div class="project-attachments">
{% for attachment in doc.attachments %}
<div class="attachment">
<a class="no-decoration attachment-link" href="{{ attachment.file_url }}" target="blank">
<div class="row">
<div class="col-xs-9">
<span class="indicator red file-name"> {{ attachment.file_name }}</span>
</div>
<div class="col-xs-3">
<span class="pull-right file-size">{{ attachment.file_size }}</span>
</div>
</div>
</a>
</div>
{% endfor %}
</div>
{% endif %}
<h4>{{ _("Attachments") }}</h4>
<div class="project-attachments">
{% for attachment in doc.attachments %}
<div class="attachment">
<a class="no-decoration attachment-link" href="{{ attachment.file_url }}" target="blank">
<div class="row">
<div class="col-xs-9">
<span class="indicator red file-name">
{{ attachment.file_name }}</span>
</div>
<div class="col-xs-3">
<span class="pull-right file-size">{{ attachment.file_size }}</span>
</div>
</div>
</a>
</div>
{% endfor %}
</div>
{% endif %}
</div>
<script>
{% include "frappe/public/js/frappe/provide.js" %}
{% include "frappe/public/js/frappe/form/formatters.js" %}
{ % include "frappe/public/js/frappe/provide.js" %
} { % include "frappe/public/js/frappe/form/formatters.js" %
}
</script>
{% endblock %}
{% macro progress_bar(percent_complete) %}
{% if percent_complete %}
<div class="progress progress-hg" style="height: 5px;">
<div class="progress-bar progress-bar-{{ 'warning' if percent_complete|round < 100 else 'success' }} active" role="progressbar" aria-valuenow="{{ percent_complete|round|int }}" aria-valuemin="0" aria-valuemax="100" style="width:{{ percent_complete|round|int }}%;"></div>
</div>
{% else %}
<hr>
{% endif %}
{% endmacro %}
{% macro task_row(task, indent) %}
<div class="row mt-5 {% if task.children %} font-weight-bold {% endif %}">
<div class="col-xs-4">
<a class="nav-link " style="color: inherit; {% if task.parent_task %} margin-left: {{ indent }}px {% endif %}" href="/tasks?name={{ task.name | urlencode }}">
{% if task.parent_task %}
<span class="">
<i class="fa fa-level-up fa-rotate-90"></i>
</span>
{% endif %}
{{ task.subject }}</a>
</div>
<div class="col-xs-2">{{ task.status }}</div>
<div class="col-xs-2">
{% if task.exp_end_date %}
{{ task.exp_end_date }}
{% else %}
--
{% endif %}
</div>
<div class="col-xs-2">
{% if task["_assign"] %}
{% set assigned_users = json.loads(task["_assign"])%}
{% for user in assigned_users %}
{% set user_details = frappe.db.get_value("User", user,
["full_name", "user_image"],
as_dict = True)%}
{% if user_details.user_image %}
<span class="avatar avatar-small" style="width:32px; height:32px;" title="{{ user_details.full_name }}">
<img src="{{ user_details.user_image }}">
</span>
{% else %}
<span class="avatar avatar-small" style="width:32px; height:32px;" title="{{ user_details.full_name }}">
<div class='standard-image' style='background-color: #F5F4F4; color: #000;'>
{{ frappe.utils.get_abbr(user_details.full_name) }}
</div>
</span>
{% endif %}
{% endfor %}
{% endif %}
</div>
<div class="col-xs-2 text-right">
{{ frappe.utils.pretty_date(task.modified) }}
</div>
</div>
{% if task.children %}
{% for child in task.children %}
{{ task_row(child, indent + 30) }}
{% endfor %}
{% endif %}
{% endmacro %}

View File

@@ -35,26 +35,16 @@ def get_tasks(project, start=0, search=None, item_status=None):
# if item_status:
# filters["status"] = item_status
tasks = frappe.get_all("Task", filters=filters,
fields=["name", "subject", "status", "_seen", "_comments", "modified", "description"],
fields=["name", "subject", "status", "modified", "_assign", "exp_end_date", "is_group", "parent_task"],
limit_start=start, limit_page_length=10)
task_nest = []
for task in tasks:
task.todo = frappe.get_all('ToDo',filters={'reference_name':task.name, 'reference_type':'Task'},
fields=["assigned_by", "owner", "modified", "modified_by"])
if task.todo:
task.todo=task.todo[0]
task.todo.user_image = frappe.db.get_value('User', task.todo.owner, 'user_image')
task.comment_count = len(json.loads(task._comments or "[]"))
task.css_seen = ''
if task._seen:
if frappe.session.user in json.loads(task._seen):
task.css_seen = 'seen'
return tasks
if task.is_group:
child_tasks = list(filter(lambda x: x.parent_task == task.name, tasks))
if len(child_tasks):
task.children = child_tasks
task_nest.append(task)
return list(filter(lambda x: not x.parent_task, tasks))
@frappe.whitelist()
def get_task_html(project, start=0, item_status=None):
@@ -74,19 +64,12 @@ def get_timesheets(project, start=0, search=None):
fields=['project','activity_type','from_time','to_time','parent'],
limit_start=start, limit_page_length=10)
for timesheet in timesheets:
timesheet.infos = frappe.get_all('Timesheet', filters={"name": timesheet.parent},
fields=['name','_comments','_seen','status','modified','modified_by'],
info = frappe.get_all('Timesheet', filters={"name": timesheet.parent},
fields=['name','status','modified','modified_by'],
limit_start=start, limit_page_length=10)
for timesheet.info in timesheet.infos:
timesheet.info.user_image = frappe.db.get_value('User', timesheet.info.modified_by, 'user_image')
timesheet.info.comment_count = len(json.loads(timesheet.info._comments or "[]"))
timesheet.info.css_seen = ''
if timesheet.info._seen:
if frappe.session.user in json.loads(timesheet.info._seen):
timesheet.info.css_seen = 'seen'
if len(info):
timesheet.update(info[0])
return timesheets
@frappe.whitelist()

View File

@@ -2,6 +2,7 @@ import frappe
import datetime
import json
import pytz
from frappe import _
WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
@@ -14,7 +15,8 @@ def get_context(context):
if is_enabled:
return context
else:
frappe.local.flags.redirect_location = '/404'
frappe.redirect_to_message(_("Appointment Scheduling Disabled"), _("Appointment Scheduling has been disabled for this site"),
http_status_code=302, indicator_color="red")
raise frappe.Redirect
@frappe.whitelist(allow_guest=True)