Merge pull request #32101 from frappe/version-14-hotfix

chore: version-14 weekly release
This commit is contained in:
Deepesh Garg
2022-09-06 21:31:39 +05:30
committed by GitHub
33 changed files with 676 additions and 277 deletions

View File

@@ -2,7 +2,7 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on("Journal Entry Template", { frappe.ui.form.on("Journal Entry Template", {
setup: function(frm) { refresh: function(frm) {
frappe.model.set_default_values(frm.doc); frappe.model.set_default_values(frm.doc);
frm.set_query("account" ,"accounts", function(){ frm.set_query("account" ,"accounts", function(){

View File

@@ -43,6 +43,7 @@
"currency", "currency",
"write_off_account", "write_off_account",
"write_off_cost_center", "write_off_cost_center",
"write_off_limit",
"account_for_change_amount", "account_for_change_amount",
"disable_rounded_total", "disable_rounded_total",
"column_break_23", "column_break_23",
@@ -360,6 +361,14 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Validate Stock on Save" "label": "Validate Stock on Save"
}, },
{
"default": "1",
"description": "Auto write off precision loss while consolidation",
"fieldname": "write_off_limit",
"fieldtype": "Currency",
"label": "Write Off Limit",
"reqd": 1
},
{ {
"default": "0", "default": "0",
"description": "If enabled, the consolidated invoices will have rounded total disabled", "description": "If enabled, the consolidated invoices will have rounded total disabled",
@@ -393,7 +402,7 @@
"link_fieldname": "pos_profile" "link_fieldname": "pos_profile"
} }
], ],
"modified": "2022-07-21 11:16:46.911173", "modified": "2022-08-10 12:57:06.241439",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Profile", "name": "POS Profile",

View File

@@ -34,4 +34,4 @@ class ProcessDeferredAccounting(Document):
filters={"against_voucher_type": self.doctype, "against_voucher": self.name}, filters={"against_voucher_type": self.doctype, "against_voucher": self.name},
) )
make_gl_entries(gl_entries=gl_entries, cancel=1) make_gl_entries(gl_map=gl_entries, cancel=1)

View File

@@ -57,3 +57,16 @@ class TestProcessDeferredAccounting(unittest.TestCase):
] ]
check_gl_entries(self, si.name, expected_gle, "2019-01-10") check_gl_entries(self, si.name, expected_gle, "2019-01-10")
def test_pda_submission_and_cancellation(self):
pda = frappe.get_doc(
dict(
doctype="Process Deferred Accounting",
posting_date="2019-01-01",
start_date="2019-01-01",
end_date="2019-01-31",
type="Income",
)
)
pda.submit()
pda.cancel()

View File

@@ -179,6 +179,11 @@ class ReceivablePayableReport(object):
key = (ple.against_voucher_type, ple.against_voucher_no, ple.party) key = (ple.against_voucher_type, ple.against_voucher_no, ple.party)
row = self.voucher_balance.get(key) row = self.voucher_balance.get(key)
if not row:
# no invoice, this is an invoice / stand-alone payment / credit note
row = self.voucher_balance.get((ple.voucher_type, ple.voucher_no, ple.party))
return row return row
def update_voucher_balance(self, ple): def update_voucher_balance(self, ple):

View File

@@ -1,19 +1,27 @@
import unittest import unittest
import frappe import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, getdate, today from frappe.utils import add_days, getdate, today
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
class TestAccountsReceivable(unittest.TestCase): class TestAccountsReceivable(FrappeTestCase):
def test_accounts_receivable(self): def setUp(self):
frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 2'") frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 2'")
frappe.db.sql("delete from `tabSales Order` where company='_Test Company 2'")
frappe.db.sql("delete from `tabPayment Entry` where company='_Test Company 2'")
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'") frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'")
frappe.db.sql("delete from `tabPayment Ledger Entry` where company='_Test Company 2'") frappe.db.sql("delete from `tabPayment Ledger Entry` where company='_Test Company 2'")
def tearDown(self):
frappe.db.rollback()
def test_accounts_receivable(self):
filters = { filters = {
"company": "_Test Company 2", "company": "_Test Company 2",
"based_on_payment_terms": 1, "based_on_payment_terms": 1,
@@ -66,6 +74,50 @@ class TestAccountsReceivable(unittest.TestCase):
], ],
) )
def test_payment_againt_po_in_receivable_report(self):
"""
Payments made against Purchase Order will show up as outstanding amount
"""
so = make_sales_order(
company="_Test Company 2",
customer="_Test Customer 2",
warehouse="Finished Goods - _TC2",
currency="EUR",
debit_to="Debtors - _TC2",
income_account="Sales - _TC2",
expense_account="Cost of Goods Sold - _TC2",
cost_center="Main - _TC2",
)
pe = get_payment_entry(so.doctype, so.name)
pe = pe.save().submit()
filters = {
"company": "_Test Company 2",
"based_on_payment_terms": 0,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
}
report = execute(filters)
expected_data_after_payment = [0, 1000, 0, -1000]
row = report[1][0]
self.assertEqual(
expected_data_after_payment,
[
row.invoiced,
row.paid,
row.credit_note,
row.outstanding,
],
)
def make_sales_invoice(): def make_sales_invoice():
frappe.set_user("Administrator") frappe.set_user("Administrator")

View File

@@ -535,7 +535,11 @@ def get_accounts(root_type, companies):
): ):
if account.account_name not in added_accounts: if account.account_name not in added_accounts:
accounts.append(account) accounts.append(account)
added_accounts.append(account.account_name) if account.account_number:
account_key = account.account_number + "-" + account.account_name
else:
account_key = account.account_name
added_accounts.append(account_key)
return accounts return accounts

View File

