Merge branch 'develop' into payments-based-dunning

This commit is contained in:
barredterra
2021-09-16 17:23:35 +02:00
69 changed files with 1396 additions and 403 deletions

View File

@@ -11,6 +11,7 @@ coverage:
comment:
layout: "diff, files"
require_changes: true
after_n_builds: 3
ignore:
- "erpnext/demo"

View File

@@ -4,7 +4,7 @@
frappe.ui.form.on('POS Invoice Merge Log', {
setup: function(frm) {
frm.set_query("pos_invoice", "pos_invoices", doc => {
return{
return {
filters: {
'docstatus': 1,
'customer': doc.customer,
@@ -12,5 +12,10 @@ frappe.ui.form.on('POS Invoice Merge Log', {
}
}
});
},
merge_invoices_based_on: function(frm) {
frm.set_value('customer', '');
frm.set_value('customer_group', '');
}
});

View File

@@ -6,9 +6,11 @@
"engine": "InnoDB",
"field_order": [
"posting_date",
"customer",
"merge_invoices_based_on",
"column_break_3",
"pos_closing_entry",
"customer",
"customer_group",
"section_break_3",
"pos_invoices",
"references_section",
@@ -88,12 +90,27 @@
"fieldtype": "Link",
"label": "POS Closing Entry",
"options": "POS Closing Entry"
},
{
"fieldname": "merge_invoices_based_on",
"fieldtype": "Select",
"label": "Merge Invoices Based On",
"options": "Customer\nCustomer Group",
"reqd": 1
},
{
"depends_on": "eval:doc.merge_invoices_based_on == 'Customer Group'",
"fieldname": "customer_group",
"fieldtype": "Link",
"label": "Customer Group",
"mandatory_depends_on": "eval:doc.merge_invoices_based_on == 'Customer Group'",
"options": "Customer Group"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-12-01 11:53:57.267579",
"modified": "2021-09-14 11:17:19.001142",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice Merge Log",

View File

@@ -23,6 +23,9 @@ class POSInvoiceMergeLog(Document):
self.validate_pos_invoice_status()
def validate_customer(self):
if self.merge_invoices_based_on == 'Customer Group':
return
for d in self.pos_invoices:
if d.customer != self.customer:
frappe.throw(_("Row #{}: POS Invoice {} is not against customer {}").format(d.idx, d.pos_invoice, self.customer))
@@ -124,7 +127,7 @@ class POSInvoiceMergeLog(Document):
found = False
for i in items:
if (i.item_code == item.item_code and not i.serial_no and not i.batch_no and
i.uom == item.uom and i.net_rate == item.net_rate):
i.uom == item.uom and i.net_rate == item.net_rate and i.warehouse == item.warehouse):
found = True
i.qty = i.qty + item.qty
@@ -172,6 +175,11 @@ class POSInvoiceMergeLog(Document):
invoice.discount_amount = 0.0
invoice.taxes_and_charges = None
invoice.ignore_pricing_rule = 1
invoice.customer = self.customer
if self.merge_invoices_based_on == 'Customer Group':
invoice.flags.ignore_pos_profile = True
invoice.pos_profile = ''
return invoice
@@ -228,7 +236,7 @@ def get_all_unconsolidated_invoices():
return pos_invoices
def get_invoice_customer_map(pos_invoices):
# pos_invoice_customer_map = { 'Customer 1': [{}, {}, {}], 'Custoemr 2' : [{}] }
# pos_invoice_customer_map = { 'Customer 1': [{}, {}, {}], 'Customer 2' : [{}] }
pos_invoice_customer_map = {}
for invoice in pos_invoices:
customer = invoice.get('customer')

View File

@@ -499,7 +499,7 @@ class SalesInvoice(SellingController):
self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account')
from erpnext.stock.get_item_details import get_pos_profile, get_pos_profile_item_details
if not self.pos_profile:
if not self.pos_profile and not self.flags.ignore_pos_profile:
pos_profile = get_pos_profile(self.company) or {}
if not pos_profile:
return

View File

@@ -260,7 +260,12 @@ def get_company_currency(filters=None):
def calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters):
for entries in gl_entries_by_account.values():
for entry in entries:
d = accounts_by_name.get(entry.account_name)
if entry.account_number:
account_name = entry.account_number + ' - ' + entry.account_name
else:
account_name = entry.account_name
d = accounts_by_name.get(account_name)
if d:
for company in companies:
# check if posting date is within the period
@@ -307,7 +312,14 @@ def update_parent_account_names(accounts):
of account_number and suffix of company abbr. This function adds key called
`parent_account_name` which does not have such prefix/suffix.
"""
name_to_account_map = { d.name : d.account_name for d in accounts }
name_to_account_map = {}
for d in accounts:
if d.account_number:
account_name = d.account_number + ' - ' + d.account_name
else:
account_name = d.account_name
name_to_account_map[d.name] = account_name
for account in accounts:
if account.parent_account:
@@ -420,7 +432,11 @@ def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, g
convert_to_presentation_currency(gl_entries, currency_info, filters.get('company'))
for entry in gl_entries:
account_name = entry.account_name
if entry.account_number:
account_name = entry.account_number + ' - ' + entry.account_name
else:
account_name = entry.account_name
validate_entries(account_name, entry, accounts_by_name, accounts)
gl_entries_by_account.setdefault(account_name, []).append(entry)
@@ -491,7 +507,12 @@ def filter_accounts(accounts, depth=10):
parent_children_map = {}
accounts_by_name = {}
for d in accounts:
accounts_by_name[d.account_name] = d
if d.account_number:
account_name = d.account_number + ' - ' + d.account_name
else:
account_name = d.account_name
accounts_by_name[account_name] = d
parent_children_map.setdefault(d.parent_account or None, []).append(d)
filtered_accounts = []

View File

@@ -4,9 +4,10 @@
frappe.query_reports["Unpaid Expense Claim"] = {
"filters": [
{
"fieldname":"employee",
"fieldname": "employee",
"label": __("Employee"),
"fieldtype": "Link"
"fieldtype": "Link",
"options": "Employee"
}
]
}

View File

@@ -28,7 +28,7 @@
"fieldname": "supp_master_name",
"fieldtype": "Select",
"label": "Supplier Naming By",
"options": "Supplier Name\nNaming Series"
"options": "Supplier Name\nNaming Series\nAuto Name"
},
{
"fieldname": "supplier_group",
@@ -123,7 +123,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-06-24 10:38:28.934525",
"modified": "2021-09-08 19:26:23.548837",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",

View File

@@ -394,12 +394,10 @@ def get_item_from_material_requests_based_on_supplier(source_name, target_doc =
@frappe.whitelist()
def get_supplier_tag():
if not frappe.cache().hget("Supplier", "Tags"):
filters = {"document_type": "Supplier"}
tags = list(set(tag.tag for tag in frappe.get_all("Tag Link", filters=filters, fields=["tag"]) if tag))
frappe.cache().hset("Supplier", "Tags", tags)
filters = {"document_type": "Supplier"}
tags = list(set(tag.tag for tag in frappe.get_all("Tag Link", filters=filters, fields=["tag"]) if tag))
return frappe.cache().hget("Supplier", "Tags")
return tags
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs

View File

@@ -433,12 +433,12 @@
"image_field": "image",
"links": [
{
"group": "Item Group",
"link_doctype": "Supplier Item Group",
"link_fieldname": "supplier"
"group": "Allowed Items",
"link_doctype": "Party Specific Item",
"link_fieldname": "party"
}
],
"modified": "2021-08-27 18:02:44.314077",
"modified": "2021-09-06 17:37:56.522233",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",

View File

@@ -10,7 +10,7 @@ from frappe.contacts.address_and_contact import (
delete_contact_and_address,
load_address_and_contact,
)
from frappe.model.naming import set_name_by_naming_series
from frappe.model.naming import set_name_by_naming_series, set_name_from_naming_options
from erpnext.accounts.party import get_dashboard_info, validate_party_accounts
from erpnext.utilities.transaction_base import TransactionBase
@@ -40,8 +40,10 @@ class Supplier(TransactionBase):
supp_master_name = frappe.defaults.get_global_default('supp_master_name')
if supp_master_name == 'Supplier Name':
self.name = self.supplier_name
else:
elif supp_master_name == 'Naming Series':
set_name_by_naming_series(self)
else:
self.name = set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self)
def on_update(self):
if not self.naming_series:

View File

@@ -1,77 +0,0 @@
{
"actions": [],
"creation": "2021-05-07 18:16:40.621421",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"supplier",
"item_group"
],
"fields": [
{
"fieldname": "supplier",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Supplier",
"options": "Supplier",
"reqd": 1
},
{
"fieldname": "item_group",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item Group",
"options": "Item Group",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-05-19 13:48:16.742303",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Item Group",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Purchase User",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Purchase Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
class SupplierItemGroup(Document):
def validate(self):
exists = frappe.db.exists({
'doctype': 'Supplier Item Group',
'supplier': self.supplier,
'item_group': self.item_group
})
if exists:
frappe.throw(_("Item Group has already been linked to this supplier."))

View File

@@ -1,11 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestSupplierItemGroup(unittest.TestCase):
pass

View File

@@ -7,6 +7,7 @@ import json
from collections import defaultdict
import frappe
from frappe import scrub
from frappe.desk.reportview import get_filters_cond, get_match_cond
from frappe.utils import nowdate, unique
@@ -223,18 +224,29 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
if not field in searchfields]
searchfields = " or ".join([field + " like %(txt)s" for field in searchfields])
if filters and isinstance(filters, dict) and filters.get('supplier'):
item_group_list = frappe.get_all('Supplier Item Group',
filters = {'supplier': filters.get('supplier')}, fields = ['item_group'])
if filters and isinstance(filters, dict):
if filters.get('customer') or filters.get('supplier'):
party = filters.get('customer') or filters.get('supplier')
item_rules_list = frappe.get_all('Party Specific Item',
filters = {'party': party}, fields = ['restrict_based_on', 'based_on_value'])
item_groups = []
for i in item_group_list:
item_groups.append(i.item_group)
filters_dict = {}
for rule in item_rules_list:
if rule['restrict_based_on'] == 'Item':
rule['restrict_based_on'] = 'name'
filters_dict[rule.restrict_based_on] = []
del filters['supplier']
for rule in item_rules_list:
filters_dict[rule.restrict_based_on].append(rule.based_on_value)
for filter in filters_dict:
filters[scrub(filter)] = ['in', filters_dict[filter]]
if filters.get('customer'):
del filters['customer']
else:
del filters['supplier']
if item_groups:
filters['item_group'] = ['in', item_groups]
description_cond = ''
if frappe.db.count('Item', cache=True) < 50000:

View File

@@ -7,6 +7,7 @@ import json
import frappe
from frappe import _
from frappe.modules.utils import get_module_app
from frappe.utils import flt, has_common
from frappe.utils.user import is_website_user
@@ -21,8 +22,32 @@ def get_list_context(context=None):
"get_list": get_transaction_list
}
def get_webform_list_context(module):
if get_module_app(module) != 'erpnext':
return
return {
"get_list": get_webform_transaction_list
}
def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by="modified"):
def get_webform_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by="modified"):
""" Get List of transactions for custom doctypes """
from frappe.www.list import get_list
if not filters:
filters = []
meta = frappe.get_meta(doctype)
for d in meta.fields:
if d.fieldtype == 'Link' and d.fieldname != 'amended_from':
allowed_docs = [d.name for d in get_transaction_list(doctype=d.options, custom=True)]
allowed_docs.append('')
filters.append((d.fieldname, 'in', allowed_docs))
return get_list(doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=False,
fields=None, order_by="modified")
def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by="modified", custom=False):
user = frappe.session.user
ignore_permissions = False
@@ -46,7 +71,7 @@ def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_p
filters.append(('customer', 'in', customers))
elif suppliers:
filters.append(('supplier', 'in', suppliers))
else:
elif not custom:
return []
if doctype == 'Request for Quotation':
@@ -56,9 +81,16 @@ def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_p
# Since customers and supplier do not have direct access to internal doctypes
ignore_permissions = True
if not customers and not suppliers and custom:
ignore_permissions = False
filters = []
transactions = get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_length,
fields='name', ignore_permissions=ignore_permissions, order_by='modified desc')
if custom:
return transactions
return post_process(doctype, transactions)
def get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_length=20,

View File

@@ -62,6 +62,7 @@ treeviews = ['Account', 'Cost Center', 'Warehouse', 'Item Group', 'Customer Grou
# website
update_website_context = ["erpnext.shopping_cart.utils.update_website_context", "erpnext.education.doctype.education_settings.education_settings.update_website_context"]
my_account_context = "erpnext.shopping_cart.utils.update_my_account_context"
webform_list_context = "erpnext.controllers.website_list_for_contact.get_webform_list_context"
calendars = ["Task", "Work Order", "Leave Application", "Sales Order", "Holiday List", "Course Schedule"]
@@ -441,7 +442,7 @@ accounting_dimension_doctypes = ["GL Entry", "Sales Invoice", "Purchase Invoice"
"Purchase Receipt Item", "Stock Entry Detail", "Payment Entry Deduction", "Sales Taxes and Charges", "Purchase Taxes and Charges", "Shipping Rule",
"Landed Cost Item", "Asset Value Adjustment", "Loyalty Program", "Fee Schedule", "Fee Structure", "Stock Reconciliation",
"Travel Request", "Fees", "POS Profile", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item", "Subscription",
"Subscription Plan"
"Subscription Plan", "POS Invoice", "POS Invoice Item"
]
regional_overrides = {

View File

@@ -73,7 +73,7 @@ frappe.ui.form.on('Employee Advance', {
frm.trigger('make_return_entry');
}, __('Create'));
} else if (frm.doc.repay_unclaimed_amount_from_salary == 1 && frappe.model.can_create("Additional Salary")) {
frm.add_custom_button(__("Deduction from salary"), function() {
frm.add_custom_button(__("Deduction from Salary"), function() {
frm.events.make_deduction_via_additional_salary(frm);
}, __('Create'));
}

View File

@@ -170,7 +170,7 @@
"default": "0",
"fieldname": "repay_unclaimed_amount_from_salary",
"fieldtype": "Check",
"label": "Repay unclaimed amount from salary"
"label": "Repay Unclaimed Amount from Salary"
},
{
"depends_on": "eval:cur_frm.doc.employee",
@@ -200,10 +200,11 @@
],
"is_submittable": 1,
"links": [],
"modified": "2021-03-31 22:31:53.746659",
"modified": "2021-09-11 18:38:38.617478",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Advance",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{

View File

@@ -172,7 +172,10 @@ def get_paying_amount_paying_exchange_rate(payment_account, doc):
@frappe.whitelist()
def create_return_through_additional_salary(doc):
import json
doc = frappe._dict(json.loads(doc))
if isinstance(doc, str):
doc = frappe._dict(json.loads(doc))
additional_salary = frappe.new_doc('Additional Salary')
additional_salary.employee = doc.employee
additional_salary.currency = doc.currency

View File

@@ -12,8 +12,11 @@ import erpnext
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.employee_advance.employee_advance import (
EmployeeAdvanceOverPayment,
create_return_through_additional_salary,
make_bank_entry,
)
from erpnext.payroll.doctype.salary_component.test_salary_component import create_salary_component
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
class TestEmployeeAdvance(unittest.TestCase):
@@ -33,6 +36,46 @@ class TestEmployeeAdvance(unittest.TestCase):
journal_entry1 = make_payment_entry(advance)
self.assertRaises(EmployeeAdvanceOverPayment, journal_entry1.submit)
def test_repay_unclaimed_amount_from_salary(self):
employee_name = make_employee("_T@employe.advance")
advance = make_employee_advance(employee_name, {"repay_unclaimed_amount_from_salary": 1})
args = {"type": "Deduction"}
create_salary_component("Advance Salary - Deduction", **args)
make_salary_structure("Test Additional Salary for Advance Return", "Monthly", employee=employee_name)
# additional salary for 700 first
advance.reload()
additional_salary = create_return_through_additional_salary(advance)
additional_salary.salary_component = "Advance Salary - Deduction"
additional_salary.payroll_date = nowdate()
additional_salary.amount = 700
additional_salary.insert()
additional_salary.submit()
advance.reload()
self.assertEqual(advance.return_amount, 700)
# additional salary for remaining 300
additional_salary = create_return_through_additional_salary(advance)
additional_salary.salary_component = "Advance Salary - Deduction"
additional_salary.payroll_date = nowdate()
additional_salary.amount = 300
additional_salary.insert()
additional_salary.submit()
advance.reload()
self.assertEqual(advance.return_amount, 1000)
# update advance return amount on additional salary cancellation
additional_salary.cancel()
advance.reload()
self.assertEqual(advance.return_amount, 700)
def tearDown(self):
frappe.db.rollback()
def make_payment_entry(advance):
journal_entry = frappe.get_doc(make_bank_entry("Employee Advance", advance.name))
journal_entry.cheque_no = "123123"
@@ -41,7 +84,7 @@ def make_payment_entry(advance):
return journal_entry
def make_employee_advance(employee_name):
def make_employee_advance(employee_name, args=None):
doc = frappe.new_doc("Employee Advance")
doc.employee = employee_name
doc.company = "_Test company"
@@ -51,6 +94,10 @@ def make_employee_advance(employee_name):
doc.advance_amount = 1000
doc.posting_date = nowdate()
doc.advance_account = "_Test Employee Advance - _TC"
if args:
doc.update(args)
doc.insert()
doc.submit()

View File

@@ -32,7 +32,10 @@ def set_employee_name(doc):
def update_employee(employee, details, date=None, cancel=False):
internal_work_history = {}
for item in details:
fieldtype = frappe.get_meta("Employee").get_field(item.fieldname).fieldtype
field = frappe.get_meta("Employee").get_field(item.fieldname)
if not field:
continue
fieldtype = field.fieldtype
new_data = item.new if not cancel else item.current
if fieldtype == "Date" and new_data:
new_data = getdate(new_data)

View File

@@ -18,7 +18,7 @@ frappe.ui.form.on('Maintenance Schedule', {
},
refresh: function (frm) {
setTimeout(() => {
frm.toggle_display('generate_schedule', !(frm.is_new()));
frm.toggle_display('generate_schedule', !(frm.is_new() || frm.doc.docstatus));
frm.toggle_display('schedule', !(frm.is_new()));
}, 10);
},

View File

@@ -16,9 +16,9 @@ from erpnext.utilities.transaction_base import TransactionBase, delete_events
class MaintenanceSchedule(TransactionBase):
@frappe.whitelist()
def generate_schedule(self):
if self.docstatus != 0:
return
self.set('schedules', [])
frappe.db.sql("""delete from `tabMaintenance Schedule Detail`
where parent=%s""", (self.name))
count = 1
for d in self.get('items'):
self.validate_maintenance_detail()

View File

@@ -510,8 +510,14 @@ class BOM(WebsiteGenerator):
if d.workstation:
self.update_rate_and_time(d, update_hour_rate)
self.operating_cost += flt(d.operating_cost)
self.base_operating_cost += flt(d.base_operating_cost)
operating_cost = d.operating_cost
base_operating_cost = d.base_operating_cost
if d.set_cost_based_on_bom_qty:
operating_cost = flt(d.cost_per_unit) * flt(self.quantity)
base_operating_cost = flt(d.base_cost_per_unit) * flt(self.quantity)
self.operating_cost += flt(operating_cost)
self.base_operating_cost += flt(base_operating_cost)
def update_rate_and_time(self, row, update_hour_rate = False):
if not row.hour_rate or update_hour_rate:
@@ -535,6 +541,8 @@ class BOM(WebsiteGenerator):
row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate)
row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0
row.base_operating_cost = flt(row.operating_cost) * flt(self.conversion_rate)
row.cost_per_unit = row.operating_cost / (row.batch_size or 1.0)
row.base_cost_per_unit = row.base_operating_cost / (row.batch_size or 1.0)
if update_hour_rate:
row.db_update()

View File

@@ -108,6 +108,24 @@ class TestBOM(unittest.TestCase):
self.assertAlmostEqual(bom.base_raw_material_cost, base_raw_material_cost)
self.assertAlmostEqual(bom.base_total_cost, base_raw_material_cost + base_op_cost)
def test_bom_cost_with_batch_size(self):
bom = frappe.copy_doc(test_records[2])
bom.docstatus = 0
op_cost = 0.0
for op_row in bom.operations:
op_row.docstatus = 0
op_row.batch_size = 2
op_row.set_cost_based_on_bom_qty = 1
op_cost += op_row.operating_cost
bom.save()
for op_row in bom.operations:
self.assertAlmostEqual(op_row.cost_per_unit, op_row.operating_cost / 2)
self.assertAlmostEqual(bom.operating_cost, op_cost/2)
bom.delete()
def test_bom_cost_multi_uom_multi_currency_based_on_price_list(self):
frappe.db.set_value("Price List", "_Test Price List", "price_not_uom_dependent", 1)
for item_code, rate in (("_Test Item", 3600), ("_Test Item Home Desktop Manufactured", 3000)):

View File

@@ -8,15 +8,23 @@
"field_order": [
"sequence_id",
"operation",
"workstation",
"description",
"col_break1",
"hour_rate",
"workstation",
"time_in_mins",
"operating_cost",
"costing_section",
"hour_rate",
"base_hour_rate",
"column_break_9",
"operating_cost",
"base_operating_cost",
"column_break_11",
"batch_size",
"set_cost_based_on_bom_qty",
"cost_per_unit",
"base_cost_per_unit",
"more_information_section",
"description",
"column_break_18",
"image"
],
"fields": [
@@ -117,13 +125,59 @@
"fieldname": "sequence_id",
"fieldtype": "Int",
"label": "Sequence ID"
},
{
"depends_on": "eval:doc.batch_size > 0 && doc.set_cost_based_on_bom_qty",
"fieldname": "cost_per_unit",
"fieldtype": "Float",
"label": "Cost Per Unit",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "base_cost_per_unit",
"fieldtype": "Float",
"hidden": 1,
"label": "Base Cost Per Unit",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "costing_section",
"fieldtype": "Section Break",
"label": "Costing"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"fieldname": "more_information_section",
"fieldtype": "Section Break",
"label": "More Information"
},
{
"fieldname": "column_break_18",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "set_cost_based_on_bom_qty",
"fieldtype": "Check",
"label": "Set Operating Cost Based On BOM Quantity"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-01-12 14:48:09.596843",
"modified": "2021-09-13 16:45:01.092868",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Operation",

View File

@@ -26,15 +26,23 @@ frappe.ui.form.on('Job Card', {
refresh: function(frm) {
frappe.flags.pause_job = 0;
frappe.flags.resume_job = 0;
let has_items = frm.doc.items && frm.doc.items.length;
if(!frm.doc.__islocal && frm.doc.items && frm.doc.items.length) {
if (frm.doc.for_quantity != frm.doc.transferred_qty) {
if (!frm.doc.__islocal && has_items && frm.doc.docstatus < 2) {
let to_request = frm.doc.for_quantity > frm.doc.transferred_qty;
let excess_transfer_allowed = frm.doc.__onload.job_card_excess_transfer;
if (to_request || excess_transfer_allowed) {
frm.add_custom_button(__("Material Request"), () => {
frm.trigger("make_material_request");
});
}
if (frm.doc.for_quantity != frm.doc.transferred_qty) {
// check if any row has untransferred materials
// in case of multiple items in JC
let to_transfer = frm.doc.items.some((row) => row.transferred_qty < row.required_qty);
if (to_transfer || excess_transfer_allowed) {
frm.add_custom_button(__("Material Transfer"), () => {
frm.trigger("make_stock_entry");
}).addClass("btn-primary");

View File

@@ -38,6 +38,8 @@
"total_time_in_mins",
"section_break_8",
"items",
"scrap_items_section",
"scrap_items",
"corrective_operation_section",
"for_job_card",
"is_corrective_job_card",
@@ -185,7 +187,7 @@
"default": "0",
"fieldname": "transferred_qty",
"fieldtype": "Float",
"label": "Transferred Qty",
"label": "FG Qty from Transferred Raw Materials",
"read_only": 1
},
{
@@ -392,14 +394,28 @@
"fieldtype": "Link",
"label": "Batch No",
"options": "Batch"
},
{
"fieldname": "scrap_items_section",
"fieldtype": "Section Break",
"label": "Scrap Items"
},
{
"fieldname": "scrap_items",
"fieldtype": "Table",
"label": "Scrap Items",
"no_copy": 1,
"options": "Job Card Scrap Item",
"print_hide": 1
}
],
"is_submittable": 1,
"links": [],
"modified": "2021-03-16 15:59:32.766484",
"modified": "2021-09-14 00:38:46.873105",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{

View File

@@ -1,9 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import datetime
import json
@@ -37,6 +34,10 @@ class OperationSequenceError(frappe.ValidationError): pass
class JobCardCancelError(frappe.ValidationError): pass
class JobCard(Document):
def onload(self):
excess_transfer = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer")
self.set_onload("job_card_excess_transfer", excess_transfer)
def validate(self):
self.validate_time_logs()
self.set_status()
@@ -449,6 +450,7 @@ class JobCard(Document):
frappe.db.set_value('Job Card Item', row.job_card_item, 'transferred_qty', flt(qty))
def set_transferred_qty(self, update_status=False):
"Set total FG Qty for which RM was transferred."
if not self.items:
self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0
@@ -457,6 +459,7 @@ class JobCard(Document):
return
if self.items:
# sum of 'For Quantity' of Stock Entries against JC
self.transferred_qty = frappe.db.get_value('Stock Entry', {
'job_card': self.name,
'work_order': self.work_order,
@@ -500,7 +503,9 @@ class JobCard(Document):
self.status = 'Work In Progress'
if (self.docstatus == 1 and
(self.for_quantity == self.transferred_qty or not self.items)):
(self.for_quantity <= self.transferred_qty or not self.items)):
# consider excess transfer
# completed qty is checked via separate validation
self.status = 'Completed'
if self.status != 'Completed':
@@ -618,7 +623,11 @@ def make_stock_entry(source_name, target_doc=None):
def set_missing_values(source, target):
target.purpose = "Material Transfer for Manufacture"
target.from_bom = 1
target.fg_completed_qty = source.get('for_quantity', 0) - source.get('transferred_qty', 0)
# avoid negative 'For Quantity'
pending_fg_qty = source.get('for_quantity', 0) - source.get('transferred_qty', 0)
target.fg_completed_qty = pending_fg_qty if pending_fg_qty > 0 else 0
target.set_transfer_qty()
target.calculate_rate_and_amount()
target.set_missing_values()

View File

@@ -1,22 +1,37 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import unittest
import frappe
from frappe.utils import random_string
from erpnext.manufacturing.doctype.job_card.job_card import OperationMismatchError, OverlapError
from erpnext.manufacturing.doctype.job_card.job_card import (
make_stock_entry as make_stock_entry_from_jc,
)
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
class TestJobCard(unittest.TestCase):
def setUp(self):
self.work_order = make_wo_order_test_record(item="_Test FG Item 2", qty=2)
transfer_material_against, source_warehouse = None, None
tests_that_transfer_against_jc = ("test_job_card_multiple_materials_transfer",
"test_job_card_excess_material_transfer")
if self._testMethodName in tests_that_transfer_against_jc:
transfer_material_against = "Job Card"
source_warehouse = "Stores - _TC"
self.work_order = make_wo_order_test_record(
item="_Test FG Item 2",
qty=2,
transfer_material_against=transfer_material_against,
source_warehouse=source_warehouse
)
def tearDown(self):
frappe.db.rollback()
@@ -96,3 +111,84 @@ class TestJobCard(unittest.TestCase):
"employee": employee,
})
self.assertRaises(OverlapError, jc2.save)
def test_job_card_multiple_materials_transfer(self):
"Test transferring RMs separately against Job Card with multiple RMs."
make_stock_entry(
item_code="_Test Item",
target="Stores - _TC",
qty=10,
basic_rate=100
)
make_stock_entry(
item_code="_Test Item Home Desktop Manufactured",
target="Stores - _TC",
qty=6,
basic_rate=100
)
job_card_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name})
job_card = frappe.get_doc("Job Card", job_card_name)
transfer_entry_1 = make_stock_entry_from_jc(job_card_name)
del transfer_entry_1.items[1] # transfer only 1 of 2 RMs
transfer_entry_1.insert()
transfer_entry_1.submit()
job_card.reload()
self.assertEqual(transfer_entry_1.fg_completed_qty, 2)
self.assertEqual(job_card.transferred_qty, 2)
# transfer second RM
transfer_entry_2 = make_stock_entry_from_jc(job_card_name)
del transfer_entry_2.items[0]
transfer_entry_2.insert()
transfer_entry_2.submit()
# 'For Quantity' here will be 0 since
# transfer was made for 2 fg qty in first transfer Stock Entry
self.assertEqual(transfer_entry_2.fg_completed_qty, 0)
def test_job_card_excess_material_transfer(self):
"Test transferring more than required RM against Job Card."
make_stock_entry(item_code="_Test Item", target="Stores - _TC",
qty=25, basic_rate=100)
make_stock_entry(item_code="_Test Item Home Desktop Manufactured",
target="Stores - _TC", qty=15, basic_rate=100)
job_card_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name})
job_card = frappe.get_doc("Job Card", job_card_name)
# fully transfer both RMs
transfer_entry_1 = make_stock_entry_from_jc(job_card_name)
transfer_entry_1.insert()
transfer_entry_1.submit()
# transfer extra qty of both RM due to previously damaged RM
transfer_entry_2 = make_stock_entry_from_jc(job_card_name)
# deliberately change 'For Quantity'
transfer_entry_2.fg_completed_qty = 1
transfer_entry_2.items[0].qty = 5
transfer_entry_2.items[1].qty = 3
transfer_entry_2.insert()
transfer_entry_2.submit()
job_card.reload()
self.assertGreater(job_card.transferred_qty, job_card.for_quantity)
# Check if 'For Quantity' is negative
# as 'transferred_qty' > Qty to Manufacture
transfer_entry_3 = make_stock_entry_from_jc(job_card_name)
self.assertEqual(transfer_entry_3.fg_completed_qty, 0)
job_card.append("time_logs", {
"from_time": "2021-01-01 00:01:00",
"to_time": "2021-01-01 06:00:00",
"completed_qty": 2
})
job_card.save()
job_card.submit()
# JC is Completed with excess transfer
self.assertEqual(job_card.status, "Completed")

View File

@@ -0,0 +1,82 @@
{
"actions": [],
"creation": "2021-09-14 00:30:28.533884",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"item_code",
"item_name",
"column_break_3",
"description",
"quantity_and_rate",
"stock_qty",
"column_break_6",
"stock_uom"
],
"fields": [
{
"fieldname": "item_code",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Scrap Item Code",
"options": "Item",
"reqd": 1
},
{
"fetch_from": "item_code.item_name",
"fieldname": "item_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Scrap Item Name"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fetch_from": "item_code.description",
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description",
"read_only": 1
},
{
"fieldname": "quantity_and_rate",
"fieldtype": "Section Break",
"label": "Quantity and Rate"
},
{
"fieldname": "stock_qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Qty",
"reqd": 1
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"fetch_from": "item_code.stock_uom",
"fieldname": "stock_uom",
"fieldtype": "Link",
"label": "Stock UOM",
"options": "UOM",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-09-14 01:20:48.588052",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card Scrap Item",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from frappe.model.document import Document
class JobCardScrapItem(Document):
pass

View File

@@ -25,9 +25,12 @@
"overproduction_percentage_for_sales_order",
"column_break_16",
"overproduction_percentage_for_work_order",
"job_card_section",
"add_corrective_operation_cost_in_finished_good_valuation",
"column_break_24",
"job_card_excess_transfer",
"other_settings_section",
"update_bom_costs_automatically",
"add_corrective_operation_cost_in_finished_good_valuation",
"column_break_23",
"make_serial_no_batch_from_work_order"
],
@@ -96,10 +99,10 @@
},
{
"default": "0",
"description": "Allow multiple material consumptions against a Work Order",
"description": "Allow material consumptions without immediately manufacturing finished goods against a Work Order",
"fieldname": "material_consumption",
"fieldtype": "Check",
"label": "Allow Multiple Material Consumption"
"label": "Allow Continuous Material Consumption"
},
{
"default": "0",
@@ -175,13 +178,29 @@
"fieldname": "add_corrective_operation_cost_in_finished_good_valuation",
"fieldtype": "Check",
"label": "Add Corrective Operation Cost in Finished Good Valuation"
},
{
"fieldname": "job_card_section",
"fieldtype": "Section Break",
"label": "Job Card"
},
{
"fieldname": "column_break_24",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "Allow transferring raw materials even after the Required Quantity is fulfilled",
"fieldname": "job_card_excess_transfer",
"fieldtype": "Check",
"label": "Allow Excess Material Transfer"
}
],
"icon": "icon-wrench",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-03-16 15:54:38.967341",
"modified": "2021-09-13 22:09:09.401559",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing Settings",

View File

@@ -242,6 +242,8 @@ frappe.ui.form.on('Production Plan', {
},
get_sub_assembly_items: function(frm) {
frm.dirty();
frappe.call({
method: "get_sub_assembly_items",
freeze: true,

View File

@@ -561,8 +561,6 @@ class ProductionPlan(Document):
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)

View File

@@ -404,6 +404,7 @@ def make_bom(**args):
'uom': item_doc.stock_uom,
'stock_uom': item_doc.stock_uom,
'rate': item_doc.valuation_rate or args.rate,
'source_warehouse': args.source_warehouse
})
if not args.do_not_save:

View File

@@ -1,9 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import unittest
import frappe
@@ -20,7 +16,7 @@ from erpnext.manufacturing.doctype.work_order.work_order import (
stop_unstop,
)
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.item.test_item import create_item, make_item
from erpnext.stock.doctype.stock_entry import test_stock_entry
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.utils import get_bin
@@ -772,6 +768,60 @@ class TestWorkOrder(unittest.TestCase):
total_pl_qty
)
def test_job_card_scrap_item(self):
items = ['Test FG Item for Scrap Item Test', 'Test RM Item 1 for Scrap Item Test',
'Test RM Item 2 for Scrap Item Test']
company = '_Test Company with perpetual inventory'
for item_code in items:
create_item(item_code = item_code, is_stock_item = 1,
is_purchase_item=1, opening_stock=100, valuation_rate=10, company=company, warehouse='Stores - TCP1')
item = 'Test FG Item for Scrap Item Test'
raw_materials = ['Test RM Item 1 for Scrap Item Test', 'Test RM Item 2 for Scrap Item Test']
if not frappe.db.get_value('BOM', {'item': item}):
bom = make_bom(item=item, source_warehouse='Stores - TCP1', raw_materials=raw_materials, do_not_save=True)
bom.with_operations = 1
bom.append('operations', {
'operation': '_Test Operation 1',
'workstation': '_Test Workstation 1',
'hour_rate': 20,
'time_in_mins': 60
})
bom.submit()
wo_order = make_wo_order_test_record(item=item, company=company, planned_start_date=now(), qty=20, skip_transfer=1)
job_card = frappe.db.get_value('Job Card', {'work_order': wo_order.name}, 'name')
update_job_card(job_card)
stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10))
for row in stock_entry.items:
if row.is_scrap_item:
self.assertEqual(row.qty, 1)
def update_job_card(job_card):
job_card_doc = frappe.get_doc('Job Card', job_card)
job_card_doc.set('scrap_items', [
{
'item_code': 'Test RM Item 1 for Scrap Item Test',
'stock_qty': 2
},
{
'item_code': 'Test RM Item 2 for Scrap Item Test',
'stock_qty': 2
},
])
job_card_doc.append('time_logs', {
'from_time': now(),
'time_in_mins': 60,
'completed_qty': job_card_doc.for_quantity
})
job_card_doc.submit()
def get_scrap_item_details(bom_no):
scrap_items = {}
for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item`
@@ -814,6 +864,7 @@ def make_wo_order_test_record(**args):
wo_order.get_items_and_operations_from_bom()
wo_order.sales_order = args.sales_order or None
wo_order.planned_start_date = args.planned_start_date or now()
wo_order.transfer_material_against = args.transfer_material_against or "Work Order"
if args.source_warehouse:
for item in wo_order.get("required_items"):

View File

@@ -304,5 +304,7 @@ erpnext.patches.v13_0.set_operation_time_based_on_operating_cost
erpnext.patches.v13_0.validate_options_for_data_field
erpnext.patches.v13_0.create_gst_payment_entry_fields
erpnext.patches.v14_0.delete_shopify_doctypes
erpnext.patches.v13_0.replace_supplier_item_group_with_party_specific_item
erpnext.patches.v13_0.update_dates_in_tax_withholding_category
erpnext.patches.v14_0.update_opportunity_currency_fields
erpnext.patches.v13_0.create_accounting_dimensions_in_pos_doctypes

View File

@@ -0,0 +1,42 @@
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
def execute():
frappe.reload_doc('accounts', 'doctype', 'accounting_dimension')
accounting_dimensions = frappe.db.sql("""select fieldname, label, document_type, disabled from
`tabAccounting Dimension`""", as_dict=1)
if not accounting_dimensions:
return
count = 1
for d in accounting_dimensions:
if count % 2 == 0:
insert_after_field = 'dimension_col_break'
else:
insert_after_field = 'accounting_dimensions_section'
for doctype in ["POS Invoice", "POS Invoice Item"]:
field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname})
if field:
continue
meta = frappe.get_meta(doctype, cached=False)
fieldnames = [d.fieldname for d in meta.get("fields")]
df = {
"fieldname": d.fieldname,
"label": d.label,
"fieldtype": "Link",
"options": d.document_type,
"insert_after": insert_after_field
}
if df['fieldname'] not in fieldnames:
create_custom_field(doctype, df)
frappe.clear_cache(doctype=doctype)
count += 1

View File

@@ -0,0 +1,17 @@
# Copyright (c) 2019, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
def execute():
if frappe.db.table_exists('Supplier Item Group'):
frappe.reload_doc("selling", "doctype", "party_specific_item")
sig = frappe.db.get_all("Supplier Item Group", fields=["name", "supplier", "item_group"])
for item in sig:
psi = frappe.new_doc("Party Specific Item")
psi.party_type = "Supplier"
psi.party = item.supplier
psi.restrict_based_on = "Item Group"
psi.based_on_value = item.item_group
psi.insert()

View File

@@ -3,8 +3,6 @@
import frappe
from erpnext.accounts.utils import get_fiscal_year
def execute():
frappe.reload_doc('accounts', 'doctype', 'Tax Withholding Rate')
@@ -13,12 +11,14 @@ def execute():
tds_category_rates = frappe.get_all('Tax Withholding Rate', fields=['name', 'fiscal_year'])
fiscal_year_map = {}
for rate in tds_category_rates:
if not fiscal_year_map.get(rate.fiscal_year):
fiscal_year_map[rate.fiscal_year] = get_fiscal_year(fiscal_year=rate.fiscal_year)
fiscal_year_details = frappe.get_all('Fiscal Year', fields=['name', 'year_start_date', 'year_end_date'])
from_date = fiscal_year_map.get(rate.fiscal_year)[1]
to_date = fiscal_year_map.get(rate.fiscal_year)[2]
for d in fiscal_year_details:
fiscal_year_map.setdefault(d.name, d)
for rate in tds_category_rates:
from_date = fiscal_year_map.get(rate.fiscal_year).get('year_start_date')
to_date = fiscal_year_map.get(rate.fiscal_year).get('year_end_date')
frappe.db.set_value('Tax Withholding Rate', rate.name, {
'from_date': from_date,

View File

@@ -14,12 +14,11 @@ from erpnext.hr.utils import validate_active_employee
class AdditionalSalary(Document):
def on_submit(self):
if self.ref_doctype == "Employee Advance" and self.ref_docname:
frappe.db.set_value("Employee Advance", self.ref_docname, "return_amount", self.amount)
self.update_return_amount_in_employee_advance()
self.update_employee_referral()
def on_cancel(self):
self.update_return_amount_in_employee_advance()
self.update_employee_referral(cancel=True)
def validate(self):
@@ -98,6 +97,17 @@ class AdditionalSalary(Document):
frappe.throw(_("Additional Salary for referral bonus can only be created against Employee Referral with status {0}").format(
frappe.bold("Accepted")))
def update_return_amount_in_employee_advance(self):
if self.ref_doctype == "Employee Advance" and self.ref_docname:
return_amount = frappe.db.get_value("Employee Advance", self.ref_docname, "return_amount")
if self.docstatus == 2:
return_amount -= self.amount
else:
return_amount += self.amount
frappe.db.set_value("Employee Advance", self.ref_docname, "return_amount", return_amount)
def update_employee_referral(self, cancel=False):
if self.ref_doctype == "Employee Referral":
status = "Unpaid" if cancel else "Paid"

View File

@@ -758,11 +758,11 @@ def set_tax_withholding_category(company):
accounts = [dict(company=company, account=tds_account)]
try:
fiscal_year = get_fiscal_year(today(), verbose=0, company=company)[0]
fiscal_year_details = get_fiscal_year(today(), verbose=0, company=company)
except FiscalYearError:
pass
docs = get_tds_details(accounts, fiscal_year)
docs = get_tds_details(accounts, fiscal_year_details)
for d in docs:
if not frappe.db.exists("Tax Withholding Category", d.get("name")):
@@ -777,9 +777,10 @@ def set_tax_withholding_category(company):
if accounts:
doc.append("accounts", accounts[0])
if fiscal_year:
if fiscal_year_details:
# if fiscal year don't match with any of the already entered data, append rate row
fy_exist = [k for k in doc.get('rates') if k.get('fiscal_year')==fiscal_year]
fy_exist = [k for k in doc.get('rates') if k.get('from_date') <= fiscal_year_details[1] \
and k.get('to_date') >= fiscal_year_details[2]]
if not fy_exist:
doc.append("rates", d.get('rates')[0])
@@ -802,149 +803,149 @@ def set_tds_account(docs, company):
}
])
def get_tds_details(accounts, fiscal_year):
def get_tds_details(accounts, fiscal_year_details):
# bootstrap default tax withholding sections
return [
dict(name="TDS - 194C - Company",
category_name="Payment to Contractors (Single / Aggregate)",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 2,
"single_threshold": 30000, "cumulative_threshold": 100000}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 2, "single_threshold": 30000, "cumulative_threshold": 100000}]),
dict(name="TDS - 194C - Individual",
category_name="Payment to Contractors (Single / Aggregate)",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 1,
"single_threshold": 30000, "cumulative_threshold": 100000}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 1, "single_threshold": 30000, "cumulative_threshold": 100000}]),
dict(name="TDS - 194C - No PAN / Invalid PAN",
category_name="Payment to Contractors (Single / Aggregate)",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20,
"single_threshold": 30000, "cumulative_threshold": 100000}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 20, "single_threshold": 30000, "cumulative_threshold": 100000}]),
dict(name="TDS - 194D - Company",
category_name="Insurance Commission",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 5,
"single_threshold": 15000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 5, "single_threshold": 15000, "cumulative_threshold": 0}]),
dict(name="TDS - 194D - Company Assessee",
category_name="Insurance Commission",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10,
"single_threshold": 15000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 10, "single_threshold": 15000, "cumulative_threshold": 0}]),
dict(name="TDS - 194D - Individual",
category_name="Insurance Commission",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 5,
"single_threshold": 15000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 5, "single_threshold": 15000, "cumulative_threshold": 0}]),
dict(name="TDS - 194D - No PAN / Invalid PAN",
category_name="Insurance Commission",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20,
"single_threshold": 15000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 20, "single_threshold": 15000, "cumulative_threshold": 0}]),
dict(name="TDS - 194DA - Company",
category_name="Non-exempt payments made under a life insurance policy",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 1,
"single_threshold": 100000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 1, "single_threshold": 100000, "cumulative_threshold": 0}]),
dict(name="TDS - 194DA - Individual",
category_name="Non-exempt payments made under a life insurance policy",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 1,
"single_threshold": 100000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 1, "single_threshold": 100000, "cumulative_threshold": 0}]),
dict(name="TDS - 194DA - No PAN / Invalid PAN",
category_name="Non-exempt payments made under a life insurance policy",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20,
"single_threshold": 100000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 20, "single_threshold": 100000, "cumulative_threshold": 0}]),
dict(name="TDS - 194H - Company",
category_name="Commission / Brokerage",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 5,
"single_threshold": 15000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 5, "single_threshold": 15000, "cumulative_threshold": 0}]),
dict(name="TDS - 194H - Individual",
category_name="Commission / Brokerage",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 5,
"single_threshold": 15000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 5, "single_threshold": 15000, "cumulative_threshold": 0}]),
dict(name="TDS - 194H - No PAN / Invalid PAN",
category_name="Commission / Brokerage",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20,
"single_threshold": 15000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 20, "single_threshold": 15000, "cumulative_threshold": 0}]),
dict(name="TDS - 194I - Rent - Company",
category_name="Rent",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10,
"single_threshold": 180000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 10, "single_threshold": 180000, "cumulative_threshold": 0}]),
dict(name="TDS - 194I - Rent - Individual",
category_name="Rent",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10,
"single_threshold": 180000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 10, "single_threshold": 180000, "cumulative_threshold": 0}]),
dict(name="TDS - 194I - Rent - No PAN / Invalid PAN",
category_name="Rent",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20,
"single_threshold": 180000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 20, "single_threshold": 180000, "cumulative_threshold": 0}]),
dict(name="TDS - 194I - Rent/Machinery - Company",
category_name="Rent-Plant / Machinery",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 2,
"single_threshold": 180000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 2, "single_threshold": 180000, "cumulative_threshold": 0}]),
dict(name="TDS - 194I - Rent/Machinery - Individual",
category_name="Rent-Plant / Machinery",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 2,
"single_threshold": 180000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 2, "single_threshold": 180000, "cumulative_threshold": 0}]),
dict(name="TDS - 194I - Rent/Machinery - No PAN / Invalid PAN",
category_name="Rent-Plant / Machinery",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20,
"single_threshold": 180000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 20, "single_threshold": 180000, "cumulative_threshold": 0}]),
dict(name="TDS - 194J - Professional Fees - Company",
category_name="Professional Fees",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10,
"single_threshold": 30000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 10, "single_threshold": 30000, "cumulative_threshold": 0}]),
dict(name="TDS - 194J - Professional Fees - Individual",
category_name="Professional Fees",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10,
"single_threshold": 30000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 10, "single_threshold": 30000, "cumulative_threshold": 0}]),
dict(name="TDS - 194J - Professional Fees - No PAN / Invalid PAN",
category_name="Professional Fees",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20,
"single_threshold": 30000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 20, "single_threshold": 30000, "cumulative_threshold": 0}]),
dict(name="TDS - 194J - Director Fees - Company",
category_name="Director Fees",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10,
"single_threshold": 0, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 10, "single_threshold": 0, "cumulative_threshold": 0}]),
dict(name="TDS - 194J - Director Fees - Individual",
category_name="Director Fees",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10,
"single_threshold": 0, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 10, "single_threshold": 0, "cumulative_threshold": 0}]),
dict(name="TDS - 194J - Director Fees - No PAN / Invalid PAN",
category_name="Director Fees",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20,
"single_threshold": 0, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 20, "single_threshold": 0, "cumulative_threshold": 0}]),
dict(name="TDS - 194 - Dividends - Company",
category_name="Dividends",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10,
"single_threshold": 2500, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 10, "single_threshold": 2500, "cumulative_threshold": 0}]),
dict(name="TDS - 194 - Dividends - Individual",
category_name="Dividends",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10,
"single_threshold": 2500, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 10, "single_threshold": 2500, "cumulative_threshold": 0}]),
dict(name="TDS - 194 - Dividends - No PAN / Invalid PAN",
category_name="Dividends",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20,
"single_threshold": 2500, "cumulative_threshold": 0}])
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 20, "single_threshold": 2500, "cumulative_threshold": 0}])
]
def create_gratuity_rule():

View File

@@ -214,7 +214,7 @@ class Gstr1Report(object):
if self.filters.get("type_of_business") == "B2B":
conditions += "AND IFNULL(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ') AND is_return != 1"
conditions += "AND IFNULL(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ') AND is_return != 1 AND is_debit_note !=1"
if self.filters.get("type_of_business") in ("B2C Large", "B2C Small"):
b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit')
@@ -223,7 +223,7 @@ class Gstr1Report(object):
if self.filters.get("type_of_business") == "B2C Large":
conditions += """ AND ifnull(SUBSTR(place_of_supply, 1, 2),'') != ifnull(SUBSTR(company_gstin, 1, 2),'')
AND grand_total > {0} AND is_return != 1 and gst_category ='Unregistered' """.format(flt(b2c_limit))
AND grand_total > {0} AND is_return != 1 AND is_debit_note !=1 AND gst_category ='Unregistered' """.format(flt(b2c_limit))
elif self.filters.get("type_of_business") == "B2C Small":
conditions += """ AND (
@@ -236,8 +236,8 @@ class Gstr1Report(object):
elif self.filters.get("type_of_business") == "CDNR-UNREG":
b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit')
conditions += """ AND ifnull(SUBSTR(place_of_supply, 1, 2),'') != ifnull(SUBSTR(company_gstin, 1, 2),'')
AND ABS(grand_total) > {0} AND (is_return = 1 OR is_debit_note = 1)
AND IFNULL(gst_category, '') in ('Unregistered', 'Overseas')""".format(flt(b2c_limit))
AND (is_return = 1 OR is_debit_note = 1)
AND IFNULL(gst_category, '') in ('Unregistered', 'Overseas')"""
elif self.filters.get("type_of_business") == "EXPORT":
conditions += """ AND is_return !=1 and gst_category = 'Overseas' """

View File

@@ -20,7 +20,6 @@
"tax_withholding_category",
"default_bank_account",
"lead_name",
"prospect",
"opportunity_name",
"image",
"column_break0",
@@ -214,7 +213,8 @@
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Represents Company",
"options": "Company"
"options": "Company",
"unique": 1
},
{
"depends_on": "represents_company",
@@ -497,14 +497,6 @@
"label": "Tax Withholding Category",
"options": "Tax Withholding Category"
},
{
"fieldname": "prospect",
"fieldtype": "Link",
"label": "Prospect",
"no_copy": 1,
"options": "Prospect",
"print_hide": 1
},
{
"fieldname": "opportunity_name",
"fieldtype": "Link",
@@ -518,8 +510,14 @@
"idx": 363,
"image_field": "image",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-08-25 18:56:09.929905",
"links": [
{
"group": "Allowed Items",
"link_doctype": "Party Specific Item",
"link_fieldname": "party"
}
],
"modified": "2021-09-06 17:38:54.196663",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",

View File

@@ -14,7 +14,7 @@ from frappe.contacts.address_and_contact import (
)
from frappe.desk.reportview import build_match_conditions, get_filters_cond
from frappe.model.mapper import get_mapped_doc
from frappe.model.naming import set_name_by_naming_series
from frappe.model.naming import set_name_by_naming_series, set_name_from_naming_options
from frappe.model.rename_doc import update_linked_doctypes
from frappe.utils import cint, cstr, flt, get_formatted_email, today
from frappe.utils.user import get_users_with_role
@@ -40,8 +40,10 @@ class Customer(TransactionBase):
cust_master_name = frappe.defaults.get_global_default('cust_master_name')
if cust_master_name == 'Customer Name':
self.name = self.get_customer_name()
else:
elif cust_master_name == 'Naming Series':
set_name_by_naming_series(self)
else:
self.name = set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self)
def get_customer_name(self):

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Supplier Item Group', {
frappe.ui.form.on('Party Specific Item', {
// refresh: function(frm) {
// }

View File

@@ -0,0 +1,77 @@
{
"actions": [],
"creation": "2021-08-27 19:28:07.559978",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"party_type",
"party",
"column_break_3",
"restrict_based_on",
"based_on_value"
],
"fields": [
{
"fieldname": "party_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Party Type",
"options": "Customer\nSupplier",
"reqd": 1
},
{
"fieldname": "party",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Party Name",
"options": "party_type",
"reqd": 1
},
{
"fieldname": "restrict_based_on",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Restrict Items Based On",
"options": "Item\nItem Group\nBrand",
"reqd": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "based_on_value",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Based On Value",
"options": "restrict_based_on",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-09-14 13:27:58.612334",
"modified_by": "Administrator",
"module": "Selling",
"name": "Party Specific Item",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "party",
"track_changes": 1
}

View File

@@ -0,0 +1,19 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
class PartySpecificItem(Document):
def validate(self):
exists = frappe.db.exists({
'doctype': 'Party Specific Item',
'party_type': self.party_type,
'party': self.party,
'restrict_based_on': self.restrict_based_on,
'based_on': self.based_on_value,
})
if exists:
frappe.throw(_("This item filter has already been applied for the {0}").format(self.party_type))

View File

@@ -0,0 +1,38 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from erpnext.controllers.queries import item_query
test_dependencies = ['Item', 'Customer', 'Supplier']
def create_party_specific_item(**args):
psi = frappe.new_doc("Party Specific Item")
psi.party_type = args.get('party_type')
psi.party = args.get('party')
psi.restrict_based_on = args.get('restrict_based_on')
psi.based_on_value = args.get('based_on_value')
psi.insert()
class TestPartySpecificItem(unittest.TestCase):
def setUp(self):
self.customer = frappe.get_last_doc("Customer")
self.supplier = frappe.get_last_doc("Supplier")
self.item = frappe.get_last_doc("Item")
def test_item_query_for_customer(self):
create_party_specific_item(party_type='Customer', party=self.customer.name, restrict_based_on='Item', based_on_value=self.item.name)
filters = {'is_sales_item': 1, 'customer': self.customer.name}
items = item_query(doctype= 'Item', txt= '', searchfield= 'name', start= 0, page_len= 20,filters=filters, as_dict= False)
for item in items:
self.assertEqual(item[0], self.item.name)
def test_item_query_for_supplier(self):
create_party_specific_item(party_type='Supplier', party=self.supplier.name, restrict_based_on='Item Group', based_on_value=self.item.item_group)
filters = {'supplier': self.supplier.name, 'is_purchase_item': 1}
items = item_query(doctype= 'Item', txt= '', searchfield= 'name', start= 0, page_len= 20,filters=filters, as_dict= False)
for item in items:
self.assertEqual(item[2], self.item.item_group)

View File

@@ -41,14 +41,14 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Customer Naming By",
"options": "Customer Name\nNaming Series"
"options": "Customer Name\nNaming Series\nAuto Name"
},
{
"fieldname": "campaign_naming_by",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Campaign Naming By",
"options": "Campaign Name\nNaming Series"
"options": "Campaign Name\nNaming Series\nAuto Name"
},
{
"fieldname": "customer_group",
@@ -204,7 +204,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-09-01 22:55:33.803624",
"modified": "2021-09-08 19:38:10.175989",
"modified_by": "Administrator",
"module": "Selling",
"name": "Selling Settings",
@@ -223,4 +223,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}
}

View File

@@ -73,7 +73,7 @@ def get_data(conditions, filters):
`tabSales Order` so,
`tabSales Order Item` soi
LEFT JOIN `tabSales Invoice Item` sii
ON sii.so_detail = soi.name
ON sii.so_detail = soi.name and sii.docstatus = 1
WHERE
soi.parent = so.name
and so.status not in ('Stopped', 'Closed', 'On Hold')

View File

@@ -63,7 +63,7 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
this.frm.set_query("item_code", "items", function() {
return {
query: "erpnext.controllers.queries.item_query",
filters: {'is_sales_item': 1}
filters: {'is_sales_item': 1, 'customer': cur_frm.doc.customer}
}
});
}

View File

@@ -39,6 +39,7 @@ class ItemGroup(NestedSet, WebsiteGenerator):
self.parent_item_group = _('All Item Groups')
self.make_route()
self.validate_item_group_defaults()
def on_update(self):
NestedSet.on_update(self)
@@ -99,7 +100,7 @@ class ItemGroup(NestedSet, WebsiteGenerator):
filter_engine = ProductFiltersBuilder(self.name)
context.field_filters = filter_engine.get_field_filters()
context.attribute_filters = filter_engine.get_attribute_fitlers()
context.attribute_filters = filter_engine.get_attribute_filters()
context.update({
"parents": get_parent_item_groups(self.parent_item_group),
@@ -134,6 +135,10 @@ class ItemGroup(NestedSet, WebsiteGenerator):
def delete_child_item_groups_key(self):
frappe.cache().hdel("child_item_groups", self.name)
def validate_item_group_defaults(self):
from erpnext.stock.doctype.item.item import validate_item_default_company_links
validate_item_default_company_links(self.item_group_defaults)
@frappe.whitelist(allow_guest=True)
def get_product_list_for_group(product_group=None, start=0, limit=10, search=None):
if product_group:

View File

@@ -1,73 +1,74 @@
[
{
"doctype": "Item Group",
"is_group": 0,
"item_group_name": "_Test Item Group",
"doctype": "Item Group",
"is_group": 0,
"item_group_name": "_Test Item Group",
"parent_item_group": "All Item Groups",
"item_group_defaults": [{
"company": "_Test Company",
"buying_cost_center": "_Test Cost Center 2 - _TC",
"selling_cost_center": "_Test Cost Center 2 - _TC"
"selling_cost_center": "_Test Cost Center 2 - _TC",
"default_warehouse": "_Test Warehouse - _TC"
}]
},
},
{
"doctype": "Item Group",
"is_group": 0,
"item_group_name": "_Test Item Group Desktops",
"doctype": "Item Group",
"is_group": 0,
"item_group_name": "_Test Item Group Desktops",
"parent_item_group": "All Item Groups"
},
},
{
"doctype": "Item Group",
"is_group": 1,
"item_group_name": "_Test Item Group A",
"doctype": "Item Group",
"is_group": 1,
"item_group_name": "_Test Item Group A",
"parent_item_group": "All Item Groups"
},
},
{
"doctype": "Item Group",
"is_group": 1,
"item_group_name": "_Test Item Group B",
"doctype": "Item Group",
"is_group": 1,
"item_group_name": "_Test Item Group B",
"parent_item_group": "All Item Groups"
},
},
{
"doctype": "Item Group",
"is_group": 1,
"item_group_name": "_Test Item Group B - 1",
"doctype": "Item Group",
"is_group": 1,
"item_group_name": "_Test Item Group B - 1",
"parent_item_group": "_Test Item Group B"
},
},
{
"doctype": "Item Group",
"is_group": 1,
"item_group_name": "_Test Item Group B - 2",
"doctype": "Item Group",
"is_group": 1,
"item_group_name": "_Test Item Group B - 2",
"parent_item_group": "_Test Item Group B"
},
},
{
"doctype": "Item Group",
"is_group": 0,
"item_group_name": "_Test Item Group B - 3",
"doctype": "Item Group",
"is_group": 0,
"item_group_name": "_Test Item Group B - 3",
"parent_item_group": "_Test Item Group B"
},
},
{
"doctype": "Item Group",
"is_group": 1,
"item_group_name": "_Test Item Group C",
"doctype": "Item Group",
"is_group": 1,
"item_group_name": "_Test Item Group C",
"parent_item_group": "All Item Groups"
},
},
{
"doctype": "Item Group",
"is_group": 1,
"item_group_name": "_Test Item Group C - 1",
"doctype": "Item Group",
"is_group": 1,
"item_group_name": "_Test Item Group C - 1",
"parent_item_group": "_Test Item Group C"
},
},
{
"doctype": "Item Group",
"is_group": 1,
"item_group_name": "_Test Item Group C - 2",
"doctype": "Item Group",
"is_group": 1,
"item_group_name": "_Test Item Group C - 2",
"parent_item_group": "_Test Item Group C"
},
},
{
"doctype": "Item Group",
"is_group": 1,
"item_group_name": "_Test Item Group D",
"doctype": "Item Group",
"is_group": 1,
"item_group_name": "_Test Item Group D",
"parent_item_group": "All Item Groups"
},
{
@@ -104,4 +105,4 @@
}
]
}
]
]

