mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-25 16:04:46 +00:00
Merge branch 'develop' into payments-based-dunning
This commit is contained in:
@@ -11,6 +11,7 @@ coverage:
|
|||||||
comment:
|
comment:
|
||||||
layout: "diff, files"
|
layout: "diff, files"
|
||||||
require_changes: true
|
require_changes: true
|
||||||
|
after_n_builds: 3
|
||||||
|
|
||||||
ignore:
|
ignore:
|
||||||
- "erpnext/demo"
|
- "erpnext/demo"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
frappe.ui.form.on('POS Invoice Merge Log', {
|
frappe.ui.form.on('POS Invoice Merge Log', {
|
||||||
setup: function(frm) {
|
setup: function(frm) {
|
||||||
frm.set_query("pos_invoice", "pos_invoices", doc => {
|
frm.set_query("pos_invoice", "pos_invoices", doc => {
|
||||||
return{
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
'docstatus': 1,
|
'docstatus': 1,
|
||||||
'customer': doc.customer,
|
'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', '');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,9 +6,11 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"posting_date",
|
"posting_date",
|
||||||
"customer",
|
"merge_invoices_based_on",
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
"pos_closing_entry",
|
"pos_closing_entry",
|
||||||
|
"customer",
|
||||||
|
"customer_group",
|
||||||
"section_break_3",
|
"section_break_3",
|
||||||
"pos_invoices",
|
"pos_invoices",
|
||||||
"references_section",
|
"references_section",
|
||||||
@@ -88,12 +90,27 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "POS Closing Entry",
|
"label": "POS Closing Entry",
|
||||||
"options": "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,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-12-01 11:53:57.267579",
|
"modified": "2021-09-14 11:17:19.001142",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "POS Invoice Merge Log",
|
"name": "POS Invoice Merge Log",
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ class POSInvoiceMergeLog(Document):
|
|||||||
self.validate_pos_invoice_status()
|
self.validate_pos_invoice_status()
|
||||||
|
|
||||||
def validate_customer(self):
|
def validate_customer(self):
|
||||||
|
if self.merge_invoices_based_on == 'Customer Group':
|
||||||
|
return
|
||||||
|
|
||||||
for d in self.pos_invoices:
|
for d in self.pos_invoices:
|
||||||
if d.customer != self.customer:
|
if d.customer != self.customer:
|
||||||
frappe.throw(_("Row #{}: POS Invoice {} is not against customer {}").format(d.idx, d.pos_invoice, 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
|
found = False
|
||||||
for i in items:
|
for i in items:
|
||||||
if (i.item_code == item.item_code and not i.serial_no and not i.batch_no and
|
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
|
found = True
|
||||||
i.qty = i.qty + item.qty
|
i.qty = i.qty + item.qty
|
||||||
|
|
||||||
@@ -172,6 +175,11 @@ class POSInvoiceMergeLog(Document):
|
|||||||
invoice.discount_amount = 0.0
|
invoice.discount_amount = 0.0
|
||||||
invoice.taxes_and_charges = None
|
invoice.taxes_and_charges = None
|
||||||
invoice.ignore_pricing_rule = 1
|
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
|
return invoice
|
||||||
|
|
||||||
@@ -228,7 +236,7 @@ def get_all_unconsolidated_invoices():
|
|||||||
return pos_invoices
|
return pos_invoices
|
||||||
|
|
||||||
def get_invoice_customer_map(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 = {}
|
pos_invoice_customer_map = {}
|
||||||
for invoice in pos_invoices:
|
for invoice in pos_invoices:
|
||||||
customer = invoice.get('customer')
|
customer = invoice.get('customer')
|
||||||
|
|||||||
@@ -499,7 +499,7 @@ class SalesInvoice(SellingController):
|
|||||||
self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account')
|
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
|
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 {}
|
pos_profile = get_pos_profile(self.company) or {}
|
||||||
if not pos_profile:
|
if not pos_profile:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -260,7 +260,12 @@ def get_company_currency(filters=None):
|
|||||||
def calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters):
|
def calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters):
|
||||||
for entries in gl_entries_by_account.values():
|
for entries in gl_entries_by_account.values():
|
||||||
for entry in entries:
|
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:
|
if d:
|
||||||
for company in companies:
|
for company in companies:
|
||||||
# check if posting date is within the period
|
# 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
|
of account_number and suffix of company abbr. This function adds key called
|
||||||
`parent_account_name` which does not have such prefix/suffix.
|
`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:
|
for account in accounts:
|
||||||
if account.parent_account:
|
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'))
|
convert_to_presentation_currency(gl_entries, currency_info, filters.get('company'))
|
||||||
|
|
||||||
for entry in gl_entries:
|
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)
|
validate_entries(account_name, entry, accounts_by_name, accounts)
|
||||||
gl_entries_by_account.setdefault(account_name, []).append(entry)
|
gl_entries_by_account.setdefault(account_name, []).append(entry)
|
||||||
|
|
||||||
@@ -491,7 +507,12 @@ def filter_accounts(accounts, depth=10):
|
|||||||
parent_children_map = {}
|
parent_children_map = {}
|
||||||
accounts_by_name = {}
|
accounts_by_name = {}
|
||||||
for d in accounts:
|
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)
|
parent_children_map.setdefault(d.parent_account or None, []).append(d)
|
||||||
|
|
||||||
filtered_accounts = []
|
filtered_accounts = []
|
||||||
|
|||||||
@@ -4,9 +4,10 @@
|
|||||||
frappe.query_reports["Unpaid Expense Claim"] = {
|
frappe.query_reports["Unpaid Expense Claim"] = {
|
||||||
"filters": [
|
"filters": [
|
||||||
{
|
{
|
||||||
"fieldname":"employee",
|
"fieldname": "employee",
|
||||||
"label": __("Employee"),
|
"label": __("Employee"),
|
||||||
"fieldtype": "Link"
|
"fieldtype": "Link",
|
||||||
|
"options": "Employee"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
"fieldname": "supp_master_name",
|
"fieldname": "supp_master_name",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Supplier Naming By",
|
"label": "Supplier Naming By",
|
||||||
"options": "Supplier Name\nNaming Series"
|
"options": "Supplier Name\nNaming Series\nAuto Name"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "supplier_group",
|
"fieldname": "supplier_group",
|
||||||
@@ -123,7 +123,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-06-24 10:38:28.934525",
|
"modified": "2021-09-08 19:26:23.548837",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Buying Settings",
|
"name": "Buying Settings",
|
||||||
|
|||||||
@@ -394,12 +394,10 @@ def get_item_from_material_requests_based_on_supplier(source_name, target_doc =
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_supplier_tag():
|
def get_supplier_tag():
|
||||||
if not frappe.cache().hget("Supplier", "Tags"):
|
filters = {"document_type": "Supplier"}
|
||||||
filters = {"document_type": "Supplier"}
|
tags = list(set(tag.tag for tag in frappe.get_all("Tag Link", filters=filters, fields=["tag"]) if tag))
|
||||||
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)
|
|
||||||
|
|
||||||
return frappe.cache().hget("Supplier", "Tags")
|
return tags
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@frappe.validate_and_sanitize_search_inputs
|
@frappe.validate_and_sanitize_search_inputs
|
||||||
|
|||||||
@@ -433,12 +433,12 @@
|
|||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"links": [
|
"links": [
|
||||||
{
|
{
|
||||||
"group": "Item Group",
|
"group": "Allowed Items",
|
||||||
"link_doctype": "Supplier Item Group",
|
"link_doctype": "Party Specific Item",
|
||||||
"link_fieldname": "supplier"
|
"link_fieldname": "party"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2021-08-27 18:02:44.314077",
|
"modified": "2021-09-06 17:37:56.522233",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Supplier",
|
"name": "Supplier",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from frappe.contacts.address_and_contact import (
|
|||||||
delete_contact_and_address,
|
delete_contact_and_address,
|
||||||
load_address_and_contact,
|
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.accounts.party import get_dashboard_info, validate_party_accounts
|
||||||
from erpnext.utilities.transaction_base import TransactionBase
|
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')
|
supp_master_name = frappe.defaults.get_global_default('supp_master_name')
|
||||||
if supp_master_name == 'Supplier Name':
|
if supp_master_name == 'Supplier Name':
|
||||||
self.name = self.supplier_name
|
self.name = self.supplier_name
|
||||||
else:
|
elif supp_master_name == 'Naming Series':
|
||||||
set_name_by_naming_series(self)
|
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):
|
def on_update(self):
|
||||||
if not self.naming_series:
|
if not self.naming_series:
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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."))
|
|
||||||
@@ -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
|
|
||||||
@@ -7,6 +7,7 @@ import json
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe import scrub
|
||||||
from frappe.desk.reportview import get_filters_cond, get_match_cond
|
from frappe.desk.reportview import get_filters_cond, get_match_cond
|
||||||
from frappe.utils import nowdate, unique
|
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]
|
if not field in searchfields]
|
||||||
searchfields = " or ".join([field + " like %(txt)s" for field in searchfields])
|
searchfields = " or ".join([field + " like %(txt)s" for field in searchfields])
|
||||||
|
|
||||||
if filters and isinstance(filters, dict) and filters.get('supplier'):
|
if filters and isinstance(filters, dict):
|
||||||
item_group_list = frappe.get_all('Supplier Item Group',
|
if filters.get('customer') or filters.get('supplier'):
|
||||||
filters = {'supplier': filters.get('supplier')}, fields = ['item_group'])
|
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 = []
|
filters_dict = {}
|
||||||
for i in item_group_list:
|
for rule in item_rules_list:
|
||||||
item_groups.append(i.item_group)
|
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 = ''
|
description_cond = ''
|
||||||
if frappe.db.count('Item', cache=True) < 50000:
|
if frappe.db.count('Item', cache=True) < 50000:
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import json
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from frappe.modules.utils import get_module_app
|
||||||
from frappe.utils import flt, has_common
|
from frappe.utils import flt, has_common
|
||||||
from frappe.utils.user import is_website_user
|
from frappe.utils.user import is_website_user
|
||||||
|
|
||||||
@@ -21,8 +22,32 @@ def get_list_context(context=None):
|
|||||||
"get_list": get_transaction_list
|
"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
|
user = frappe.session.user
|
||||||
ignore_permissions = False
|
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))
|
filters.append(('customer', 'in', customers))
|
||||||
elif suppliers:
|
elif suppliers:
|
||||||
filters.append(('supplier', 'in', suppliers))
|
filters.append(('supplier', 'in', suppliers))
|
||||||
else:
|
elif not custom:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
if doctype == 'Request for Quotation':
|
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
|
# Since customers and supplier do not have direct access to internal doctypes
|
||||||
ignore_permissions = True
|
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,
|
transactions = get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_length,
|
||||||
fields='name', ignore_permissions=ignore_permissions, order_by='modified desc')
|
fields='name', ignore_permissions=ignore_permissions, order_by='modified desc')
|
||||||
|
|
||||||
|
if custom:
|
||||||
|
return transactions
|
||||||
|
|
||||||
return post_process(doctype, transactions)
|
return post_process(doctype, transactions)
|
||||||
|
|
||||||
def get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_length=20,
|
def get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_length=20,
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ treeviews = ['Account', 'Cost Center', 'Warehouse', 'Item Group', 'Customer Grou
|
|||||||
# website
|
# website
|
||||||
update_website_context = ["erpnext.shopping_cart.utils.update_website_context", "erpnext.education.doctype.education_settings.education_settings.update_website_context"]
|
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"
|
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"]
|
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",
|
"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",
|
"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",
|
"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 = {
|
regional_overrides = {
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ frappe.ui.form.on('Employee Advance', {
|
|||||||
frm.trigger('make_return_entry');
|
frm.trigger('make_return_entry');
|
||||||
}, __('Create'));
|
}, __('Create'));
|
||||||
} else if (frm.doc.repay_unclaimed_amount_from_salary == 1 && frappe.model.can_create("Additional Salary")) {
|
} 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);
|
frm.events.make_deduction_via_additional_salary(frm);
|
||||||
}, __('Create'));
|
}, __('Create'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,7 +170,7 @@
|
|||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "repay_unclaimed_amount_from_salary",
|
"fieldname": "repay_unclaimed_amount_from_salary",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Repay unclaimed amount from salary"
|
"label": "Repay Unclaimed Amount from Salary"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:cur_frm.doc.employee",
|
"depends_on": "eval:cur_frm.doc.employee",
|
||||||
@@ -200,10 +200,11 @@
|
|||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-03-31 22:31:53.746659",
|
"modified": "2021-09-11 18:38:38.617478",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "Employee Advance",
|
"name": "Employee Advance",
|
||||||
|
"naming_rule": "By \"Naming Series\" field",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -172,7 +172,10 @@ def get_paying_amount_paying_exchange_rate(payment_account, doc):
|
|||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def create_return_through_additional_salary(doc):
|
def create_return_through_additional_salary(doc):
|
||||||
import json
|
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 = frappe.new_doc('Additional Salary')
|
||||||
additional_salary.employee = doc.employee
|
additional_salary.employee = doc.employee
|
||||||
additional_salary.currency = doc.currency
|
additional_salary.currency = doc.currency
|
||||||
|
|||||||
@@ -12,8 +12,11 @@ import erpnext
|
|||||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||||
from erpnext.hr.doctype.employee_advance.employee_advance import (
|
from erpnext.hr.doctype.employee_advance.employee_advance import (
|
||||||
EmployeeAdvanceOverPayment,
|
EmployeeAdvanceOverPayment,
|
||||||
|
create_return_through_additional_salary,
|
||||||
make_bank_entry,
|
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):
|
class TestEmployeeAdvance(unittest.TestCase):
|
||||||
@@ -33,6 +36,46 @@ class TestEmployeeAdvance(unittest.TestCase):
|
|||||||
journal_entry1 = make_payment_entry(advance)
|
journal_entry1 = make_payment_entry(advance)
|
||||||
self.assertRaises(EmployeeAdvanceOverPayment, journal_entry1.submit)
|
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):
|
def make_payment_entry(advance):
|
||||||
journal_entry = frappe.get_doc(make_bank_entry("Employee Advance", advance.name))
|
journal_entry = frappe.get_doc(make_bank_entry("Employee Advance", advance.name))
|
||||||
journal_entry.cheque_no = "123123"
|
journal_entry.cheque_no = "123123"
|
||||||
@@ -41,7 +84,7 @@ def make_payment_entry(advance):
|
|||||||
|
|
||||||
return journal_entry
|
return journal_entry
|
||||||
|
|
||||||
def make_employee_advance(employee_name):
|
def make_employee_advance(employee_name, args=None):
|
||||||
doc = frappe.new_doc("Employee Advance")
|
doc = frappe.new_doc("Employee Advance")
|
||||||
doc.employee = employee_name
|
doc.employee = employee_name
|
||||||
doc.company = "_Test company"
|
doc.company = "_Test company"
|
||||||
@@ -51,6 +94,10 @@ def make_employee_advance(employee_name):
|
|||||||
doc.advance_amount = 1000
|
doc.advance_amount = 1000
|
||||||
doc.posting_date = nowdate()
|
doc.posting_date = nowdate()
|
||||||
doc.advance_account = "_Test Employee Advance - _TC"
|
doc.advance_account = "_Test Employee Advance - _TC"
|
||||||
|
|
||||||
|
if args:
|
||||||
|
doc.update(args)
|
||||||
|
|
||||||
doc.insert()
|
doc.insert()
|
||||||
doc.submit()
|
doc.submit()
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,10 @@ def set_employee_name(doc):
|
|||||||
def update_employee(employee, details, date=None, cancel=False):
|
def update_employee(employee, details, date=None, cancel=False):
|
||||||
internal_work_history = {}
|
internal_work_history = {}
|
||||||
for item in details:
|
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
|
new_data = item.new if not cancel else item.current
|
||||||
if fieldtype == "Date" and new_data:
|
if fieldtype == "Date" and new_data:
|
||||||
new_data = getdate(new_data)
|
new_data = getdate(new_data)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ frappe.ui.form.on('Maintenance Schedule', {
|
|||||||
},
|
},
|
||||||
refresh: function (frm) {
|
refresh: function (frm) {
|
||||||
setTimeout(() => {
|
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()));
|
frm.toggle_display('schedule', !(frm.is_new()));
|
||||||
}, 10);
|
}, 10);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ from erpnext.utilities.transaction_base import TransactionBase, delete_events
|
|||||||
class MaintenanceSchedule(TransactionBase):
|
class MaintenanceSchedule(TransactionBase):
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def generate_schedule(self):
|
def generate_schedule(self):
|
||||||
|
if self.docstatus != 0:
|
||||||
|
return
|
||||||
self.set('schedules', [])
|
self.set('schedules', [])
|
||||||
frappe.db.sql("""delete from `tabMaintenance Schedule Detail`
|
|
||||||
where parent=%s""", (self.name))
|
|
||||||
count = 1
|
count = 1
|
||||||
for d in self.get('items'):
|
for d in self.get('items'):
|
||||||
self.validate_maintenance_detail()
|
self.validate_maintenance_detail()
|
||||||
|
|||||||
@@ -510,8 +510,14 @@ class BOM(WebsiteGenerator):
|
|||||||
if d.workstation:
|
if d.workstation:
|
||||||
self.update_rate_and_time(d, update_hour_rate)
|
self.update_rate_and_time(d, update_hour_rate)
|
||||||
|
|
||||||
self.operating_cost += flt(d.operating_cost)
|
operating_cost = d.operating_cost
|
||||||
self.base_operating_cost += flt(d.base_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):
|
def update_rate_and_time(self, row, update_hour_rate = False):
|
||||||
if not row.hour_rate or update_hour_rate:
|
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.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.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.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:
|
if update_hour_rate:
|
||||||
row.db_update()
|
row.db_update()
|
||||||
|
|||||||
@@ -108,6 +108,24 @@ class TestBOM(unittest.TestCase):
|
|||||||
self.assertAlmostEqual(bom.base_raw_material_cost, base_raw_material_cost)
|
self.assertAlmostEqual(bom.base_raw_material_cost, base_raw_material_cost)
|
||||||
self.assertAlmostEqual(bom.base_total_cost, base_raw_material_cost + base_op_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):
|
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)
|
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)):
|
for item_code, rate in (("_Test Item", 3600), ("_Test Item Home Desktop Manufactured", 3000)):
|
||||||
|
|||||||
@@ -8,15 +8,23 @@
|
|||||||
"field_order": [
|
"field_order": [
|
||||||
"sequence_id",
|
"sequence_id",
|
||||||
"operation",
|
"operation",
|
||||||
"workstation",
|
|
||||||
"description",
|
|
||||||
"col_break1",
|
"col_break1",
|
||||||
"hour_rate",
|
"workstation",
|
||||||
"time_in_mins",
|
"time_in_mins",
|
||||||
"operating_cost",
|
"costing_section",
|
||||||
|
"hour_rate",
|
||||||
"base_hour_rate",
|
"base_hour_rate",
|
||||||
|
"column_break_9",
|
||||||
|
"operating_cost",
|
||||||
"base_operating_cost",
|
"base_operating_cost",
|
||||||
|
"column_break_11",
|
||||||
"batch_size",
|
"batch_size",
|
||||||
|
"set_cost_based_on_bom_qty",
|
||||||
|
"cost_per_unit",
|
||||||
|
"base_cost_per_unit",
|
||||||
|
"more_information_section",
|
||||||
|
"description",
|
||||||
|
"column_break_18",
|
||||||
"image"
|
"image"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
@@ -117,13 +125,59 @@
|
|||||||
"fieldname": "sequence_id",
|
"fieldname": "sequence_id",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"label": "Sequence ID"
|
"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,
|
"idx": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-01-12 14:48:09.596843",
|
"modified": "2021-09-13 16:45:01.092868",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "BOM Operation",
|
"name": "BOM Operation",
|
||||||
|
|||||||
@@ -26,15 +26,23 @@ frappe.ui.form.on('Job Card', {
|
|||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
frappe.flags.pause_job = 0;
|
frappe.flags.pause_job = 0;
|
||||||
frappe.flags.resume_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.__islocal && has_items && frm.doc.docstatus < 2) {
|
||||||
if (frm.doc.for_quantity != frm.doc.transferred_qty) {
|
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.add_custom_button(__("Material Request"), () => {
|
||||||
frm.trigger("make_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.add_custom_button(__("Material Transfer"), () => {
|
||||||
frm.trigger("make_stock_entry");
|
frm.trigger("make_stock_entry");
|
||||||
}).addClass("btn-primary");
|
}).addClass("btn-primary");
|
||||||
|
|||||||
@@ -38,6 +38,8 @@
|
|||||||
"total_time_in_mins",
|
"total_time_in_mins",
|
||||||
"section_break_8",
|
"section_break_8",
|
||||||
"items",
|
"items",
|
||||||
|
"scrap_items_section",
|
||||||
|
"scrap_items",
|
||||||
"corrective_operation_section",
|
"corrective_operation_section",
|
||||||
"for_job_card",
|
"for_job_card",
|
||||||
"is_corrective_job_card",
|
"is_corrective_job_card",
|
||||||
@@ -185,7 +187,7 @@
|
|||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "transferred_qty",
|
"fieldname": "transferred_qty",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Transferred Qty",
|
"label": "FG Qty from Transferred Raw Materials",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -392,14 +394,28 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Batch No",
|
"label": "Batch No",
|
||||||
"options": "Batch"
|
"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,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-03-16 15:59:32.766484",
|
"modified": "2021-09-14 00:38:46.873105",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Job Card",
|
"name": "Job Card",
|
||||||
|
"naming_rule": "By \"Naming Series\" field",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- 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
|
# For license information, please see license.txt
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
|
|
||||||
@@ -37,6 +34,10 @@ class OperationSequenceError(frappe.ValidationError): pass
|
|||||||
class JobCardCancelError(frappe.ValidationError): pass
|
class JobCardCancelError(frappe.ValidationError): pass
|
||||||
|
|
||||||
class JobCard(Document):
|
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):
|
def validate(self):
|
||||||
self.validate_time_logs()
|
self.validate_time_logs()
|
||||||
self.set_status()
|
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))
|
frappe.db.set_value('Job Card Item', row.job_card_item, 'transferred_qty', flt(qty))
|
||||||
|
|
||||||
def set_transferred_qty(self, update_status=False):
|
def set_transferred_qty(self, update_status=False):
|
||||||
|
"Set total FG Qty for which RM was transferred."
|
||||||
if not self.items:
|
if not self.items:
|
||||||
self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0
|
self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0
|
||||||
|
|
||||||
@@ -457,6 +459,7 @@ class JobCard(Document):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if self.items:
|
if self.items:
|
||||||
|
# sum of 'For Quantity' of Stock Entries against JC
|
||||||
self.transferred_qty = frappe.db.get_value('Stock Entry', {
|
self.transferred_qty = frappe.db.get_value('Stock Entry', {
|
||||||
'job_card': self.name,
|
'job_card': self.name,
|
||||||
'work_order': self.work_order,
|
'work_order': self.work_order,
|
||||||
@@ -500,7 +503,9 @@ class JobCard(Document):
|
|||||||
self.status = 'Work In Progress'
|
self.status = 'Work In Progress'
|
||||||
|
|
||||||
if (self.docstatus == 1 and
|
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'
|
self.status = 'Completed'
|
||||||
|
|
||||||
if 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):
|
def set_missing_values(source, target):
|
||||||
target.purpose = "Material Transfer for Manufacture"
|
target.purpose = "Material Transfer for Manufacture"
|
||||||
target.from_bom = 1
|
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.set_transfer_qty()
|
||||||
target.calculate_rate_and_amount()
|
target.calculate_rate_and_amount()
|
||||||
target.set_missing_values()
|
target.set_missing_values()
|
||||||
|
|||||||
@@ -1,22 +1,37 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# See license.txt
|
# See license.txt
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import random_string
|
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 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.work_order.test_work_order import make_wo_order_test_record
|
||||||
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
|
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):
|
class TestJobCard(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
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):
|
def tearDown(self):
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
@@ -96,3 +111,84 @@ class TestJobCard(unittest.TestCase):
|
|||||||
"employee": employee,
|
"employee": employee,
|
||||||
})
|
})
|
||||||
self.assertRaises(OverlapError, jc2.save)
|
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")
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -25,9 +25,12 @@
|
|||||||
"overproduction_percentage_for_sales_order",
|
"overproduction_percentage_for_sales_order",
|
||||||
"column_break_16",
|
"column_break_16",
|
||||||
"overproduction_percentage_for_work_order",
|
"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",
|
"other_settings_section",
|
||||||
"update_bom_costs_automatically",
|
"update_bom_costs_automatically",
|
||||||
"add_corrective_operation_cost_in_finished_good_valuation",
|
|
||||||
"column_break_23",
|
"column_break_23",
|
||||||
"make_serial_no_batch_from_work_order"
|
"make_serial_no_batch_from_work_order"
|
||||||
],
|
],
|
||||||
@@ -96,10 +99,10 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"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",
|
"fieldname": "material_consumption",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Allow Multiple Material Consumption"
|
"label": "Allow Continuous Material Consumption"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
@@ -175,13 +178,29 @@
|
|||||||
"fieldname": "add_corrective_operation_cost_in_finished_good_valuation",
|
"fieldname": "add_corrective_operation_cost_in_finished_good_valuation",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Add Corrective Operation Cost in Finished Good Valuation"
|
"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",
|
"icon": "icon-wrench",
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-03-16 15:54:38.967341",
|
"modified": "2021-09-13 22:09:09.401559",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Manufacturing Settings",
|
"name": "Manufacturing Settings",
|
||||||
|
|||||||
@@ -242,6 +242,8 @@ frappe.ui.form.on('Production Plan', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
get_sub_assembly_items: function(frm) {
|
get_sub_assembly_items: function(frm) {
|
||||||
|
frm.dirty();
|
||||||
|
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "get_sub_assembly_items",
|
method: "get_sub_assembly_items",
|
||||||
freeze: true,
|
freeze: true,
|
||||||
|
|||||||
@@ -561,8 +561,6 @@ class ProductionPlan(Document):
|
|||||||
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty)
|
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.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):
|
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)
|
bom_data = sorted(bom_data, key = lambda i: i.bom_level)
|
||||||
|
|
||||||
|
|||||||
@@ -404,6 +404,7 @@ def make_bom(**args):
|
|||||||
'uom': item_doc.stock_uom,
|
'uom': item_doc.stock_uom,
|
||||||
'stock_uom': item_doc.stock_uom,
|
'stock_uom': item_doc.stock_uom,
|
||||||
'rate': item_doc.valuation_rate or args.rate,
|
'rate': item_doc.valuation_rate or args.rate,
|
||||||
|
'source_warehouse': args.source_warehouse
|
||||||
})
|
})
|
||||||
|
|
||||||
if not args.do_not_save:
|
if not args.do_not_save:
|
||||||
|
|||||||
@@ -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
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
@@ -20,7 +16,7 @@ from erpnext.manufacturing.doctype.work_order.work_order import (
|
|||||||
stop_unstop,
|
stop_unstop,
|
||||||
)
|
)
|
||||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
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.stock_entry import test_stock_entry
|
||||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||||
from erpnext.stock.utils import get_bin
|
from erpnext.stock.utils import get_bin
|
||||||
@@ -772,6 +768,60 @@ class TestWorkOrder(unittest.TestCase):
|
|||||||
total_pl_qty
|
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):
|
def get_scrap_item_details(bom_no):
|
||||||
scrap_items = {}
|
scrap_items = {}
|
||||||
for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item`
|
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.get_items_and_operations_from_bom()
|
||||||
wo_order.sales_order = args.sales_order or None
|
wo_order.sales_order = args.sales_order or None
|
||||||
wo_order.planned_start_date = args.planned_start_date or now()
|
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:
|
if args.source_warehouse:
|
||||||
for item in wo_order.get("required_items"):
|
for item in wo_order.get("required_items"):
|
||||||
|
|||||||
@@ -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.validate_options_for_data_field
|
||||||
erpnext.patches.v13_0.create_gst_payment_entry_fields
|
erpnext.patches.v13_0.create_gst_payment_entry_fields
|
||||||
erpnext.patches.v14_0.delete_shopify_doctypes
|
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.v13_0.update_dates_in_tax_withholding_category
|
||||||
erpnext.patches.v14_0.update_opportunity_currency_fields
|
erpnext.patches.v14_0.update_opportunity_currency_fields
|
||||||
|
erpnext.patches.v13_0.create_accounting_dimensions_in_pos_doctypes
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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()
|
||||||
@@ -3,8 +3,6 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
from erpnext.accounts.utils import get_fiscal_year
|
|
||||||
|
|
||||||
|
|
||||||
def execute():
|
def execute():
|
||||||
frappe.reload_doc('accounts', 'doctype', 'Tax Withholding Rate')
|
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'])
|
tds_category_rates = frappe.get_all('Tax Withholding Rate', fields=['name', 'fiscal_year'])
|
||||||
|
|
||||||
fiscal_year_map = {}
|
fiscal_year_map = {}
|
||||||
for rate in tds_category_rates:
|
fiscal_year_details = frappe.get_all('Fiscal Year', fields=['name', 'year_start_date', 'year_end_date'])
|
||||||
if not fiscal_year_map.get(rate.fiscal_year):
|
|
||||||
fiscal_year_map[rate.fiscal_year] = get_fiscal_year(fiscal_year=rate.fiscal_year)
|
|
||||||
|
|
||||||
from_date = fiscal_year_map.get(rate.fiscal_year)[1]
|
for d in fiscal_year_details:
|
||||||
to_date = fiscal_year_map.get(rate.fiscal_year)[2]
|
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, {
|
frappe.db.set_value('Tax Withholding Rate', rate.name, {
|
||||||
'from_date': from_date,
|
'from_date': from_date,
|
||||||
|
|||||||
@@ -14,12 +14,11 @@ from erpnext.hr.utils import validate_active_employee
|
|||||||
|
|
||||||
class AdditionalSalary(Document):
|
class AdditionalSalary(Document):
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
if self.ref_doctype == "Employee Advance" and self.ref_docname:
|
self.update_return_amount_in_employee_advance()
|
||||||
frappe.db.set_value("Employee Advance", self.ref_docname, "return_amount", self.amount)
|
|
||||||
|
|
||||||
self.update_employee_referral()
|
self.update_employee_referral()
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
|
self.update_return_amount_in_employee_advance()
|
||||||
self.update_employee_referral(cancel=True)
|
self.update_employee_referral(cancel=True)
|
||||||
|
|
||||||
def validate(self):
|
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.throw(_("Additional Salary for referral bonus can only be created against Employee Referral with status {0}").format(
|
||||||
frappe.bold("Accepted")))
|
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):
|
def update_employee_referral(self, cancel=False):
|
||||||
if self.ref_doctype == "Employee Referral":
|
if self.ref_doctype == "Employee Referral":
|
||||||
status = "Unpaid" if cancel else "Paid"
|
status = "Unpaid" if cancel else "Paid"
|
||||||
|
|||||||
@@ -758,11 +758,11 @@ def set_tax_withholding_category(company):
|
|||||||
accounts = [dict(company=company, account=tds_account)]
|
accounts = [dict(company=company, account=tds_account)]
|
||||||
|
|
||||||
try:
|
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:
|
except FiscalYearError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
docs = get_tds_details(accounts, fiscal_year)
|
docs = get_tds_details(accounts, fiscal_year_details)
|
||||||
|
|
||||||
for d in docs:
|
for d in docs:
|
||||||
if not frappe.db.exists("Tax Withholding Category", d.get("name")):
|
if not frappe.db.exists("Tax Withholding Category", d.get("name")):
|
||||||
@@ -777,9 +777,10 @@ def set_tax_withholding_category(company):
|
|||||||
if accounts:
|
if accounts:
|
||||||
doc.append("accounts", accounts[0])
|
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
|
# 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:
|
if not fy_exist:
|
||||||
doc.append("rates", d.get('rates')[0])
|
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
|
# bootstrap default tax withholding sections
|
||||||
return [
|
return [
|
||||||
dict(name="TDS - 194C - Company",
|
dict(name="TDS - 194C - Company",
|
||||||
category_name="Payment to Contractors (Single / Aggregate)",
|
category_name="Payment to Contractors (Single / Aggregate)",
|
||||||
doctype="Tax Withholding Category", accounts=accounts,
|
doctype="Tax Withholding Category", accounts=accounts,
|
||||||
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 2,
|
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
|
||||||
"single_threshold": 30000, "cumulative_threshold": 100000}]),
|
"tax_withholding_rate": 2, "single_threshold": 30000, "cumulative_threshold": 100000}]),
|
||||||
dict(name="TDS - 194C - Individual",
|
dict(name="TDS - 194C - Individual",
|
||||||
category_name="Payment to Contractors (Single / Aggregate)",
|
category_name="Payment to Contractors (Single / Aggregate)",
|
||||||
doctype="Tax Withholding Category", accounts=accounts,
|
doctype="Tax Withholding Category", accounts=accounts,
|
||||||
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 1,
|
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
|
||||||
"single_threshold": 30000, "cumulative_threshold": 100000}]),
|
"tax_withholding_rate": 1, "single_threshold": 30000, "cumulative_threshold": 100000}]),
|
||||||
dict(name="TDS - 194C - No PAN / Invalid PAN",
|
dict(name="TDS - 194C - No PAN / Invalid PAN",
|
||||||
category_name="Payment to Contractors (Single / Aggregate)",
|
category_name="Payment to Contractors (Single / Aggregate)",
|
||||||
doctype="Tax Withholding Category", accounts=accounts,
|
doctype="Tax Withholding Category", accounts=accounts,
|
||||||
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20,
|
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
|
||||||
"single_threshold": 30000, "cumulative_threshold": 100000}]),
|
"tax_withholding_rate": 20, "single_threshold": 30000, "cumulative_threshold": 100000}]),
|
||||||
dict(name="TDS - 194D - Company",
|
dict(name="TDS - 194D - Company",
|
||||||
category_name="Insurance Commission",
|
category_name="Insurance Commission",
|
||||||
doctype="Tax Withholding Category", accounts=accounts,
|
doctype="Tax Withholding Category", accounts=accounts,
|
||||||
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 5,
|
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
|
||||||
"single_threshold": 15000, "cumulative_threshold": 0}]),
|
"tax_withholding_rate": 5, "single_threshold": 15000, "cumulative_threshold": 0}]),
|
||||||
dict(name="TDS - 194D - Company Assessee",
|
dict(name="TDS - 194D - Company Assessee",
|
||||||
category_name="Insurance Commission",
|
category_name="Insurance Commission",
|
||||||
doctype="Tax Withholding Category", accounts=accounts,
|
doctype="Tax Withholding Category", accounts=accounts,
|
||||||
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10,
|
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
|
||||||
"single_threshold": 15000, "cumulative_threshold": 0}]),
|
"tax_withholding_rate": 10, "single_threshold": 15000, "cumulative_threshold": 0}]),
|
||||||
dict(name="TDS - 194D - Individual",
|
dict(name="TDS - 194D - Individual",
|
||||||
category_name="Insurance Commission",
|
category_name="Insurance Commission",
|
||||||
doctype="Tax Withholding Category", accounts=accounts,
|
doctype="Tax Withholding Category", accounts=accounts,
|
||||||
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 5,
|
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
|
||||||
"single_threshold": 15000, "cumulative_threshold": 0}]),
|
"tax_withholding_rate": 5, "single_threshold": 15000, "cumulative_threshold": 0}]),
|
||||||
dict(name="TDS - 194D - No PAN / Invalid PAN",
|
dict(name="TDS - 194D - No PAN / Invalid PAN",
|
||||||
category_name="Insurance Commission",
|
category_name="Insurance Commission",
|
||||||
doctype="Tax Withholding Category", accounts=accounts,
|
doctype="Tax Withholding Category", accounts=accounts,
|
||||||
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20,
|
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
|
||||||
"single_threshold": 15000, "cumulative_threshold": 0}]),
|
"tax_withholding_rate": 20, "single_threshold": 15000, "cumulative_threshold": 0}]),
|
||||||
dict(name="TDS - 194DA - Company",
|
dict(name="TDS - 194DA - Company",
|
||||||
category_name="Non-exempt payments made under a life insurance policy",
|
category_name="Non-exempt payments made under a life insurance policy",
|
||||||
doctype="Tax Withholding Category", accounts=accounts,
|
doctype="Tax Withholding Category", accounts=accounts,
|
||||||
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 1,
|
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
|
||||||
"single_threshold": 100000, "cumulative_threshold": 0}]),
|
"tax_withholding_rate": 1, "single_threshold": 100000, "cumulative_threshold": 0}]),
|
||||||
dict(name="TDS - 194DA - Individual",
|
dict(name="TDS - 194DA - Individual",
|
||||||
category_name="Non-exempt payments made under a life insurance policy",
|
category_name="Non-exempt payments made under a life insurance policy",
|
||||||
doctype="Tax Withholding Category", accounts=accounts,
|
doctype="Tax Withholding Category", accounts=accounts,
|
||||||
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 1,
|
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
|
||||||
"single_threshold": 100000, "cumulative_threshold": 0}]),
|
"tax_withholding_rate": 1, "single_threshold": 100000, "cumulative_threshold": 0}]),
|
||||||
dict(name="TDS - 194DA - No PAN / Invalid PAN",
|
dict(name="TDS - 194DA - No PAN / Invalid PAN",
|
||||||
category_name="Non-exempt payments made under a life insurance policy",
|
category_name="Non-exempt payments made under a life insurance policy",
|
||||||
doctype="Tax Withholding Category", accounts=accounts,
|
doctype="Tax Withholding Category", accounts=accounts,
|
||||||
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20,
|
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
|
||||||
"single_threshold": 100000, "cumulative_threshold": 0}]),
|
"tax_withholding_rate": 20, "single_threshold": 100000, "cumulative_threshold": 0}]),
|
||||||
dict(name="TDS - 194H - Company",
|
dict(name="TDS - 194H - Company",
|
||||||
category_name="Commission / Brokerage",
|
category_name="Commission / Brokerage",
|
||||||
doctype="Tax Withholding Category", accounts=accounts,
|
doctype="Tax Withholding Category", accounts=accounts,
|
||||||
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 5,
|
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
|
||||||
"single_threshold": 15000, "cumulative_threshold": 0}]),
|
"tax_withholding_rate": 5, "single_threshold": 15000, "cumulative_threshold": 0}]),
|
||||||
dict(name="TDS - 194H - Individual",
|
dict(name="TDS - 194H - Individual",
|
||||||
category_name="Commission / Brokerage",
|
category_name="Commission / Brokerage",
|
||||||
doctype="Tax Withholding Category", accounts=accounts,
|
doctype="Tax Withholding Category", accounts=accounts,
|
||||||
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 5,
|
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
|
||||||
"single_threshold": 15000, "cumulative_threshold": 0}]),
|
"tax_withholding_rate": 5, "single_threshold": 15000, "cumulative_threshold": 0}]),
|
||||||
dict(name="TDS - 194H - No PAN / Invalid PAN",
|
dict(name="TDS - 194H - No PAN / Invalid PAN",
|
||||||
category_name="Commission / Brokerage",
|
category_name="Commission / Brokerage",
|
||||||
doctype="Tax Withholding Category", accounts=accounts,
|
doctype="Tax Withholding Category", accounts=accounts,
|
||||||
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20,
|
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
|
||||||
"single_threshold": 15000, "cumulative_threshold": 0}]),
|
"tax_withholding_rate": 20, "single_threshold": 15000, "cumulative_threshold": 0}]),
|
||||||
dict(name="TDS - 194I - Rent - Company",
|
dict(name="TDS - 194I - Rent - Company",
|
||||||
category_name="Rent",
|
category_name="Rent",
|
||||||
doctype="Tax Withholding Category", accounts=accounts,
|
doctype="Tax Withholding Category", accounts=accounts,
|
||||||
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10,
|
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
|
||||||
"single_threshold": 180000, "cumulative_threshold": 0}]),
|
"tax_withholding_rate": 10, "single_threshold": 180000, "cumulative_threshold": 0}]),
|
||||||
dict(name="TDS - 194I - Rent - Individual",
|
dict(name="TDS - 194I - Rent - Individual",
|
||||||
category_name="Rent",
|
category_name="Rent",
|
||||||
doctype="Tax Withholding Category", accounts=accounts,
|
doctype="Tax Withholding Category", accounts=accounts,
|
||||||
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10,
|
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
|
||||||
"single_threshold": 180000, "cumulative_threshold": 0}]),
|
"tax_withholding_rate": 10, "single_threshold": 180000, "cumulative_threshold": 0}]),
|
||||||
dict(name="TDS - 194I - Rent - No PAN / Invalid PAN",
|
dict(name="TDS - 194I - Rent - No PAN / Invalid PAN",
|
||||||
category_name="Rent",
|
category_name="Rent",
|
||||||
doctype="Tax Withholding Category", accounts=accounts,
|
doctype="Tax Withholding Category", accounts=accounts,
|
||||||
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20,
|
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
|
||||||
"single_threshold": 180000, "cumulative_threshold": 0}]),
|
"tax_withholding_rate": 20, "single_threshold": 180000, "cumulative_threshold": 0}]),
|
||||||
dict(name="TDS - 194I - Rent/Machinery - Company",
|
dict(name="TDS - 194I - Rent/Machinery - Company",
|
||||||
category_name="Rent-Plant / Machinery",
|
category_name="Rent-Plant / Machinery",
|
||||||
doctype="Tax Withholding Category", accounts=accounts,
|
doctype="Tax Withholding Category", accounts=accounts,
|
||||||
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 2,
|
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
|
||||||
"single_threshold": 180000, "cumulative_threshold": 0}]),
|
"tax_withholding_rate": 2, "single_threshold": 180000, "cumulative_threshold": 0}]),
|
||||||
dict(name="TDS - 194I - Rent/Machinery - Individual",
|
dict(name="TDS - 194I - Rent/Machinery - Individual",
|
||||||
category_name="Rent-Plant / Machinery",
|
category_name="Rent-Plant / Machinery",
|
||||||
doctype="Tax Withholding Category", accounts=accounts,
|
doctype="Tax Withholding Category", accounts=accounts,
|
||||||
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 2,
|
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
|
||||||
"single_threshold": 180000, "cumulative_threshold": 0}]),
|
"tax_withholding_rate": 2, "single_threshold": 180000, "cumulative_threshold": 0}]),
|
||||||
dict(name="TDS - 194I - Rent/Machinery - No PAN / Invalid PAN",
|
dict(name="TDS - 194I - Rent/Machinery - No PAN / Invalid PAN",
|
||||||
category_name="Rent-Plant / Machinery",
|
category_name="Rent-Plant / Machinery",
|
||||||
doctype="Tax Withholding Category", accounts=accounts,
|
doctype="Tax Withholding Category", accounts=accounts,
|
||||||
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20,
|
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
|
||||||
"single_threshold": 180000, "cumulative_threshold": 0}]),
|
"tax_withholding_rate": 20, "single_threshold": 180000, "cumulative_threshold": 0}]),
|
||||||
dict(name="TDS - 194J - Professional Fees - Company",
|
dict(name="TDS - 194J - Professional Fees - Company",
|
||||||
category_name="Professional Fees",
|
category_name="Professional Fees",
|
||||||
doctype="Tax Withholding Category", accounts=accounts,
|
doctype="Tax Withholding Category", accounts=accounts,
|
||||||
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10,
|
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
|
||||||
"single_threshold": 30000, "cumulative_threshold": 0}]),
|
"tax_withholding_rate": 10, "single_threshold": 30000, "cumulative_threshold": 0}]),
|
||||||
dict(name="TDS - 194J - Professional Fees - Individual",
|
dict(name="TDS - 194J - Professional Fees - Individual",
|
||||||
category_name="Professional Fees",
|
category_name="Professional Fees",
|
||||||
doctype="Tax Withholding Category", accounts=accounts,
|
doctype="Tax Withholding Category", accounts=accounts,
|
||||||
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10,
|
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
|
||||||
"single_threshold": 30000, "cumulative_threshold": 0}]),
|
"tax_withholding_rate": 10, "single_threshold": 30000, "cumulative_threshold": 0}]),
|
||||||
dict(name="TDS - 194J - Professional Fees - No PAN / Invalid PAN",
|
dict(name="TDS - 194J - Professional Fees - No PAN / Invalid PAN",
|
||||||
category_name="Professional Fees",
|
category_name="Professional Fees",
|
||||||
doctype="Tax Withholding Category", accounts=accounts,
|
doctype="Tax Withholding Category", accounts=accounts,
|
||||||
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20,
|
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
|
||||||
"single_threshold": 30000, "cumulative_threshold": 0}]),
|
"tax_withholding_rate": 20, "single_threshold": 30000, "cumulative_threshold": 0}]),
|
||||||
dict(name="TDS - 194J - Director Fees - Company",
|
dict(name="TDS - 194J - Director Fees - Company",
|
||||||
category_name="Director Fees",
|
category_name="Director Fees",
|
||||||
doctype="Tax Withholding Category", accounts=accounts,
|
doctype="Tax Withholding Category", accounts=accounts,
|
||||||
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10,
|
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
|
||||||
"single_threshold": 0, "cumulative_threshold": 0}]),
|
"tax_withholding_rate": 10, "single_threshold": 0, "cumulative_threshold": 0}]),
|
||||||
dict(name="TDS - 194J - Director Fees - Individual",
|
dict(name="TDS - 194J - Director Fees - Individual",
|
||||||
category_name="Director Fees",
|
category_name="Director Fees",
|
||||||
doctype="Tax Withholding Category", accounts=accounts,
|
doctype="Tax Withholding Category", accounts=accounts,
|
||||||
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10,
|
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
|
||||||
"single_threshold": 0, "cumulative_threshold": 0}]),
|
"tax_withholding_rate": 10, "single_threshold": 0, "cumulative_threshold": 0}]),
|
||||||
dict(name="TDS - 194J - Director Fees - No PAN / Invalid PAN",
|
dict(name="TDS - 194J - Director Fees - No PAN / Invalid PAN",
|
||||||
category_name="Director Fees",
|
category_name="Director Fees",
|
||||||
doctype="Tax Withholding Category", accounts=accounts,
|
doctype="Tax Withholding Category", accounts=accounts,
|
||||||
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20,
|
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
|
||||||
"single_threshold": 0, "cumulative_threshold": 0}]),
|
"tax_withholding_rate": 20, "single_threshold": 0, "cumulative_threshold": 0}]),
|
||||||
dict(name="TDS - 194 - Dividends - Company",
|
dict(name="TDS - 194 - Dividends - Company",
|
||||||
category_name="Dividends",
|
category_name="Dividends",
|
||||||
doctype="Tax Withholding Category", accounts=accounts,
|
doctype="Tax Withholding Category", accounts=accounts,
|
||||||
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10,
|
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
|
||||||
"single_threshold": 2500, "cumulative_threshold": 0}]),
|
"tax_withholding_rate": 10, "single_threshold": 2500, "cumulative_threshold": 0}]),
|
||||||
dict(name="TDS - 194 - Dividends - Individual",
|
dict(name="TDS - 194 - Dividends - Individual",
|
||||||
category_name="Dividends",
|
category_name="Dividends",
|
||||||
doctype="Tax Withholding Category", accounts=accounts,
|
doctype="Tax Withholding Category", accounts=accounts,
|
||||||
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10,
|
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
|
||||||
"single_threshold": 2500, "cumulative_threshold": 0}]),
|
"tax_withholding_rate": 10, "single_threshold": 2500, "cumulative_threshold": 0}]),
|
||||||
dict(name="TDS - 194 - Dividends - No PAN / Invalid PAN",
|
dict(name="TDS - 194 - Dividends - No PAN / Invalid PAN",
|
||||||
category_name="Dividends",
|
category_name="Dividends",
|
||||||
doctype="Tax Withholding Category", accounts=accounts,
|
doctype="Tax Withholding Category", accounts=accounts,
|
||||||
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20,
|
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
|
||||||
"single_threshold": 2500, "cumulative_threshold": 0}])
|
"tax_withholding_rate": 20, "single_threshold": 2500, "cumulative_threshold": 0}])
|
||||||
]
|
]
|
||||||
|
|
||||||
def create_gratuity_rule():
|
def create_gratuity_rule():
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ class Gstr1Report(object):
|
|||||||
|
|
||||||
|
|
||||||
if self.filters.get("type_of_business") == "B2B":
|
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"):
|
if self.filters.get("type_of_business") in ("B2C Large", "B2C Small"):
|
||||||
b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit')
|
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":
|
if self.filters.get("type_of_business") == "B2C Large":
|
||||||
conditions += """ AND ifnull(SUBSTR(place_of_supply, 1, 2),'') != ifnull(SUBSTR(company_gstin, 1, 2),'')
|
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":
|
elif self.filters.get("type_of_business") == "B2C Small":
|
||||||
conditions += """ AND (
|
conditions += """ AND (
|
||||||
@@ -236,8 +236,8 @@ class Gstr1Report(object):
|
|||||||
elif self.filters.get("type_of_business") == "CDNR-UNREG":
|
elif self.filters.get("type_of_business") == "CDNR-UNREG":
|
||||||
b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit')
|
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),'')
|
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 (is_return = 1 OR is_debit_note = 1)
|
||||||
AND IFNULL(gst_category, '') in ('Unregistered', 'Overseas')""".format(flt(b2c_limit))
|
AND IFNULL(gst_category, '') in ('Unregistered', 'Overseas')"""
|
||||||
|
|
||||||
elif self.filters.get("type_of_business") == "EXPORT":
|
elif self.filters.get("type_of_business") == "EXPORT":
|
||||||
conditions += """ AND is_return !=1 and gst_category = 'Overseas' """
|
conditions += """ AND is_return !=1 and gst_category = 'Overseas' """
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
"tax_withholding_category",
|
"tax_withholding_category",
|
||||||
"default_bank_account",
|
"default_bank_account",
|
||||||
"lead_name",
|
"lead_name",
|
||||||
"prospect",
|
|
||||||
"opportunity_name",
|
"opportunity_name",
|
||||||
"image",
|
"image",
|
||||||
"column_break0",
|
"column_break0",
|
||||||
@@ -214,7 +213,8 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"ignore_user_permissions": 1,
|
"ignore_user_permissions": 1,
|
||||||
"label": "Represents Company",
|
"label": "Represents Company",
|
||||||
"options": "Company"
|
"options": "Company",
|
||||||
|
"unique": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "represents_company",
|
"depends_on": "represents_company",
|
||||||
@@ -497,14 +497,6 @@
|
|||||||
"label": "Tax Withholding Category",
|
"label": "Tax Withholding Category",
|
||||||
"options": "Tax Withholding Category"
|
"options": "Tax Withholding Category"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "prospect",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"label": "Prospect",
|
|
||||||
"no_copy": 1,
|
|
||||||
"options": "Prospect",
|
|
||||||
"print_hide": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "opportunity_name",
|
"fieldname": "opportunity_name",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
@@ -518,8 +510,14 @@
|
|||||||
"idx": 363,
|
"idx": 363,
|
||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [
|
||||||
"modified": "2021-08-25 18:56:09.929905",
|
{
|
||||||
|
"group": "Allowed Items",
|
||||||
|
"link_doctype": "Party Specific Item",
|
||||||
|
"link_fieldname": "party"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"modified": "2021-09-06 17:38:54.196663",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Customer",
|
"name": "Customer",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from frappe.contacts.address_and_contact import (
|
|||||||
)
|
)
|
||||||
from frappe.desk.reportview import build_match_conditions, get_filters_cond
|
from frappe.desk.reportview import build_match_conditions, get_filters_cond
|
||||||
from frappe.model.mapper import get_mapped_doc
|
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.model.rename_doc import update_linked_doctypes
|
||||||
from frappe.utils import cint, cstr, flt, get_formatted_email, today
|
from frappe.utils import cint, cstr, flt, get_formatted_email, today
|
||||||
from frappe.utils.user import get_users_with_role
|
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')
|
cust_master_name = frappe.defaults.get_global_default('cust_master_name')
|
||||||
if cust_master_name == 'Customer Name':
|
if cust_master_name == 'Customer Name':
|
||||||
self.name = self.get_customer_name()
|
self.name = self.get_customer_name()
|
||||||
else:
|
elif cust_master_name == 'Naming Series':
|
||||||
set_name_by_naming_series(self)
|
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):
|
def get_customer_name(self):
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on('Supplier Item Group', {
|
frappe.ui.form.on('Party Specific Item', {
|
||||||
// refresh: function(frm) {
|
// refresh: function(frm) {
|
||||||
|
|
||||||
// }
|
// }
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
@@ -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)
|
||||||
@@ -41,14 +41,14 @@
|
|||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Customer Naming By",
|
"label": "Customer Naming By",
|
||||||
"options": "Customer Name\nNaming Series"
|
"options": "Customer Name\nNaming Series\nAuto Name"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "campaign_naming_by",
|
"fieldname": "campaign_naming_by",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Campaign Naming By",
|
"label": "Campaign Naming By",
|
||||||
"options": "Campaign Name\nNaming Series"
|
"options": "Campaign Name\nNaming Series\nAuto Name"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "customer_group",
|
"fieldname": "customer_group",
|
||||||
@@ -204,7 +204,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-09-01 22:55:33.803624",
|
"modified": "2021-09-08 19:38:10.175989",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Selling Settings",
|
"name": "Selling Settings",
|
||||||
@@ -223,4 +223,4 @@
|
|||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
@@ -73,7 +73,7 @@ def get_data(conditions, filters):
|
|||||||
`tabSales Order` so,
|
`tabSales Order` so,
|
||||||
`tabSales Order Item` soi
|
`tabSales Order Item` soi
|
||||||
LEFT JOIN `tabSales Invoice Item` sii
|
LEFT JOIN `tabSales Invoice Item` sii
|
||||||
ON sii.so_detail = soi.name
|
ON sii.so_detail = soi.name and sii.docstatus = 1
|
||||||
WHERE
|
WHERE
|
||||||
soi.parent = so.name
|
soi.parent = so.name
|
||||||
and so.status not in ('Stopped', 'Closed', 'On Hold')
|
and so.status not in ('Stopped', 'Closed', 'On Hold')
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
|
|||||||
this.frm.set_query("item_code", "items", function() {
|
this.frm.set_query("item_code", "items", function() {
|
||||||
return {
|
return {
|
||||||
query: "erpnext.controllers.queries.item_query",
|
query: "erpnext.controllers.queries.item_query",
|
||||||
filters: {'is_sales_item': 1}
|
filters: {'is_sales_item': 1, 'customer': cur_frm.doc.customer}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class ItemGroup(NestedSet, WebsiteGenerator):
|
|||||||
self.parent_item_group = _('All Item Groups')
|
self.parent_item_group = _('All Item Groups')
|
||||||
|
|
||||||
self.make_route()
|
self.make_route()
|
||||||
|
self.validate_item_group_defaults()
|
||||||
|
|
||||||
def on_update(self):
|
def on_update(self):
|
||||||
NestedSet.on_update(self)
|
NestedSet.on_update(self)
|
||||||
@@ -99,7 +100,7 @@ class ItemGroup(NestedSet, WebsiteGenerator):
|
|||||||
filter_engine = ProductFiltersBuilder(self.name)
|
filter_engine = ProductFiltersBuilder(self.name)
|
||||||
|
|
||||||
context.field_filters = filter_engine.get_field_filters()
|
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({
|
context.update({
|
||||||
"parents": get_parent_item_groups(self.parent_item_group),
|
"parents": get_parent_item_groups(self.parent_item_group),
|
||||||
@@ -134,6 +135,10 @@ class ItemGroup(NestedSet, WebsiteGenerator):
|
|||||||
def delete_child_item_groups_key(self):
|
def delete_child_item_groups_key(self):
|
||||||
frappe.cache().hdel("child_item_groups", self.name)
|
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)
|
@frappe.whitelist(allow_guest=True)
|
||||||
def get_product_list_for_group(product_group=None, start=0, limit=10, search=None):
|
def get_product_list_for_group(product_group=None, start=0, limit=10, search=None):
|
||||||
if product_group:
|
if product_group:
|
||||||
|
|||||||
@@ -1,73 +1,74 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"doctype": "Item Group",
|
"doctype": "Item Group",
|
||||||
"is_group": 0,
|
"is_group": 0,
|
||||||
"item_group_name": "_Test Item Group",
|
"item_group_name": "_Test Item Group",
|
||||||
"parent_item_group": "All Item Groups",
|
"parent_item_group": "All Item Groups",
|
||||||
"item_group_defaults": [{
|
"item_group_defaults": [{
|
||||||
"company": "_Test Company",
|
"company": "_Test Company",
|
||||||
"buying_cost_center": "_Test Cost Center 2 - _TC",
|
"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",
|
"doctype": "Item Group",
|
||||||
"is_group": 0,
|
"is_group": 0,
|
||||||
"item_group_name": "_Test Item Group Desktops",
|
"item_group_name": "_Test Item Group Desktops",
|
||||||
"parent_item_group": "All Item Groups"
|
"parent_item_group": "All Item Groups"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"doctype": "Item Group",
|
"doctype": "Item Group",
|
||||||
"is_group": 1,
|
"is_group": 1,
|
||||||
"item_group_name": "_Test Item Group A",
|
"item_group_name": "_Test Item Group A",
|
||||||
"parent_item_group": "All Item Groups"
|
"parent_item_group": "All Item Groups"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"doctype": "Item Group",
|
"doctype": "Item Group",
|
||||||
"is_group": 1,
|
"is_group": 1,
|
||||||
"item_group_name": "_Test Item Group B",
|
"item_group_name": "_Test Item Group B",
|
||||||
"parent_item_group": "All Item Groups"
|
"parent_item_group": "All Item Groups"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"doctype": "Item Group",
|
"doctype": "Item Group",
|
||||||
"is_group": 1,
|
"is_group": 1,
|
||||||
"item_group_name": "_Test Item Group B - 1",
|
"item_group_name": "_Test Item Group B - 1",
|
||||||
"parent_item_group": "_Test Item Group B"
|
"parent_item_group": "_Test Item Group B"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"doctype": "Item Group",
|
"doctype": "Item Group",
|
||||||
"is_group": 1,
|
"is_group": 1,
|
||||||
"item_group_name": "_Test Item Group B - 2",
|
"item_group_name": "_Test Item Group B - 2",
|
||||||
"parent_item_group": "_Test Item Group B"
|
"parent_item_group": "_Test Item Group B"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"doctype": "Item Group",
|
"doctype": "Item Group",
|
||||||
"is_group": 0,
|
"is_group": 0,
|
||||||
"item_group_name": "_Test Item Group B - 3",
|
"item_group_name": "_Test Item Group B - 3",
|
||||||
"parent_item_group": "_Test Item Group B"
|
"parent_item_group": "_Test Item Group B"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"doctype": "Item Group",
|
"doctype": "Item Group",
|
||||||
"is_group": 1,
|
"is_group": 1,
|
||||||
"item_group_name": "_Test Item Group C",
|
"item_group_name": "_Test Item Group C",
|
||||||
"parent_item_group": "All Item Groups"
|
"parent_item_group": "All Item Groups"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"doctype": "Item Group",
|
"doctype": "Item Group",
|
||||||
"is_group": 1,
|
"is_group": 1,
|
||||||
"item_group_name": "_Test Item Group C - 1",
|
"item_group_name": "_Test Item Group C - 1",
|
||||||
"parent_item_group": "_Test Item Group C"
|
"parent_item_group": "_Test Item Group C"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"doctype": "Item Group",
|
"doctype": "Item Group",
|
||||||
"is_group": 1,
|
"is_group": 1,
|
||||||
"item_group_name": "_Test Item Group C - 2",
|
"item_group_name": "_Test Item Group C - 2",
|
||||||
"parent_item_group": "_Test Item Group C"
|
"parent_item_group": "_Test Item Group C"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"doctype": "Item Group",
|
"doctype": "Item Group",
|
||||||
"is_group": 1,
|
"is_group": 1,
|
||||||
"item_group_name": "_Test Item Group D",
|
"item_group_name": "_Test Item Group D",
|
||||||
"parent_item_group": "All Item Groups"
|
"parent_item_group": "All Item Groups"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -104,4 +105,4 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _dict
|
|
||||||
|
|
||||||
|
|
||||||
class ProductFiltersBuilder:
|
class ProductFiltersBuilder:
|
||||||
@@ -57,37 +56,31 @@ class ProductFiltersBuilder:
|
|||||||
|
|
||||||
return filter_data
|
return filter_data
|
||||||
|
|
||||||
def get_attribute_fitlers(self):
|
def get_attribute_filters(self):
|
||||||
attributes = [row.attribute for row in self.doc.filter_attributes]
|
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:
|
result = frappe.db.sql(
|
||||||
selected_attributes = []
|
"""
|
||||||
for attr in attr_doc.item_attribute_values:
|
select
|
||||||
or_filters = []
|
distinct attribute, attribute_value
|
||||||
filters= [
|
from
|
||||||
["Item Variant Attribute", "attribute", "=", attr.parent],
|
`tabItem Variant Attribute`
|
||||||
["Item Variant Attribute", "attribute_value", "=", attr.attribute_value]
|
where
|
||||||
]
|
attribute in %(attributes)s
|
||||||
if self.item_group:
|
and attribute_value is not null
|
||||||
or_filters.extend([
|
""",
|
||||||
["item_group", "=", self.item_group],
|
{"attributes": attributes},
|
||||||
["Website Item Group", "item_group", "=", self.item_group]
|
as_dict=1,
|
||||||
])
|
)
|
||||||
|
|
||||||
if frappe.db.get_all("Item", filters, or_filters=or_filters, limit=1):
|
attribute_value_map = {}
|
||||||
selected_attributes.append(attr)
|
for d in result:
|
||||||
|
attribute_value_map.setdefault(d.attribute, []).append(d.attribute_value)
|
||||||
|
|
||||||
if selected_attributes:
|
out = []
|
||||||
valid_attributes.append(
|
for name, values in attribute_value_map.items():
|
||||||
_dict(
|
out.append(frappe._dict(name=name, item_attribute_values=values))
|
||||||
item_attribute_values=selected_attributes,
|
return out
|
||||||
name=attr_doc.name
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return valid_attributes
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import copy
|
import copy
|
||||||
import itertools
|
import itertools
|
||||||
import json
|
import json
|
||||||
|
from typing import List
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
@@ -36,6 +37,7 @@ from erpnext.setup.doctype.item_group.item_group import (
|
|||||||
get_parent_item_groups,
|
get_parent_item_groups,
|
||||||
invalidate_cache_for,
|
invalidate_cache_for,
|
||||||
)
|
)
|
||||||
|
from erpnext.stock.doctype.item_default.item_default import ItemDefault
|
||||||
|
|
||||||
|
|
||||||
class DuplicateReorderRows(frappe.ValidationError):
|
class DuplicateReorderRows(frappe.ValidationError):
|
||||||
@@ -134,9 +136,9 @@ class Item(WebsiteGenerator):
|
|||||||
self.validate_fixed_asset()
|
self.validate_fixed_asset()
|
||||||
self.validate_retain_sample()
|
self.validate_retain_sample()
|
||||||
self.validate_uom_conversion_factor()
|
self.validate_uom_conversion_factor()
|
||||||
self.validate_item_defaults()
|
|
||||||
self.validate_customer_provided_part()
|
self.validate_customer_provided_part()
|
||||||
self.update_defaults_from_item_group()
|
self.update_defaults_from_item_group()
|
||||||
|
self.validate_item_defaults()
|
||||||
self.validate_auto_reorder_enabled_in_stock_settings()
|
self.validate_auto_reorder_enabled_in_stock_settings()
|
||||||
self.cant_change()
|
self.cant_change()
|
||||||
self.update_show_in_website()
|
self.update_show_in_website()
|
||||||
@@ -782,35 +784,39 @@ class Item(WebsiteGenerator):
|
|||||||
if len(companies) != len(self.item_defaults):
|
if len(companies) != len(self.item_defaults):
|
||||||
frappe.throw(_("Cannot set multiple Item Defaults for a company."))
|
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):
|
def update_defaults_from_item_group(self):
|
||||||
"""Get defaults from Item Group"""
|
"""Get defaults from Item Group"""
|
||||||
if self.item_group and not self.item_defaults:
|
if self.item_defaults or not self.item_group:
|
||||||
item_defaults = frappe.db.get_values("Item Default", {"parent": self.item_group},
|
return
|
||||||
['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 {}
|
|
||||||
|
|
||||||
# To check default warehouse is belong to the default company
|
item_defaults = frappe.db.get_values("Item Default", {"parent": self.item_group},
|
||||||
if defaults.get("default_warehouse") and defaults.company and frappe.db.exists("Warehouse",
|
['company', 'default_warehouse','default_price_list','buying_cost_center','default_supplier',
|
||||||
{'name': defaults.default_warehouse, 'company': defaults.company}):
|
'expense_account','selling_cost_center','income_account'], as_dict = 1)
|
||||||
self.append("item_defaults", {
|
if item_defaults:
|
||||||
"company": defaults.get("company"),
|
for item in item_defaults:
|
||||||
"default_warehouse": defaults.default_warehouse
|
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):
|
def update_variants(self):
|
||||||
if self.flags.dont_update_variants or \
|
if self.flags.dont_update_variants or \
|
||||||
@@ -1328,3 +1334,25 @@ def on_doctype_update():
|
|||||||
@erpnext.allow_regional
|
@erpnext.allow_regional
|
||||||
def set_item_tax_from_hsn_code(item):
|
def set_item_tax_from_hsn_code(item):
|
||||||
pass
|
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"))
|
||||||
|
|||||||
@@ -232,6 +232,23 @@ class TestItem(unittest.TestCase):
|
|||||||
for key, value in purchase_item_check.items():
|
for key, value in purchase_item_check.items():
|
||||||
self.assertEqual(value, purchase_item_details.get(key))
|
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):
|
def test_item_attribute_change_after_variant(self):
|
||||||
frappe.delete_doc_if_exists("Item", "_Test Variant Item-L", force=1)
|
frappe.delete_doc_if_exists("Item", "_Test Variant Item-L", force=1)
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
@@ -684,7 +685,7 @@ class StockEntry(StockController):
|
|||||||
|
|
||||||
def validate_bom(self):
|
def validate_bom(self):
|
||||||
for d in self.get('items'):
|
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
|
item_code = d.original_item or d.item_code
|
||||||
validate_bom_no(item_code, d.bom_no)
|
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 = { item_code: {qty, description, stock_uom} }
|
||||||
item_dict = get_bom_items_as_dict(self.bom_no, self.company, qty=qty,
|
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):
|
for item in itervalues(item_dict):
|
||||||
item.from_warehouse = ""
|
item.from_warehouse = ""
|
||||||
item.is_scrap_item = 1
|
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
|
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):
|
def get_unconsumed_raw_materials(self):
|
||||||
wo = frappe.get_doc("Work Order", self.work_order)
|
wo = frappe.get_doc("Work Order", self.work_order)
|
||||||
wo_items = frappe.get_all('Work Order Item',
|
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
|
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]
|
`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)
|
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:
|
for item in transferred_materials:
|
||||||
qty= item.qty
|
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_scrap_item = item_dict[d].get("is_scrap_item", 0)
|
||||||
se_child.is_process_loss = item_dict[d].get("is_process_loss", 0)
|
se_child.is_process_loss = item_dict[d].get("is_process_loss", 0)
|
||||||
|
|
||||||
for field in ["idx", "po_detail", "original_item",
|
for field in ["idx", "po_detail", "original_item", "expense_account",
|
||||||
"expense_account", "description", "item_name", "serial_no", "batch_no"]:
|
"description", "item_name", "serial_no", "batch_no", "allow_zero_valuation_rate"]:
|
||||||
if item_dict[d].get(field):
|
if item_dict[d].get(field):
|
||||||
se_child.set(field, item_dict[d].get(field))
|
se_child.set(field, item_dict[d].get(field))
|
||||||
|
|
||||||
|
|||||||
@@ -619,6 +619,11 @@ def get_stock_balance_for(item_code, warehouse,
|
|||||||
item_dict = frappe.db.get_value("Item", item_code,
|
item_dict = frappe.db.get_value("Item", item_code,
|
||||||
["has_serial_no", "has_batch_no"], as_dict=1)
|
["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 = ""
|
serial_nos = ""
|
||||||
with_serial_no = True if item_dict.get("has_serial_no") else False
|
with_serial_no = True if item_dict.get("has_serial_no") else False
|
||||||
data = get_stock_balance(item_code, warehouse, posting_date, posting_time,
|
data = get_stock_balance(item_code, warehouse, posting_date, posting_time,
|
||||||
|
|||||||
63
erpnext/stock/report/test_reports.py
Normal file
63
erpnext/stock/report/test_reports.py
Normal 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,
|
||||||
|
)
|
||||||
@@ -407,7 +407,8 @@ class update_entries_after(object):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Get dynamic incoming/outgoing rate
|
# 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:
|
if sle.serial_no:
|
||||||
self.get_serialized_values(sle)
|
self.get_serialized_values(sle)
|
||||||
@@ -447,7 +448,8 @@ class update_entries_after(object):
|
|||||||
sle.doctype="Stock Ledger Entry"
|
sle.doctype="Stock Ledger Entry"
|
||||||
frappe.get_doc(sle).db_update()
|
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):
|
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:
|
if self.wh_data.stock_queue[-1][1]==incoming_rate:
|
||||||
self.wh_data.stock_queue[-1][0] += actual_qty
|
self.wh_data.stock_queue[-1][0] += actual_qty
|
||||||
else:
|
else:
|
||||||
|
# Item has a positive balance qty, add new entry
|
||||||
if self.wh_data.stock_queue[-1][0] > 0:
|
if self.wh_data.stock_queue[-1][0] > 0:
|
||||||
self.wh_data.stock_queue.append([actual_qty, incoming_rate])
|
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
|
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:
|
else:
|
||||||
qty_to_pop = abs(actual_qty)
|
qty_to_pop = abs(actual_qty)
|
||||||
while qty_to_pop:
|
while qty_to_pop:
|
||||||
|
|||||||
138
erpnext/tests/test_webform.py
Normal file
138
erpnext/tests/test_webform.py
Normal 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)
|
||||||
@@ -3,8 +3,13 @@
|
|||||||
|
|
||||||
import copy
|
import copy
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
from typing import Any, Dict, NewType, Optional
|
||||||
|
|
||||||
import frappe
|
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():
|
def create_test_contact_and_address():
|
||||||
@@ -78,3 +83,39 @@ def change_settings(doctype, settings_dict):
|
|||||||
for key, value in previous_settings.items():
|
for key, value in previous_settings.items():
|
||||||
setattr(settings, key, value)
|
setattr(settings, key, value)
|
||||||
settings.save()
|
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
|
||||||
|
|||||||
@@ -98,14 +98,14 @@
|
|||||||
<div class="filter-options">
|
<div class="filter-options">
|
||||||
{% for attr_value in attribute.item_attribute_values %}
|
{% for attr_value in attribute.item_attribute_values %}
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label data-value="{{ value }}">
|
<label>
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
class="product-filter attribute-filter"
|
class="product-filter attribute-filter"
|
||||||
id="{{attr_value.name}}"
|
id="{{attr_value}}"
|
||||||
data-attribute-name="{{ attribute.name }}"
|
data-attribute-name="{{ attribute.name }}"
|
||||||
data-attribute-value="{{ attr_value.attribute_value }}"
|
data-attribute-value="{{ attr_value }}"
|
||||||
{% if attr_value.checked %} checked {% endif %}>
|
{% if attr_value.checked %} checked {% endif %}>
|
||||||
<span class="label-area">{{ attr_value.attribute_value }}</span>
|
<span class="label-area">{{ attr_value }}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ def get_context(context):
|
|||||||
filter_engine = ProductFiltersBuilder()
|
filter_engine = ProductFiltersBuilder()
|
||||||
|
|
||||||
context.field_filters = filter_engine.get_field_filters()
|
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.product_settings = product_settings
|
||||||
context.body_class = "product-page"
|
context.body_class = "product-page"
|
||||||
|
|||||||
Reference in New Issue
Block a user