@@ -4,7 +4,7 @@
frappe.query_reports["Gross Profit"] = { frappe.query_reports["Gross Profit"] = {
"filters": [ "filters": [
{ {
"fieldname":"company", "fieldname": "company",
"label": __("Company"), "label": __("Company"),
"fieldtype": "Link", "fieldtype": "Link",
"options": "Company", "options": "Company",
@@ -12,32 +12,44 @@ frappe.query_reports["Gross Profit"] = {
"reqd": 1 "reqd": 1
}, },
{ {
"fieldname":"from_date", "fieldname": "from_date",
"label": __("From Date"), "label": __("From Date"),
"fieldtype": "Date", "fieldtype": "Date",
"default": frappe.defaults.get_user_default("year_start_date"), "default": frappe.defaults.get_user_default("year_start_date"),
"reqd": 1 "reqd": 1
}, },
{ {
"fieldname":"to_date", "fieldname": "to_date",
"label": __("To Date"), "label": __("To Date"),
"fieldtype": "Date", "fieldtype": "Date",
"default": frappe.defaults.get_user_default("year_end_date"), "default": frappe.defaults.get_user_default("year_end_date"),
"reqd": 1 "reqd": 1
}, },
{ {
"fieldname":"sales_invoice", "fieldname": "sales_invoice",
"label": __("Sales Invoice"), "label": __("Sales Invoice"),
"fieldtype": "Link", "fieldtype": "Link",
"options": "Sales Invoice" "options": "Sales Invoice"
}, },
{ {
"fieldname":"group_by", "fieldname": "group_by",
"label": __("Group By"), "label": __("Group By"),
"fieldtype": "Select", "fieldtype": "Select",
"options": "Invoice\nItem Code\nItem Group\nBrand\nWarehouse\nCustomer\nCustomer Group\nTerritory\nSales Person\nProject\nMonthly\nPayment Term", "options": "Invoice\nItem Code\nItem Group\nBrand\nWarehouse\nCustomer\nCustomer Group\nTerritory\nSales Person\nProject\nMonthly\nPayment Term",
"default": "Invoice" "default": "Invoice"
}, },
{
"fieldname": "item_group",
"label": __("Item Group"),
"fieldtype": "Link",
"options": "Item Group"
},
{
"fieldname": "sales_person",
"label": __("Sales Person"),
"fieldtype": "Link",
"options": "Sales Person"
},
], ],
"tree": true, "tree": true,
"name_field": "parent", "name_field": "parent",

View File

@@ -7,6 +7,7 @@ from frappe import _, scrub
from frappe.utils import cint, flt, formatdate from frappe.utils import cint, flt, formatdate
from erpnext.controllers.queries import get_match_cond from erpnext.controllers.queries import get_match_cond
from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition
from erpnext.stock.utils import get_incoming_rate from erpnext.stock.utils import get_incoming_rate
@@ -676,6 +677,17 @@ class GrossProfitGenerator(object):
if self.filters.to_date: if self.filters.to_date:
conditions += " and posting_date <= %(to_date)s" conditions += " and posting_date <= %(to_date)s"
if self.filters.item_group:
conditions += " and {0}".format(get_item_group_condition(self.filters.item_group))
if self.filters.sales_person:
conditions += """
and exists(select 1
from `tabSales Team` st
where st.parent = `tabSales Invoice`.name
and st.sales_person = %(sales_person)s)
"""
if self.filters.group_by == "Sales Person": if self.filters.group_by == "Sales Person":
sales_person_cols = ", sales.sales_person, sales.allocated_amount, sales.incentives" sales_person_cols = ", sales.sales_person, sales.allocated_amount, sales.incentives"
sales_team_table = "left join `tabSales Team` sales on sales.parent = `tabSales Invoice`.name" sales_team_table = "left join `tabSales Team` sales on sales.parent = `tabSales Invoice`.name"
@@ -723,6 +735,7 @@ class GrossProfitGenerator(object):
from from
`tabSales Invoice` inner join `tabSales Invoice Item` `tabSales Invoice` inner join `tabSales Invoice Item`
on `tabSales Invoice Item`.parent = `tabSales Invoice`.name on `tabSales Invoice Item`.parent = `tabSales Invoice`.name
join `tabItem` item on item.name = `tabSales Invoice Item`.item_code
{sales_team_table} {sales_team_table}
{payment_term_table} {payment_term_table}
where where

View File

@@ -76,7 +76,7 @@
"label": "Subcontracting Settings" "label": "Subcontracting Settings"
}, },
{ {
"default": "Material Transferred for Subcontract", "default": "BOM",
"fieldname": "backflush_raw_materials_of_subcontract_based_on", "fieldname": "backflush_raw_materials_of_subcontract_based_on",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Backflush Raw Materials of Subcontract Based On", "label": "Backflush Raw Materials of Subcontract Based On",
@@ -148,7 +148,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2022-05-31 19:40:26.103909", "modified": "2022-09-01 18:01:34.994657",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Buying Settings", "name": "Buying Settings",

View File

@@ -311,6 +311,7 @@ class SellingController(StockController):
"sales_invoice_item": d.get("sales_invoice_item"), "sales_invoice_item": d.get("sales_invoice_item"),
"dn_detail": d.get("dn_detail"), "dn_detail": d.get("dn_detail"),
"incoming_rate": p.get("incoming_rate"), "incoming_rate": p.get("incoming_rate"),
"item_row": p,
} }
) )
) )
@@ -334,6 +335,7 @@ class SellingController(StockController):
"sales_invoice_item": d.get("sales_invoice_item"), "sales_invoice_item": d.get("sales_invoice_item"),
"dn_detail": d.get("dn_detail"), "dn_detail": d.get("dn_detail"),
"incoming_rate": d.get("incoming_rate"), "incoming_rate": d.get("incoming_rate"),
"item_row": d,
} }
) )
) )

View File