View File

@@ -4,7 +4,6 @@
from __future__ import unicode_literals
import frappe
from frappe import _dict
class ProductFiltersBuilder:
@@ -57,37 +56,31 @@ class ProductFiltersBuilder:
return filter_data
def get_attribute_fitlers(self):
def get_attribute_filters(self):
attributes = [row.attribute for row in self.doc.filter_attributes]
attribute_docs = [
frappe.get_doc('Item Attribute', attribute) for attribute in attributes
]
valid_attributes = []
if not attributes:
return []
for attr_doc in attribute_docs:
selected_attributes = []
for attr in attr_doc.item_attribute_values:
or_filters = []
filters= [
["Item Variant Attribute", "attribute", "=", attr.parent],
["Item Variant Attribute", "attribute_value", "=", attr.attribute_value]
]
if self.item_group:
or_filters.extend([
["item_group", "=", self.item_group],
["Website Item Group", "item_group", "=", self.item_group]
])
result = frappe.db.sql(
"""
select
distinct attribute, attribute_value
from
`tabItem Variant Attribute`
where
attribute in %(attributes)s
and attribute_value is not null
""",
{"attributes": attributes},
as_dict=1,
)
if frappe.db.get_all("Item", filters, or_filters=or_filters, limit=1):
selected_attributes.append(attr)
attribute_value_map = {}
for d in result:
attribute_value_map.setdefault(d.attribute, []).append(d.attribute_value)
if selected_attributes:
valid_attributes.append(
_dict(
item_attribute_values=selected_attributes,
name=attr_doc.name
)
)
return valid_attributes
out = []
for name, values in attribute_value_map.items():
out.append(frappe._dict(name=name, item_attribute_values=values))
return out

