mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-13 11:55:11 +00:00
Merge pull request #30886 from frappe/version-13-pre-release
chore: weekly release for version-13
This commit is contained in:
7
.github/helper/install.sh
vendored
7
.github/helper/install.sh
vendored
@@ -2,6 +2,13 @@
|
||||
|
||||
set -e
|
||||
|
||||
# Check for merge conflicts before proceeding
|
||||
python -m compileall -f "${GITHUB_WORKSPACE}"
|
||||
if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}"
|
||||
then echo "Found merge conflicts"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd ~ || exit
|
||||
|
||||
sudo apt-get install redis-server libcups2-dev
|
||||
|
||||
@@ -6,6 +6,7 @@ from erpnext.hooks import regional_overrides
|
||||
|
||||
__version__ = "13.27.1"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
"""Get default company for user"""
|
||||
from frappe.defaults import get_user_default_as_list
|
||||
|
||||
@@ -114,10 +114,13 @@ class OpeningInvoiceCreationTool(Document):
|
||||
)
|
||||
or {}
|
||||
)
|
||||
|
||||
default_currency = frappe.db.get_value(row.party_type, row.party, "default_currency")
|
||||
|
||||
if company_details:
|
||||
invoice.update(
|
||||
{
|
||||
"currency": company_details.get("default_currency"),
|
||||
"currency": default_currency or company_details.get("default_currency"),
|
||||
"letter_head": company_details.get("default_letter_head"),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -38,6 +38,15 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
this.frm.set_query("cost_center", () => {
|
||||
return {
|
||||
"filters": {
|
||||
"company": this.frm.doc.company,
|
||||
"is_group": 0
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function() {
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"invoice_limit",
|
||||
"payment_limit",
|
||||
"bank_cash_account",
|
||||
"cost_center",
|
||||
"sec_break1",
|
||||
"invoices",
|
||||
"column_break_15",
|
||||
@@ -178,13 +179,19 @@
|
||||
{
|
||||
"fieldname": "column_break_11",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"icon": "icon-resize-horizontal",
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-10-04 20:27:11.114194",
|
||||
"modified": "2022-04-29 15:37:10.246831",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Reconciliation",
|
||||
@@ -209,5 +216,6 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -332,6 +332,9 @@ class PaymentReconciliation(Document):
|
||||
def get_conditions(self, get_invoices=False, get_payments=False, get_return_invoices=False):
|
||||
condition = " and company = '{0}' ".format(self.company)
|
||||
|
||||
if self.get("cost_center") and (get_invoices or get_payments or get_return_invoices):
|
||||
condition = " and cost_center = '{0}' ".format(self.cost_center)
|
||||
|
||||
if get_invoices:
|
||||
condition += (
|
||||
" and posting_date >= {0}".format(frappe.db.escape(self.from_invoice_date))
|
||||
|
||||
@@ -1,9 +1,96 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, getdate
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
|
||||
class TestPaymentReconciliation(unittest.TestCase):
|
||||
pass
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
make_customer()
|
||||
make_invoice_and_payment()
|
||||
|
||||
def test_payment_reconciliation(self):
|
||||
payment_reco = frappe.get_doc("Payment Reconciliation")
|
||||
payment_reco.company = "_Test Company"
|
||||
payment_reco.party_type = "Customer"
|
||||
payment_reco.party = "_Test Payment Reco Customer"
|
||||
payment_reco.receivable_payable_account = "Debtors - _TC"
|
||||
payment_reco.from_invoice_date = add_days(getdate(), -1)
|
||||
payment_reco.to_invoice_date = getdate()
|
||||
payment_reco.from_payment_date = add_days(getdate(), -1)
|
||||
payment_reco.to_payment_date = getdate()
|
||||
payment_reco.maximum_invoice_amount = 1000
|
||||
payment_reco.maximum_payment_amount = 1000
|
||||
payment_reco.invoice_limit = 10
|
||||
payment_reco.payment_limit = 10
|
||||
payment_reco.bank_cash_account = "_Test Bank - _TC"
|
||||
payment_reco.cost_center = "_Test Cost Center - _TC"
|
||||
payment_reco.get_unreconciled_entries()
|
||||
|
||||
self.assertEqual(len(payment_reco.get("invoices")), 1)
|
||||
self.assertEqual(len(payment_reco.get("payments")), 1)
|
||||
|
||||
payment_entry = payment_reco.get("payments")[0].reference_name
|
||||
invoice = payment_reco.get("invoices")[0].invoice_number
|
||||
|
||||
payment_reco.allocate_entries(
|
||||
{
|
||||
"payments": [payment_reco.get("payments")[0].as_dict()],
|
||||
"invoices": [payment_reco.get("invoices")[0].as_dict()],
|
||||
}
|
||||
)
|
||||
payment_reco.reconcile()
|
||||
|
||||
payment_entry_doc = frappe.get_doc("Payment Entry", payment_entry)
|
||||
self.assertEqual(payment_entry_doc.get("references")[0].reference_name, invoice)
|
||||
|
||||
|
||||
def make_customer():
|
||||
if not frappe.db.get_value("Customer", "_Test Payment Reco Customer"):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Customer",
|
||||
"customer_name": "_Test Payment Reco Customer",
|
||||
"customer_type": "Individual",
|
||||
"customer_group": "_Test Customer Group",
|
||||
"territory": "_Test Territory",
|
||||
}
|
||||
).insert()
|
||||
|
||||
|
||||
def make_invoice_and_payment():
|
||||
si = create_sales_invoice(
|
||||
customer="_Test Payment Reco Customer", qty=1, rate=690, do_not_save=True
|
||||
)
|
||||
si.cost_center = "_Test Cost Center - _TC"
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
pe = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Payment Entry",
|
||||
"payment_type": "Receive",
|
||||
"party_type": "Customer",
|
||||
"party": "_Test Payment Reco Customer",
|
||||
"company": "_Test Company",
|
||||
"paid_from_account_currency": "INR",
|
||||
"paid_to_account_currency": "INR",
|
||||
"source_exchange_rate": 1,
|
||||
"target_exchange_rate": 1,
|
||||
"reference_no": "1",
|
||||
"reference_date": getdate(),
|
||||
"received_amount": 690,
|
||||
"paid_amount": 690,
|
||||
"paid_from": "Debtors - _TC",
|
||||
"paid_to": "_Test Bank - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
}
|
||||
)
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
@@ -166,7 +166,7 @@ class PeriodClosingVoucher(AccountsController):
|
||||
sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as bal_in_account_currency,
|
||||
sum(t1.debit) - sum(t1.credit) as bal_in_company_currency
|
||||
from `tabGL Entry` t1, `tabAccount` t2
|
||||
where t1.account = t2.name and t2.report_type = 'Profit and Loss'
|
||||
where t1.is_cancelled = 0 and t1.account = t2.name and t2.report_type = 'Profit and Loss'
|
||||
and t2.docstatus < 2 and t2.company = %s
|
||||
and t1.posting_date between %s and %s
|
||||
group by t1.account, {dimension_fields}
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
"order_confirmation_no",
|
||||
"order_confirmation_date",
|
||||
"amended_from",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"drop_ship",
|
||||
"customer",
|
||||
"customer_name",
|
||||
@@ -1138,16 +1142,39 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Tax Withholding Category",
|
||||
"options": "Tax Withholding Category"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions "
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-28 13:10:47.955401",
|
||||
"modified": "2022-04-26 12:16:38.694276",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -1194,6 +1221,7 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"timeline_field": "supplier",
|
||||
"title_field": "supplier_name",
|
||||
"track_changes": 1
|
||||
|
||||
@@ -592,6 +592,9 @@ accounting_dimension_doctypes = [
|
||||
"Subscription Plan",
|
||||
"POS Invoice",
|
||||
"POS Invoice Item",
|
||||
"Purchase Order",
|
||||
"Purchase Receipt",
|
||||
"Sales Order",
|
||||
]
|
||||
|
||||
regional_overrides = {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, get_datetime
|
||||
from frappe.utils import cint, get_datetime, get_link_to_form
|
||||
|
||||
from erpnext.hr.doctype.shift_assignment.shift_assignment import (
|
||||
get_actual_start_end_datetime_of_shift,
|
||||
@@ -127,19 +127,17 @@ def mark_attendance_and_link_log(
|
||||
log_names = [x.name for x in logs]
|
||||
employee = logs[0].employee
|
||||
if attendance_status == "Skip":
|
||||
frappe.db.sql(
|
||||
"""update `tabEmployee Checkin`
|
||||
set skip_auto_attendance = %s
|
||||
where name in %s""",
|
||||
("1", log_names),
|
||||
)
|
||||
skip_attendance_in_checkins(log_names)
|
||||
return None
|
||||
|
||||
elif attendance_status in ("Present", "Absent", "Half Day"):
|
||||
employee_doc = frappe.get_doc("Employee", employee)
|
||||
if not frappe.db.exists(
|
||||
duplicate = frappe.db.exists(
|
||||
"Attendance",
|
||||
{"employee": employee, "attendance_date": attendance_date, "docstatus": ("!=", "2")},
|
||||
):
|
||||
)
|
||||
|
||||
if not duplicate:
|
||||
doc_dict = {
|
||||
"doctype": "Attendance",
|
||||
"employee": employee,
|
||||
@@ -155,6 +153,12 @@ def mark_attendance_and_link_log(
|
||||
}
|
||||
attendance = frappe.get_doc(doc_dict).insert()
|
||||
attendance.submit()
|
||||
|
||||
if attendance_status == "Absent":
|
||||
attendance.add_comment(
|
||||
text=_("Employee was marked Absent for not meeting the working hours threshold.")
|
||||
)
|
||||
|
||||
frappe.db.sql(
|
||||
"""update `tabEmployee Checkin`
|
||||
set attendance = %s
|
||||
@@ -163,12 +167,10 @@ def mark_attendance_and_link_log(
|
||||
)
|
||||
return attendance
|
||||
else:
|
||||
frappe.db.sql(
|
||||
"""update `tabEmployee Checkin`
|
||||
set skip_auto_attendance = %s
|
||||
where name in %s""",
|
||||
("1", log_names),
|
||||
)
|
||||
skip_attendance_in_checkins(log_names)
|
||||
if duplicate:
|
||||
add_comment_in_checkins(log_names, duplicate)
|
||||
|
||||
return None
|
||||
else:
|
||||
frappe.throw(_("{} is an invalid Attendance Status.").format(attendance_status))
|
||||
@@ -237,3 +239,29 @@ def time_diff_in_hours(start, end):
|
||||
|
||||
def find_index_in_dict(dict_list, key, value):
|
||||
return next((index for (index, d) in enumerate(dict_list) if d[key] == value), None)
|
||||
|
||||
|
||||
def add_comment_in_checkins(log_names, duplicate):
|
||||
text = _("Auto Attendance skipped due to duplicate attendance record: {}").format(
|
||||
get_link_to_form("Attendance", duplicate)
|
||||
)
|
||||
|
||||
for name in log_names:
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Comment",
|
||||
"comment_type": "Comment",
|
||||
"reference_doctype": "Employee Checkin",
|
||||
"reference_name": name,
|
||||
"content": text,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
|
||||
def skip_attendance_in_checkins(log_names):
|
||||
EmployeeCheckin = frappe.qb.DocType("Employee Checkin")
|
||||
(
|
||||
frappe.qb.update(EmployeeCheckin)
|
||||
.set("skip_auto_attendance", 1)
|
||||
.where(EmployeeCheckin.name.isin(log_names))
|
||||
).run()
|
||||
|
||||
@@ -84,7 +84,7 @@ class ShiftAssignment(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_events(start, end, filters=None):
|
||||
events = []
|
||||
from frappe.desk.calendar import get_event_conditions
|
||||
|
||||
employee = frappe.db.get_value(
|
||||
"Employee", {"user_id": frappe.session.user}, ["name", "company"], as_dict=True
|
||||
@@ -95,20 +95,22 @@ def get_events(start, end, filters=None):
|
||||
employee = ""
|
||||
company = frappe.db.get_value("Global Defaults", None, "default_company")
|
||||
|
||||
from frappe.desk.reportview import get_filters_cond
|
||||
|
||||
conditions = get_filters_cond("Shift Assignment", filters, [])
|
||||
add_assignments(events, start, end, conditions=conditions)
|
||||
conditions = get_event_conditions("Shift Assignment", filters)
|
||||
events = add_assignments(start, end, conditions=conditions)
|
||||
return events
|
||||
|
||||
|
||||
def add_assignments(events, start, end, conditions=None):
|
||||
def add_assignments(start, end, conditions=None):
|
||||
events = []
|
||||
|
||||
query = """select name, start_date, end_date, employee_name,
|
||||
employee, docstatus, shift_type
|
||||
from `tabShift Assignment` where
|
||||
start_date >= %(start_date)s
|
||||
or end_date <= %(end_date)s
|
||||
or (%(start_date)s between start_date and end_date and %(end_date)s between start_date and end_date)
|
||||
(
|
||||
start_date >= %(start_date)s
|
||||
or end_date <= %(end_date)s
|
||||
or (%(start_date)s between start_date and end_date and %(end_date)s between start_date and end_date)
|
||||
)
|
||||
and docstatus = 1"""
|
||||
if conditions:
|
||||
query += conditions
|
||||
|
||||
@@ -4,14 +4,22 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, nowdate
|
||||
|
||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||
from erpnext.hr.doctype.shift_assignment.shift_assignment import get_events
|
||||
|
||||
test_dependencies = ["Shift Type"]
|
||||
|
||||
|
||||
class TestShiftAssignment(unittest.TestCase):
|
||||
class TestShiftAssignment(FrappeTestCase):
|
||||
def setUp(self):
|
||||
frappe.db.sql("delete from `tabShift Assignment`")
|
||||
frappe.db.delete("Shift Assignment")
|
||||
if not frappe.db.exists("Shift Type", "Day Shift"):
|
||||
frappe.get_doc(
|
||||
{"doctype": "Shift Type", "name": "Day Shift", "start_time": "9:00:00", "end_time": "18:00:00"}
|
||||
).insert()
|
||||
|
||||
def test_make_shift_assignment(self):
|
||||
shift_assignment = frappe.get_doc(
|
||||
@@ -86,3 +94,36 @@ class TestShiftAssignment(unittest.TestCase):
|
||||
)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, shift_assignment_3.save)
|
||||
|
||||
def test_shift_assignment_calendar(self):
|
||||
employee1 = make_employee("test_shift_assignment1@example.com", company="_Test Company")
|
||||
employee2 = make_employee("test_shift_assignment2@example.com", company="_Test Company")
|
||||
date = nowdate()
|
||||
|
||||
shift_1 = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Shift Assignment",
|
||||
"shift_type": "Day Shift",
|
||||
"company": "_Test Company",
|
||||
"employee": employee1,
|
||||
"start_date": date,
|
||||
"status": "Active",
|
||||
}
|
||||
).submit()
|
||||
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Shift Assignment",
|
||||
"shift_type": "Day Shift",
|
||||
"company": "_Test Company",
|
||||
"employee": employee2,
|
||||
"start_date": date,
|
||||
"status": "Active",
|
||||
}
|
||||
).submit()
|
||||
|
||||
events = get_events(
|
||||
start=date, end=date, filters=[["Shift Assignment", "employee", "=", employee1, False]]
|
||||
)
|
||||
self.assertEqual(len(events), 1)
|
||||
self.assertEqual(events[0]["name"], shift_1.name)
|
||||
|
||||
@@ -139,7 +139,17 @@ class ShiftType(Document):
|
||||
for date in dates:
|
||||
shift_details = get_employee_shift(employee, date, True)
|
||||
if shift_details and shift_details.shift_type.name == self.name:
|
||||
mark_attendance(employee, date, "Absent", self.name)
|
||||
attendance = mark_attendance(employee, date, "Absent", self.name)
|
||||
if attendance:
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Comment",
|
||||
"comment_type": "Comment",
|
||||
"reference_doctype": "Attendance",
|
||||
"reference_name": attendance,
|
||||
"content": frappe._("Employee was marked Absent due to missing Employee Checkins."),
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
def get_assigned_employee(self, from_date=None, consider_default_shift=False):
|
||||
filters = {"start_date": (">", from_date), "shift_type": self.name, "docstatus": "1"}
|
||||
|
||||
@@ -358,5 +358,8 @@ erpnext.patches.v13_0.rename_non_profit_fields
|
||||
erpnext.patches.v13_0.enable_ksa_vat_docs #1
|
||||
erpnext.patches.v13_0.create_gst_custom_fields_in_quotation
|
||||
erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances
|
||||
erpnext.patches.v13_0.change_default_item_manufacturer_fieldtype
|
||||
erpnext.patches.v13_0.set_return_against_in_pos_invoice_references
|
||||
erpnext.patches.v13_0.copy_custom_field_filters_to_website_item
|
||||
erpnext.patches.v13_0.copy_custom_field_filters_to_website_item
|
||||
erpnext.patches.v13_0.education_deprecation_warning
|
||||
erpnext.patches.v13_0.create_accounting_dimensions_in_orders
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
|
||||
# Erase all default item manufacturers that dont exist.
|
||||
item = frappe.qb.DocType("Item")
|
||||
manufacturer = frappe.qb.DocType("Manufacturer")
|
||||
|
||||
(
|
||||
frappe.qb.update(item)
|
||||
.set(item.default_item_manufacturer, None)
|
||||
.left_join(manufacturer)
|
||||
.on(item.default_item_manufacturer == manufacturer.name)
|
||||
.where(manufacturer.name.isnull() & item.default_item_manufacturer.isnotnull())
|
||||
).run()
|
||||
@@ -0,0 +1,39 @@
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||
|
||||
|
||||
def execute():
|
||||
accounting_dimensions = frappe.db.get_all(
|
||||
"Accounting Dimension", fields=["fieldname", "label", "document_type", "disabled"]
|
||||
)
|
||||
|
||||
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 ["Purchase Order", "Purchase Receipt", "Sales Order"]:
|
||||
|
||||
field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname})
|
||||
|
||||
if field:
|
||||
continue
|
||||
|
||||
df = {
|
||||
"fieldname": d.fieldname,
|
||||
"label": d.label,
|
||||
"fieldtype": "Link",
|
||||
"options": d.document_type,
|
||||
"insert_after": insert_after_field,
|
||||
}
|
||||
|
||||
create_custom_field(doctype, df, ignore_validate=False)
|
||||
frappe.clear_cache(doctype=doctype)
|
||||
|
||||
count += 1
|
||||
10
erpnext/patches/v13_0/education_deprecation_warning.py
Normal file
10
erpnext/patches/v13_0/education_deprecation_warning.py
Normal file
@@ -0,0 +1,10 @@
|
||||
import click
|
||||
|
||||
|
||||
def execute():
|
||||
|
||||
click.secho(
|
||||
"Education Domain is moved to a separate app and will be removed from ERPNext in version-14.\n"
|
||||
"When upgrading to ERPNext version-14, please install the app to continue using the Education domain: https://github.com/frappe/education",
|
||||
fg="yellow",
|
||||
)
|
||||
@@ -376,13 +376,19 @@ class SalarySlip(TransactionBase):
|
||||
if joining_date and (getdate(self.start_date) < joining_date <= getdate(self.end_date)):
|
||||
start_date = joining_date
|
||||
unmarked_days = self.get_unmarked_days_based_on_doj_or_relieving(
|
||||
unmarked_days, include_holidays_in_total_working_days, self.start_date, joining_date
|
||||
unmarked_days,
|
||||
include_holidays_in_total_working_days,
|
||||
self.start_date,
|
||||
add_days(joining_date, -1),
|
||||
)
|
||||
|
||||
if relieving_date and (getdate(self.start_date) <= relieving_date < getdate(self.end_date)):
|
||||
end_date = relieving_date
|
||||
unmarked_days = self.get_unmarked_days_based_on_doj_or_relieving(
|
||||
unmarked_days, include_holidays_in_total_working_days, relieving_date, self.end_date
|
||||
unmarked_days,
|
||||
include_holidays_in_total_working_days,
|
||||
add_days(relieving_date, 1),
|
||||
self.end_date,
|
||||
)
|
||||
|
||||
# exclude days for which attendance has been marked
|
||||
@@ -408,10 +414,10 @@ class SalarySlip(TransactionBase):
|
||||
from erpnext.hr.doctype.employee.employee import is_holiday
|
||||
|
||||
if include_holidays_in_total_working_days:
|
||||
unmarked_days -= date_diff(end_date, start_date)
|
||||
unmarked_days -= date_diff(end_date, start_date) + 1
|
||||
else:
|
||||
# exclude only if not holidays
|
||||
for days in range(date_diff(end_date, start_date)):
|
||||
for days in range(date_diff(end_date, start_date) + 1):
|
||||
date = add_days(end_date, -days)
|
||||
if not is_holiday(self.employee, date):
|
||||
unmarked_days -= 1
|
||||
|
||||
@@ -128,6 +128,72 @@ class TestSalarySlip(unittest.TestCase):
|
||||
},
|
||||
)
|
||||
def test_payment_days_for_mid_joinee_including_holidays(self):
|
||||
no_of_days = self.get_no_of_days()
|
||||
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
|
||||
|
||||
new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
|
||||
joining_date, relieving_date = add_days(month_start_date, 3), add_days(month_end_date, -5)
|
||||
|
||||
for days in range(date_diff(month_end_date, month_start_date) + 1):
|
||||
date = add_days(month_start_date, days)
|
||||
mark_attendance(new_emp_id, date, "Present", ignore_validate=True)
|
||||
|
||||
# Case 1: relieving in mid month
|
||||
frappe.db.set_value(
|
||||
"Employee",
|
||||
new_emp_id,
|
||||
{"date_of_joining": month_start_date, "relieving_date": relieving_date, "status": "Active"},
|
||||
)
|
||||
|
||||
new_ss = make_employee_salary_slip(
|
||||
"test_payment_days_based_on_joining_date@salary.com",
|
||||
"Monthly",
|
||||
"Test Payment Based On Attendence",
|
||||
)
|
||||
self.assertEqual(new_ss.payment_days, no_of_days[0] - 5)
|
||||
|
||||
# Case 2: joining in mid month
|
||||
frappe.db.set_value(
|
||||
"Employee",
|
||||
new_emp_id,
|
||||
{"date_of_joining": joining_date, "relieving_date": month_end_date, "status": "Active"},
|
||||
)
|
||||
|
||||
frappe.delete_doc("Salary Slip", new_ss.name, force=True)
|
||||
new_ss = make_employee_salary_slip(
|
||||
"test_payment_days_based_on_joining_date@salary.com",
|
||||
"Monthly",
|
||||
"Test Payment Based On Attendence",
|
||||
)
|
||||
self.assertEqual(new_ss.payment_days, no_of_days[0] - 3)
|
||||
|
||||
# Case 3: joining and relieving in mid-month
|
||||
frappe.db.set_value(
|
||||
"Employee",
|
||||
new_emp_id,
|
||||
{"date_of_joining": joining_date, "relieving_date": relieving_date, "status": "Left"},
|
||||
)
|
||||
|
||||
frappe.delete_doc("Salary Slip", new_ss.name, force=True)
|
||||
new_ss = make_employee_salary_slip(
|
||||
"test_payment_days_based_on_joining_date@salary.com",
|
||||
"Monthly",
|
||||
"Test Payment Based On Attendence",
|
||||
)
|
||||
|
||||
self.assertEqual(new_ss.total_working_days, no_of_days[0])
|
||||
self.assertEqual(new_ss.payment_days, no_of_days[0] - 8)
|
||||
|
||||
@change_settings(
|
||||
"Payroll Settings",
|
||||
{
|
||||
"payroll_based_on": "Attendance",
|
||||
"consider_unmarked_attendance_as": "Absent",
|
||||
"include_holidays_in_total_working_days": True,
|
||||
},
|
||||
)
|
||||
def test_payment_days_for_mid_joinee_including_holidays_and_unmarked_days(self):
|
||||
# tests mid month joining and relieving along with unmarked days
|
||||
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
|
||||
|
||||
no_of_days = self.get_no_of_days()
|
||||
@@ -135,12 +201,6 @@ class TestSalarySlip(unittest.TestCase):
|
||||
|
||||
new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
|
||||
joining_date, relieving_date = add_days(month_start_date, 3), add_days(month_end_date, -5)
|
||||
frappe.db.set_value(
|
||||
"Employee",
|
||||
new_emp_id,
|
||||
{"date_of_joining": joining_date, "relieving_date": relieving_date, "status": "Left"},
|
||||
)
|
||||
|
||||
holidays = 0
|
||||
|
||||
for days in range(date_diff(relieving_date, joining_date) + 1):
|
||||
@@ -150,6 +210,12 @@ class TestSalarySlip(unittest.TestCase):
|
||||
else:
|
||||
holidays += 1
|
||||
|
||||
frappe.db.set_value(
|
||||
"Employee",
|
||||
new_emp_id,
|
||||
{"date_of_joining": joining_date, "relieving_date": relieving_date, "status": "Left"},
|
||||
)
|
||||
|
||||
new_ss = make_employee_salary_slip(
|
||||
"test_payment_days_based_on_joining_date@salary.com",
|
||||
"Monthly",
|
||||
|
||||
@@ -84,6 +84,7 @@ class Project(Document):
|
||||
type=task_details.type,
|
||||
issue=task_details.issue,
|
||||
is_group=task_details.is_group,
|
||||
color=task_details.color,
|
||||
)
|
||||
).insert()
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ def validate_eligibility(doc):
|
||||
invalid_company = not frappe.db.get_value("E Invoice User", {"company": doc.get("company")})
|
||||
invalid_supply_type = doc.get("gst_category") not in [
|
||||
"Registered Regular",
|
||||
"Registered Composition",
|
||||
"SEZ",
|
||||
"Overseas",
|
||||
"Deemed Export",
|
||||
@@ -125,24 +126,33 @@ def read_json(name):
|
||||
|
||||
def get_transaction_details(invoice):
|
||||
supply_type = ""
|
||||
if invoice.gst_category == "Registered Regular":
|
||||
if (
|
||||
invoice.gst_category == "Registered Regular" or invoice.gst_category == "Registered Composition"
|
||||
):
|
||||
supply_type = "B2B"
|
||||
elif invoice.gst_category == "SEZ":
|
||||
supply_type = "SEZWOP"
|
||||
if invoice.export_type == "Without Payment of Tax":
|
||||
supply_type = "SEZWOP"
|
||||
else:
|
||||
supply_type = "SEZWP"
|
||||
elif invoice.gst_category == "Overseas":
|
||||
supply_type = "EXPWOP"
|
||||
if invoice.export_type == "Without Payment of Tax":
|
||||
supply_type = "EXPWOP"
|
||||
else:
|
||||
supply_type = "EXPWP"
|
||||
elif invoice.gst_category == "Deemed Export":
|
||||
supply_type = "DEXP"
|
||||
|
||||
if not supply_type:
|
||||
rr, sez, overseas, export = (
|
||||
rr, rc, sez, overseas, export = (
|
||||
bold("Registered Regular"),
|
||||
bold("Registered Composition"),
|
||||
bold("SEZ"),
|
||||
bold("Overseas"),
|
||||
bold("Deemed Export"),
|
||||
)
|
||||
frappe.throw(
|
||||
_("GST category should be one of {}, {}, {}, {}").format(rr, sez, overseas, export),
|
||||
_("GST category should be one of {}, {}, {}, {}, {}").format(rr, rc, sez, overseas, export),
|
||||
title=_("Invalid Supply Type"),
|
||||
)
|
||||
|
||||
|
||||
@@ -714,7 +714,7 @@ def get_custom_fields():
|
||||
insert_after="customer",
|
||||
no_copy=1,
|
||||
print_hide=1,
|
||||
depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0',
|
||||
depends_on='eval:in_list(["Registered Regular", "Registered Composition", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0',
|
||||
),
|
||||
dict(
|
||||
fieldname="irn_cancelled",
|
||||
|
||||
@@ -95,10 +95,9 @@ class VATAuditReport(object):
|
||||
as_dict=1,
|
||||
)
|
||||
for d in items:
|
||||
if d.item_code not in self.invoice_items.get(d.parent, {}):
|
||||
self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, {"net_amount": 0.0})
|
||||
self.invoice_items[d.parent][d.item_code]["net_amount"] += d.get("base_net_amount", 0)
|
||||
self.invoice_items[d.parent][d.item_code]["is_zero_rated"] = d.is_zero_rated
|
||||
self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, {"net_amount": 0.0})
|
||||
self.invoice_items[d.parent][d.item_code]["net_amount"] += d.get("base_net_amount", 0)
|
||||
self.invoice_items[d.parent][d.item_code]["is_zero_rated"] = d.is_zero_rated
|
||||
|
||||
def get_items_based_on_tax_rate(self, doctype):
|
||||
self.items_based_on_tax_rate = frappe._dict()
|
||||
@@ -110,7 +109,7 @@ class VATAuditReport(object):
|
||||
self.tax_details = frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
parent, account_head, item_wise_tax_detail, base_tax_amount_after_discount_amount
|
||||
parent, account_head, item_wise_tax_detail
|
||||
FROM
|
||||
`tab%s`
|
||||
WHERE
|
||||
@@ -123,7 +122,7 @@ class VATAuditReport(object):
|
||||
tuple([doctype] + list(self.invoices.keys())),
|
||||
)
|
||||
|
||||
for parent, account, item_wise_tax_detail, tax_amount in self.tax_details:
|
||||
for parent, account, item_wise_tax_detail in self.tax_details:
|
||||
if item_wise_tax_detail:
|
||||
try:
|
||||
if account in self.sa_vat_accounts:
|
||||
@@ -135,7 +134,7 @@ class VATAuditReport(object):
|
||||
# to skip items with non-zero tax rate in multiple rows
|
||||
if taxes[0] == 0 and not is_zero_rated:
|
||||
continue
|
||||
tax_rate, item_amount_map = self.get_item_amount_map(parent, item_code, taxes)
|
||||
tax_rate = self.get_item_amount_map(parent, item_code, taxes)
|
||||
|
||||
if tax_rate is not None:
|
||||
rate_based_dict = self.items_based_on_tax_rate.setdefault(parent, {}).setdefault(
|
||||
@@ -151,16 +150,22 @@ class VATAuditReport(object):
|
||||
tax_rate = taxes[0]
|
||||
tax_amount = taxes[1]
|
||||
gross_amount = net_amount + tax_amount
|
||||
item_amount_map = self.item_tax_rate.setdefault(parent, {}).setdefault(item_code, [])
|
||||
amount_dict = {
|
||||
"tax_rate": tax_rate,
|
||||
"gross_amount": gross_amount,
|
||||
"tax_amount": tax_amount,
|
||||
"net_amount": net_amount,
|
||||
}
|
||||
item_amount_map.append(amount_dict)
|
||||
|
||||
return tax_rate, item_amount_map
|
||||
self.item_tax_rate.setdefault(parent, {}).setdefault(
|
||||
item_code,
|
||||
{
|
||||
"tax_rate": tax_rate,
|
||||
"gross_amount": 0.0,
|
||||
"tax_amount": 0.0,
|
||||
"net_amount": 0.0,
|
||||
},
|
||||
)
|
||||
|
||||
self.item_tax_rate[parent][item_code]["net_amount"] += net_amount
|
||||
self.item_tax_rate[parent][item_code]["tax_amount"] += tax_amount
|
||||
self.item_tax_rate[parent][item_code]["gross_amount"] += gross_amount
|
||||
|
||||
return tax_rate
|
||||
|
||||
def get_conditions(self):
|
||||
conditions = ""
|
||||
@@ -205,9 +210,10 @@ class VATAuditReport(object):
|
||||
for inv, inv_data in self.invoices.items():
|
||||
if self.items_based_on_tax_rate.get(inv):
|
||||
for rate, items in self.items_based_on_tax_rate.get(inv).items():
|
||||
row = {"tax_amount": 0.0, "gross_amount": 0.0, "net_amount": 0.0}
|
||||
|
||||
consolidated_data_map.setdefault(rate, {"data": []})
|
||||
for item in items:
|
||||
row = {}
|
||||
item_details = self.item_tax_rate.get(inv).get(item)
|
||||
row["account"] = inv_data.get("account")
|
||||
row["posting_date"] = formatdate(inv_data.get("posting_date"), "dd-mm-yyyy")
|
||||
@@ -216,10 +222,11 @@ class VATAuditReport(object):
|
||||
row["party_type"] = "Customer" if doctype == "Sales Invoice" else "Supplier"
|
||||
row["party"] = inv_data.get("party")
|
||||
row["remarks"] = inv_data.get("remarks")
|
||||
row["gross_amount"] = item_details[0].get("gross_amount")
|
||||
row["tax_amount"] = item_details[0].get("tax_amount")
|
||||
row["net_amount"] = item_details[0].get("net_amount")
|
||||
consolidated_data_map[rate]["data"].append(row)
|
||||
row["gross_amount"] += item_details.get("gross_amount")
|
||||
row["tax_amount"] += item_details.get("tax_amount")
|
||||
row["net_amount"] += item_details.get("net_amount")
|
||||
|
||||
consolidated_data_map[rate]["data"].append(row)
|
||||
|
||||
return consolidated_data_map
|
||||
|
||||
|
||||
@@ -245,7 +245,7 @@ def make_custom_fields():
|
||||
"Supplier Quotation Item": invoice_item_fields,
|
||||
}
|
||||
|
||||
create_custom_fields(custom_fields)
|
||||
create_custom_fields(custom_fields, ignore_validate=True)
|
||||
|
||||
|
||||
def add_print_formats():
|
||||
|
||||
@@ -152,7 +152,9 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
|
||||
}
|
||||
}
|
||||
|
||||
this.frm.add_custom_button(__('Pick List'), () => this.create_pick_list(), __('Create'));
|
||||
if (flt(doc.per_picked, 6) < 100 && flt(doc.per_delivered, 6) < 100) {
|
||||
this.frm.add_custom_button(__('Pick List'), () => this.create_pick_list(), __('Create'));
|
||||
}
|
||||
|
||||
const order_is_a_sale = ["Sales", "Shopping Cart"].indexOf(doc.order_type) !== -1;
|
||||
const order_is_maintenance = ["Maintenance"].indexOf(doc.order_type) !== -1;
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
"po_no",
|
||||
"po_date",
|
||||
"tax_id",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"contact_info",
|
||||
"customer_address",
|
||||
"address_display",
|
||||
@@ -113,7 +117,6 @@
|
||||
"is_internal_customer",
|
||||
"represents_company",
|
||||
"inter_company_order_reference",
|
||||
"project",
|
||||
"party_account_currency",
|
||||
"column_break_77",
|
||||
"source",
|
||||
@@ -1520,14 +1523,31 @@
|
||||
"fieldname": "per_picked",
|
||||
"fieldtype": "Percent",
|
||||
"label": "% Picked",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions"
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-03-15 21:38:31.437586",
|
||||
"modified": "2022-04-26 14:38:18.350207",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order",
|
||||
@@ -1606,4 +1626,4 @@
|
||||
"title_field": "customer_name",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,6 +455,16 @@ class SalesOrder(SellingController):
|
||||
if tot_qty != 0:
|
||||
self.db_set("per_delivered", flt(delivered_qty / tot_qty) * 100, update_modified=False)
|
||||
|
||||
def update_picking_status(self):
|
||||
total_picked_qty = 0.0
|
||||
total_qty = 0.0
|
||||
for so_item in self.items:
|
||||
total_picked_qty += flt(so_item.picked_qty)
|
||||
total_qty += flt(so_item.stock_qty)
|
||||
per_picked = total_picked_qty / total_qty * 100
|
||||
|
||||
self.db_set("per_picked", flt(per_picked), update_modified=False)
|
||||
|
||||
def set_indicator(self):
|
||||
"""Set indicator for portal"""
|
||||
if self.per_billed < 100 and self.per_delivered < 100:
|
||||
@@ -1302,9 +1312,30 @@ def make_inter_company_purchase_order(source_name, target_doc=None):
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_pick_list(source_name, target_doc=None):
|
||||
def update_item_quantity(source, target, source_parent):
|
||||
target.qty = flt(source.qty) - flt(source.delivered_qty)
|
||||
target.stock_qty = (flt(source.qty) - flt(source.delivered_qty)) * flt(source.conversion_factor)
|
||||
from erpnext.stock.doctype.packed_item.packed_item import is_product_bundle
|
||||
|
||||
def update_item_quantity(source, target, source_parent) -> None:
|
||||
picked_qty = flt(source.picked_qty) / (flt(source.conversion_factor) or 1)
|
||||
qty_to_be_picked = flt(source.qty) - max(picked_qty, flt(source.delivered_qty))
|
||||
|
||||
target.qty = qty_to_be_picked
|
||||
target.stock_qty = qty_to_be_picked * flt(source.conversion_factor)
|
||||
|
||||
def update_packed_item_qty(source, target, source_parent) -> None:
|
||||
qty = flt(source.qty)
|
||||
for item in source_parent.items:
|
||||
if source.parent_detail_docname == item.name:
|
||||
picked_qty = flt(item.picked_qty) / (flt(item.conversion_factor) or 1)
|
||||
pending_percent = (item.qty - max(picked_qty, item.delivered_qty)) / item.qty
|
||||
target.qty = target.stock_qty = qty * pending_percent
|
||||
return
|
||||
|
||||
def should_pick_order_item(item) -> bool:
|
||||
return (
|
||||
abs(item.delivered_qty) < abs(item.qty)
|
||||
and item.delivered_by_supplier != 1
|
||||
and not is_product_bundle(item.item_code)
|
||||
)
|
||||
|
||||
doc = get_mapped_doc(
|
||||
"Sales Order",
|
||||
@@ -1315,8 +1346,17 @@ def create_pick_list(source_name, target_doc=None):
|
||||
"doctype": "Pick List Item",
|
||||
"field_map": {"parent": "sales_order", "name": "sales_order_item"},
|
||||
"postprocess": update_item_quantity,
|
||||
"condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty)
|
||||
and doc.delivered_by_supplier != 1,
|
||||
"condition": should_pick_order_item,
|
||||
},
|
||||
"Packed Item": {
|
||||
"doctype": "Pick List Item",
|
||||
"field_map": {
|
||||
"parent": "sales_order",
|
||||
"name": "sales_order_item",
|
||||
"parent_detail_docname": "product_bundle_item",
|
||||
},
|
||||
"field_no_map": ["picked_qty"],
|
||||
"postprocess": update_packed_item_qty,
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
|
||||
@@ -801,13 +801,15 @@
|
||||
{
|
||||
"fieldname": "picked_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Picked Qty"
|
||||
"label": "Picked Qty (in Stock UOM)",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-03-15 20:17:33.984799",
|
||||
"modified": "2022-04-27 03:15:34.366563",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order Item",
|
||||
|
||||
@@ -343,9 +343,9 @@ erpnext.PointOfSale.Controller = class {
|
||||
toggle_other_sections: (show) => {
|
||||
if (show) {
|
||||
this.item_details.$component.is(':visible') ? this.item_details.$component.css('display', 'none') : '';
|
||||
this.item_selector.$component.css('display', 'none');
|
||||
this.item_selector.toggle_component(false);
|
||||
} else {
|
||||
this.item_selector.$component.css('display', 'flex');
|
||||
this.item_selector.toggle_component(true);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -130,10 +130,10 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
},
|
||||
cols: 5,
|
||||
keys: [
|
||||
[ 1, 2, 3, __('Quantity') ],
|
||||
[ 4, 5, 6, __('Discount') ],
|
||||
[ 7, 8, 9, __('Rate') ],
|
||||
[ '.', 0, __('Delete'), __('Remove') ]
|
||||
[ 1, 2, 3, 'Quantity' ],
|
||||
[ 4, 5, 6, 'Discount' ],
|
||||
[ 7, 8, 9, 'Rate' ],
|
||||
[ '.', 0, 'Delete', 'Remove' ]
|
||||
],
|
||||
css_classes: [
|
||||
[ '', '', '', 'col-span-2' ],
|
||||
|
||||
@@ -179,6 +179,25 @@ erpnext.PointOfSale.ItemSelector = class {
|
||||
});
|
||||
this.search_field.toggle_label(false);
|
||||
this.item_group_field.toggle_label(false);
|
||||
|
||||
this.attach_clear_btn();
|
||||
}
|
||||
|
||||
attach_clear_btn() {
|
||||
this.search_field.$wrapper.find('.control-input').append(
|
||||
`<span class="link-btn" style="top: 2px;">
|
||||
<a class="btn-open no-decoration" title="${__("Clear")}">
|
||||
${frappe.utils.icon('close', 'sm')}
|
||||
</a>
|
||||
</span>`
|
||||
);
|
||||
|
||||
this.$clear_search_btn = this.search_field.$wrapper.find('.link-btn');
|
||||
|
||||
this.$clear_search_btn.on('click', 'a', () => {
|
||||
this.set_search_value('');
|
||||
this.search_field.set_focus();
|
||||
});
|
||||
}
|
||||
|
||||
set_search_value(value) {
|
||||
@@ -252,6 +271,16 @@ erpnext.PointOfSale.ItemSelector = class {
|
||||
const search_term = e.target.value;
|
||||
this.filter_items({ search_term });
|
||||
}, 300);
|
||||
|
||||
this.$clear_search_btn.toggle(
|
||||
Boolean(this.search_field.$input.val())
|
||||
);
|
||||
});
|
||||
|
||||
this.search_field.$input.on('focus', () => {
|
||||
this.$clear_search_btn.toggle(
|
||||
Boolean(this.search_field.$input.val())
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -284,7 +313,7 @@ erpnext.PointOfSale.ItemSelector = class {
|
||||
if (this.items.length == 1) {
|
||||
this.$items_container.find(".item-wrapper").click();
|
||||
frappe.utils.play_sound("submit");
|
||||
$(this.search_field.$input[0]).val("").trigger("input");
|
||||
this.set_search_value('');
|
||||
} else if (this.items.length == 0 && this.barcode_scanned) {
|
||||
// only show alert of barcode is scanned and enter is pressed
|
||||
frappe.show_alert({
|
||||
@@ -293,7 +322,7 @@ erpnext.PointOfSale.ItemSelector = class {
|
||||
});
|
||||
frappe.utils.play_sound("error");
|
||||
this.barcode_scanned = false;
|
||||
$(this.search_field.$input[0]).val("").trigger("input");
|
||||
this.set_search_value('');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -350,6 +379,7 @@ erpnext.PointOfSale.ItemSelector = class {
|
||||
}
|
||||
|
||||
toggle_component(show) {
|
||||
show ? this.$component.css('display', 'flex') : this.$component.css('display', 'none');
|
||||
this.set_search_value('');
|
||||
this.$component.css('display', show ? 'flex': 'none');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ erpnext.PointOfSale.NumberPad = class {
|
||||
const fieldname = fieldnames && fieldnames[number] ?
|
||||
fieldnames[number] : typeof number === 'string' ? frappe.scrub(number) : number;
|
||||
|
||||
return a2 + `<div class="numpad-btn ${class_to_append}" data-button-value="${fieldname}">${number}</div>`;
|
||||
return a2 + `<div class="numpad-btn ${class_to_append}" data-button-value="${fieldname}">${__(number)}</div>`;
|
||||
}, '');
|
||||
}, '');
|
||||
}
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
"is_return",
|
||||
"issue_credit_note",
|
||||
"return_against",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"customer_po_details",
|
||||
"po_no",
|
||||
"column_break_17",
|
||||
@@ -115,7 +119,6 @@
|
||||
"driver_name",
|
||||
"lr_date",
|
||||
"more_info",
|
||||
"project",
|
||||
"campaign",
|
||||
"source",
|
||||
"column_break5",
|
||||
@@ -1309,13 +1312,29 @@
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount Eligible for Commission",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions"
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-truck",
|
||||
"idx": 146,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-03-10 14:29:13.428984",
|
||||
"modified": "2022-04-26 14:48:08.781837",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Delivery Note",
|
||||
@@ -1380,6 +1399,7 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"timeline_field": "customer",
|
||||
"title_field": "title",
|
||||
"track_changes": 1,
|
||||
|
||||
@@ -78,56 +78,6 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
|
||||
self.assertFalse(get_gl_entries("Delivery Note", dn.name))
|
||||
|
||||
# def test_delivery_note_gl_entry(self):
|
||||
# company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company')
|
||||
|
||||
# set_valuation_method("_Test Item", "FIFO")
|
||||
|
||||
# make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100)
|
||||
|
||||
# stock_in_hand_account = get_inventory_account('_Test Company with perpetual inventory')
|
||||
# prev_bal = get_balance_on(stock_in_hand_account)
|
||||
|
||||
# dn = create_delivery_note(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1")
|
||||
|
||||
# gl_entries = get_gl_entries("Delivery Note", dn.name)
|
||||
# self.assertTrue(gl_entries)
|
||||
|
||||
# stock_value_difference = abs(frappe.db.get_value("Stock Ledger Entry",
|
||||
# {"voucher_type": "Delivery Note", "voucher_no": dn.name}, "stock_value_difference"))
|
||||
|
||||
# expected_values = {
|
||||
# stock_in_hand_account: [0.0, stock_value_difference],
|
||||
# "Cost of Goods Sold - TCP1": [stock_value_difference, 0.0]
|
||||
# }
|
||||
# for i, gle in enumerate(gl_entries):
|
||||
# self.assertEqual([gle.debit, gle.credit], expected_values.get(gle.account))
|
||||
|
||||
# # check stock in hand balance
|
||||
# bal = get_balance_on(stock_in_hand_account)
|
||||
# self.assertEqual(bal, prev_bal - stock_value_difference)
|
||||
|
||||
# # back dated incoming entry
|
||||
# make_stock_entry(posting_date=add_days(nowdate(), -2), target="Stores - TCP1",
|
||||
# qty=5, basic_rate=100)
|
||||
|
||||
# gl_entries = get_gl_entries("Delivery Note", dn.name)
|
||||
# self.assertTrue(gl_entries)
|
||||
|
||||
# stock_value_difference = abs(frappe.db.get_value("Stock Ledger Entry",
|
||||
# {"voucher_type": "Delivery Note", "voucher_no": dn.name}, "stock_value_difference"))
|
||||
|
||||
# expected_values = {
|
||||
# stock_in_hand_account: [0.0, stock_value_difference],
|
||||
# "Cost of Goods Sold - TCP1": [stock_value_difference, 0.0]
|
||||
# }
|
||||
# for i, gle in enumerate(gl_entries):
|
||||
# self.assertEqual([gle.debit, gle.credit], expected_values.get(gle.account))
|
||||
|
||||
# dn.cancel()
|
||||
# self.assertTrue(get_gl_entries("Delivery Note", dn.name))
|
||||
# set_perpetual_inventory(0, company)
|
||||
|
||||
def test_delivery_note_gl_entry_packing_item(self):
|
||||
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
|
||||
|
||||
@@ -854,8 +804,6 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
company="_Test Company with perpetual inventory",
|
||||
)
|
||||
|
||||
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
|
||||
|
||||
set_valuation_method("_Test Item", "FIFO")
|
||||
|
||||
make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100)
|
||||
@@ -881,8 +829,6 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
def test_delivery_note_cost_center_with_balance_sheet_account(self):
|
||||
cost_center = "Main - TCP1"
|
||||
|
||||
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
|
||||
|
||||
set_valuation_method("_Test Item", "FIFO")
|
||||
|
||||
make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100)
|
||||
|
||||
@@ -918,8 +918,9 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "default_item_manufacturer",
|
||||
"fieldtype": "Data",
|
||||
"fieldtype": "Link",
|
||||
"label": "Default Item Manufacturer",
|
||||
"options": "Manufacturer",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -954,7 +955,7 @@
|
||||
"image_field": "image",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-12-14 04:13:16.857534",
|
||||
"modified": "2022-04-28 04:52:10.272256",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Item",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"ordered_qty",
|
||||
"column_break_16",
|
||||
"incoming_rate",
|
||||
"picked_qty",
|
||||
"page_break",
|
||||
"prevdoc_doctype",
|
||||
"parent_detail_docname"
|
||||
@@ -234,13 +235,20 @@
|
||||
"label": "Ordered Qty",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "picked_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Picked Qty",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-03-10 15:42:00.265915",
|
||||
"modified": "2022-04-27 05:23:08.683245",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Packed Item",
|
||||
|
||||
@@ -32,7 +32,7 @@ def make_packing_list(doc):
|
||||
reset = reset_packing_list(doc)
|
||||
|
||||
for item_row in doc.get("items"):
|
||||
if frappe.db.exists("Product Bundle", {"new_item_code": item_row.item_code}):
|
||||
if is_product_bundle(item_row.item_code):
|
||||
for bundle_item in get_product_bundle_items(item_row.item_code):
|
||||
pi_row = add_packed_item_row(
|
||||
doc=doc,
|
||||
@@ -54,6 +54,10 @@ def make_packing_list(doc):
|
||||
set_product_bundle_rate_amount(doc, parent_items_price) # set price in bundle item
|
||||
|
||||
|
||||
def is_product_bundle(item_code: str) -> bool:
|
||||
return bool(frappe.db.exists("Product Bundle", {"new_item_code": item_code}))
|
||||
|
||||
|
||||
def get_indexed_packed_items_table(doc):
|
||||
"""
|
||||
Create dict from stale packed items table like:
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_to_date, nowdate
|
||||
|
||||
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
@@ -12,6 +14,33 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
|
||||
def create_product_bundle(
|
||||
quantities: Optional[List[int]] = None, warehouse: Optional[str] = None
|
||||
) -> Tuple[str, List[str]]:
|
||||
"""Get a new product_bundle for use in tests.
|
||||
|
||||
Create 10x required stock if warehouse is specified.
|
||||
"""
|
||||
if not quantities:
|
||||
quantities = [2, 2]
|
||||
|
||||
bundle = make_item(properties={"is_stock_item": 0}).name
|
||||
|
||||
bundle_doc = frappe.get_doc({"doctype": "Product Bundle", "new_item_code": bundle})
|
||||
|
||||
components = []
|
||||
for qty in quantities:
|
||||
compoenent = make_item().name
|
||||
components.append(compoenent)
|
||||
bundle_doc.append("items", {"item_code": compoenent, "qty": qty})
|
||||
if warehouse:
|
||||
make_stock_entry(item=compoenent, to_warehouse=warehouse, qty=10 * qty, rate=100)
|
||||
|
||||
bundle_doc.insert()
|
||||
|
||||
return bundle, components
|
||||
|
||||
|
||||
class TestPackedItem(FrappeTestCase):
|
||||
"Test impact on Packed Items table in various scenarios."
|
||||
|
||||
@@ -19,24 +48,11 @@ class TestPackedItem(FrappeTestCase):
|
||||
def setUpClass(cls) -> None:
|
||||
super().setUpClass()
|
||||
cls.warehouse = "_Test Warehouse - _TC"
|
||||
cls.bundle = "_Test Product Bundle X"
|
||||
cls.bundle_items = ["_Test Bundle Item 1", "_Test Bundle Item 2"]
|
||||
|
||||
cls.bundle2 = "_Test Product Bundle Y"
|
||||
cls.bundle2_items = ["_Test Bundle Item 3", "_Test Bundle Item 4"]
|
||||
cls.bundle, cls.bundle_items = create_product_bundle(warehouse=cls.warehouse)
|
||||
cls.bundle2, cls.bundle2_items = create_product_bundle(warehouse=cls.warehouse)
|
||||
|
||||
make_item(cls.bundle, {"is_stock_item": 0})
|
||||
make_item(cls.bundle2, {"is_stock_item": 0})
|
||||
for item in cls.bundle_items + cls.bundle2_items:
|
||||
make_item(item, {"is_stock_item": 1})
|
||||
|
||||
make_item("_Test Normal Stock Item", {"is_stock_item": 1})
|
||||
|
||||
make_product_bundle(cls.bundle, cls.bundle_items, qty=2)
|
||||
make_product_bundle(cls.bundle2, cls.bundle2_items, qty=2)
|
||||
|
||||
for item in cls.bundle_items + cls.bundle2_items:
|
||||
make_stock_entry(item_code=item, to_warehouse=cls.warehouse, qty=100, rate=100)
|
||||
cls.normal_item = make_item().name
|
||||
|
||||
def test_adding_bundle_item(self):
|
||||
"Test impact on packed items if bundle item row is added."
|
||||
@@ -58,7 +74,7 @@ class TestPackedItem(FrappeTestCase):
|
||||
self.assertEqual(so.packed_items[1].qty, 4)
|
||||
|
||||
# change item code to non bundle item
|
||||
so.items[0].item_code = "_Test Normal Stock Item"
|
||||
so.items[0].item_code = self.normal_item
|
||||
so.save()
|
||||
|
||||
self.assertEqual(len(so.packed_items), 0)
|
||||
|
||||
@@ -114,6 +114,7 @@
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "print_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Print Settings"
|
||||
@@ -129,7 +130,7 @@
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-10-05 15:08:40.369957",
|
||||
"modified": "2022-04-21 07:56:40.646473",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Pick List",
|
||||
@@ -199,5 +200,6 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -4,13 +4,14 @@
|
||||
import json
|
||||
from collections import OrderedDict, defaultdict
|
||||
from itertools import groupby
|
||||
from operator import itemgetter
|
||||
from typing import Dict, List, Set
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import map_child_doc
|
||||
from frappe.utils import cint, floor, flt, today
|
||||
from frappe.utils.nestedset import get_descendants_of
|
||||
|
||||
from erpnext.selling.doctype.sales_order.sales_order import (
|
||||
make_delivery_note as create_delivery_note_from_sales_order,
|
||||
@@ -36,6 +37,7 @@ class PickList(Document):
|
||||
frappe.throw("Row " + str(location.idx) + " has been picked already!")
|
||||
|
||||
def before_submit(self):
|
||||
update_sales_orders = set()
|
||||
for item in self.locations:
|
||||
# if the user has not entered any picked qty, set it to stock_qty, before submit
|
||||
if item.picked_qty == 0:
|
||||
@@ -43,7 +45,8 @@ class PickList(Document):
|
||||
|
||||
if item.sales_order_item:
|
||||
# update the picked_qty in SO Item
|
||||
self.update_so(item.sales_order_item, item.picked_qty, item.item_code)
|
||||
self.update_sales_order_item(item, item.picked_qty, item.item_code)
|
||||
update_sales_orders.add(item.sales_order)
|
||||
|
||||
if not frappe.get_cached_value("Item", item.item_code, "has_serial_no"):
|
||||
continue
|
||||
@@ -63,18 +66,29 @@ class PickList(Document):
|
||||
title=_("Quantity Mismatch"),
|
||||
)
|
||||
|
||||
self.update_bundle_picked_qty()
|
||||
self.update_sales_order_picking_status(update_sales_orders)
|
||||
|
||||
def before_cancel(self):
|
||||
# update picked_qty in SO Item on cancel of PL
|
||||
"""Deduct picked qty on cancelling pick list"""
|
||||
updated_sales_orders = set()
|
||||
|
||||
for item in self.get("locations"):
|
||||
if item.sales_order_item:
|
||||
self.update_so(item.sales_order_item, -1 * item.picked_qty, item.item_code)
|
||||
self.update_sales_order_item(item, -1 * item.picked_qty, item.item_code)
|
||||
updated_sales_orders.add(item.sales_order)
|
||||
|
||||
self.update_bundle_picked_qty()
|
||||
self.update_sales_order_picking_status(updated_sales_orders)
|
||||
|
||||
def update_sales_order_item(self, item, picked_qty, item_code):
|
||||
item_table = "Sales Order Item" if not item.product_bundle_item else "Packed Item"
|
||||
stock_qty_field = "stock_qty" if not item.product_bundle_item else "qty"
|
||||
|
||||
def update_so(self, so_item, picked_qty, item_code):
|
||||
so_doc = frappe.get_doc(
|
||||
"Sales Order", frappe.db.get_value("Sales Order Item", so_item, "parent")
|
||||
)
|
||||
already_picked, actual_qty = frappe.db.get_value(
|
||||
"Sales Order Item", so_item, ["picked_qty", "qty"]
|
||||
item_table,
|
||||
item.sales_order_item,
|
||||
["picked_qty", stock_qty_field],
|
||||
)
|
||||
|
||||
if self.docstatus == 1:
|
||||
@@ -82,23 +96,18 @@ class PickList(Document):
|
||||
100 + flt(frappe.db.get_single_value("Stock Settings", "over_delivery_receipt_allowance"))
|
||||
):
|
||||
frappe.throw(
|
||||
"You are picking more than required quantity for "
|
||||
+ item_code
|
||||
+ ". Check if there is any other pick list created for "
|
||||
+ so_doc.name
|
||||
_(
|
||||
"You are picking more than required quantity for {}. Check if there is any other pick list created for {}"
|
||||
).format(item_code, item.sales_order)
|
||||
)
|
||||
|
||||
frappe.db.set_value("Sales Order Item", so_item, "picked_qty", already_picked + picked_qty)
|
||||
frappe.db.set_value(item_table, item.sales_order_item, "picked_qty", already_picked + picked_qty)
|
||||
|
||||
total_picked_qty = 0
|
||||
total_so_qty = 0
|
||||
for item in so_doc.get("items"):
|
||||
total_picked_qty += flt(item.picked_qty)
|
||||
total_so_qty += flt(item.stock_qty)
|
||||
total_picked_qty = total_picked_qty + picked_qty
|
||||
per_picked = total_picked_qty / total_so_qty * 100
|
||||
|
||||
so_doc.db_set("per_picked", flt(per_picked), update_modified=False)
|
||||
@staticmethod
|
||||
def update_sales_order_picking_status(sales_orders: Set[str]) -> None:
|
||||
for sales_order in sales_orders:
|
||||
if sales_order:
|
||||
frappe.get_doc("Sales Order", sales_order).update_picking_status()
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_item_locations(self, save=False):
|
||||
@@ -108,7 +117,7 @@ class PickList(Document):
|
||||
|
||||
from_warehouses = None
|
||||
if self.parent_warehouse:
|
||||
from_warehouses = frappe.db.get_descendants("Warehouse", self.parent_warehouse)
|
||||
from_warehouses = get_descendants_of("Warehouse", self.parent_warehouse)
|
||||
|
||||
# Create replica before resetting, to handle empty table on update after submit.
|
||||
locations_replica = self.get("locations")
|
||||
@@ -189,8 +198,7 @@ class PickList(Document):
|
||||
frappe.throw(_("Qty of Finished Goods Item should be greater than 0."))
|
||||
|
||||
def before_print(self, settings=None):
|
||||
if self.get("group_same_items"):
|
||||
self.group_similar_items()
|
||||
self.group_similar_items()
|
||||
|
||||
def group_similar_items(self):
|
||||
group_item_qty = defaultdict(float)
|
||||
@@ -216,6 +224,58 @@ class PickList(Document):
|
||||
for idx, item in enumerate(self.locations, start=1):
|
||||
item.idx = idx
|
||||
|
||||
def update_bundle_picked_qty(self):
|
||||
product_bundles = self._get_product_bundles()
|
||||
product_bundle_qty_map = self._get_product_bundle_qty_map(product_bundles.values())
|
||||
|
||||
for so_row, item_code in product_bundles.items():
|
||||
picked_qty = self._compute_picked_qty_for_bundle(so_row, product_bundle_qty_map[item_code])
|
||||
item_table = "Sales Order Item"
|
||||
already_picked = frappe.db.get_value(item_table, so_row, "picked_qty")
|
||||
frappe.db.set_value(
|
||||
item_table,
|
||||
so_row,
|
||||
"picked_qty",
|
||||
already_picked + (picked_qty * (1 if self.docstatus == 1 else -1)),
|
||||
)
|
||||
|
||||
def _get_product_bundles(self) -> Dict[str, str]:
|
||||
# Dict[so_item_row: item_code]
|
||||
product_bundles = {}
|
||||
for item in self.locations:
|
||||
if not item.product_bundle_item:
|
||||
continue
|
||||
product_bundles[item.product_bundle_item] = frappe.db.get_value(
|
||||
"Sales Order Item",
|
||||
item.product_bundle_item,
|
||||
"item_code",
|
||||
)
|
||||
return product_bundles
|
||||
|
||||
def _get_product_bundle_qty_map(self, bundles: List[str]) -> Dict[str, Dict[str, float]]:
|
||||
# bundle_item_code: Dict[component, qty]
|
||||
product_bundle_qty_map = {}
|
||||
for bundle_item_code in bundles:
|
||||
bundle = frappe.get_last_doc("Product Bundle", {"new_item_code": bundle_item_code})
|
||||
product_bundle_qty_map[bundle_item_code] = {item.item_code: item.qty for item in bundle.items}
|
||||
return product_bundle_qty_map
|
||||
|
||||
def _compute_picked_qty_for_bundle(self, bundle_row, bundle_items) -> int:
|
||||
"""Compute how many full bundles can be created from picked items."""
|
||||
precision = frappe.get_precision("Stock Ledger Entry", "qty_after_transaction")
|
||||
|
||||
possible_bundles = []
|
||||
for item in self.locations:
|
||||
if item.product_bundle_item != bundle_row:
|
||||
continue
|
||||
|
||||
qty_in_bundle = bundle_items.get(item.item_code)
|
||||
if qty_in_bundle:
|
||||
possible_bundles.append(item.picked_qty / qty_in_bundle)
|
||||
else:
|
||||
possible_bundles.append(0)
|
||||
return int(flt(min(possible_bundles), precision or 6))
|
||||
|
||||
|
||||
def validate_item_locations(pick_list):
|
||||
if not pick_list.locations:
|
||||
@@ -449,22 +509,18 @@ def create_delivery_note(source_name, target_doc=None):
|
||||
for location in pick_list.locations:
|
||||
if location.sales_order:
|
||||
sales_orders.append(
|
||||
[frappe.db.get_value("Sales Order", location.sales_order, "customer"), location.sales_order]
|
||||
frappe.db.get_value(
|
||||
"Sales Order", location.sales_order, ["customer", "name as sales_order"], as_dict=True
|
||||
)
|
||||
)
|
||||
# Group sales orders by customer
|
||||
for key, keydata in groupby(sales_orders, key=itemgetter(0)):
|
||||
sales_dict[key] = set([d[1] for d in keydata])
|
||||
|
||||
for customer, rows in groupby(sales_orders, key=lambda so: so["customer"]):
|
||||
sales_dict[customer] = {row.sales_order for row in rows}
|
||||
|
||||
if sales_dict:
|
||||
delivery_note = create_dn_with_so(sales_dict, pick_list)
|
||||
|
||||
is_item_wo_so = 0
|
||||
for location in pick_list.locations:
|
||||
if not location.sales_order:
|
||||
is_item_wo_so = 1
|
||||
break
|
||||
if is_item_wo_so == 1:
|
||||
# Create a DN for items without sales orders as well
|
||||
if not all(item.sales_order for item in pick_list.locations):
|
||||
delivery_note = create_dn_wo_so(pick_list)
|
||||
|
||||
frappe.msgprint(_("Delivery Note(s) created for the Pick List"))
|
||||
@@ -491,27 +547,30 @@ def create_dn_wo_so(pick_list):
|
||||
def create_dn_with_so(sales_dict, pick_list):
|
||||
delivery_note = None
|
||||
|
||||
item_table_mapper = {
|
||||
"doctype": "Delivery Note Item",
|
||||
"field_map": {
|
||||
"rate": "rate",
|
||||
"name": "so_detail",
|
||||
"parent": "against_sales_order",
|
||||
},
|
||||
"condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty)
|
||||
and doc.delivered_by_supplier != 1,
|
||||
}
|
||||
|
||||
for customer in sales_dict:
|
||||
for so in sales_dict[customer]:
|
||||
delivery_note = None
|
||||
delivery_note = create_delivery_note_from_sales_order(so, delivery_note, skip_item_mapping=True)
|
||||
|
||||
item_table_mapper = {
|
||||
"doctype": "Delivery Note Item",
|
||||
"field_map": {
|
||||
"rate": "rate",
|
||||
"name": "so_detail",
|
||||
"parent": "against_sales_order",
|
||||
},
|
||||
"condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty)
|
||||
and doc.delivered_by_supplier != 1,
|
||||
}
|
||||
break
|
||||
if delivery_note:
|
||||
# map all items of all sales orders of that customer
|
||||
for so in sales_dict[customer]:
|
||||
map_pl_locations(pick_list, item_table_mapper, delivery_note, so)
|
||||
delivery_note.insert(ignore_mandatory=True)
|
||||
delivery_note.flags.ignore_mandatory = True
|
||||
delivery_note.insert()
|
||||
update_packed_item_details(pick_list, delivery_note)
|
||||
delivery_note.save()
|
||||
|
||||
return delivery_note
|
||||
|
||||
@@ -519,28 +578,28 @@ def create_dn_with_so(sales_dict, pick_list):
|
||||
def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None):
|
||||
|
||||
for location in pick_list.locations:
|
||||
if location.sales_order == sales_order:
|
||||
if location.sales_order_item:
|
||||
sales_order_item = frappe.get_cached_doc(
|
||||
"Sales Order Item", {"name": location.sales_order_item}
|
||||
)
|
||||
else:
|
||||
sales_order_item = None
|
||||
if location.sales_order != sales_order or location.product_bundle_item:
|
||||
continue
|
||||
|
||||
source_doc, table_mapper = (
|
||||
[sales_order_item, item_mapper] if sales_order_item else [location, item_mapper]
|
||||
)
|
||||
if location.sales_order_item:
|
||||
sales_order_item = frappe.get_doc("Sales Order Item", location.sales_order_item)
|
||||
else:
|
||||
sales_order_item = None
|
||||
|
||||
dn_item = map_child_doc(source_doc, delivery_note, table_mapper)
|
||||
source_doc = sales_order_item or location
|
||||
|
||||
if dn_item:
|
||||
dn_item.pick_list_item = location.name
|
||||
dn_item.warehouse = location.warehouse
|
||||
dn_item.qty = flt(location.picked_qty) / (flt(location.conversion_factor) or 1)
|
||||
dn_item.batch_no = location.batch_no
|
||||
dn_item.serial_no = location.serial_no
|
||||
dn_item = map_child_doc(source_doc, delivery_note, item_mapper)
|
||||
|
||||
update_delivery_note_item(source_doc, dn_item, delivery_note)
|
||||
if dn_item:
|
||||
dn_item.pick_list_item = location.name
|
||||
dn_item.warehouse = location.warehouse
|
||||
dn_item.qty = flt(location.picked_qty) / (flt(location.conversion_factor) or 1)
|
||||
dn_item.batch_no = location.batch_no
|
||||
dn_item.serial_no = location.serial_no
|
||||
|
||||
update_delivery_note_item(source_doc, dn_item, delivery_note)
|
||||
|
||||
add_product_bundles_to_delivery_note(pick_list, delivery_note, item_mapper)
|
||||
set_delivery_note_missing_values(delivery_note)
|
||||
|
||||
delivery_note.pick_list = pick_list.name
|
||||
@@ -548,6 +607,50 @@ def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None):
|
||||
delivery_note.customer = frappe.get_value("Sales Order", sales_order, "customer")
|
||||
|
||||
|
||||
def add_product_bundles_to_delivery_note(
|
||||
pick_list: "PickList", delivery_note, item_mapper
|
||||
) -> None:
|
||||
"""Add product bundles found in pick list to delivery note.
|
||||
|
||||
When mapping pick list items, the bundle item itself isn't part of the
|
||||
locations. Dynamically fetch and add parent bundle item into DN."""
|
||||
product_bundles = pick_list._get_product_bundles()
|
||||
product_bundle_qty_map = pick_list._get_product_bundle_qty_map(product_bundles.values())
|
||||
|
||||
for so_row, item_code in product_bundles.items():
|
||||
sales_order_item = frappe.get_doc("Sales Order Item", so_row)
|
||||
dn_bundle_item = map_child_doc(sales_order_item, delivery_note, item_mapper)
|
||||
dn_bundle_item.qty = pick_list._compute_picked_qty_for_bundle(
|
||||
so_row, product_bundle_qty_map[item_code]
|
||||
)
|
||||
update_delivery_note_item(sales_order_item, dn_bundle_item, delivery_note)
|
||||
|
||||
|
||||
def update_packed_item_details(pick_list: "PickList", delivery_note) -> None:
|
||||
"""Update stock details on packed items table of delivery note."""
|
||||
|
||||
def _find_so_row(packed_item):
|
||||
for item in delivery_note.items:
|
||||
if packed_item.parent_detail_docname == item.name:
|
||||
return item.so_detail
|
||||
|
||||
def _find_pick_list_location(bundle_row, packed_item):
|
||||
if not bundle_row:
|
||||
return
|
||||
for loc in pick_list.locations:
|
||||
if loc.product_bundle_item == bundle_row and loc.item_code == packed_item.item_code:
|
||||
return loc
|
||||
|
||||
for packed_item in delivery_note.packed_items:
|
||||
so_row = _find_so_row(packed_item)
|
||||
location = _find_pick_list_location(so_row, packed_item)
|
||||
if not location:
|
||||
continue
|
||||
packed_item.warehouse = location.warehouse
|
||||
packed_item.batch_no = location.batch_no
|
||||
packed_item.serial_no = location.serial_no
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_stock_entry(pick_list):
|
||||
pick_list = frappe.get_doc(json.loads(pick_list))
|
||||
|
||||
@@ -3,18 +3,21 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _dict
|
||||
|
||||
test_dependencies = ["Item", "Sales Invoice", "Stock Entry", "Batch"]
|
||||
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.selling.doctype.sales_order.sales_order import create_pick_list
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.stock.doctype.item.test_item import create_item, make_item
|
||||
from erpnext.stock.doctype.packed_item.test_packed_item import create_product_bundle
|
||||
from erpnext.stock.doctype.pick_list.pick_list import create_delivery_note
|
||||
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_reconciliation.stock_reconciliation import (
|
||||
EmptyStockReconciliationItemsError,
|
||||
)
|
||||
|
||||
test_dependencies = ["Item", "Sales Invoice", "Stock Entry", "Batch"]
|
||||
|
||||
|
||||
class TestPickList(FrappeTestCase):
|
||||
def test_pick_list_picks_warehouse_for_each_item(self):
|
||||
@@ -566,14 +569,79 @@ class TestPickList(FrappeTestCase):
|
||||
if dn_item.item_code == "_Test Item 2":
|
||||
self.assertEqual(dn_item.qty, 2)
|
||||
|
||||
# def test_pick_list_skips_items_in_expired_batch(self):
|
||||
# pass
|
||||
def test_picklist_with_multi_uom(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
item = make_item(properties={"uoms": [dict(uom="Box", conversion_factor=24)]}).name
|
||||
make_stock_entry(item=item, to_warehouse=warehouse, qty=1000)
|
||||
|
||||
# def test_pick_list_from_sales_order(self):
|
||||
# pass
|
||||
so = make_sales_order(item_code=item, qty=10, rate=42, uom="Box")
|
||||
pl = create_pick_list(so.name)
|
||||
# pick half the qty
|
||||
for loc in pl.locations:
|
||||
loc.picked_qty = loc.stock_qty / 2
|
||||
pl.save()
|
||||
pl.submit()
|
||||
|
||||
# def test_pick_list_from_work_order(self):
|
||||
# pass
|
||||
so.reload()
|
||||
self.assertEqual(so.per_picked, 50)
|
||||
|
||||
# def test_pick_list_from_material_request(self):
|
||||
# pass
|
||||
def test_picklist_with_bundles(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
quantities = [5, 2]
|
||||
bundle, components = create_product_bundle(quantities, warehouse=warehouse)
|
||||
bundle_items = dict(zip(components, quantities))
|
||||
|
||||
so = make_sales_order(item_code=bundle, qty=3, rate=42)
|
||||
|
||||
pl = create_pick_list(so.name)
|
||||
pl.save()
|
||||
self.assertEqual(len(pl.locations), 2)
|
||||
for item in pl.locations:
|
||||
self.assertEqual(item.stock_qty, bundle_items[item.item_code] * 3)
|
||||
|
||||
# check picking status on sales order
|
||||
pl.submit()
|
||||
so.reload()
|
||||
self.assertEqual(so.per_picked, 100)
|
||||
|
||||
# deliver
|
||||
dn = create_delivery_note(pl.name).submit()
|
||||
self.assertEqual(dn.items[0].rate, 42)
|
||||
self.assertEqual(dn.packed_items[0].warehouse, warehouse)
|
||||
so.reload()
|
||||
self.assertEqual(so.per_delivered, 100)
|
||||
|
||||
def test_picklist_with_partial_bundles(self):
|
||||
# from test_records.json
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
quantities = [5, 2]
|
||||
bundle, components = create_product_bundle(quantities, warehouse=warehouse)
|
||||
|
||||
so = make_sales_order(item_code=bundle, qty=4, rate=42)
|
||||
|
||||
pl = create_pick_list(so.name)
|
||||
for loc in pl.locations:
|
||||
loc.picked_qty = loc.qty / 2
|
||||
|
||||
pl.save().submit()
|
||||
so.reload()
|
||||
self.assertEqual(so.per_picked, 50)
|
||||
|
||||
# deliver half qty
|
||||
dn = create_delivery_note(pl.name).submit()
|
||||
self.assertEqual(dn.items[0].rate, 42)
|
||||
so.reload()
|
||||
self.assertEqual(so.per_delivered, 50)
|
||||
|
||||
pl = create_pick_list(so.name)
|
||||
pl.save().submit()
|
||||
so.reload()
|
||||
self.assertEqual(so.per_picked, 100)
|
||||
|
||||
# deliver remaining
|
||||
dn = create_delivery_note(pl.name).submit()
|
||||
self.assertEqual(dn.items[0].rate, 42)
|
||||
so.reload()
|
||||
self.assertEqual(so.per_delivered, 100)
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"column_break_15",
|
||||
"sales_order",
|
||||
"sales_order_item",
|
||||
"product_bundle_item",
|
||||
"material_request",
|
||||
"material_request_item"
|
||||
],
|
||||
@@ -146,6 +147,7 @@
|
||||
{
|
||||
"fieldname": "sales_order_item",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Sales Order Item",
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -177,11 +179,19 @@
|
||||
"fieldtype": "Data",
|
||||
"label": "Item Group",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"description": "product bundle item row's name in sales order. Also indicates that picked item is to be used for a product bundle",
|
||||
"fieldname": "product_bundle_item",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Product Bundle Item",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-28 12:02:16.923056",
|
||||
"modified": "2022-04-22 05:27:38.497997",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Pick List Item",
|
||||
@@ -190,5 +200,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -24,6 +24,10 @@
|
||||
"apply_putaway_rule",
|
||||
"is_return",
|
||||
"return_against",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"section_addresses",
|
||||
"supplier_address",
|
||||
"contact_person",
|
||||
@@ -107,7 +111,6 @@
|
||||
"bill_no",
|
||||
"bill_date",
|
||||
"more_info",
|
||||
"project",
|
||||
"status",
|
||||
"amended_from",
|
||||
"range",
|
||||
@@ -1144,13 +1147,29 @@
|
||||
"label": "Represents Company",
|
||||
"options": "Company",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions"
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-truck",
|
||||
"idx": 261,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-04-10 22:50:37.761362",
|
||||
"modified": "2022-04-26 13:41:32.625197",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Purchase Receipt",
|
||||
|
||||
@@ -46,9 +46,9 @@
|
||||
"items",
|
||||
"get_stock_and_rate",
|
||||
"section_break_19",
|
||||
"total_incoming_value",
|
||||
"column_break_22",
|
||||
"total_outgoing_value",
|
||||
"column_break_22",
|
||||
"total_incoming_value",
|
||||
"value_difference",
|
||||
"additional_costs_section",
|
||||
"additional_costs",
|
||||
@@ -374,7 +374,7 @@
|
||||
{
|
||||
"fieldname": "total_incoming_value",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Incoming Value",
|
||||
"label": "Total Incoming Value (Receipt)",
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
@@ -386,7 +386,7 @@
|
||||
{
|
||||
"fieldname": "total_outgoing_value",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Outgoing Value",
|
||||
"label": "Total Outgoing Value (Consumption)",
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
@@ -394,7 +394,7 @@
|
||||
{
|
||||
"fieldname": "value_difference",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Value Difference (Out - In)",
|
||||
"label": "Total Value Difference (Incoming - Outgoing)",
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide_if_no_value": 1,
|
||||
"read_only": 1
|
||||
@@ -619,7 +619,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-02-07 12:55:14.614077",
|
||||
"modified": "2022-05-02 05:21:39.060501",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Entry",
|
||||
|
||||
@@ -557,7 +557,7 @@ class StockEntry(StockController):
|
||||
)
|
||||
|
||||
def set_actual_qty(self):
|
||||
allow_negative_stock = cint(frappe.db.get_value("Stock Settings", None, "allow_negative_stock"))
|
||||
allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
|
||||
|
||||
for d in self.get("items"):
|
||||
previous_sle = get_previous_sle(
|
||||
|
||||
@@ -30,7 +30,6 @@ class TestStockReconciliation(FrappeTestCase):
|
||||
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
|
||||
|
||||
def tearDown(self):
|
||||
frappe.flags.dont_execute_stock_reposts = None
|
||||
frappe.local.future_sle = {}
|
||||
|
||||
def test_reco_for_fifo(self):
|
||||
@@ -40,7 +39,9 @@ class TestStockReconciliation(FrappeTestCase):
|
||||
self._test_reco_sle_gle("Moving Average")
|
||||
|
||||
def _test_reco_sle_gle(self, valuation_method):
|
||||
se1, se2, se3 = insert_existing_sle(warehouse="Stores - TCP1")
|
||||
item_code = make_item(properties={"valuation_method": valuation_method}).name
|
||||
|
||||
se1, se2, se3 = insert_existing_sle(warehouse="Stores - TCP1", item_code=item_code)
|
||||
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
|
||||
# [[qty, valuation_rate, posting_date,
|
||||
# posting_time, expected_stock_value, bin_qty, bin_valuation]]
|
||||
@@ -54,11 +55,9 @@ class TestStockReconciliation(FrappeTestCase):
|
||||
]
|
||||
|
||||
for d in input_data:
|
||||
set_valuation_method("_Test Item", valuation_method)
|
||||
|
||||
last_sle = get_previous_sle(
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"item_code": item_code,
|
||||
"warehouse": "Stores - TCP1",
|
||||
"posting_date": d[2],
|
||||
"posting_time": d[3],
|
||||
@@ -67,6 +66,7 @@ class TestStockReconciliation(FrappeTestCase):
|
||||
|
||||
# submit stock reconciliation
|
||||
stock_reco = create_stock_reconciliation(
|
||||
item_code=item_code,
|
||||
qty=d[0],
|
||||
rate=d[1],
|
||||
posting_date=d[2],
|
||||
@@ -480,9 +480,11 @@ class TestStockReconciliation(FrappeTestCase):
|
||||
"""
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
frappe.db.rollback()
|
||||
|
||||
# repost will make this test useless, qty should update in realtime without reposts
|
||||
frappe.flags.dont_execute_stock_reposts = True
|
||||
frappe.db.rollback()
|
||||
self.addCleanup(frappe.flags.pop, "dont_execute_stock_reposts")
|
||||
|
||||
item_code = make_item().name
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
@@ -593,26 +595,26 @@ def create_batch_item_with_batch(item_name, batch_id):
|
||||
b.save()
|
||||
|
||||
|
||||
def insert_existing_sle(warehouse):
|
||||
def insert_existing_sle(warehouse, item_code="_Test Item"):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
se1 = make_stock_entry(
|
||||
posting_date="2012-12-15",
|
||||
posting_time="02:00",
|
||||
item_code="_Test Item",
|
||||
item_code=item_code,
|
||||
target=warehouse,
|
||||
qty=10,
|
||||
basic_rate=700,
|
||||
)
|
||||
|
||||
se2 = make_stock_entry(
|
||||
posting_date="2012-12-25", posting_time="03:00", item_code="_Test Item", source=warehouse, qty=15
|
||||
posting_date="2012-12-25", posting_time="03:00", item_code=item_code, source=warehouse, qty=15
|
||||
)
|
||||
|
||||
se3 = make_stock_entry(
|
||||
posting_date="2013-01-05",
|
||||
posting_time="07:00",
|
||||
item_code="_Test Item",
|
||||
item_code=item_code,
|
||||
target=warehouse,
|
||||
qty=15,
|
||||
basic_rate=1200,
|
||||
|
||||
@@ -36,6 +36,9 @@ class Warehouse(NestedSet):
|
||||
self.set_onload("account", account)
|
||||
load_address_and_contact(self)
|
||||
|
||||
def validate(self):
|
||||
self.warn_about_multiple_warehouse_account()
|
||||
|
||||
def on_update(self):
|
||||
self.update_nsm_model()
|
||||
|
||||
@@ -70,6 +73,53 @@ class Warehouse(NestedSet):
|
||||
self.update_nsm_model()
|
||||
self.unlink_from_items()
|
||||
|
||||
def warn_about_multiple_warehouse_account(self):
|
||||
"If Warehouse value is split across multiple accounts, warn."
|
||||
|
||||
def get_accounts_where_value_is_booked(name):
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
ac = frappe.qb.DocType("Account")
|
||||
|
||||
return (
|
||||
frappe.qb.from_(sle)
|
||||
.join(gle)
|
||||
.on(sle.voucher_no == gle.voucher_no)
|
||||
.join(ac)
|
||||
.on(ac.name == gle.account)
|
||||
.select(gle.account)
|
||||
.distinct()
|
||||
.where((sle.warehouse == name) & (ac.account_type == "Stock"))
|
||||
.orderby(sle.creation)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
if self.is_new():
|
||||
return
|
||||
|
||||
old_wh_account = frappe.db.get_value("Warehouse", self.name, "account")
|
||||
|
||||
# WH account is being changed or set get all accounts against which wh value is booked
|
||||
if self.account != old_wh_account:
|
||||
accounts = get_accounts_where_value_is_booked(self.name)
|
||||
accounts = [d.account for d in accounts]
|
||||
|
||||
if not accounts or (len(accounts) == 1 and self.account in accounts):
|
||||
# if same singular account has stock value booked ignore
|
||||
return
|
||||
|
||||
warning = _("Warehouse's Stock Value has already been booked in the following accounts:")
|
||||
account_str = "<br>" + ", ".join(frappe.bold(ac) for ac in accounts)
|
||||
reason = "<br><br>" + _(
|
||||
"Booking stock value across multiple accounts will make it harder to track stock and account value."
|
||||
)
|
||||
|
||||
frappe.msgprint(
|
||||
warning + account_str + reason,
|
||||
title=_("Multiple Warehouse Accounts"),
|
||||
indicator="orange",
|
||||
)
|
||||
|
||||
def check_if_sle_exists(self):
|
||||
return frappe.db.exists("Stock Ledger Entry", {"warehouse": self.name})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user