@@ -390,6 +390,10 @@ class StockController(AccountsController):
return sl_dict return sl_dict
def update_inventory_dimensions(self, row, sl_dict) -> None: def update_inventory_dimensions(self, row, sl_dict) -> None:
# To handle delivery note and sales invoice
if row.get("item_row"):
row = row.get("item_row")
dimensions = get_evaluated_inventory_dimension(row, sl_dict, parent_doc=self) dimensions = get_evaluated_inventory_dimension(row, sl_dict, parent_doc=self)
for dimension in dimensions: for dimension in dimensions:
if not dimension: if not dimension:
@@ -407,9 +411,17 @@ class StockController(AccountsController):
"DocField", {"parent": self.doctype, "options": dimension.fetch_from_parent}, "fieldname" "DocField", {"parent": self.doctype, "options": dimension.fetch_from_parent}, "fieldname"
) )
if not fieldname:
fieldname = frappe.get_cached_value(
"Custom Field", {"dt": self.doctype, "options": dimension.fetch_from_parent}, "fieldname"
)
if fieldname and self.get(fieldname): if fieldname and self.get(fieldname):
sl_dict[dimension.target_fieldname] = self.get(fieldname) sl_dict[dimension.target_fieldname] = self.get(fieldname)
if sl_dict[dimension.target_fieldname] and self.docstatus == 1:
row.db_set(dimension.source_fieldname, sl_dict[dimension.target_fieldname])
def make_sl_entries(self, sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): def make_sl_entries(self, sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
from erpnext.stock.stock_ledger import make_sl_entries from erpnext.stock.stock_ledger import make_sl_entries

View File

@@ -770,6 +770,18 @@ class calculate_taxes_and_totals(object):
self.doc.precision("outstanding_amount"), self.doc.precision("outstanding_amount"),
) )
if (
self.doc.doctype == "Sales Invoice"
and self.doc.get("is_pos")
and self.doc.get("pos_profile")
and self.doc.get("is_consolidated")
):
write_off_limit = flt(
frappe.db.get_value("POS Profile", self.doc.pos_profile, "write_off_limit")
)
if write_off_limit and abs(self.doc.outstanding_amount) <= write_off_limit:
self.doc.write_off_outstanding_amount_automatically = 1
if ( if (
self.doc.doctype == "Sales Invoice" self.doc.doctype == "Sales Invoice"
and self.doc.get("is_pos") and self.doc.get("is_pos")

View File

@@ -7,7 +7,7 @@ from collections import Counter
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import get_url, getdate from frappe.utils import get_url, getdate, now
from frappe.utils.verified_command import get_signed_params from frappe.utils.verified_command import get_signed_params
@@ -104,16 +104,28 @@ class Appointment(Document):
# Return if already linked # Return if already linked
if self.party: if self.party:
return return
lead = frappe.get_doc( lead = frappe.get_doc(
{ {
"doctype": "Lead", "doctype": "Lead",
"lead_name": self.customer_name, "lead_name": self.customer_name,
"email_id": self.customer_email, "email_id": self.customer_email,
"notes": self.customer_details,
"phone": self.customer_phone_number, "phone": self.customer_phone_number,
} }
) )
if self.customer_details:
lead.append(
"notes",
{
"note": self.customer_details,
"added_by": frappe.session.user,
"added_on": now(),
},
)
lead.insert(ignore_permissions=True) lead.insert(ignore_permissions=True)
# Link lead # Link lead
self.party = lead.name self.party = lead.name

View File

@@ -6,29 +6,20 @@ import unittest
import frappe import frappe
LEAD_EMAIL = "test_appointment_lead@example.com"
def create_test_lead():
test_lead = frappe.db.get_value("Lead", {"email_id": "test@example.com"})
if test_lead:
return frappe.get_doc("Lead", test_lead)
test_lead = frappe.get_doc(
{"doctype": "Lead", "lead_name": "Test Lead", "email_id": "test@example.com"}
)
test_lead.insert(ignore_permissions=True)
return test_lead
def create_test_appointments(): def create_test_appointment():
test_appointment = frappe.get_doc( test_appointment = frappe.get_doc(
{ {
"doctype": "Appointment", "doctype": "Appointment",
"email": "test@example.com",
"status": "Open", "status": "Open",
"customer_name": "Test Lead", "customer_name": "Test Lead",
"customer_phone_number": "666", "customer_phone_number": "666",
"customer_skype": "test", "customer_skype": "test",
"customer_email": "test@example.com", "customer_email": LEAD_EMAIL,
"scheduled_time": datetime.datetime.now(), "scheduled_time": datetime.datetime.now(),
"customer_details": "Hello, Friend!",
} }
) )
test_appointment.insert() test_appointment.insert()
@@ -36,16 +27,16 @@ def create_test_appointments():
class TestAppointment(unittest.TestCase): class TestAppointment(unittest.TestCase):
test_appointment = test_lead = None def setUpClass():
frappe.db.delete("Lead", {"email_id": LEAD_EMAIL})
def setUp(self): def setUp(self):
self.test_lead = create_test_lead() self.test_appointment = create_test_appointment()
self.test_appointment = create_test_appointments() self.test_appointment.set_verified(self.test_appointment.customer_email)
def test_calendar_event_created(self): def test_calendar_event_created(self):
cal_event = frappe.get_doc("Event", self.test_appointment.calendar_event) cal_event = frappe.get_doc("Event", self.test_appointment.calendar_event)
self.assertEqual(cal_event.starts_on, self.test_appointment.scheduled_time) self.assertEqual(cal_event.starts_on, self.test_appointment.scheduled_time)
def test_lead_linked(self): def test_lead_linked(self):
lead = frappe.get_doc("Lead", self.test_lead.name) self.assertTrue(self.test_appointment.party)
self.assertIsNotNone(lead)

View File

@@ -233,7 +233,7 @@ def get_term_loans(date, term_loan=None, loan_type=None):
AND l.is_term_loan =1 AND l.is_term_loan =1
AND rs.payment_date <= %s AND rs.payment_date <= %s
AND rs.is_accrued=0 {0} AND rs.is_accrued=0 {0}
AND rs.interest_amount > 0 AND rs.principal_amount > 0
AND l.status = 'Disbursed' AND l.status = 'Disbursed'
ORDER BY rs.payment_date""".format( ORDER BY rs.payment_date""".format(
condition condition

View File

@@ -732,6 +732,7 @@ def get_amounts(amounts, against_loan, posting_date):
) )
amounts["pending_accrual_entries"] = pending_accrual_entries amounts["pending_accrual_entries"] = pending_accrual_entries
amounts["unaccrued_interest"] = flt(unaccrued_interest, precision) amounts["unaccrued_interest"] = flt(unaccrued_interest, precision)
amounts["written_off_amount"] = flt(against_loan_doc.written_off_amount, precision)
if final_due_date: if final_due_date:
amounts["due_date"] = final_due_date amounts["due_date"] = final_due_date

View File

@@ -57,7 +57,7 @@ def process_loan_interest_accrual_for_demand_loans(
def process_loan_interest_accrual_for_term_loans(posting_date=None, loan_type=None, loan=None): def process_loan_interest_accrual_for_term_loans(posting_date=None, loan_type=None, loan=None):
if not term_loan_accrual_pending(posting_date or nowdate()): if not term_loan_accrual_pending(posting_date or nowdate(), loan=loan):
return return
loan_process = frappe.new_doc("Process Loan Interest Accrual") loan_process = frappe.new_doc("Process Loan Interest Accrual")
@@ -71,9 +71,12 @@ def process_loan_interest_accrual_for_term_loans(posting_date=None, loan_type=No
return loan_process.name return loan_process.name
def term_loan_accrual_pending(date): def term_loan_accrual_pending(date, loan=None):
pending_accrual = frappe.db.get_value( filters = {"payment_date": ("<=", date), "is_accrued": 0}
"Repayment Schedule", {"payment_date": ("<=", date), "is_accrued": 0}
) if loan:
filters.update({"parent": loan})
pending_accrual = frappe.db.get_value("Repayment Schedule", filters)
return pending_accrual return pending_accrual

View File

@@ -16,18 +16,18 @@ def execute():
delete_auto_email_reports(report) delete_auto_email_reports(report)
check_and_delete_linked_reports(report) check_and_delete_linked_reports(report)
frappe.delete_doc("Report", report) frappe.delete_doc("Report", report, force=True)
def delete_auto_email_reports(report): def delete_auto_email_reports(report):
"""Check for one or multiple Auto Email Reports and delete""" """Check for one or multiple Auto Email Reports and delete"""
auto_email_reports = frappe.db.get_values("Auto Email Report", {"report": report}, ["name"]) auto_email_reports = frappe.db.get_values("Auto Email Report", {"report": report}, ["name"])
for auto_email_report in auto_email_reports: for auto_email_report in auto_email_reports:
frappe.delete_doc("Auto Email Report", auto_email_report[0]) frappe.delete_doc("Auto Email Report", auto_email_report[0], force=True)
def delete_links_from_desktop_icons(report): def delete_links_from_desktop_icons(report):
"""Check for one or multiple Desktop Icons and delete""" """Check for one or multiple Desktop Icons and delete"""
desktop_icons = frappe.db.get_values("Desktop Icon", {"_report": report}, ["name"]) desktop_icons = frappe.db.get_values("Desktop Icon", {"_report": report}, ["name"])
for desktop_icon in desktop_icons: for desktop_icon in desktop_icons:
frappe.delete_doc("Desktop Icon", desktop_icon[0]) frappe.delete_doc("Desktop Icon", desktop_icon[0], force=True)

View File

@@ -1,3 +1,4 @@
import click
import frappe import frappe
from frappe.utils import flt from frappe.utils import flt
@@ -16,6 +17,19 @@ def execute():
for opportunity in opportunities: for opportunity in opportunities:
company_currency = erpnext.get_company_currency(opportunity.company) company_currency = erpnext.get_company_currency(opportunity.company)
if opportunity.currency is None or opportunity.currency == "":
opportunity.currency = company_currency
frappe.db.set_value(
"Opportunity",
opportunity.name,
{"currency": opportunity.currency},
update_modified=False,
)
click.secho(
f' Opportunity `{opportunity.name}` has no currency set. Setting it to company currency as default: `{opportunity.currency}`"\n',
fg="yellow",
)
# base total and total will be 0 only since item table did not have amount field earlier # base total and total will be 0 only since item table did not have amount field earlier
if opportunity.currency != company_currency: if opportunity.currency != company_currency:
conversion_rate = get_exchange_rate(opportunity.currency, company_currency) conversion_rate = get_exchange_rate(opportunity.currency, company_currency)

View File

@@ -177,16 +177,16 @@ def get_tax_data_for_each_vat_setting(vat_setting, filters, doctype):
"parent": invoice.name, "parent": invoice.name,
"item_tax_template": vat_setting.item_tax_template, "item_tax_template": vat_setting.item_tax_template,
}, },
fields=["item_code", "net_amount"], fields=["item_code", "base_net_amount"],
) )
for item in invoice_items: for item in invoice_items:
# Summing up total taxable amount # Summing up total taxable amount
if invoice.is_return == 0: if invoice.is_return == 0:
total_taxable_amount += item.net_amount total_taxable_amount += item.base_net_amount
if invoice.is_return == 1: if invoice.is_return == 1:
total_taxable_adjustment_amount += item.net_amount total_taxable_adjustment_amount += item.base_net_amount
# Summing up total tax # Summing up total tax
total_tax += get_tax_amount(item.item_code, vat_setting.account, doctype, invoice.name) total_tax += get_tax_amount(item.item_code, vat_setting.account, doctype, invoice.name)