View File

@@ -4,6 +4,7 @@
import copy
import itertools
import json
from typing import List
import frappe
from frappe import _
@@ -36,6 +37,7 @@ from erpnext.setup.doctype.item_group.item_group import (
get_parent_item_groups,
invalidate_cache_for,
)
from erpnext.stock.doctype.item_default.item_default import ItemDefault
class DuplicateReorderRows(frappe.ValidationError):
@@ -134,9 +136,9 @@ class Item(WebsiteGenerator):
self.validate_fixed_asset()
self.validate_retain_sample()
self.validate_uom_conversion_factor()
self.validate_item_defaults()
self.validate_customer_provided_part()
self.update_defaults_from_item_group()
self.validate_item_defaults()
self.validate_auto_reorder_enabled_in_stock_settings()
self.cant_change()
self.update_show_in_website()
@@ -782,35 +784,39 @@ class Item(WebsiteGenerator):
if len(companies) != len(self.item_defaults):
frappe.throw(_("Cannot set multiple Item Defaults for a company."))
validate_item_default_company_links(self.item_defaults)
def update_defaults_from_item_group(self):
"""Get defaults from Item Group"""
if self.item_group and not self.item_defaults:
item_defaults = frappe.db.get_values("Item Default", {"parent": self.item_group},
['company', 'default_warehouse','default_price_list','buying_cost_center','default_supplier',
'expense_account','selling_cost_center','income_account'], as_dict = 1)
if item_defaults:
for item in item_defaults:
self.append('item_defaults', {
'company': item.company,
'default_warehouse': item.default_warehouse,
'default_price_list': item.default_price_list,
'buying_cost_center': item.buying_cost_center,
'default_supplier': item.default_supplier,
'expense_account': item.expense_account,
'selling_cost_center': item.selling_cost_center,
'income_account': item.income_account
})
else:
warehouse = ''
defaults = frappe.defaults.get_defaults() or {}
if self.item_defaults or not self.item_group:
return
# To check default warehouse is belong to the default company
if defaults.get("default_warehouse") and defaults.company and frappe.db.exists("Warehouse",
{'name': defaults.default_warehouse, 'company': defaults.company}):
self.append("item_defaults", {
"company": defaults.get("company"),
"default_warehouse": defaults.default_warehouse
})
item_defaults = frappe.db.get_values("Item Default", {"parent": self.item_group},
['company', 'default_warehouse','default_price_list','buying_cost_center','default_supplier',
'expense_account','selling_cost_center','income_account'], as_dict = 1)
if item_defaults:
for item in item_defaults:
self.append('item_defaults', {
'company': item.company,
'default_warehouse': item.default_warehouse,
'default_price_list': item.default_price_list,
'buying_cost_center': item.buying_cost_center,
'default_supplier': item.default_supplier,
'expense_account': item.expense_account,
'selling_cost_center': item.selling_cost_center,
'income_account': item.income_account
})
else:
defaults = frappe.defaults.get_defaults() or {}
# To check default warehouse is belong to the default company
if defaults.get("default_warehouse") and defaults.company and frappe.db.exists("Warehouse",
{'name': defaults.default_warehouse, 'company': defaults.company}):
self.append("item_defaults", {
"company": defaults.get("company"),
"default_warehouse": defaults.default_warehouse
})
def update_variants(self):
if self.flags.dont_update_variants or \
@@ -1328,3 +1334,25 @@ def on_doctype_update():
@erpnext.allow_regional
def set_item_tax_from_hsn_code(item):
pass
def validate_item_default_company_links(item_defaults: List[ItemDefault]) -> None:
for item_default in item_defaults:
for doctype, field in [
['Warehouse', 'default_warehouse'],
['Cost Center', 'buying_cost_center'],
['Cost Center', 'selling_cost_center'],
['Account', 'expense_account'],
['Account', 'income_account']
]:
if item_default.get(field):
company = frappe.db.get_value(doctype, item_default.get(field), 'company', cache=True)
if company and company != item_default.company:
frappe.throw(_("Row #{}: {} {} doesn't belong to Company {}. Please select valid {}.")
.format(
item_default.idx,
doctype,
frappe.bold(item_default.get(field)),
frappe.bold(item_default.company),
frappe.bold(frappe.unscrub(field))
), title=_("Invalid Item Defaults"))