View File

@@ -10,79 +10,89 @@
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"basic_details_tab",
"basic_information", "basic_information",
"employee", "employee",
"naming_series", "naming_series",
"first_name", "first_name",
"middle_name", "middle_name",
"last_name", "last_name",
"salutation",
"employee_name", "employee_name",
"image", "column_break_9",
"column_break1",
"company",
"status",
"gender", "gender",
"date_of_birth", "date_of_birth",
"salutation",
"column_break1",
"date_of_joining", "date_of_joining",
"employee_number", "image",
"emergency_contact_details", "status",
"person_to_be_contacted",
"relation",
"column_break_19",
"emergency_phone_number",
"erpnext_user", "erpnext_user",
"user_id", "user_id",
"create_user", "create_user",
"create_user_permission", "create_user_permission",
"employment_details", "company_details_section",
"scheduled_confirmation_date", "company",
"final_confirmation_date",
"col_break_22",
"contract_end_date",
"notice_number_of_days",
"date_of_retirement",
"job_profile",
"department", "department",
"employee_number",
"column_break_25",
"designation", "designation",
"reports_to", "reports_to",
"column_break_31", "column_break_18",
"branch", "branch",
"employment_details",
"scheduled_confirmation_date",
"column_break_32",
"final_confirmation_date",
"contract_end_date",
"col_break_22",
"notice_number_of_days",
"date_of_retirement",
"contact_details",
"cell_number",
"column_break_40",
"personal_email",
"company_email",
"column_break4",
"prefered_contact_email",
"prefered_email",
"unsubscribed",
"address_section",
"current_address",
"current_accommodation_type",
"column_break_46",
"permanent_address",
"permanent_accommodation_type",
"emergency_contact_details",
"person_to_be_contacted",
"column_break_55",
"emergency_phone_number",
"column_break_19",
"relation",
"attendance_and_leave_details", "attendance_and_leave_details",
"attendance_device_id", "attendance_device_id",
"column_break_44", "column_break_44",
"holiday_list", "holiday_list",
"salary_information", "salary_information",
"salary_currency",
"ctc", "ctc",
"payroll_cost_center", "salary_currency",
"column_break_52", "salary_mode",
"bank_details_section",
"bank_name", "bank_name",
"bank_ac_no", "bank_ac_no",
"contact_details",
"cell_number",
"prefered_email",
"personal_email",
"unsubscribed",
"permanent_accommodation_type",
"permanent_address",
"column_break4",
"prefered_contact_email",
"company_email",
"current_accommodation_type",
"current_address",
"sb53",
"bio",
"personal_details", "personal_details",
"passport_number",
"date_of_issue",
"valid_upto",
"place_of_issue",
"marital_status", "marital_status",
"blood_group",
"column_break6",
"family_background", "family_background",
"column_break6",
"blood_group",
"health_details", "health_details",
"passport_details_section",
"passport_number",
"valid_upto",
"column_break_73",
"date_of_issue",
"place_of_issue",
"profile_tab",
"bio",
"educational_qualification", "educational_qualification",
"education", "education",
"previous_work_experience", "previous_work_experience",
@@ -92,16 +102,20 @@
"exit", "exit",
"resignation_letter_date", "resignation_letter_date",
"relieving_date", "relieving_date",
"reason_for_leaving",
"leave_encashed",
"encashment_date",
"exit_interview_details", "exit_interview_details",
"held_on", "held_on",
"new_workplace", "new_workplace",
"column_break_99",
"leave_encashed",
"encashment_date",
"feedback_section",
"reason_for_leaving",
"column_break_104",
"feedback", "feedback",
"lft", "lft",
"rgt", "rgt",
"old_parent" "old_parent",
"connections_tab"
], ],
"fields": [ "fields": [
{ {
@@ -261,7 +275,7 @@
"collapsible": 1, "collapsible": 1,
"fieldname": "erpnext_user", "fieldname": "erpnext_user",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "ERPNext User" "label": "User Details"
}, },
{ {
"description": "System User (login) ID. If set, it will become default for all HR forms.", "description": "System User (login) ID. If set, it will become default for all HR forms.",
@@ -289,8 +303,8 @@
"allow_in_quick_entry": 1, "allow_in_quick_entry": 1,
"collapsible": 1, "collapsible": 1,
"fieldname": "employment_details", "fieldname": "employment_details",
"fieldtype": "Section Break", "fieldtype": "Tab Break",
"label": "Joining Details" "label": "Joining"
}, },
{ {
"fieldname": "scheduled_confirmation_date", "fieldname": "scheduled_confirmation_date",
@@ -331,12 +345,6 @@
"oldfieldname": "date_of_retirement", "oldfieldname": "date_of_retirement",
"oldfieldtype": "Date" "oldfieldtype": "Date"
}, },
{
"collapsible": 1,
"fieldname": "job_profile",
"fieldtype": "Section Break",
"label": "Department"
},
{ {
"fieldname": "department", "fieldname": "department",
"fieldtype": "Link", "fieldtype": "Link",
@@ -366,10 +374,6 @@
"oldfieldtype": "Link", "oldfieldtype": "Link",
"options": "Employee" "options": "Employee"
}, },
{
"fieldname": "column_break_31",
"fieldtype": "Column Break"
},
{ {
"fieldname": "branch", "fieldname": "branch",
"fieldtype": "Link", "fieldtype": "Link",
@@ -391,7 +395,7 @@
{ {
"collapsible": 1, "collapsible": 1,
"fieldname": "salary_information", "fieldname": "salary_information",
"fieldtype": "Section Break", "fieldtype": "Tab Break",
"label": "Salary Details", "label": "Salary Details",
"oldfieldtype": "Section Break", "oldfieldtype": "Section Break",
"width": "50%" "width": "50%"
@@ -423,8 +427,8 @@
{ {
"collapsible": 1, "collapsible": 1,
"fieldname": "contact_details", "fieldname": "contact_details",
"fieldtype": "Section Break", "fieldtype": "Tab Break",
"label": "Contact Details" "label": "Contact"
}, },
{ {
"fieldname": "cell_number", "fieldname": "cell_number",
@@ -493,12 +497,6 @@
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Current Address" "label": "Current Address"
}, },
{
"collapsible": 1,
"fieldname": "sb53",
"fieldtype": "Section Break",
"label": "Personal Bio"
},
{ {
"description": "Short biography for website and other publications.", "description": "Short biography for website and other publications.",
"fieldname": "bio", "fieldname": "bio",
@@ -508,7 +506,7 @@
{ {
"collapsible": 1, "collapsible": 1,
"fieldname": "personal_details", "fieldname": "personal_details",
"fieldtype": "Section Break", "fieldtype": "Tab Break",
"label": "Personal Details" "label": "Personal Details"
}, },
{ {
@@ -601,7 +599,7 @@
{ {
"collapsible": 1, "collapsible": 1,
"fieldname": "exit", "fieldname": "exit",
"fieldtype": "Section Break", "fieldtype": "Tab Break",
"label": "Exit", "label": "Exit",
"oldfieldtype": "Section Break" "oldfieldtype": "Section Break"
}, },
@@ -702,7 +700,7 @@
{ {
"collapsible": 1, "collapsible": 1,
"fieldname": "attendance_and_leave_details", "fieldname": "attendance_and_leave_details",
"fieldtype": "Section Break", "fieldtype": "Tab Break",
"label": "Attendance and Leave Details" "label": "Attendance and Leave Details"
}, },
{ {
@@ -713,10 +711,6 @@
"fieldname": "column_break_19", "fieldname": "column_break_19",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"fieldname": "column_break_52",
"fieldtype": "Column Break"
},
{ {
"fieldname": "salary_currency", "fieldname": "salary_currency",
"fieldtype": "Link", "fieldtype": "Link",
@@ -728,13 +722,95 @@
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Cost to Company (CTC)", "label": "Cost to Company (CTC)",
"options": "salary_currency" "options": "salary_currency"
},
{
"fieldname": "basic_details_tab",
"fieldtype": "Tab Break",
"label": "Basic Details"
},
{
"fieldname": "company_details_section",
"fieldtype": "Section Break",
"label": "Company Details"
},
{
"fieldname": "column_break_18",
"fieldtype": "Column Break"
},
{
"collapsible": 1,
"fieldname": "address_section",
"fieldtype": "Section Break",
"label": "Address"
},
{
"fieldname": "column_break_46",
"fieldtype": "Column Break"
},
{
"fieldname": "profile_tab",
"fieldtype": "Tab Break",
"label": "Profile"
},
{
"fieldname": "passport_details_section",
"fieldtype": "Section Break",
"label": "Passport Details"
},
{
"fieldname": "column_break_73",
"fieldtype": "Column Break"
},
{
"fieldname": "bank_details_section",
"fieldtype": "Section Break",
"label": "Bank Details"
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_25",
"fieldtype": "Column Break"
},
{
"fieldname": "connections_tab",
"fieldtype": "Tab Break",
"label": "Connections",
"show_dashboard": 1
},
{
"fieldname": "column_break_32",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_40",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_55",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_99",
"fieldtype": "Column Break"
},
{
"fieldname": "feedback_section",
"fieldtype": "Section Break",
"label": "Feedback"
},
{
"fieldname": "column_break_104",
"fieldtype": "Column Break"
} }
], ],
"icon": "fa fa-user", "icon": "fa fa-user",
"idx": 24, "idx": 24,
"image_field": "image", "image_field": "image",
"links": [], "links": [],
"modified": "2022-06-27 01:29:32.952091", "modified": "2022-08-23 13:47:46.944993",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Setup", "module": "Setup",
"name": "Employee", "name": "Employee",

View File

@@ -30,6 +30,7 @@ frappe.ui.form.on('Inventory Dimension', {
onload(frm) { onload(frm) {
frm.trigger('render_traget_field'); frm.trigger('render_traget_field');
frm.trigger("set_parent_fields");
}, },
refresh(frm) { refresh(frm) {
@@ -52,6 +53,30 @@ frappe.ui.form.on('Inventory Dimension', {
} }
}, },
document_type(frm) {
frm.trigger("set_parent_fields");
},
set_parent_fields(frm) {
if (frm.doc.apply_to_all_doctypes) {
frm.set_df_property("fetch_from_parent", "options", frm.doc.reference_document);
} else if (frm.doc.document_type && frm.doc.istable) {
frappe.call({
method: 'erpnext.stock.doctype.inventory_dimension.inventory_dimension.get_parent_fields',
args: {
child_doctype: frm.doc.document_type,
dimension_name: frm.doc.reference_document
},
callback: (r) => {
if (r.message && r.message.length) {
frm.set_df_property("fetch_from_parent", "options",
[""].concat(r.message));
}
}
});
}
},
delete_dimension(frm) { delete_dimension(frm) {
let msg = (` let msg = (`
Custom fields related to this dimension will be deleted on deletion of dimension. Custom fields related to this dimension will be deleted on deletion of dimension.

View File

@@ -144,16 +144,15 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"depends_on": "istable",
"description": "Set fieldname or DocType name like Supplier, Customer etc.", "description": "Set fieldname or DocType name like Supplier, Customer etc.",
"fieldname": "fetch_from_parent", "fieldname": "fetch_from_parent",
"fieldtype": "Data", "fieldtype": "Select",
"label": "Fetch Value From Parent Form" "label": "Fetch Value From Parent Form"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2022-08-17 11:43:24.722441", "modified": "2022-09-02 13:29:04.098469",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Inventory Dimension", "name": "Inventory Dimension",

View File

@@ -236,3 +236,30 @@ def get_inventory_dimensions():
def delete_dimension(dimension): def delete_dimension(dimension):
doc = frappe.get_doc("Inventory Dimension", dimension) doc = frappe.get_doc("Inventory Dimension", dimension)
doc.delete() doc.delete()
@frappe.whitelist()
def get_parent_fields(child_doctype, dimension_name):
parent_doctypes = frappe.get_all(
"DocField", fields=["parent"], filters={"options": child_doctype}
)
fields = []
fields.extend(
frappe.get_all(
"DocField",
fields=["fieldname as value", "label"],
filters={"options": dimension_name, "parent": ("in", [d.parent for d in parent_doctypes])},
)
)
fields.extend(
frappe.get_all(
"Custom Field",
fields=["fieldname as value", "label"],
filters={"options": dimension_name, "dt": ("in", [d.parent for d in parent_doctypes])},
)
)
return fields

View File

@@ -2,14 +2,17 @@
# See license.txt # See license.txt
import frappe import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import ( from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
CanNotBeChildDoc, CanNotBeChildDoc,
CanNotBeDefaultDimension, CanNotBeDefaultDimension,
DoNotChangeError, DoNotChangeError,
delete_dimension, delete_dimension,
) )
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
@@ -136,6 +139,58 @@ class TestInventoryDimension(FrappeTestCase):
self.assertTrue(inv_dim1.has_stock_ledger()) self.assertTrue(inv_dim1.has_stock_ledger())
self.assertRaises(DoNotChangeError, inv_dim1.save) self.assertRaises(DoNotChangeError, inv_dim1.save)
def test_inventory_dimension_for_purchase_receipt_and_delivery_note(self):
create_inventory_dimension(
reference_document="Rack",
type_of_transaction="Both",
dimension_name="Rack",
apply_to_all_doctypes=1,
fetch_from_parent="Rack",
)
create_custom_field(
"Purchase Receipt", dict(fieldname="rack", label="Rack", fieldtype="Link", options="Rack")
)
create_custom_field(
"Delivery Note", dict(fieldname="rack", label="Rack", fieldtype="Link", options="Rack")
)
frappe.reload_doc("stock", "doctype", "purchase_receipt_item")
frappe.reload_doc("stock", "doctype", "delivery_note_item")
pr_doc = make_purchase_receipt(qty=2, do_not_submit=True)
pr_doc.rack = "Rack 1"
pr_doc.save()
pr_doc.submit()
pr_doc.load_from_db()
self.assertEqual(pr_doc.items[0].rack, "Rack 1")
sle_rack = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_detail_no": pr_doc.items[0].name, "voucher_type": pr_doc.doctype},
"rack",
)
self.assertEqual(sle_rack, "Rack 1")
dn_doc = create_delivery_note(qty=2, do_not_submit=True)
dn_doc.rack = "Rack 1"
dn_doc.save()
dn_doc.submit()
dn_doc.load_from_db()
self.assertEqual(dn_doc.items[0].rack, "Rack 1")
sle_rack = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_detail_no": dn_doc.items[0].name, "voucher_type": dn_doc.doctype},
"rack",
)
self.assertEqual(sle_rack, "Rack 1")
def prepare_test_data(): def prepare_test_data():
if not frappe.db.exists("DocType", "Shelf"): if not frappe.db.exists("DocType", "Shelf"):
@@ -160,6 +215,28 @@ def prepare_test_data():
create_warehouse("Shelf Warehouse") create_warehouse("Shelf Warehouse")
if not frappe.db.exists("DocType", "Rack"):
frappe.get_doc(
{
"doctype": "DocType",
"name": "Rack",
"module": "Stock",
"custom": 1,
"naming_rule": "By fieldname",
"autoname": "field:rack_name",
"fields": [{"label": "Rack Name", "fieldname": "rack_name", "fieldtype": "Data"}],
"permissions": [
{"role": "System Manager", "permlevel": 0, "read": 1, "write": 1, "create": 1, "delete": 1}
],
}
).insert(ignore_permissions=True)
for rack in ["Rack 1"]:
if not frappe.db.exists("Rack", rack):
frappe.get_doc({"doctype": "Rack", "rack_name": rack}).insert(ignore_permissions=True)
create_warehouse("Rack Warehouse")
def create_inventory_dimension(**args): def create_inventory_dimension(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@@ -562,7 +562,7 @@ $.extend(erpnext.item, {
let selected_attributes = {}; let selected_attributes = {};
me.multiple_variant_dialog.$wrapper.find('.form-column').each((i, col) => { me.multiple_variant_dialog.$wrapper.find('.form-column').each((i, col) => {
if(i===0) return; if(i===0) return;
let attribute_name = $(col).find('label').html().trim(); let attribute_name = $(col).find('.control-label').html().trim();
selected_attributes[attribute_name] = []; selected_attributes[attribute_name] = [];
let checked_opts = $(col).find('.checkbox input'); let checked_opts = $(col).find('.checkbox input');
checked_opts.each((i, opt) => { checked_opts.each((i, opt) => {

View File

@@ -48,41 +48,31 @@
"oldfieldtype": "Select", "oldfieldtype": "Select",
"options": "Item", "options": "Item",
"reqd": 1, "reqd": 1,
"search_index": 1, "search_index": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "uom", "fieldname": "uom",
"fieldtype": "Link", "fieldtype": "Link",
"label": "UOM", "label": "UOM",
"options": "UOM", "options": "UOM"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "0", "default": "0",
"description": "Quantity that must be bought or sold per UOM", "description": "Quantity that must be bought or sold per UOM",
"fieldname": "packing_unit", "fieldname": "packing_unit",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Packing Unit", "label": "Packing Unit"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "column_break_17", "fieldname": "column_break_17",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "item_name", "fieldname": "item_name",
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1, "in_list_view": 1,
"label": "Item Name", "label": "Item Name",
"read_only": 1, "read_only": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fetch_from": "item_code.brand", "fetch_from": "item_code.brand",
@@ -90,36 +80,29 @@
"fieldtype": "Read Only", "fieldtype": "Read Only",
"in_list_view": 1, "in_list_view": 1,
"label": "Brand", "label": "Brand",
"read_only": 1, "read_only": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "item_description", "fieldname": "item_description",
"fieldtype": "Text", "fieldtype": "Text",
"label": "Item Description", "label": "Item Description",
"read_only": 1, "read_only": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "price_list_details", "fieldname": "price_list_details",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Price List", "label": "Price List",
"options": "fa fa-tags", "options": "fa fa-tags"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "price_list", "fieldname": "price_list",
"fieldtype": "Link", "fieldtype": "Link",
"in_global_search": 1, "in_global_search": 1,
"in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Price List", "label": "Price List",
"options": "Price List", "options": "Price List",
"reqd": 1, "reqd": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"bold": 1, "bold": 1,
@@ -127,49 +110,37 @@
"fieldname": "customer", "fieldname": "customer",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Customer", "label": "Customer",
"options": "Customer", "options": "Customer"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"depends_on": "eval:doc.buying == 1", "depends_on": "eval:doc.buying == 1",
"fieldname": "supplier", "fieldname": "supplier",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Supplier", "label": "Supplier",
"options": "Supplier", "options": "Supplier"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "column_break_3", "fieldname": "column_break_3",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "0", "default": "0",
"fieldname": "buying", "fieldname": "buying",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Buying", "label": "Buying",
"read_only": 1, "read_only": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "0", "default": "0",
"fieldname": "selling", "fieldname": "selling",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Selling", "label": "Selling",
"read_only": 1, "read_only": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "item_details", "fieldname": "item_details",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"options": "fa fa-tag", "options": "fa fa-tag"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"bold": 1, "bold": 1,
@@ -177,15 +148,11 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Currency", "label": "Currency",
"options": "Currency", "options": "Currency",
"read_only": 1, "read_only": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "col_br_1", "fieldname": "col_br_1",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "price_list_rate", "fieldname": "price_list_rate",
@@ -197,80 +164,61 @@
"oldfieldname": "ref_rate", "oldfieldname": "ref_rate",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"options": "currency", "options": "currency",
"reqd": 1, "reqd": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "section_break_15", "fieldname": "section_break_15",
"fieldtype": "Section Break", "fieldtype": "Section Break"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "Today", "default": "Today",
"fieldname": "valid_from", "fieldname": "valid_from",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Valid From", "label": "Valid From"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "0", "default": "0",
"fieldname": "lead_time_days", "fieldname": "lead_time_days",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Lead Time in days", "label": "Lead Time in days"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "column_break_18", "fieldname": "column_break_18",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "valid_upto", "fieldname": "valid_upto",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Valid Upto", "label": "Valid Upto"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "section_break_24", "fieldname": "section_break_24",
"fieldtype": "Section Break", "fieldtype": "Section Break"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "note", "fieldname": "note",
"fieldtype": "Text", "fieldtype": "Text",
"label": "Note", "label": "Note"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "reference", "fieldname": "reference",
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1, "in_list_view": 1,
"label": "Reference", "in_standard_filter": 1,
"show_days": 1, "label": "Reference"
"show_seconds": 1
}, },
{ {
"fieldname": "batch_no", "fieldname": "batch_no",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Batch No", "label": "Batch No",
"options": "Batch", "options": "Batch"
"show_days": 1,
"show_seconds": 1
} }
], ],
"icon": "fa fa-flag", "icon": "fa fa-flag",
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2020-12-08 18:12:15.395772", "modified": "2022-09-02 16:33:55.612992",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Item Price", "name": "Item Price",
@@ -307,6 +255,7 @@
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC", "sort_order": "ASC",
"states": [],
"title_field": "item_name", "title_field": "item_name",
"track_changes": 1 "track_changes": 1
} }

View File

@@ -0,0 +1,3 @@
frappe.listview_settings['Item Price'] = {
hide_name_column: true,
};

View File

@@ -187,22 +187,13 @@ class TestSubcontractingOrder(FrappeTestCase):
self.assertEqual(len(ste.items), len(rm_items)) self.assertEqual(len(ste.items), len(rm_items))
def test_update_reserved_qty_for_subcontracting(self): def test_update_reserved_qty_for_subcontracting(self):
# Make stock available for raw materials # Create RM Material Receipt
make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100) make_stock_entry(target="_Test Warehouse - _TC", item_code="_Test Item", qty=10, basic_rate=100)
make_stock_entry( make_stock_entry(
target="_Test Warehouse - _TC", item_code="_Test Item Home Desktop 100", qty=20, basic_rate=100 target="_Test Warehouse - _TC", item_code="_Test Item Home Desktop 100", qty=20, basic_rate=100
) )
make_stock_entry(
target="_Test Warehouse 1 - _TC", item_code="_Test Item", qty=30, basic_rate=100
)
make_stock_entry(
target="_Test Warehouse 1 - _TC",
item_code="_Test Item Home Desktop 100",
qty=30,
basic_rate=100,
)
bin1 = frappe.db.get_value( bin_before_sco = frappe.db.get_value(
"Bin", "Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"], fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"],
@@ -222,102 +213,97 @@ class TestSubcontractingOrder(FrappeTestCase):
] ]
sco = get_subcontracting_order(service_items=service_items) sco = get_subcontracting_order(service_items=service_items)
bin2 = frappe.db.get_value( bin_after_sco = frappe.db.get_value(
"Bin", "Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"], fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"],
as_dict=1, as_dict=1,
) )
self.assertEqual(bin2.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10) # reserved_qty_for_sub_contract should be increased by 10
self.assertEqual(bin2.projected_qty, bin1.projected_qty - 10) self.assertEqual(
self.assertNotEqual(bin1.modified, bin2.modified) bin_after_sco.reserved_qty_for_sub_contract, bin_before_sco.reserved_qty_for_sub_contract + 10
)
# Create stock transfer # projected_qty should be decreased by 10
self.assertEqual(bin_after_sco.projected_qty, bin_before_sco.projected_qty - 10)
self.assertNotEqual(bin_before_sco.modified, bin_after_sco.modified)
# Create Stock Entry(Send to Subcontractor)
rm_items = [ rm_items = [
{ {
"item_code": "_Test FG Item", "item_code": "_Test FG Item",
"rm_item_code": "_Test Item", "rm_item_code": "_Test Item",
"item_name": "_Test Item", "item_name": "_Test Item",
"qty": 6, "qty": 10,
"warehouse": "_Test Warehouse - _TC", "warehouse": "_Test Warehouse - _TC",
"rate": 100, "rate": 100,
"amount": 600, "amount": 1000,
"stock_uom": "Nos", "stock_uom": "Nos",
} },
{
"item_code": "_Test FG Item",
"rm_item_code": "_Test Item Home Desktop 100",
"item_name": "_Test Item Home Desktop 100",
"qty": 20,
"warehouse": "_Test Warehouse - _TC",
"rate": 100,
"amount": 2000,
"stock_uom": "Nos",
},
] ]
ste = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items)) ste = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items))
ste.to_warehouse = "_Test Warehouse 1 - _TC" ste.to_warehouse = "_Test Warehouse 1 - _TC"
ste.save() ste.save()
ste.submit() ste.submit()
bin3 = frappe.db.get_value( bin_after_rm_transfer = frappe.db.get_value(
"Bin", "Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
fieldname="reserved_qty_for_sub_contract", fieldname="reserved_qty_for_sub_contract",
as_dict=1, as_dict=1,
) )
self.assertEqual(bin3.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6) # reserved_qty_for_sub_contract should be decreased by 10
self.assertEqual(
make_stock_entry( bin_after_rm_transfer.reserved_qty_for_sub_contract,
target="_Test Warehouse 1 - _TC", item_code="_Test Item", qty=40, basic_rate=100 bin_after_sco.reserved_qty_for_sub_contract - 10,
)
make_stock_entry(
target="_Test Warehouse 1 - _TC",
item_code="_Test Item Home Desktop 100",
qty=40,
basic_rate=100,
) )
# Make SCR against the SCO # Cancel Stock Entry(Send to Subcontractor)
scr = make_subcontracting_receipt(sco.name)
scr.save()
scr.submit()
bin4 = frappe.db.get_value(
"Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
fieldname="reserved_qty_for_sub_contract",
as_dict=1,
)
self.assertEqual(bin4.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
# Cancel SCR
scr.reload()
scr.cancel()
bin5 = frappe.db.get_value(
"Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
fieldname="reserved_qty_for_sub_contract",
as_dict=1,
)
self.assertEqual(bin5.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6)
# Cancel Stock Entry
ste.cancel() ste.cancel()
bin6 = frappe.db.get_value( bin_after_cancel_ste = frappe.db.get_value(
"Bin", "Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
fieldname="reserved_qty_for_sub_contract", fieldname="reserved_qty_for_sub_contract",
as_dict=1, as_dict=1,
) )
self.assertEqual(bin6.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10) # reserved_qty_for_sub_contract should be increased by 10
self.assertEqual(
bin_after_cancel_ste.reserved_qty_for_sub_contract,
bin_after_rm_transfer.reserved_qty_for_sub_contract + 10,
)
# Cancel PO # Cancel SCO
sco.reload() sco.reload()
sco.cancel() sco.cancel()
bin7 = frappe.db.get_value( bin_after_cancel_sco = frappe.db.get_value(
"Bin", "Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
fieldname="reserved_qty_for_sub_contract", fieldname="reserved_qty_for_sub_contract",
as_dict=1, as_dict=1,
) )
self.assertEqual(bin7.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract) # reserved_qty_for_sub_contract should be decreased by 10
self.assertEqual(
bin_after_cancel_sco.reserved_qty_for_sub_contract,
bin_after_cancel_ste.reserved_qty_for_sub_contract - 10,
)
self.assertEqual(
bin_after_cancel_sco.reserved_qty_for_sub_contract, bin_before_sco.reserved_qty_for_sub_contract
)
def test_exploded_items(self): def test_exploded_items(self):
item_code = "_Test Subcontracted FG Item 11" item_code = "_Test Subcontracted FG Item 11"

View File

@@ -75,6 +75,7 @@ class SubcontractingReceipt(SubcontractingController):
self.get_current_stock() self.get_current_stock()
def on_submit(self): def on_submit(self):
self.validate_available_qty_for_consumption()
self.update_status_updater_args() self.update_status_updater_args()
self.update_prevdoc_status() self.update_prevdoc_status()
self.set_subcontracting_order_status() self.set_subcontracting_order_status()
@@ -107,10 +108,42 @@ class SubcontractingReceipt(SubcontractingController):
self.set_missing_values_in_supplied_items() self.set_missing_values_in_supplied_items()
self.set_missing_values_in_items() self.set_missing_values_in_items()
def set_available_qty_for_consumption(self):
supplied_items_details = {}
sco_supplied_item = frappe.qb.DocType("Subcontracting Order Supplied Item")
for item in self.get("items"):
supplied_items = (
frappe.qb.from_(sco_supplied_item)
.select(
sco_supplied_item.rm_item_code,
sco_supplied_item.reference_name,
(sco_supplied_item.total_supplied_qty - sco_supplied_item.consumed_qty).as_("available_qty"),
)
.where(
(sco_supplied_item.parent == item.subcontracting_order)
& (sco_supplied_item.main_item_code == item.item_code)
& (sco_supplied_item.reference_name == item.subcontracting_order_item)
)
).run(as_dict=True)
if supplied_items:
supplied_items_details[item.name] = {}
for supplied_item in supplied_items:
supplied_items_details[item.name][supplied_item.rm_item_code] = supplied_item.available_qty
else:
for item in self.get("supplied_items"):
item.available_qty_for_consumption = supplied_items_details.get(item.reference_name, {}).get(
item.rm_item_code, 0
)
def set_missing_values_in_supplied_items(self): def set_missing_values_in_supplied_items(self):
for item in self.get("supplied_items") or []: for item in self.get("supplied_items") or []:
item.amount = item.rate * item.consumed_qty item.amount = item.rate * item.consumed_qty
self.set_available_qty_for_consumption()
def set_missing_values_in_items(self): def set_missing_values_in_items(self):
rm_supp_cost = {} rm_supp_cost = {}
for item in self.get("supplied_items") or []: for item in self.get("supplied_items") or []:
@@ -147,6 +180,17 @@ class SubcontractingReceipt(SubcontractingController):
_("Rejected Warehouse is mandatory against rejected Item {0}").format(item.item_code) _("Rejected Warehouse is mandatory against rejected Item {0}").format(item.item_code)
) )
def validate_available_qty_for_consumption(self):
for item in self.get("supplied_items"):
if (
item.available_qty_for_consumption and item.available_qty_for_consumption < item.consumed_qty
):
frappe.throw(
_(
"Row {0}: Consumed Qty must be less than or equal to Available Qty For Consumption in Consumed Items Table."
).format(item.idx)
)
def set_items_cost_center(self): def set_items_cost_center(self):
if self.company: if self.company:
cost_center = frappe.get_cached_value("Company", self.company, "cost_center") cost_center = frappe.get_cached_value("Company", self.company, "cost_center")

View File

@@ -70,6 +70,55 @@ class TestSubcontractingReceipt(FrappeTestCase):
rm_supp_cost = sum(item.amount for item in scr.get("supplied_items")) rm_supp_cost = sum(item.amount for item in scr.get("supplied_items"))
self.assertEqual(scr.get("items")[0].rm_supp_cost, flt(rm_supp_cost)) self.assertEqual(scr.get("items")[0].rm_supp_cost, flt(rm_supp_cost))
def test_available_qty_for_consumption(self):
make_stock_entry(
item_code="_Test Item", qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100
)
make_stock_entry(
item_code="_Test Item Home Desktop 100",
qty=100,
target="_Test Warehouse 1 - _TC",
basic_rate=100,
)
service_items = [
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Service Item 1",
"qty": 10,
"rate": 100,
"fg_item": "_Test FG Item",
"fg_item_qty": 10,
},
]
sco = get_subcontracting_order(service_items=service_items)
rm_items = [
{
"main_item_code": "_Test FG Item",
"item_code": "_Test Item",
"qty": 5.0,
"rate": 100.0,
"stock_uom": "_Test UOM",
"warehouse": "_Test Warehouse - _TC",
},
{
"main_item_code": "_Test FG Item",
"item_code": "_Test Item Home Desktop 100",
"qty": 10.0,
"rate": 100.0,
"stock_uom": "_Test UOM",
"warehouse": "_Test Warehouse - _TC",
},
]
itemwise_details = make_stock_in_entry(rm_items=rm_items)
make_stock_transfer_entry(
sco_no=sco.name,
rm_items=rm_items,
itemwise_details=copy.deepcopy(itemwise_details),
)
scr = make_subcontracting_receipt(sco.name)
scr.save()
self.assertRaises(frappe.ValidationError, scr.submit)
def test_subcontracting_gle_fg_item_rate_zero(self): def test_subcontracting_gle_fg_item_rate_zero(self):
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries

View File

@@ -19,6 +19,7 @@
"col_break2", "col_break2",
"amount", "amount",
"secbreak_2", "secbreak_2",
"available_qty_for_consumption",
"required_qty", "required_qty",
"col_break3", "col_break3",
"consumed_qty", "consumed_qty",
@@ -75,8 +76,7 @@
{ {
"fieldname": "required_qty", "fieldname": "required_qty",
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "label": "Required Qty",
"label": "Available Qty For Consumption",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
@@ -85,7 +85,7 @@
"fieldname": "consumed_qty", "fieldname": "consumed_qty",
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
"label": "Qty to be Consumed", "label": "Consumed Qty",
"reqd": 1 "reqd": 1
}, },
{ {
@@ -179,12 +179,21 @@
"options": "Subcontracting Order", "options": "Subcontracting Order",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"default": "0",
"fieldname": "available_qty_for_consumption",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Available Qty For Consumption",
"print_hide": 1,
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-04-18 10:45:16.538479", "modified": "2022-09-02 22:28:53.392381",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Subcontracting", "module": "Subcontracting",
"name": "Subcontracting Receipt Supplied Item", "name": "Subcontracting Receipt Supplied Item",
@@ -193,6 +202,6 @@
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1, "states": [],
"states": [] "track_changes": 1
} }