View File

@@ -232,6 +232,23 @@ class TestItem(unittest.TestCase):
for key, value in purchase_item_check.items():
self.assertEqual(value, purchase_item_details.get(key))
def test_item_default_validations(self):
with self.assertRaises(frappe.ValidationError) as ve:
make_item("Bad Item defaults", {
"item_group": "_Test Item Group",
"item_defaults": [{
"company": "_Test Company 1",
"default_warehouse": "_Test Warehouse - _TC",
"expense_account": "Stock In Hand - _TC",
"buying_cost_center": "_Test Cost Center - _TC",
"selling_cost_center": "_Test Cost Center - _TC",
}]
})
self.assertTrue("belong to company" in str(ve.exception).lower(),
msg="Mismatching company entities in item defaults should not be allowed.")
def test_item_attribute_change_after_variant(self):
frappe.delete_doc_if_exists("Item", "_Test Variant Item-L", force=1)

View File

@@ -4,6 +4,7 @@
from __future__ import unicode_literals
import json
from collections import defaultdict
import frappe
from frappe import _
@@ -684,7 +685,7 @@ class StockEntry(StockController):
def validate_bom(self):
for d in self.get('items'):
if d.bom_no and (d.t_warehouse != getattr(self, "pro_doc", frappe._dict()).scrap_warehouse):
if d.bom_no and d.is_finished_item:
item_code = d.original_item or d.item_code
validate_bom_no(item_code, d.bom_no)
@@ -1191,13 +1192,88 @@ class StockEntry(StockController):
# item dict = { item_code: {qty, description, stock_uom} }
item_dict = get_bom_items_as_dict(self.bom_no, self.company, qty=qty,
fetch_exploded = 0, fetch_scrap_items = 1)
fetch_exploded = 0, fetch_scrap_items = 1) or {}
for item in itervalues(item_dict):
item.from_warehouse = ""
item.is_scrap_item = 1
for row in self.get_scrap_items_from_job_card():
if row.stock_qty <= 0:
continue
item_row = item_dict.get(row.item_code)
if not item_row:
item_row = frappe._dict({})
item_row.update({
'uom': row.stock_uom,
'from_warehouse': '',
'qty': row.stock_qty + flt(item_row.stock_qty),
'converison_factor': 1,
'is_scrap_item': 1,
'item_name': row.item_name,
'description': row.description,
'allow_zero_valuation_rate': 1
})
item_dict[row.item_code] = item_row
return item_dict
def get_scrap_items_from_job_card(self):
if not self.pro_doc:
self.set_work_order_details()
scrap_items = frappe.db.sql('''
SELECT
JCSI.item_code, JCSI.item_name, SUM(JCSI.stock_qty) as stock_qty, JCSI.stock_uom, JCSI.description
FROM
`tabJob Card` JC, `tabJob Card Scrap Item` JCSI
WHERE
JCSI.parent = JC.name AND JC.docstatus = 1
AND JCSI.item_code IS NOT NULL AND JC.work_order = %s
GROUP BY
JCSI.item_code
''', self.work_order, as_dict=1)
pending_qty = flt(self.pro_doc.qty) - flt(self.pro_doc.produced_qty)
if pending_qty <=0:
return []
used_scrap_items = self.get_used_scrap_items()
for row in scrap_items:
row.stock_qty -= flt(used_scrap_items.get(row.item_code))
row.stock_qty = (row.stock_qty) * flt(self.fg_completed_qty) / flt(pending_qty)
if used_scrap_items.get(row.item_code):
used_scrap_items[row.item_code] -= row.stock_qty
if cint(frappe.get_cached_value('UOM', row.stock_uom, 'must_be_whole_number')):
row.stock_qty = frappe.utils.ceil(row.stock_qty)
return scrap_items
def get_used_scrap_items(self):
used_scrap_items = defaultdict(float)
data = frappe.get_all(
'Stock Entry',
fields = [
'`tabStock Entry Detail`.`item_code`', '`tabStock Entry Detail`.`qty`'
],
filters = [
['Stock Entry', 'work_order', '=', self.work_order],
['Stock Entry Detail', 'is_scrap_item', '=', 1],
['Stock Entry', 'docstatus', '=', 1],
['Stock Entry', 'purpose', 'in', ['Repack', 'Manufacture']]
]
)
for row in data:
used_scrap_items[row.item_code] += row.qty
return used_scrap_items
def get_unconsumed_raw_materials(self):
wo = frappe.get_doc("Work Order", self.work_order)
wo_items = frappe.get_all('Work Order Item',
@@ -1264,9 +1340,9 @@ class StockEntry(StockController):
po_qty = frappe.db.sql("""select qty, produced_qty, material_transferred_for_manufacturing from
`tabWork Order` where name=%s""", self.work_order, as_dict=1)[0]
manufacturing_qty = flt(po_qty.qty)
manufacturing_qty = flt(po_qty.qty) or 1
produced_qty = flt(po_qty.produced_qty)
trans_qty = flt(po_qty.material_transferred_for_manufacturing)
trans_qty = flt(po_qty.material_transferred_for_manufacturing) or 1
for item in transferred_materials:
qty= item.qty
@@ -1417,8 +1493,8 @@ class StockEntry(StockController):
se_child.is_scrap_item = item_dict[d].get("is_scrap_item", 0)
se_child.is_process_loss = item_dict[d].get("is_process_loss", 0)
for field in ["idx", "po_detail", "original_item",
"expense_account", "description", "item_name", "serial_no", "batch_no"]:
for field in ["idx", "po_detail", "original_item", "expense_account",
"description", "item_name", "serial_no", "batch_no", "allow_zero_valuation_rate"]:
if item_dict[d].get(field):
se_child.set(field, item_dict[d].get(field))

View File

@@ -619,6 +619,11 @@ def get_stock_balance_for(item_code, warehouse,
item_dict = frappe.db.get_value("Item", item_code,
["has_serial_no", "has_batch_no"], as_dict=1)
if not item_dict:
# In cases of data upload to Items table
msg = _("Item {} does not exist.").format(item_code)
frappe.throw(msg, title=_("Missing"))
serial_nos = ""
with_serial_no = True if item_dict.get("has_serial_no") else False
data = get_stock_balance(item_code, warehouse, posting_date, posting_time,

View File

@@ -0,0 +1,63 @@
import unittest
from typing import List, Tuple
from erpnext.tests.utils import ReportFilters, ReportName, execute_script_report
DEFAULT_FILTERS = {
"company": "_Test Company",
"from_date": "2010-01-01",
"to_date": "2030-01-01",
}
REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
("Stock Ledger", {"_optional": True}),
("Stock Balance", {"_optional": True}),
("Stock Projected Qty", {"_optional": True}),
("Batch-Wise Balance History", {}),
("Itemwise Recommended Reorder Level", {"item_group": "All Item Groups"}),
("COGS By Item Group", {}),
("Stock Qty vs Serial No Count", {"warehouse": "_Test Warehouse - _TC"}),
(
"Stock and Account Value Comparison",
{
"company": "_Test Company with perpetual inventory",
"account": "Stock In Hand - TCP1",
"as_on_date": "2021-01-01",
},
),
("Product Bundle Balance", {"date": "2022-01-01", "_optional": True}),
(
"Stock Analytics",
{
"from_date": "2021-01-01",
"to_date": "2021-12-31",
"value_quantity": "Quantity",
"_optional": True,
},
),
("Warehouse wise Item Balance Age and Value", {"_optional": True}),
("Item Variant Details", {"item": "_Test Variant Item",}),
("Total Stock Summary", {"group_by": "warehouse",}),
("Batch Item Expiry Status", {}),
("Stock Ageing", {"range1": 30, "range2": 60, "range3": 90, "_optional": True}),
]
OPTIONAL_FILTERS = {
"warehouse": "_Test Warehouse - _TC",
"item": "_Test Item",
"item_group": "_Test Item Group",
}
class TestReports(unittest.TestCase):
def test_execute_all_stock_reports(self):
"""Test that all script report in stock modules are executable with supported filters"""
for report, filter in REPORT_FILTER_TEST_CASES:
execute_script_report(
report_name=report,
module="Stock",
filters=filter,
default_filters=DEFAULT_FILTERS,
optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
)

View File

@@ -407,7 +407,8 @@ class update_entries_after(object):
return
# Get dynamic incoming/outgoing rate
self.get_dynamic_incoming_outgoing_rate(sle)
if not self.args.get("sle_id"):
self.get_dynamic_incoming_outgoing_rate(sle)
if sle.serial_no:
self.get_serialized_values(sle)
@@ -447,7 +448,8 @@ class update_entries_after(object):
sle.doctype="Stock Ledger Entry"
frappe.get_doc(sle).db_update()
self.update_outgoing_rate_on_transaction(sle)
if not self.args.get("sle_id"):
self.update_outgoing_rate_on_transaction(sle)
def validate_negative_stock(self, sle):
"""
@@ -681,11 +683,15 @@ class update_entries_after(object):
if self.wh_data.stock_queue[-1][1]==incoming_rate:
self.wh_data.stock_queue[-1][0] += actual_qty
else:
# Item has a positive balance qty, add new entry
if self.wh_data.stock_queue[-1][0] > 0:
self.wh_data.stock_queue.append([actual_qty, incoming_rate])
else:
else: # negative balance qty
qty = self.wh_data.stock_queue[-1][0] + actual_qty
self.wh_data.stock_queue[-1] = [qty, incoming_rate]
if qty > 0: # new balance qty is positive
self.wh_data.stock_queue[-1] = [qty, incoming_rate]
else: # new balance qty is still negative, maintain same rate
self.wh_data.stock_queue[-1][0] = qty
else:
qty_to_pop = abs(actual_qty)
while qty_to_pop:

View File

@@ -0,0 +1,138 @@
import unittest
import frappe
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
class TestWebsite(unittest.TestCase):
def test_permission_for_custom_doctype(self):
create_user('Supplier 1', 'supplier1@gmail.com')
create_user('Supplier 2', 'supplier2@gmail.com')
create_supplier_with_contact('Supplier1', 'All Supplier Groups', 'Supplier 1', 'supplier1@gmail.com')
create_supplier_with_contact('Supplier2', 'All Supplier Groups', 'Supplier 2', 'supplier2@gmail.com')
po1 = create_purchase_order(supplier='Supplier1')
po2 = create_purchase_order(supplier='Supplier2')
create_custom_doctype()
create_webform()
create_order_assignment(supplier='Supplier1', po = po1.name)
create_order_assignment(supplier='Supplier2', po = po2.name)
frappe.set_user("Administrator")
# checking if data consist of all order assignment of Supplier1 and Supplier2
self.assertTrue('Supplier1' and 'Supplier2' in [data.supplier for data in get_data()])
frappe.set_user("supplier1@gmail.com")
# checking if data only consist of order assignment of Supplier1
self.assertTrue('Supplier1' in [data.supplier for data in get_data()])
self.assertFalse([data.supplier for data in get_data() if data.supplier != 'Supplier1'])
frappe.set_user("supplier2@gmail.com")
# checking if data only consist of order assignment of Supplier2
self.assertTrue('Supplier2' in [data.supplier for data in get_data()])
self.assertFalse([data.supplier for data in get_data() if data.supplier != 'Supplier2'])
frappe.set_user("Administrator")
def get_data():
webform_list_contexts = frappe.get_hooks('webform_list_context')
if webform_list_contexts:
context = frappe._dict(frappe.get_attr(webform_list_contexts[0])('Buying') or {})
kwargs = dict(doctype='Order Assignment', order_by = 'modified desc')
return context.get_list(**kwargs)
def create_user(name, email):
frappe.get_doc({
'doctype': 'User',
'send_welcome_email': 0,
'user_type': 'Website User',
'first_name': name,
'email': email,
'roles': [{"doctype": "Has Role", "role": "Supplier"}]
}).insert(ignore_if_duplicate = True)
def create_supplier_with_contact(name, group, contact_name, contact_email):
supplier = frappe.get_doc({
'doctype': 'Supplier',
'supplier_name': name,
'supplier_group': group
}).insert(ignore_if_duplicate = True)
if not frappe.db.exists('Contact', contact_name+'-1-'+name):
new_contact = frappe.new_doc("Contact")
new_contact.first_name = contact_name
new_contact.is_primary_contact = True,
new_contact.append('links', {
"link_doctype": "Supplier",
"link_name": supplier.name
})
new_contact.append('email_ids', {
"email_id": contact_email,
"is_primary": 1
})
new_contact.insert(ignore_mandatory=True)
def create_custom_doctype():
frappe.get_doc({
'doctype': 'DocType',
'name': 'Order Assignment',
'module': 'Buying',
'custom': 1,
'autoname': 'field:po',
'fields': [
{'label': 'PO', 'fieldname': 'po', 'fieldtype': 'Link', 'options': 'Purchase Order'},
{'label': 'Supplier', 'fieldname': 'supplier', 'fieldtype': 'Data', "fetch_from": "po.supplier"}
],
'permissions': [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"read": 1,
"role": "Supplier"
}
]
}).insert(ignore_if_duplicate = True)
def create_webform():
frappe.get_doc({
'doctype': 'Web Form',
'module': 'Buying',
'title': 'SO Schedule',
'route': 'so-schedule',
'doc_type': 'Order Assignment',
'web_form_fields': [
{
'doctype': 'Web Form Field',
'fieldname': 'po',
'fieldtype': 'Link',
'options': 'Purchase Order',
'label': 'PO'
},
{
'doctype': 'Web Form Field',
'fieldname': 'supplier',
'fieldtype': 'Data',
'label': 'Supplier'
}
]
}).insert(ignore_if_duplicate = True)
def create_order_assignment(supplier, po):
frappe.get_doc({
'doctype': 'Order Assignment',
'po': po,
'supplier': supplier,
}).insert(ignore_if_duplicate = True)

View File

@@ -3,8 +3,13 @@
import copy
from contextlib import contextmanager
from typing import Any, Dict, NewType, Optional
import frappe
from frappe.core.doctype.report.report import get_report_module_dotted_path
ReportFilters = Dict[str, Any]
ReportName = NewType("ReportName", str)
def create_test_contact_and_address():
@@ -78,3 +83,39 @@ def change_settings(doctype, settings_dict):
for key, value in previous_settings.items():
setattr(settings, key, value)
settings.save()
def execute_script_report(
report_name: ReportName,
module: str,
filters: ReportFilters,
default_filters: Optional[ReportFilters] = None,
optional_filters: Optional[ReportFilters] = None
):
"""Util for testing execution of a report with specified filters.
Tests the execution of report with default_filters + filters.
Tests the execution using optional_filters one at a time.
Args:
report_name: Human readable name of report (unscrubbed)
module: module to which report belongs to
filters: specific values for filters
default_filters: default values for filters such as company name.
optional_filters: filters which should be tested one at a time in addition to default filters.
"""
if default_filters is None:
default_filters = {}
report_execute_fn = frappe.get_attr(get_report_module_dotted_path(module, report_name) + ".execute")
report_filters = frappe._dict(default_filters).copy().update(filters)
report_data = report_execute_fn(report_filters)
if optional_filters:
for key, value in optional_filters.items():
filter_with_optional_param = report_filters.copy().update({key: value})
report_execute_fn(filter_with_optional_param)
return report_data

View File

@@ -98,14 +98,14 @@
<div class="filter-options">
{% for attr_value in attribute.item_attribute_values %}
<div class="checkbox">
<label data-value="{{ value }}">
<label>
<input type="checkbox"
class="product-filter attribute-filter"
id="{{attr_value.name}}"
id="{{attr_value}}"
data-attribute-name="{{ attribute.name }}"
data-attribute-value="{{ attr_value.attribute_value }}"
data-attribute-value="{{ attr_value }}"
{% if attr_value.checked %} checked {% endif %}>
<span class="label-area">{{ attr_value.attribute_value }}</span>
<span class="label-area">{{ attr_value }}</span>
</label>
</div>
{% endfor %}

View File

@@ -27,7 +27,7 @@ def get_context(context):
filter_engine = ProductFiltersBuilder()
context.field_filters = filter_engine.get_field_filters()
context.attribute_filters = filter_engine.get_attribute_fitlers()
context.attribute_filters = filter_engine.get_attribute_filters()
context.product_settings = product_settings
context.body_class = "product-page"