Merge branch 'develop' into fix-customer-credit-limit-update
This commit is contained in:
commit
1061256358
@ -346,6 +346,8 @@ frappe.ui.form.on('Payment Entry', {
|
|||||||
}
|
}
|
||||||
frm.set_party_account_based_on_party = true;
|
frm.set_party_account_based_on_party = true;
|
||||||
|
|
||||||
|
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||||
|
|
||||||
return frappe.call({
|
return frappe.call({
|
||||||
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_details",
|
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_details",
|
||||||
args: {
|
args: {
|
||||||
@ -379,7 +381,11 @@ frappe.ui.form.on('Payment Entry', {
|
|||||||
if (r.message.bank_account) {
|
if (r.message.bank_account) {
|
||||||
frm.set_value("bank_account", r.message.bank_account);
|
frm.set_value("bank_account", r.message.bank_account);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
() => frm.events.set_current_exchange_rate(frm, "source_exchange_rate",
|
||||||
|
frm.doc.paid_from_account_currency, company_currency),
|
||||||
|
() => frm.events.set_current_exchange_rate(frm, "target_exchange_rate",
|
||||||
|
frm.doc.paid_to_account_currency, company_currency)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -483,14 +489,14 @@ frappe.ui.form.on('Payment Entry', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
paid_from_account_currency: function(frm) {
|
paid_from_account_currency: function(frm) {
|
||||||
if(!frm.doc.paid_from_account_currency) return;
|
if(!frm.doc.paid_from_account_currency || !frm.doc.company) return;
|
||||||
var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||||
|
|
||||||
if (frm.doc.paid_from_account_currency == company_currency) {
|
if (frm.doc.paid_from_account_currency == company_currency) {
|
||||||
frm.set_value("source_exchange_rate", 1);
|
frm.set_value("source_exchange_rate", 1);
|
||||||
} else if (frm.doc.paid_from){
|
} else if (frm.doc.paid_from){
|
||||||
if (in_list(["Internal Transfer", "Pay"], frm.doc.payment_type)) {
|
if (in_list(["Internal Transfer", "Pay"], frm.doc.payment_type)) {
|
||||||
var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "erpnext.setup.utils.get_exchange_rate",
|
method: "erpnext.setup.utils.get_exchange_rate",
|
||||||
args: {
|
args: {
|
||||||
@ -510,8 +516,8 @@ frappe.ui.form.on('Payment Entry', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
paid_to_account_currency: function(frm) {
|
paid_to_account_currency: function(frm) {
|
||||||
if(!frm.doc.paid_to_account_currency) return;
|
if(!frm.doc.paid_to_account_currency || !frm.doc.company) return;
|
||||||
var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||||
|
|
||||||
frm.events.set_current_exchange_rate(frm, "target_exchange_rate",
|
frm.events.set_current_exchange_rate(frm, "target_exchange_rate",
|
||||||
frm.doc.paid_to_account_currency, company_currency);
|
frm.doc.paid_to_account_currency, company_currency);
|
||||||
|
|||||||
@ -84,20 +84,12 @@ class POSInvoiceMergeLog(Document):
|
|||||||
sales_invoice.set_posting_time = 1
|
sales_invoice.set_posting_time = 1
|
||||||
sales_invoice.posting_date = getdate(self.posting_date)
|
sales_invoice.posting_date = getdate(self.posting_date)
|
||||||
sales_invoice.save()
|
sales_invoice.save()
|
||||||
self.write_off_fractional_amount(sales_invoice, data)
|
|
||||||
sales_invoice.submit()
|
sales_invoice.submit()
|
||||||
|
|
||||||
self.consolidated_invoice = sales_invoice.name
|
self.consolidated_invoice = sales_invoice.name
|
||||||
|
|
||||||
return sales_invoice.name
|
return sales_invoice.name
|
||||||
|
|
||||||
def write_off_fractional_amount(self, invoice, data):
|
|
||||||
pos_invoice_grand_total = sum(d.grand_total for d in data)
|
|
||||||
|
|
||||||
if abs(pos_invoice_grand_total - invoice.grand_total) < 1:
|
|
||||||
invoice.write_off_amount += -1 * (pos_invoice_grand_total - invoice.grand_total)
|
|
||||||
invoice.save()
|
|
||||||
|
|
||||||
def process_merging_into_credit_note(self, data):
|
def process_merging_into_credit_note(self, data):
|
||||||
credit_note = self.get_new_sales_invoice()
|
credit_note = self.get_new_sales_invoice()
|
||||||
credit_note.is_return = 1
|
credit_note.is_return = 1
|
||||||
@ -110,7 +102,6 @@ class POSInvoiceMergeLog(Document):
|
|||||||
# TODO: return could be against multiple sales invoice which could also have been consolidated?
|
# TODO: return could be against multiple sales invoice which could also have been consolidated?
|
||||||
# credit_note.return_against = self.consolidated_invoice
|
# credit_note.return_against = self.consolidated_invoice
|
||||||
credit_note.save()
|
credit_note.save()
|
||||||
self.write_off_fractional_amount(credit_note, data)
|
|
||||||
credit_note.submit()
|
credit_note.submit()
|
||||||
|
|
||||||
self.consolidated_credit_note = credit_note.name
|
self.consolidated_credit_note = credit_note.name
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import json
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe.tests.utils import change_settings
|
||||||
|
|
||||||
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
|
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
|
||||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
|
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
|
||||||
@ -280,3 +281,100 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
|||||||
frappe.set_user("Administrator")
|
frappe.set_user("Administrator")
|
||||||
frappe.db.sql("delete from `tabPOS Profile`")
|
frappe.db.sql("delete from `tabPOS Profile`")
|
||||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||||
|
|
||||||
|
@change_settings("System Settings", {"number_format": "#,###.###", "currency_precision": 3, "float_precision": 3})
|
||||||
|
def test_consolidation_round_off_error_3(self):
|
||||||
|
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||||
|
|
||||||
|
try:
|
||||||
|
make_stock_entry(
|
||||||
|
to_warehouse="_Test Warehouse - _TC",
|
||||||
|
item_code="_Test Item",
|
||||||
|
rate=8000,
|
||||||
|
qty=10,
|
||||||
|
)
|
||||||
|
init_user_and_profile()
|
||||||
|
|
||||||
|
item_rates = [69, 59, 29]
|
||||||
|
for i in [1, 2]:
|
||||||
|
inv = create_pos_invoice(is_return=1, do_not_save=1)
|
||||||
|
inv.items = []
|
||||||
|
for rate in item_rates:
|
||||||
|
inv.append("items", {
|
||||||
|
"item_code": "_Test Item",
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
"qty": -1,
|
||||||
|
"rate": rate,
|
||||||
|
"income_account": "Sales - _TC",
|
||||||
|
"expense_account": "Cost of Goods Sold - _TC",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
})
|
||||||
|
inv.append("taxes", {
|
||||||
|
"account_head": "_Test Account VAT - _TC",
|
||||||
|
"charge_type": "On Net Total",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
"description": "VAT",
|
||||||
|
"doctype": "Sales Taxes and Charges",
|
||||||
|
"rate": 15,
|
||||||
|
"included_in_print_rate": 1
|
||||||
|
})
|
||||||
|
inv.payments = []
|
||||||
|
inv.append('payments', {
|
||||||
|
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': -157
|
||||||
|
})
|
||||||
|
inv.paid_amount = -157
|
||||||
|
inv.save()
|
||||||
|
inv.submit()
|
||||||
|
|
||||||
|
consolidate_pos_invoices()
|
||||||
|
|
||||||
|
inv.load_from_db()
|
||||||
|
consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice)
|
||||||
|
self.assertEqual(consolidated_invoice.status, 'Return')
|
||||||
|
self.assertEqual(consolidated_invoice.rounding_adjustment, -0.001)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
frappe.db.sql("delete from `tabPOS Profile`")
|
||||||
|
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||||
|
|
||||||
|
def test_consolidation_rounding_adjustment(self):
|
||||||
|
'''
|
||||||
|
Test if the rounding adjustment is calculated correctly
|
||||||
|
'''
|
||||||
|
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||||
|
|
||||||
|
try:
|
||||||
|
make_stock_entry(
|
||||||
|
to_warehouse="_Test Warehouse - _TC",
|
||||||
|
item_code="_Test Item",
|
||||||
|
rate=8000,
|
||||||
|
qty=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
init_user_and_profile()
|
||||||
|
|
||||||
|
inv = create_pos_invoice(qty=1, rate=69.5, do_not_save=True)
|
||||||
|
inv.append('payments', {
|
||||||
|
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 70
|
||||||
|
})
|
||||||
|
inv.insert()
|
||||||
|
inv.submit()
|
||||||
|
|
||||||
|
inv2 = create_pos_invoice(qty=1, rate=59.5, do_not_save=True)
|
||||||
|
inv2.append('payments', {
|
||||||
|
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60
|
||||||
|
})
|
||||||
|
inv2.insert()
|
||||||
|
inv2.submit()
|
||||||
|
|
||||||
|
consolidate_pos_invoices()
|
||||||
|
|
||||||
|
inv.load_from_db()
|
||||||
|
consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice)
|
||||||
|
self.assertEqual(consolidated_invoice.rounding_adjustment, 1)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
frappe.db.sql("delete from `tabPOS Profile`")
|
||||||
|
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||||
@ -263,6 +263,9 @@ class SalesInvoice(SellingController):
|
|||||||
self.process_common_party_accounting()
|
self.process_common_party_accounting()
|
||||||
|
|
||||||
def validate_pos_return(self):
|
def validate_pos_return(self):
|
||||||
|
if self.is_consolidated:
|
||||||
|
# pos return is already validated in pos invoice
|
||||||
|
return
|
||||||
|
|
||||||
if self.is_pos and self.is_return:
|
if self.is_pos and self.is_return:
|
||||||
total_amount_in_payments = 0
|
total_amount_in_payments = 0
|
||||||
|
|||||||
@ -55,5 +55,8 @@ def validate_disabled(doc):
|
|||||||
frappe.throw(_("Disabled template must not be default template"))
|
frappe.throw(_("Disabled template must not be default template"))
|
||||||
|
|
||||||
def validate_for_tax_category(doc):
|
def validate_for_tax_category(doc):
|
||||||
|
if not doc.tax_category:
|
||||||
|
return
|
||||||
|
|
||||||
if frappe.db.exists(doc.doctype, {"company": doc.company, "tax_category": doc.tax_category, "disabled": 0, "name": ["!=", doc.name]}):
|
if frappe.db.exists(doc.doctype, {"company": doc.company, "tax_category": doc.tax_category, "disabled": 0, "name": ["!=", doc.name]}):
|
||||||
frappe.throw(_("A template with tax category {0} already exists. Only one template is allowed with each tax category").format(frappe.bold(doc.tax_category)))
|
frappe.throw(_("A template with tax category {0} already exists. Only one template is allowed with each tax category").format(frappe.bold(doc.tax_category)))
|
||||||
|
|||||||
@ -316,6 +316,16 @@ class PurchaseOrder(BuyingController):
|
|||||||
'target_ref_field': 'stock_qty',
|
'target_ref_field': 'stock_qty',
|
||||||
'source_field': 'stock_qty'
|
'source_field': 'stock_qty'
|
||||||
})
|
})
|
||||||
|
self.status_updater.append({
|
||||||
|
'source_dt': 'Purchase Order Item',
|
||||||
|
'target_dt': 'Packed Item',
|
||||||
|
'target_field': 'ordered_qty',
|
||||||
|
'target_parent_dt': 'Sales Order',
|
||||||
|
'target_parent_field': '',
|
||||||
|
'join_field': 'sales_order_packed_item',
|
||||||
|
'target_ref_field': 'qty',
|
||||||
|
'source_field': 'stock_qty'
|
||||||
|
})
|
||||||
|
|
||||||
def update_delivered_qty_in_sales_order(self):
|
def update_delivered_qty_in_sales_order(self):
|
||||||
"""Update delivered qty in Sales Order for drop ship"""
|
"""Update delivered qty in Sales Order for drop ship"""
|
||||||
|
|||||||
@ -63,6 +63,7 @@
|
|||||||
"material_request_item",
|
"material_request_item",
|
||||||
"sales_order",
|
"sales_order",
|
||||||
"sales_order_item",
|
"sales_order_item",
|
||||||
|
"sales_order_packed_item",
|
||||||
"supplier_quotation",
|
"supplier_quotation",
|
||||||
"supplier_quotation_item",
|
"supplier_quotation_item",
|
||||||
"col_break5",
|
"col_break5",
|
||||||
@ -837,21 +838,30 @@
|
|||||||
"label": "Product Bundle",
|
"label": "Product Bundle",
|
||||||
"options": "Product Bundle",
|
"options": "Product Bundle",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "sales_order_packed_item",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Sales Order Packed Item",
|
||||||
|
"no_copy": 1,
|
||||||
|
"print_hide": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-08-30 20:06:26.712097",
|
"modified": "2022-02-02 13:10:18.398976",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Purchase Order Item",
|
"name": "Purchase Order Item",
|
||||||
|
"naming_rule": "Random",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"search_fields": "item_name",
|
"search_fields": "item_name",
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
@ -6,6 +6,7 @@ import copy
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from frappe.query_builder.functions import Coalesce, Sum
|
||||||
from frappe.utils import date_diff, flt, getdate
|
from frappe.utils import date_diff, flt, getdate
|
||||||
|
|
||||||
|
|
||||||
@ -16,12 +17,9 @@ def execute(filters=None):
|
|||||||
validate_filters(filters)
|
validate_filters(filters)
|
||||||
|
|
||||||
columns = get_columns(filters)
|
columns = get_columns(filters)
|
||||||
conditions = get_conditions(filters)
|
data = get_data(filters)
|
||||||
|
|
||||||
#get queried data
|
# prepare data for report and chart views
|
||||||
data = get_data(filters, conditions)
|
|
||||||
|
|
||||||
#prepare data for report and chart views
|
|
||||||
data, chart_data = prepare_data(data, filters)
|
data, chart_data = prepare_data(data, filters)
|
||||||
|
|
||||||
return columns, data, None, chart_data
|
return columns, data, None, chart_data
|
||||||
@ -34,53 +32,70 @@ def validate_filters(filters):
|
|||||||
elif date_diff(to_date, from_date) < 0:
|
elif date_diff(to_date, from_date) < 0:
|
||||||
frappe.throw(_("To Date cannot be before From Date."))
|
frappe.throw(_("To Date cannot be before From Date."))
|
||||||
|
|
||||||
def get_conditions(filters):
|
def get_data(filters):
|
||||||
conditions = ''
|
mr = frappe.qb.DocType("Material Request")
|
||||||
|
mr_item = frappe.qb.DocType("Material Request Item")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(mr)
|
||||||
|
.join(mr_item).on(mr_item.parent == mr.name)
|
||||||
|
.select(
|
||||||
|
mr.name.as_("material_request"),
|
||||||
|
mr.transaction_date.as_("date"),
|
||||||
|
mr_item.schedule_date.as_("required_date"),
|
||||||
|
mr_item.item_code.as_("item_code"),
|
||||||
|
Sum(Coalesce(mr_item.stock_qty, 0)).as_("qty"),
|
||||||
|
Coalesce(mr_item.stock_uom, '').as_("uom"),
|
||||||
|
Sum(Coalesce(mr_item.ordered_qty, 0)).as_("ordered_qty"),
|
||||||
|
Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"),
|
||||||
|
(
|
||||||
|
Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.received_qty, 0))
|
||||||
|
).as_("qty_to_receive"),
|
||||||
|
Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"),
|
||||||
|
(
|
||||||
|
Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.ordered_qty, 0))
|
||||||
|
).as_("qty_to_order"),
|
||||||
|
mr_item.item_name,
|
||||||
|
mr_item.description,
|
||||||
|
mr.company
|
||||||
|
).where(
|
||||||
|
(mr.material_request_type == "Purchase")
|
||||||
|
& (mr.docstatus == 1)
|
||||||
|
& (mr.status != "Stopped")
|
||||||
|
& (mr.per_received < 100)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
query = get_conditions(filters, query, mr, mr_item) # add conditional conditions
|
||||||
|
|
||||||
|
query = (
|
||||||
|
query.groupby(
|
||||||
|
mr.name, mr_item.item_code
|
||||||
|
).orderby(
|
||||||
|
mr.transaction_date, mr.schedule_date
|
||||||
|
)
|
||||||
|
)
|
||||||
|
data = query.run(as_dict=True)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get_conditions(filters, query, mr, mr_item):
|
||||||
if filters.get("from_date") and filters.get("to_date"):
|
if filters.get("from_date") and filters.get("to_date"):
|
||||||
conditions += " and mr.transaction_date between '{0}' and '{1}'".format(filters.get("from_date"),filters.get("to_date"))
|
query = (
|
||||||
|
query.where(
|
||||||
|
(mr.transaction_date >= filters.get("from_date"))
|
||||||
|
& (mr.transaction_date <= filters.get("to_date"))
|
||||||
|
)
|
||||||
|
)
|
||||||
if filters.get("company"):
|
if filters.get("company"):
|
||||||
conditions += " and mr.company = '{0}'".format(filters.get("company"))
|
query = query.where(mr.company == filters.get("company"))
|
||||||
|
|
||||||
if filters.get("material_request"):
|
if filters.get("material_request"):
|
||||||
conditions += " and mr.name = '{0}'".format(filters.get("material_request"))
|
query = query.where(mr.name == filters.get("material_request"))
|
||||||
|
|
||||||
if filters.get("item_code"):
|
if filters.get("item_code"):
|
||||||
conditions += " and mr_item.item_code = '{0}'".format(filters.get("item_code"))
|
query = query.where(mr_item.item_code == filters.get("item_code"))
|
||||||
|
|
||||||
return conditions
|
return query
|
||||||
|
|
||||||
def get_data(filters, conditions):
|
|
||||||
data = frappe.db.sql("""
|
|
||||||
select
|
|
||||||
mr.name as material_request,
|
|
||||||
mr.transaction_date as date,
|
|
||||||
mr_item.schedule_date as required_date,
|
|
||||||
mr_item.item_code as item_code,
|
|
||||||
sum(ifnull(mr_item.stock_qty, 0)) as qty,
|
|
||||||
ifnull(mr_item.stock_uom, '') as uom,
|
|
||||||
sum(ifnull(mr_item.ordered_qty, 0)) as ordered_qty,
|
|
||||||
sum(ifnull(mr_item.received_qty, 0)) as received_qty,
|
|
||||||
(sum(ifnull(mr_item.stock_qty, 0)) - sum(ifnull(mr_item.received_qty, 0))) as qty_to_receive,
|
|
||||||
(sum(ifnull(mr_item.stock_qty, 0)) - sum(ifnull(mr_item.ordered_qty, 0))) as qty_to_order,
|
|
||||||
mr_item.item_name as item_name,
|
|
||||||
mr_item.description as "description",
|
|
||||||
mr.company as company
|
|
||||||
from
|
|
||||||
`tabMaterial Request` mr, `tabMaterial Request Item` mr_item
|
|
||||||
where
|
|
||||||
mr_item.parent = mr.name
|
|
||||||
and mr.material_request_type = "Purchase"
|
|
||||||
and mr.docstatus = 1
|
|
||||||
and mr.status != "Stopped"
|
|
||||||
{conditions}
|
|
||||||
group by mr.name, mr_item.item_code
|
|
||||||
having
|
|
||||||
sum(ifnull(mr_item.ordered_qty, 0)) < sum(ifnull(mr_item.stock_qty, 0))
|
|
||||||
order by mr.transaction_date, mr.schedule_date""".format(conditions=conditions), as_dict=1)
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
def update_qty_columns(row_to_update, data_row):
|
def update_qty_columns(row_to_update, data_row):
|
||||||
fields = ["qty", "ordered_qty", "received_qty", "qty_to_receive", "qty_to_order"]
|
fields = ["qty", "ordered_qty", "received_qty", "qty_to_receive", "qty_to_order"]
|
||||||
|
|||||||
@ -0,0 +1,69 @@
|
|||||||
|
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
|
from frappe.utils import add_days, today
|
||||||
|
|
||||||
|
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
|
||||||
|
from erpnext.buying.report.requested_items_to_order_and_receive.requested_items_to_order_and_receive import (
|
||||||
|
get_data,
|
||||||
|
)
|
||||||
|
from erpnext.stock.doctype.item.test_item import create_item
|
||||||
|
from erpnext.stock.doctype.material_request.material_request import make_purchase_order
|
||||||
|
|
||||||
|
|
||||||
|
class TestRequestedItemsToOrderAndReceive(FrappeTestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
create_item("Test MR Report Item")
|
||||||
|
self.setup_material_request() # to order and receive
|
||||||
|
self.setup_material_request(order=True) # to receive (ordered)
|
||||||
|
self.setup_material_request(order=True, receive=True) # complete (ordered & received)
|
||||||
|
|
||||||
|
self.filters = frappe._dict(
|
||||||
|
company="_Test Company", from_date=today(), to_date=add_days(today(), 30),
|
||||||
|
item_code="Test MR Report Item"
|
||||||
|
)
|
||||||
|
|
||||||
|
def tearDown(self) -> None:
|
||||||
|
frappe.db.rollback()
|
||||||
|
|
||||||
|
def test_date_range(self):
|
||||||
|
data = get_data(self.filters)
|
||||||
|
self.assertEqual(len(data), 2) # MRs today should be fetched
|
||||||
|
|
||||||
|
self.filters.from_date = add_days(today(), 1)
|
||||||
|
data = get_data(self.filters)
|
||||||
|
self.assertEqual(len(data), 0) # MRs today should not be fetched as from date is tomorrow
|
||||||
|
|
||||||
|
def test_ordered_received_material_requests(self):
|
||||||
|
data = get_data(self.filters)
|
||||||
|
|
||||||
|
# from the 3 MRs made, only 2 (to receive) should be fetched
|
||||||
|
self.assertEqual(len(data), 2)
|
||||||
|
self.assertEqual(data[0].ordered_qty, 0.0)
|
||||||
|
self.assertEqual(data[1].ordered_qty, 57.0)
|
||||||
|
|
||||||
|
def setup_material_request(self, order=False, receive=False):
|
||||||
|
po = None
|
||||||
|
test_records = frappe.get_test_records('Material Request')
|
||||||
|
|
||||||
|
mr = frappe.copy_doc(test_records[0])
|
||||||
|
mr.transaction_date = today()
|
||||||
|
mr.schedule_date = add_days(today(), 1)
|
||||||
|
for row in mr.items:
|
||||||
|
row.item_code = "Test MR Report Item"
|
||||||
|
row.item_name = "Test MR Report Item"
|
||||||
|
row.description = "Test MR Report Item"
|
||||||
|
row.uom = "Nos"
|
||||||
|
row.schedule_date = add_days(today(), 1)
|
||||||
|
mr.submit()
|
||||||
|
|
||||||
|
if order or receive:
|
||||||
|
po = make_purchase_order(mr.name)
|
||||||
|
po.supplier = "_Test Supplier"
|
||||||
|
po.submit()
|
||||||
|
if receive:
|
||||||
|
pr = make_purchase_receipt(po.name)
|
||||||
|
pr.submit()
|
||||||
|
|
||||||
@ -507,13 +507,41 @@ class StockController(AccountsController):
|
|||||||
"voucher_no": self.name,
|
"voucher_no": self.name,
|
||||||
"company": self.company
|
"company": self.company
|
||||||
})
|
})
|
||||||
if future_sle_exists(args):
|
|
||||||
|
if future_sle_exists(args) or repost_required_for_queue(self):
|
||||||
item_based_reposting = cint(frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"))
|
item_based_reposting = cint(frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"))
|
||||||
if item_based_reposting:
|
if item_based_reposting:
|
||||||
create_item_wise_repost_entries(voucher_type=self.doctype, voucher_no=self.name)
|
create_item_wise_repost_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||||
else:
|
else:
|
||||||
create_repost_item_valuation_entry(args)
|
create_repost_item_valuation_entry(args)
|
||||||
|
|
||||||
|
def repost_required_for_queue(doc: StockController) -> bool:
|
||||||
|
"""check if stock document contains repeated item-warehouse with queue based valuation.
|
||||||
|
|
||||||
|
if queue exists for repeated items then SLEs need to reprocessed in background again.
|
||||||
|
"""
|
||||||
|
|
||||||
|
consuming_sles = frappe.db.get_all("Stock Ledger Entry",
|
||||||
|
filters={
|
||||||
|
"voucher_type": doc.doctype,
|
||||||
|
"voucher_no": doc.name,
|
||||||
|
"actual_qty": ("<", 0),
|
||||||
|
"is_cancelled": 0
|
||||||
|
},
|
||||||
|
fields=["item_code", "warehouse", "stock_queue"]
|
||||||
|
)
|
||||||
|
item_warehouses = [(sle.item_code, sle.warehouse) for sle in consuming_sles]
|
||||||
|
|
||||||
|
unique_item_warehouses = set(item_warehouses)
|
||||||
|
|
||||||
|
if len(unique_item_warehouses) == len(item_warehouses):
|
||||||
|
return False
|
||||||
|
|
||||||
|
for sle in consuming_sles:
|
||||||
|
if sle.stock_queue != "[]": # using FIFO/LIFO valuation
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def make_quality_inspections(doctype, docname, items):
|
def make_quality_inspections(doctype, docname, items):
|
||||||
|
|||||||
@ -113,17 +113,24 @@ class calculate_taxes_and_totals(object):
|
|||||||
for item in self.doc.get("items"):
|
for item in self.doc.get("items"):
|
||||||
self.doc.round_floats_in(item)
|
self.doc.round_floats_in(item)
|
||||||
|
|
||||||
|
if not item.rate:
|
||||||
|
item.rate = item.price_list_rate
|
||||||
|
|
||||||
if item.discount_percentage == 100:
|
if item.discount_percentage == 100:
|
||||||
item.rate = 0.0
|
item.rate = 0.0
|
||||||
elif item.price_list_rate:
|
elif item.price_list_rate:
|
||||||
if not item.rate or (item.pricing_rules and item.discount_percentage > 0):
|
if item.pricing_rules or abs(item.discount_percentage) > 0:
|
||||||
item.rate = flt(item.price_list_rate *
|
item.rate = flt(item.price_list_rate *
|
||||||
(1.0 - (item.discount_percentage / 100.0)), item.precision("rate"))
|
(1.0 - (item.discount_percentage / 100.0)), item.precision("rate"))
|
||||||
item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0)
|
|
||||||
elif item.discount_amount and item.pricing_rules:
|
if abs(item.discount_percentage) > 0:
|
||||||
|
item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0)
|
||||||
|
|
||||||
|
elif item.discount_amount or item.pricing_rules:
|
||||||
item.rate = item.price_list_rate - item.discount_amount
|
item.rate = item.price_list_rate - item.discount_amount
|
||||||
|
|
||||||
if item.doctype in ['Quotation Item', 'Sales Order Item', 'Delivery Note Item', 'Sales Invoice Item', 'POS Invoice Item', 'Purchase Invoice Item', 'Purchase Order Item', 'Purchase Receipt Item']:
|
if item.doctype in ['Quotation Item', 'Sales Order Item', 'Delivery Note Item', 'Sales Invoice Item',
|
||||||
|
'POS Invoice Item', 'Purchase Invoice Item', 'Purchase Order Item', 'Purchase Receipt Item']:
|
||||||
item.rate_with_margin, item.base_rate_with_margin = self.calculate_margin(item)
|
item.rate_with_margin, item.base_rate_with_margin = self.calculate_margin(item)
|
||||||
if flt(item.rate_with_margin) > 0:
|
if flt(item.rate_with_margin) > 0:
|
||||||
item.rate = flt(item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate"))
|
item.rate = flt(item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate"))
|
||||||
@ -270,7 +277,8 @@ class calculate_taxes_and_totals(object):
|
|||||||
shipping_rule.apply(self.doc)
|
shipping_rule.apply(self.doc)
|
||||||
|
|
||||||
def calculate_taxes(self):
|
def calculate_taxes(self):
|
||||||
if not self.doc.get('is_consolidated'):
|
rounding_adjustment_computed = self.doc.get('is_consolidated') and self.doc.get('rounding_adjustment')
|
||||||
|
if not rounding_adjustment_computed:
|
||||||
self.doc.rounding_adjustment = 0
|
self.doc.rounding_adjustment = 0
|
||||||
|
|
||||||
# maintain actual tax rate based on idx
|
# maintain actual tax rate based on idx
|
||||||
@ -326,7 +334,7 @@ class calculate_taxes_and_totals(object):
|
|||||||
if i == (len(self.doc.get("taxes")) - 1) and self.discount_amount_applied \
|
if i == (len(self.doc.get("taxes")) - 1) and self.discount_amount_applied \
|
||||||
and self.doc.discount_amount \
|
and self.doc.discount_amount \
|
||||||
and self.doc.apply_discount_on == "Grand Total" \
|
and self.doc.apply_discount_on == "Grand Total" \
|
||||||
and not self.doc.get('is_consolidated'):
|
and not rounding_adjustment_computed:
|
||||||
self.doc.rounding_adjustment = flt(self.doc.grand_total
|
self.doc.rounding_adjustment = flt(self.doc.grand_total
|
||||||
- flt(self.doc.discount_amount) - tax.total,
|
- flt(self.doc.discount_amount) - tax.total,
|
||||||
self.doc.precision("rounding_adjustment"))
|
self.doc.precision("rounding_adjustment"))
|
||||||
@ -465,20 +473,22 @@ class calculate_taxes_and_totals(object):
|
|||||||
self.doc.total_net_weight += d.total_weight
|
self.doc.total_net_weight += d.total_weight
|
||||||
|
|
||||||
def set_rounded_total(self):
|
def set_rounded_total(self):
|
||||||
if not self.doc.get('is_consolidated'):
|
if self.doc.get('is_consolidated') and self.doc.get('rounding_adjustment'):
|
||||||
if self.doc.meta.get_field("rounded_total"):
|
return
|
||||||
if self.doc.is_rounded_total_disabled():
|
|
||||||
self.doc.rounded_total = self.doc.base_rounded_total = 0
|
|
||||||
return
|
|
||||||
|
|
||||||
self.doc.rounded_total = round_based_on_smallest_currency_fraction(self.doc.grand_total,
|
if self.doc.meta.get_field("rounded_total"):
|
||||||
self.doc.currency, self.doc.precision("rounded_total"))
|
if self.doc.is_rounded_total_disabled():
|
||||||
|
self.doc.rounded_total = self.doc.base_rounded_total = 0
|
||||||
|
return
|
||||||
|
|
||||||
#if print_in_rate is set, we would have already calculated rounding adjustment
|
self.doc.rounded_total = round_based_on_smallest_currency_fraction(self.doc.grand_total,
|
||||||
self.doc.rounding_adjustment += flt(self.doc.rounded_total - self.doc.grand_total,
|
self.doc.currency, self.doc.precision("rounded_total"))
|
||||||
self.doc.precision("rounding_adjustment"))
|
|
||||||
|
|
||||||
self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"])
|
#if print_in_rate is set, we would have already calculated rounding adjustment
|
||||||
|
self.doc.rounding_adjustment += flt(self.doc.rounded_total - self.doc.grand_total,
|
||||||
|
self.doc.precision("rounding_adjustment"))
|
||||||
|
|
||||||
|
self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"])
|
||||||
|
|
||||||
def _cleanup(self):
|
def _cleanup(self):
|
||||||
if not self.doc.get('is_consolidated'):
|
if not self.doc.get('is_consolidated'):
|
||||||
|
|||||||
@ -174,16 +174,22 @@ def get_month_map():
|
|||||||
def get_unmarked_days(employee, month, exclude_holidays=0):
|
def get_unmarked_days(employee, month, exclude_holidays=0):
|
||||||
import calendar
|
import calendar
|
||||||
month_map = get_month_map()
|
month_map = get_month_map()
|
||||||
|
|
||||||
today = get_datetime()
|
today = get_datetime()
|
||||||
|
|
||||||
dates_of_month = ['{}-{}-{}'.format(today.year, month_map[month], r) for r in range(1, calendar.monthrange(today.year, month_map[month])[1] + 1)]
|
joining_date, relieving_date = frappe.get_cached_value("Employee", employee, ["date_of_joining", "relieving_date"])
|
||||||
|
start_day = 1
|
||||||
|
end_day = calendar.monthrange(today.year, month_map[month])[1] + 1
|
||||||
|
|
||||||
length = len(dates_of_month)
|
if joining_date and joining_date.month == month_map[month]:
|
||||||
month_start, month_end = dates_of_month[0], dates_of_month[length-1]
|
start_day = joining_date.day
|
||||||
|
|
||||||
|
if relieving_date and relieving_date.month == month_map[month]:
|
||||||
|
end_day = relieving_date.day + 1
|
||||||
|
|
||||||
records = frappe.get_all("Attendance", fields = ['attendance_date', 'employee'] , filters = [
|
dates_of_month = ['{}-{}-{}'.format(today.year, month_map[month], r) for r in range(start_day, end_day)]
|
||||||
|
month_start, month_end = dates_of_month[0], dates_of_month[-1]
|
||||||
|
|
||||||
|
records = frappe.get_all("Attendance", fields=['attendance_date', 'employee'], filters=[
|
||||||
["attendance_date", ">=", month_start],
|
["attendance_date", ">=", month_start],
|
||||||
["attendance_date", "<=", month_end],
|
["attendance_date", "<=", month_end],
|
||||||
["employee", "=", employee],
|
["employee", "=", employee],
|
||||||
@ -200,7 +206,7 @@ def get_unmarked_days(employee, month, exclude_holidays=0):
|
|||||||
|
|
||||||
for date in dates_of_month:
|
for date in dates_of_month:
|
||||||
date_time = get_datetime(date)
|
date_time = get_datetime(date)
|
||||||
if today.day == date_time.day and today.month == date_time.month:
|
if today.day <= date_time.day and today.month <= date_time.month:
|
||||||
break
|
break
|
||||||
if date_time not in marked_days:
|
if date_time not in marked_days:
|
||||||
unmarked_days.append(date)
|
unmarked_days.append(date)
|
||||||
|
|||||||
@ -4,17 +4,104 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import nowdate
|
from frappe.utils import add_days, get_first_day, getdate, nowdate
|
||||||
|
|
||||||
|
from erpnext.hr.doctype.attendance.attendance import (
|
||||||
|
get_month_map,
|
||||||
|
get_unmarked_days,
|
||||||
|
mark_attendance,
|
||||||
|
)
|
||||||
|
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||||
|
from erpnext.hr.doctype.leave_application.test_leave_application import get_first_sunday
|
||||||
|
|
||||||
test_records = frappe.get_test_records('Attendance')
|
test_records = frappe.get_test_records('Attendance')
|
||||||
|
|
||||||
class TestAttendance(unittest.TestCase):
|
class TestAttendance(unittest.TestCase):
|
||||||
def test_mark_absent(self):
|
def test_mark_absent(self):
|
||||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
|
||||||
employee = make_employee("test_mark_absent@example.com")
|
employee = make_employee("test_mark_absent@example.com")
|
||||||
date = nowdate()
|
date = nowdate()
|
||||||
frappe.db.delete('Attendance', {'employee':employee, 'attendance_date':date})
|
frappe.db.delete('Attendance', {'employee':employee, 'attendance_date':date})
|
||||||
from erpnext.hr.doctype.attendance.attendance import mark_attendance
|
|
||||||
attendance = mark_attendance(employee, date, 'Absent')
|
attendance = mark_attendance(employee, date, 'Absent')
|
||||||
fetch_attendance = frappe.get_value('Attendance', {'employee':employee, 'attendance_date':date, 'status':'Absent'})
|
fetch_attendance = frappe.get_value('Attendance', {'employee':employee, 'attendance_date':date, 'status':'Absent'})
|
||||||
self.assertEqual(attendance, fetch_attendance)
|
self.assertEqual(attendance, fetch_attendance)
|
||||||
|
|
||||||
|
def test_unmarked_days(self):
|
||||||
|
first_day = get_first_day(getdate())
|
||||||
|
|
||||||
|
employee = make_employee('test_unmarked_days@example.com', date_of_joining=add_days(first_day, -1))
|
||||||
|
frappe.db.delete('Attendance', {'employee': employee})
|
||||||
|
|
||||||
|
from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
|
||||||
|
holiday_list = make_holiday_list()
|
||||||
|
frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list)
|
||||||
|
|
||||||
|
first_sunday = get_first_sunday(holiday_list)
|
||||||
|
mark_attendance(employee, first_day, 'Present')
|
||||||
|
month_name = get_month_name(first_day)
|
||||||
|
|
||||||
|
unmarked_days = get_unmarked_days(employee, month_name)
|
||||||
|
unmarked_days = [getdate(date) for date in unmarked_days]
|
||||||
|
|
||||||
|
# attendance already marked for the day
|
||||||
|
self.assertNotIn(first_day, unmarked_days)
|
||||||
|
# attendance unmarked
|
||||||
|
self.assertIn(getdate(add_days(first_day, 1)), unmarked_days)
|
||||||
|
# holiday considered in unmarked days
|
||||||
|
self.assertIn(first_sunday, unmarked_days)
|
||||||
|
|
||||||
|
def test_unmarked_days_excluding_holidays(self):
|
||||||
|
first_day = get_first_day(getdate())
|
||||||
|
|
||||||
|
employee = make_employee('test_unmarked_days@example.com', date_of_joining=add_days(first_day, -1))
|
||||||
|
frappe.db.delete('Attendance', {'employee': employee})
|
||||||
|
|
||||||
|
from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
|
||||||
|
holiday_list = make_holiday_list()
|
||||||
|
frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list)
|
||||||
|
|
||||||
|
first_sunday = get_first_sunday(holiday_list)
|
||||||
|
mark_attendance(employee, first_day, 'Present')
|
||||||
|
month_name = get_month_name(first_day)
|
||||||
|
|
||||||
|
unmarked_days = get_unmarked_days(employee, month_name, exclude_holidays=True)
|
||||||
|
unmarked_days = [getdate(date) for date in unmarked_days]
|
||||||
|
|
||||||
|
# attendance already marked for the day
|
||||||
|
self.assertNotIn(first_day, unmarked_days)
|
||||||
|
# attendance unmarked
|
||||||
|
self.assertIn(getdate(add_days(first_day, 1)), unmarked_days)
|
||||||
|
# holidays not considered in unmarked days
|
||||||
|
self.assertNotIn(first_sunday, unmarked_days)
|
||||||
|
|
||||||
|
def test_unmarked_days_as_per_joining_and_relieving_dates(self):
|
||||||
|
first_day = get_first_day(getdate())
|
||||||
|
|
||||||
|
doj = add_days(first_day, 1)
|
||||||
|
relieving_date = add_days(first_day, 5)
|
||||||
|
employee = make_employee('test_unmarked_days_as_per_doj@example.com', date_of_joining=doj,
|
||||||
|
date_of_relieving=relieving_date)
|
||||||
|
frappe.db.delete('Attendance', {'employee': employee})
|
||||||
|
|
||||||
|
attendance_date = add_days(first_day, 2)
|
||||||
|
mark_attendance(employee, attendance_date, 'Present')
|
||||||
|
month_name = get_month_name(first_day)
|
||||||
|
|
||||||
|
unmarked_days = get_unmarked_days(employee, month_name)
|
||||||
|
unmarked_days = [getdate(date) for date in unmarked_days]
|
||||||
|
|
||||||
|
# attendance already marked for the day
|
||||||
|
self.assertNotIn(attendance_date, unmarked_days)
|
||||||
|
# date before doj not in unmarked days
|
||||||
|
self.assertNotIn(add_days(doj, -1), unmarked_days)
|
||||||
|
# date after relieving not in unmarked days
|
||||||
|
self.assertNotIn(add_days(relieving_date, 1), unmarked_days)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
frappe.db.rollback()
|
||||||
|
|
||||||
|
|
||||||
|
def get_month_name(date):
|
||||||
|
month_number = date.month
|
||||||
|
for month, number in get_month_map().items():
|
||||||
|
if number == month_number:
|
||||||
|
return month
|
||||||
@ -2,7 +2,7 @@
|
|||||||
"actions": [],
|
"actions": [],
|
||||||
"allow_import": 1,
|
"allow_import": 1,
|
||||||
"autoname": "naming_series:",
|
"autoname": "naming_series:",
|
||||||
"creation": "2017-10-09 14:26:29.612365",
|
"creation": "2022-01-17 18:36:51.450395",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
@ -121,7 +121,7 @@
|
|||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Status",
|
"label": "Status",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Draft\nPaid\nUnpaid\nClaimed\nCancelled",
|
"options": "Draft\nPaid\nUnpaid\nClaimed\nReturned\nPartly Claimed and Returned\nCancelled",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -200,7 +200,7 @@
|
|||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-09-11 18:38:38.617478",
|
"modified": "2022-01-17 19:33:52.345823",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "Employee Advance",
|
"name": "Employee Advance",
|
||||||
@ -237,5 +237,41 @@
|
|||||||
"search_fields": "employee,employee_name",
|
"search_fields": "employee,employee_name",
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [
|
||||||
|
{
|
||||||
|
"color": "Red",
|
||||||
|
"custom": 1,
|
||||||
|
"title": "Draft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "Green",
|
||||||
|
"custom": 1,
|
||||||
|
"title": "Paid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "Orange",
|
||||||
|
"custom": 1,
|
||||||
|
"title": "Unpaid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "Blue",
|
||||||
|
"custom": 1,
|
||||||
|
"title": "Claimed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "Gray",
|
||||||
|
"title": "Returned"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "Yellow",
|
||||||
|
"title": "Partly Claimed and Returned"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "Red",
|
||||||
|
"custom": 1,
|
||||||
|
"title": "Cancelled"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title_field": "employee_name",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
@ -27,19 +27,33 @@ class EmployeeAdvance(Document):
|
|||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.ignore_linked_doctypes = ('GL Entry')
|
self.ignore_linked_doctypes = ('GL Entry')
|
||||||
|
self.set_status(update=True)
|
||||||
|
|
||||||
|
def set_status(self, update=False):
|
||||||
|
precision = self.precision("paid_amount")
|
||||||
|
total_amount = flt(flt(self.claimed_amount) + flt(self.return_amount), precision)
|
||||||
|
status = None
|
||||||
|
|
||||||
def set_status(self):
|
|
||||||
if self.docstatus == 0:
|
if self.docstatus == 0:
|
||||||
self.status = "Draft"
|
status = "Draft"
|
||||||
if self.docstatus == 1:
|
elif self.docstatus == 1:
|
||||||
if self.claimed_amount and flt(self.claimed_amount) == flt(self.paid_amount):
|
if flt(self.claimed_amount) > 0 and flt(self.claimed_amount, precision) == flt(self.paid_amount, precision):
|
||||||
self.status = "Claimed"
|
status = "Claimed"
|
||||||
elif self.paid_amount and self.advance_amount == flt(self.paid_amount):
|
elif flt(self.return_amount) > 0 and flt(self.return_amount, precision) == flt(self.paid_amount, precision):
|
||||||
self.status = "Paid"
|
status = "Returned"
|
||||||
|
elif flt(self.claimed_amount) > 0 and (flt(self.return_amount) > 0) and total_amount == flt(self.paid_amount, precision):
|
||||||
|
status = "Partly Claimed and Returned"
|
||||||
|
elif flt(self.paid_amount) > 0 and flt(self.advance_amount, precision) == flt(self.paid_amount, precision):
|
||||||
|
status = "Paid"
|
||||||
else:
|
else:
|
||||||
self.status = "Unpaid"
|
status = "Unpaid"
|
||||||
elif self.docstatus == 2:
|
elif self.docstatus == 2:
|
||||||
self.status = "Cancelled"
|
status = "Cancelled"
|
||||||
|
|
||||||
|
if update:
|
||||||
|
self.db_set("status", status)
|
||||||
|
else:
|
||||||
|
self.status = status
|
||||||
|
|
||||||
def set_total_advance_paid(self):
|
def set_total_advance_paid(self):
|
||||||
gle = frappe.qb.DocType("GL Entry")
|
gle = frappe.qb.DocType("GL Entry")
|
||||||
@ -85,9 +99,7 @@ class EmployeeAdvance(Document):
|
|||||||
|
|
||||||
self.db_set("paid_amount", paid_amount)
|
self.db_set("paid_amount", paid_amount)
|
||||||
self.db_set("return_amount", return_amount)
|
self.db_set("return_amount", return_amount)
|
||||||
self.set_status()
|
self.set_status(update=True)
|
||||||
frappe.db.set_value("Employee Advance", self.name , "status", self.status)
|
|
||||||
|
|
||||||
|
|
||||||
def update_claimed_amount(self):
|
def update_claimed_amount(self):
|
||||||
claimed_amount = frappe.db.sql("""
|
claimed_amount = frappe.db.sql("""
|
||||||
@ -103,8 +115,8 @@ class EmployeeAdvance(Document):
|
|||||||
|
|
||||||
frappe.db.set_value("Employee Advance", self.name, "claimed_amount", flt(claimed_amount))
|
frappe.db.set_value("Employee Advance", self.name, "claimed_amount", flt(claimed_amount))
|
||||||
self.reload()
|
self.reload()
|
||||||
self.set_status()
|
self.set_status(update=True)
|
||||||
frappe.db.set_value("Employee Advance", self.name, "status", self.status)
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_pending_amount(employee, posting_date):
|
def get_pending_amount(employee, posting_date):
|
||||||
@ -222,7 +234,8 @@ def make_return_entry(employee, company, employee_advance_name, return_amount,
|
|||||||
'reference_name': employee_advance_name,
|
'reference_name': employee_advance_name,
|
||||||
'party_type': 'Employee',
|
'party_type': 'Employee',
|
||||||
'party': employee,
|
'party': employee,
|
||||||
'is_advance': 'Yes'
|
'is_advance': 'Yes',
|
||||||
|
'cost_center': erpnext.get_default_cost_center(company)
|
||||||
})
|
})
|
||||||
|
|
||||||
bank_amount = flt(return_amount) if bank_cash_account.account_currency==currency \
|
bank_amount = flt(return_amount) if bank_cash_account.account_currency==currency \
|
||||||
@ -233,7 +246,8 @@ def make_return_entry(employee, company, employee_advance_name, return_amount,
|
|||||||
"debit_in_account_currency": bank_amount,
|
"debit_in_account_currency": bank_amount,
|
||||||
"account_currency": bank_cash_account.account_currency,
|
"account_currency": bank_cash_account.account_currency,
|
||||||
"account_type": bank_cash_account.account_type,
|
"account_type": bank_cash_account.account_type,
|
||||||
"exchange_rate": flt(exchange_rate) if bank_cash_account.account_currency == currency else 1
|
"exchange_rate": flt(exchange_rate) if bank_cash_account.account_currency == currency else 1,
|
||||||
|
"cost_center": erpnext.get_default_cost_center(company)
|
||||||
})
|
})
|
||||||
|
|
||||||
return je.as_dict()
|
return je.as_dict()
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import nowdate
|
from frappe.utils import flt, nowdate
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||||
@ -12,12 +12,21 @@ from erpnext.hr.doctype.employee_advance.employee_advance import (
|
|||||||
EmployeeAdvanceOverPayment,
|
EmployeeAdvanceOverPayment,
|
||||||
create_return_through_additional_salary,
|
create_return_through_additional_salary,
|
||||||
make_bank_entry,
|
make_bank_entry,
|
||||||
|
make_return_entry,
|
||||||
|
)
|
||||||
|
from erpnext.hr.doctype.expense_claim.expense_claim import get_advances
|
||||||
|
from erpnext.hr.doctype.expense_claim.test_expense_claim import (
|
||||||
|
get_payable_account,
|
||||||
|
make_expense_claim,
|
||||||
)
|
)
|
||||||
from erpnext.payroll.doctype.salary_component.test_salary_component import create_salary_component
|
from erpnext.payroll.doctype.salary_component.test_salary_component import create_salary_component
|
||||||
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
||||||
|
|
||||||
|
|
||||||
class TestEmployeeAdvance(unittest.TestCase):
|
class TestEmployeeAdvance(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
frappe.db.delete("Employee Advance")
|
||||||
|
|
||||||
def test_paid_amount_and_status(self):
|
def test_paid_amount_and_status(self):
|
||||||
employee_name = make_employee("_T@employe.advance")
|
employee_name = make_employee("_T@employe.advance")
|
||||||
advance = make_employee_advance(employee_name)
|
advance = make_employee_advance(employee_name)
|
||||||
@ -52,9 +61,102 @@ class TestEmployeeAdvance(unittest.TestCase):
|
|||||||
self.assertEqual(advance.paid_amount, 0)
|
self.assertEqual(advance.paid_amount, 0)
|
||||||
self.assertEqual(advance.status, "Unpaid")
|
self.assertEqual(advance.status, "Unpaid")
|
||||||
|
|
||||||
|
advance.cancel()
|
||||||
|
advance.reload()
|
||||||
|
self.assertEqual(advance.status, "Cancelled")
|
||||||
|
|
||||||
|
def test_claimed_status(self):
|
||||||
|
# CLAIMED Status check, full amount claimed
|
||||||
|
payable_account = get_payable_account("_Test Company")
|
||||||
|
claim = make_expense_claim(payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True)
|
||||||
|
|
||||||
|
advance = make_employee_advance(claim.employee)
|
||||||
|
pe = make_payment_entry(advance)
|
||||||
|
pe.submit()
|
||||||
|
|
||||||
|
claim = get_advances_for_claim(claim, advance.name)
|
||||||
|
claim.save()
|
||||||
|
claim.submit()
|
||||||
|
|
||||||
|
advance.reload()
|
||||||
|
self.assertEqual(advance.claimed_amount, 1000)
|
||||||
|
self.assertEqual(advance.status, "Claimed")
|
||||||
|
|
||||||
|
# advance should not be shown in claims
|
||||||
|
advances = get_advances(claim.employee)
|
||||||
|
advances = [entry.name for entry in advances]
|
||||||
|
self.assertTrue(advance.name not in advances)
|
||||||
|
|
||||||
|
# cancel claim; status should be Paid
|
||||||
|
claim.cancel()
|
||||||
|
advance.reload()
|
||||||
|
self.assertEqual(advance.claimed_amount, 0)
|
||||||
|
self.assertEqual(advance.status, "Paid")
|
||||||
|
|
||||||
|
def test_partly_claimed_and_returned_status(self):
|
||||||
|
payable_account = get_payable_account("_Test Company")
|
||||||
|
claim = make_expense_claim(payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True)
|
||||||
|
|
||||||
|
advance = make_employee_advance(claim.employee)
|
||||||
|
pe = make_payment_entry(advance)
|
||||||
|
pe.submit()
|
||||||
|
|
||||||
|
# PARTLY CLAIMED AND RETURNED status check
|
||||||
|
# 500 Claimed, 500 Returned
|
||||||
|
claim = make_expense_claim(payable_account, 500, 500, "_Test Company", "Travel Expenses - _TC", do_not_submit=True)
|
||||||
|
|
||||||
|
advance = make_employee_advance(claim.employee)
|
||||||
|
pe = make_payment_entry(advance)
|
||||||
|
pe.submit()
|
||||||
|
|
||||||
|
claim = get_advances_for_claim(claim, advance.name, amount=500)
|
||||||
|
claim.save()
|
||||||
|
claim.submit()
|
||||||
|
|
||||||
|
advance.reload()
|
||||||
|
self.assertEqual(advance.claimed_amount, 500)
|
||||||
|
self.assertEqual(advance.status, "Paid")
|
||||||
|
|
||||||
|
entry = make_return_entry(
|
||||||
|
employee=advance.employee,
|
||||||
|
company=advance.company,
|
||||||
|
employee_advance_name=advance.name,
|
||||||
|
return_amount=flt(advance.paid_amount - advance.claimed_amount),
|
||||||
|
advance_account=advance.advance_account,
|
||||||
|
mode_of_payment=advance.mode_of_payment,
|
||||||
|
currency=advance.currency,
|
||||||
|
exchange_rate=advance.exchange_rate
|
||||||
|
)
|
||||||
|
|
||||||
|
entry = frappe.get_doc(entry)
|
||||||
|
entry.insert()
|
||||||
|
entry.submit()
|
||||||
|
|
||||||
|
advance.reload()
|
||||||
|
self.assertEqual(advance.return_amount, 500)
|
||||||
|
self.assertEqual(advance.status, "Partly Claimed and Returned")
|
||||||
|
|
||||||
|
# advance should not be shown in claims
|
||||||
|
advances = get_advances(claim.employee)
|
||||||
|
advances = [entry.name for entry in advances]
|
||||||
|
self.assertTrue(advance.name not in advances)
|
||||||
|
|
||||||
|
# Cancel return entry; status should change to PAID
|
||||||
|
entry.cancel()
|
||||||
|
advance.reload()
|
||||||
|
self.assertEqual(advance.return_amount, 0)
|
||||||
|
self.assertEqual(advance.status, "Paid")
|
||||||
|
|
||||||
|
# advance should be shown in claims
|
||||||
|
advances = get_advances(claim.employee)
|
||||||
|
advances = [entry.name for entry in advances]
|
||||||
|
self.assertTrue(advance.name in advances)
|
||||||
|
|
||||||
def test_repay_unclaimed_amount_from_salary(self):
|
def test_repay_unclaimed_amount_from_salary(self):
|
||||||
employee_name = make_employee("_T@employe.advance")
|
employee_name = make_employee("_T@employe.advance")
|
||||||
advance = make_employee_advance(employee_name, {"repay_unclaimed_amount_from_salary": 1})
|
advance = make_employee_advance(employee_name, {"repay_unclaimed_amount_from_salary": 1})
|
||||||
|
pe = make_payment_entry(advance)
|
||||||
|
pe.submit()
|
||||||
|
|
||||||
args = {"type": "Deduction"}
|
args = {"type": "Deduction"}
|
||||||
create_salary_component("Advance Salary - Deduction", **args)
|
create_salary_component("Advance Salary - Deduction", **args)
|
||||||
@ -82,11 +184,13 @@ class TestEmployeeAdvance(unittest.TestCase):
|
|||||||
|
|
||||||
advance.reload()
|
advance.reload()
|
||||||
self.assertEqual(advance.return_amount, 1000)
|
self.assertEqual(advance.return_amount, 1000)
|
||||||
|
self.assertEqual(advance.status, "Returned")
|
||||||
|
|
||||||
# update advance return amount on additional salary cancellation
|
# update advance return amount on additional salary cancellation
|
||||||
additional_salary.cancel()
|
additional_salary.cancel()
|
||||||
advance.reload()
|
advance.reload()
|
||||||
self.assertEqual(advance.return_amount, 700)
|
self.assertEqual(advance.return_amount, 700)
|
||||||
|
self.assertEqual(advance.status, "Paid")
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
@ -118,3 +222,24 @@ def make_employee_advance(employee_name, args=None):
|
|||||||
doc.submit()
|
doc.submit()
|
||||||
|
|
||||||
return doc
|
return doc
|
||||||
|
|
||||||
|
|
||||||
|
def get_advances_for_claim(claim, advance_name, amount=None):
|
||||||
|
advances = get_advances(claim.employee, advance_name)
|
||||||
|
|
||||||
|
for entry in advances:
|
||||||
|
if amount:
|
||||||
|
allocated_amount = amount
|
||||||
|
else:
|
||||||
|
allocated_amount = flt(entry.paid_amount) - flt(entry.claimed_amount)
|
||||||
|
|
||||||
|
claim.append("advances", {
|
||||||
|
"employee_advance": entry.name,
|
||||||
|
"posting_date": entry.posting_date,
|
||||||
|
"advance_account": entry.advance_account,
|
||||||
|
"advance_paid": entry.paid_amount,
|
||||||
|
"unclaimed_amount": allocated_amount,
|
||||||
|
"allocated_amount": allocated_amount
|
||||||
|
})
|
||||||
|
|
||||||
|
return claim
|
||||||
@ -171,7 +171,7 @@ frappe.ui.form.on("Expense Claim", {
|
|||||||
['docstatus', '=', 1],
|
['docstatus', '=', 1],
|
||||||
['employee', '=', frm.doc.employee],
|
['employee', '=', frm.doc.employee],
|
||||||
['paid_amount', '>', 0],
|
['paid_amount', '>', 0],
|
||||||
['status', '!=', 'Claimed']
|
['status', 'not in', ['Claimed', 'Returned', 'Partly Claimed and Returned']]
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@ -23,10 +23,10 @@ class ExpenseClaim(AccountsController):
|
|||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
validate_active_employee(self.employee)
|
validate_active_employee(self.employee)
|
||||||
self.validate_advances()
|
set_employee_name(self)
|
||||||
self.validate_sanctioned_amount()
|
self.validate_sanctioned_amount()
|
||||||
self.calculate_total_amount()
|
self.calculate_total_amount()
|
||||||
set_employee_name(self)
|
self.validate_advances()
|
||||||
self.set_expense_account(validate=True)
|
self.set_expense_account(validate=True)
|
||||||
self.set_payable_account()
|
self.set_payable_account()
|
||||||
self.set_cost_center()
|
self.set_cost_center()
|
||||||
@ -341,18 +341,27 @@ def get_expense_claim_account(expense_claim_type, company):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_advances(employee, advance_id=None):
|
def get_advances(employee, advance_id=None):
|
||||||
if not advance_id:
|
advance = frappe.qb.DocType("Employee Advance")
|
||||||
condition = 'docstatus=1 and employee={0} and paid_amount > 0 and paid_amount > claimed_amount + return_amount'.format(frappe.db.escape(employee))
|
|
||||||
else:
|
|
||||||
condition = 'name={0}'.format(frappe.db.escape(advance_id))
|
|
||||||
|
|
||||||
return frappe.db.sql("""
|
query = (
|
||||||
select
|
frappe.qb.from_(advance)
|
||||||
name, posting_date, paid_amount, claimed_amount, advance_account
|
.select(
|
||||||
from
|
advance.name, advance.posting_date, advance.paid_amount,
|
||||||
`tabEmployee Advance`
|
advance.claimed_amount, advance.advance_account
|
||||||
where {0}
|
)
|
||||||
""".format(condition), as_dict=1)
|
)
|
||||||
|
|
||||||
|
if not advance_id:
|
||||||
|
query = query.where(
|
||||||
|
(advance.docstatus == 1)
|
||||||
|
& (advance.employee == employee)
|
||||||
|
& (advance.paid_amount > 0)
|
||||||
|
& (advance.status.notin(["Claimed", "Returned", "Partly Claimed and Returned"]))
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
query = query.where(advance.name == advance_id)
|
||||||
|
|
||||||
|
return query.run(as_dict=True)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|||||||
@ -918,7 +918,7 @@ def validate_bom_no(item, bom_no):
|
|||||||
frappe.throw(_("BOM {0} does not belong to Item {1}").format(bom_no, item))
|
frappe.throw(_("BOM {0} does not belong to Item {1}").format(bom_no, item))
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_children(doctype, parent=None, is_root=False, **filters):
|
def get_children(parent=None, is_root=False, **filters):
|
||||||
if not parent or parent=="BOM":
|
if not parent or parent=="BOM":
|
||||||
frappe.msgprint(_('Please select a BOM'))
|
frappe.msgprint(_('Please select a BOM'))
|
||||||
return
|
return
|
||||||
|
|||||||
@ -7,7 +7,7 @@ def get_data():
|
|||||||
'transactions': [
|
'transactions': [
|
||||||
{
|
{
|
||||||
'label': _('Manufacture'),
|
'label': _('Manufacture'),
|
||||||
'items': ['BOM', 'Work Order', 'Job Card', 'Timesheet']
|
'items': ['BOM', 'Work Order', 'Job Card']
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -232,7 +232,7 @@ frappe.ui.form.on('Production Plan', {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
combine_items: function (frm) {
|
combine_items: function (frm) {
|
||||||
frm.clear_table('prod_plan_references');
|
frm.clear_table("prod_plan_references");
|
||||||
|
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "get_items",
|
method: "get_items",
|
||||||
@ -247,6 +247,13 @@ frappe.ui.form.on('Production Plan', {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
combine_sub_items: (frm) => {
|
||||||
|
if (frm.doc.sub_assembly_items.length > 0) {
|
||||||
|
frm.clear_table("sub_assembly_items");
|
||||||
|
frm.trigger("get_sub_assembly_items");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
get_sub_assembly_items: function(frm) {
|
get_sub_assembly_items: function(frm) {
|
||||||
frm.dirty();
|
frm.dirty();
|
||||||
|
|
||||||
|
|||||||
@ -36,6 +36,7 @@
|
|||||||
"prod_plan_references",
|
"prod_plan_references",
|
||||||
"section_break_24",
|
"section_break_24",
|
||||||
"get_sub_assembly_items",
|
"get_sub_assembly_items",
|
||||||
|
"combine_sub_items",
|
||||||
"sub_assembly_items",
|
"sub_assembly_items",
|
||||||
"material_request_planning",
|
"material_request_planning",
|
||||||
"include_non_stock_items",
|
"include_non_stock_items",
|
||||||
@ -340,7 +341,6 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "prod_plan_references",
|
"fieldname": "prod_plan_references",
|
||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
"hidden": 1,
|
|
||||||
"label": "Production Plan Item Reference",
|
"label": "Production Plan Item Reference",
|
||||||
"options": "Production Plan Item Reference"
|
"options": "Production Plan Item Reference"
|
||||||
},
|
},
|
||||||
@ -370,16 +370,23 @@
|
|||||||
"fieldname": "to_delivery_date",
|
"fieldname": "to_delivery_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"label": "To Delivery Date"
|
"label": "To Delivery Date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "combine_sub_items",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Consolidate Sub Assembly Items"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-calendar",
|
"icon": "fa fa-calendar",
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-09-06 18:35:59.642232",
|
"modified": "2022-02-23 17:16:10.629378",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Production Plan",
|
"name": "Production Plan",
|
||||||
|
"naming_rule": "By \"Naming Series\" field",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -21,7 +21,8 @@ from frappe.utils import (
|
|||||||
)
|
)
|
||||||
from frappe.utils.csvutils import build_csv_response
|
from frappe.utils.csvutils import build_csv_response
|
||||||
|
|
||||||
from erpnext.manufacturing.doctype.bom.bom import get_children, validate_bom_no
|
from erpnext.manufacturing.doctype.bom.bom import get_children as get_bom_children
|
||||||
|
from erpnext.manufacturing.doctype.bom.bom import validate_bom_no
|
||||||
from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
|
from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
|
||||||
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
|
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
|
||||||
|
|
||||||
@ -570,17 +571,28 @@ class ProductionPlan(Document):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_sub_assembly_items(self, manufacturing_type=None):
|
def get_sub_assembly_items(self, manufacturing_type=None):
|
||||||
|
"Fetch sub assembly items and optionally combine them."
|
||||||
self.sub_assembly_items = []
|
self.sub_assembly_items = []
|
||||||
|
sub_assembly_items_store = [] # temporary store to process all subassembly items
|
||||||
|
|
||||||
for row in self.po_items:
|
for row in self.po_items:
|
||||||
bom_data = []
|
bom_data = []
|
||||||
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty)
|
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty)
|
||||||
self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
|
self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
|
||||||
|
sub_assembly_items_store.extend(bom_data)
|
||||||
|
|
||||||
self.sub_assembly_items.sort(key= lambda d: d.bom_level, reverse=True)
|
if self.combine_sub_items:
|
||||||
for idx, row in enumerate(self.sub_assembly_items, start=1):
|
# Combine subassembly items
|
||||||
row.idx = idx
|
sub_assembly_items_store = self.combine_subassembly_items(sub_assembly_items_store)
|
||||||
|
|
||||||
|
sub_assembly_items_store.sort(key= lambda d: d.bom_level, reverse=True) # sort by bom level
|
||||||
|
|
||||||
|
for idx, row in enumerate(sub_assembly_items_store):
|
||||||
|
row.idx = idx + 1
|
||||||
|
self.append("sub_assembly_items", row)
|
||||||
|
|
||||||
def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None):
|
def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None):
|
||||||
|
"Modify bom_data, set additional details."
|
||||||
for data in bom_data:
|
for data in bom_data:
|
||||||
data.qty = data.stock_qty
|
data.qty = data.stock_qty
|
||||||
data.production_plan_item = row.name
|
data.production_plan_item = row.name
|
||||||
@ -589,7 +601,32 @@ class ProductionPlan(Document):
|
|||||||
data.type_of_manufacturing = manufacturing_type or ("Subcontract" if data.is_sub_contracted_item
|
data.type_of_manufacturing = manufacturing_type or ("Subcontract" if data.is_sub_contracted_item
|
||||||
else "In House")
|
else "In House")
|
||||||
|
|
||||||
self.append("sub_assembly_items", data)
|
def combine_subassembly_items(self, sub_assembly_items_store):
|
||||||
|
"Aggregate if same: Item, Warehouse, Inhouse/Outhouse Manu.g, BOM No."
|
||||||
|
key_wise_data = {}
|
||||||
|
for row in sub_assembly_items_store:
|
||||||
|
key = (
|
||||||
|
row.get("production_item"), row.get("fg_warehouse"),
|
||||||
|
row.get("bom_no"), row.get("type_of_manufacturing")
|
||||||
|
)
|
||||||
|
if key not in key_wise_data:
|
||||||
|
# intialise (item, wh, bom no, man.g type) wise dict
|
||||||
|
key_wise_data[key] = row
|
||||||
|
continue
|
||||||
|
|
||||||
|
existing_row = key_wise_data[key]
|
||||||
|
if existing_row:
|
||||||
|
# if row with same (item, wh, bom no, man.g type) key, merge
|
||||||
|
existing_row.qty += flt(row.qty)
|
||||||
|
existing_row.stock_qty += flt(row.stock_qty)
|
||||||
|
existing_row.bom_level = max(existing_row.bom_level, row.bom_level)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
# add row with key
|
||||||
|
key_wise_data[key] = row
|
||||||
|
|
||||||
|
sub_assembly_items_store = [key_wise_data[key] for key in key_wise_data] # unpack into single level list
|
||||||
|
return sub_assembly_items_store
|
||||||
|
|
||||||
def all_items_completed(self):
|
def all_items_completed(self):
|
||||||
all_items_produced = all(flt(d.planned_qty) - flt(d.produced_qty) < 0.000001
|
all_items_produced = all(flt(d.planned_qty) - flt(d.produced_qty) < 0.000001
|
||||||
@ -1031,7 +1068,7 @@ def get_item_data(item_code):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0):
|
def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0):
|
||||||
data = get_children('BOM', parent = bom_no)
|
data = get_bom_children(parent=bom_no)
|
||||||
for d in data:
|
for d in data:
|
||||||
if d.expandable:
|
if d.expandable:
|
||||||
parent_item_code = frappe.get_cached_value("BOM", bom_no, "item")
|
parent_item_code = frappe.get_cached_value("BOM", bom_no, "item")
|
||||||
|
|||||||
@ -38,6 +38,9 @@ class TestProductionPlan(FrappeTestCase):
|
|||||||
if not frappe.db.get_value('BOM', {'item': item}):
|
if not frappe.db.get_value('BOM', {'item': item}):
|
||||||
make_bom(item = item, raw_materials = raw_materials)
|
make_bom(item = item, raw_materials = raw_materials)
|
||||||
|
|
||||||
|
def tearDown(self) -> None:
|
||||||
|
frappe.db.rollback()
|
||||||
|
|
||||||
def test_production_plan_mr_creation(self):
|
def test_production_plan_mr_creation(self):
|
||||||
"Test if MRs are created for unavailable raw materials."
|
"Test if MRs are created for unavailable raw materials."
|
||||||
pln = create_production_plan(item_code='Test Production Item 1')
|
pln = create_production_plan(item_code='Test Production Item 1')
|
||||||
@ -110,7 +113,7 @@ class TestProductionPlan(FrappeTestCase):
|
|||||||
item_code='Test Production Item 1',
|
item_code='Test Production Item 1',
|
||||||
ignore_existing_ordered_qty=1
|
ignore_existing_ordered_qty=1
|
||||||
)
|
)
|
||||||
self.assertTrue(len(pln.mr_items), 1)
|
self.assertTrue(len(pln.mr_items))
|
||||||
self.assertTrue(flt(pln.mr_items[0].quantity), 1.0)
|
self.assertTrue(flt(pln.mr_items[0].quantity), 1.0)
|
||||||
|
|
||||||
sr1.cancel()
|
sr1.cancel()
|
||||||
@ -151,7 +154,7 @@ class TestProductionPlan(FrappeTestCase):
|
|||||||
use_multi_level_bom=0,
|
use_multi_level_bom=0,
|
||||||
ignore_existing_ordered_qty=0
|
ignore_existing_ordered_qty=0
|
||||||
)
|
)
|
||||||
self.assertTrue(len(pln.mr_items), 0)
|
self.assertFalse(len(pln.mr_items))
|
||||||
|
|
||||||
sr1.cancel()
|
sr1.cancel()
|
||||||
sr2.cancel()
|
sr2.cancel()
|
||||||
@ -258,6 +261,51 @@ class TestProductionPlan(FrappeTestCase):
|
|||||||
pln.reload()
|
pln.reload()
|
||||||
pln.cancel()
|
pln.cancel()
|
||||||
|
|
||||||
|
def test_production_plan_combine_subassembly(self):
|
||||||
|
"""
|
||||||
|
Test combining Sub assembly items belonging to the same BOM in Prod Plan.
|
||||||
|
1) Red-Car -> Wheel (sub assembly) > BOM-WHEEL-001
|
||||||
|
2) Green-Car -> Wheel (sub assembly) > BOM-WHEEL-001
|
||||||
|
"""
|
||||||
|
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
||||||
|
|
||||||
|
bom_tree_1 = {
|
||||||
|
"Red-Car": {"Wheel": {"Rubber": {}}}
|
||||||
|
}
|
||||||
|
bom_tree_2 = {
|
||||||
|
"Green-Car": {"Wheel": {"Rubber": {}}}
|
||||||
|
}
|
||||||
|
|
||||||
|
parent_bom_1 = create_nested_bom(bom_tree_1, prefix="")
|
||||||
|
parent_bom_2 = create_nested_bom(bom_tree_2, prefix="")
|
||||||
|
|
||||||
|
# make sure both boms use same subassembly bom
|
||||||
|
subassembly_bom = parent_bom_1.items[0].bom_no
|
||||||
|
frappe.db.set_value("BOM Item", parent_bom_2.items[0].name, "bom_no", subassembly_bom)
|
||||||
|
|
||||||
|
plan = create_production_plan(item_code="Red-Car", use_multi_level_bom=1, do_not_save=True)
|
||||||
|
plan.append("po_items", { # Add Green-Car to Prod Plan
|
||||||
|
'use_multi_level_bom': 1,
|
||||||
|
'item_code': "Green-Car",
|
||||||
|
'bom_no': frappe.db.get_value('Item', "Green-Car", 'default_bom'),
|
||||||
|
'planned_qty': 1,
|
||||||
|
'planned_start_date': now_datetime()
|
||||||
|
})
|
||||||
|
plan.get_sub_assembly_items()
|
||||||
|
self.assertTrue(len(plan.sub_assembly_items), 2)
|
||||||
|
|
||||||
|
plan.combine_sub_items = 1
|
||||||
|
plan.get_sub_assembly_items()
|
||||||
|
|
||||||
|
self.assertTrue(len(plan.sub_assembly_items), 1) # check if sub-assembly items merged
|
||||||
|
self.assertEqual(plan.sub_assembly_items[0].qty, 2.0)
|
||||||
|
self.assertEqual(plan.sub_assembly_items[0].stock_qty, 2.0)
|
||||||
|
|
||||||
|
# change warehouse in one row, sub-assemblies should not merge
|
||||||
|
plan.po_items[0].warehouse = "Finished Goods - _TC"
|
||||||
|
plan.get_sub_assembly_items()
|
||||||
|
self.assertTrue(len(plan.sub_assembly_items), 2)
|
||||||
|
|
||||||
def test_pp_to_mr_customer_provided(self):
|
def test_pp_to_mr_customer_provided(self):
|
||||||
" Test Material Request from Production Plan for Customer Provided Item."
|
" Test Material Request from Production Plan for Customer Provided Item."
|
||||||
create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)
|
create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)
|
||||||
@ -532,6 +580,7 @@ class TestProductionPlan(FrappeTestCase):
|
|||||||
wip_warehouse='Work In Progress - _TC',
|
wip_warehouse='Work In Progress - _TC',
|
||||||
fg_warehouse='Finished Goods - _TC',
|
fg_warehouse='Finished Goods - _TC',
|
||||||
skip_transfer=1,
|
skip_transfer=1,
|
||||||
|
use_multi_level_bom=1,
|
||||||
do_not_submit=True
|
do_not_submit=True
|
||||||
)
|
)
|
||||||
wo.production_plan = pln.name
|
wo.production_plan = pln.name
|
||||||
@ -576,6 +625,7 @@ class TestProductionPlan(FrappeTestCase):
|
|||||||
wip_warehouse='Work In Progress - _TC',
|
wip_warehouse='Work In Progress - _TC',
|
||||||
fg_warehouse='Finished Goods - _TC',
|
fg_warehouse='Finished Goods - _TC',
|
||||||
skip_transfer=1,
|
skip_transfer=1,
|
||||||
|
use_multi_level_bom=1,
|
||||||
do_not_submit=True
|
do_not_submit=True
|
||||||
)
|
)
|
||||||
wo.production_plan = pln.name
|
wo.production_plan = pln.name
|
||||||
|
|||||||
@ -17,7 +17,7 @@ frappe.ui.form.on('Routing', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
calculate_operating_cost: function(frm, child) {
|
calculate_operating_cost: function(frm, child) {
|
||||||
const operating_cost = flt(flt(child.hour_rate) * flt(child.time_in_mins) / 60, 2);
|
const operating_cost = flt(flt(child.hour_rate) * flt(child.time_in_mins) / 60, precision("operating_cost", child));
|
||||||
frappe.model.set_value(child.doctype, child.name, "operating_cost", operating_cost);
|
frappe.model.set_value(child.doctype, child.name, "operating_cost", operating_cost);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -20,7 +20,8 @@ class Routing(Document):
|
|||||||
for operation in self.operations:
|
for operation in self.operations:
|
||||||
if not operation.hour_rate:
|
if not operation.hour_rate:
|
||||||
operation.hour_rate = frappe.db.get_value("Workstation", operation.workstation, 'hour_rate')
|
operation.hour_rate = frappe.db.get_value("Workstation", operation.workstation, 'hour_rate')
|
||||||
operation.operating_cost = flt(flt(operation.hour_rate) * flt(operation.time_in_mins) / 60, 2)
|
operation.operating_cost = flt(flt(operation.hour_rate) * flt(operation.time_in_mins) / 60,
|
||||||
|
operation.precision("operating_cost"))
|
||||||
|
|
||||||
def set_routing_id(self):
|
def set_routing_id(self):
|
||||||
sequence_id = 0
|
sequence_id = 0
|
||||||
|
|||||||
@ -1040,7 +1040,7 @@ def make_wo_order_test_record(**args):
|
|||||||
wo_order.scrap_warehouse = args.fg_warehouse or "_Test Scrap Warehouse - _TC"
|
wo_order.scrap_warehouse = args.fg_warehouse or "_Test Scrap Warehouse - _TC"
|
||||||
wo_order.company = args.company or "_Test Company"
|
wo_order.company = args.company or "_Test Company"
|
||||||
wo_order.stock_uom = args.stock_uom or "_Test UOM"
|
wo_order.stock_uom = args.stock_uom or "_Test UOM"
|
||||||
wo_order.use_multi_level_bom=0
|
wo_order.use_multi_level_bom= args.use_multi_level_bom or 0
|
||||||
wo_order.skip_transfer=args.skip_transfer or 0
|
wo_order.skip_transfer=args.skip_transfer or 0
|
||||||
wo_order.get_items_and_operations_from_bom()
|
wo_order.get_items_and_operations_from_bom()
|
||||||
wo_order.sales_order = args.sales_order or None
|
wo_order.sales_order = args.sales_order or None
|
||||||
|
|||||||
@ -11,9 +11,9 @@ def get_data():
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': _('Transaction'),
|
'label': _('Transaction'),
|
||||||
'items': ['Work Order', 'Job Card', 'Timesheet']
|
'items': ['Work Order', 'Job Card',]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
'disable_create_buttons': ['BOM', 'Routing', 'Operation',
|
'disable_create_buttons': ['BOM', 'Routing', 'Operation',
|
||||||
'Work Order', 'Job Card', 'Timesheet']
|
'Work Order', 'Job Card',]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -356,4 +356,5 @@ erpnext.patches.v14_0.delete_amazon_mws_doctype
|
|||||||
erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr
|
erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr
|
||||||
erpnext.patches.v13_0.update_accounts_in_loan_docs
|
erpnext.patches.v13_0.update_accounts_in_loan_docs
|
||||||
erpnext.patches.v14_0.update_batch_valuation_flag
|
erpnext.patches.v14_0.update_batch_valuation_flag
|
||||||
erpnext.patches.v14_0.delete_non_profit_doctypes
|
erpnext.patches.v14_0.delete_non_profit_doctypes
|
||||||
|
erpnext.patches.v14_0.update_employee_advance_status
|
||||||
26
erpnext/patches/v14_0/update_employee_advance_status.py
Normal file
26
erpnext/patches/v14_0/update_employee_advance_status.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.reload_doc('hr', 'doctype', 'employee_advance')
|
||||||
|
|
||||||
|
advance = frappe.qb.DocType('Employee Advance')
|
||||||
|
(frappe.qb
|
||||||
|
.update(advance)
|
||||||
|
.set(advance.status, 'Returned')
|
||||||
|
.where(
|
||||||
|
(advance.docstatus == 1)
|
||||||
|
& ((advance.return_amount) & (advance.paid_amount == advance.return_amount))
|
||||||
|
& (advance.status == 'Paid')
|
||||||
|
)
|
||||||
|
).run()
|
||||||
|
|
||||||
|
(frappe.qb
|
||||||
|
.update(advance)
|
||||||
|
.set(advance.status, 'Partly Claimed and Returned')
|
||||||
|
.where(
|
||||||
|
(advance.docstatus == 1)
|
||||||
|
& ((advance.claimed_amount & advance.return_amount) & (advance.paid_amount == (advance.return_amount + advance.claimed_amount)))
|
||||||
|
& (advance.status == 'Paid')
|
||||||
|
)
|
||||||
|
).run()
|
||||||
@ -105,6 +105,8 @@ class AdditionalSalary(Document):
|
|||||||
return_amount += self.amount
|
return_amount += self.amount
|
||||||
|
|
||||||
frappe.db.set_value("Employee Advance", self.ref_docname, "return_amount", return_amount)
|
frappe.db.set_value("Employee Advance", self.ref_docname, "return_amount", return_amount)
|
||||||
|
advance = frappe.get_doc("Employee Advance", self.ref_docname)
|
||||||
|
advance.set_status(update=True)
|
||||||
|
|
||||||
def update_employee_referral(self, cancel=False):
|
def update_employee_referral(self, cancel=False):
|
||||||
if self.ref_doctype == "Employee Referral":
|
if self.ref_doctype == "Employee Referral":
|
||||||
|
|||||||
@ -307,28 +307,59 @@ class SalarySlip(TransactionBase):
|
|||||||
if payroll_based_on == "Attendance":
|
if payroll_based_on == "Attendance":
|
||||||
self.payment_days -= flt(absent)
|
self.payment_days -= flt(absent)
|
||||||
|
|
||||||
unmarked_days = self.get_unmarked_days()
|
|
||||||
consider_unmarked_attendance_as = frappe.db.get_value("Payroll Settings", None, "consider_unmarked_attendance_as") or "Present"
|
consider_unmarked_attendance_as = frappe.db.get_value("Payroll Settings", None, "consider_unmarked_attendance_as") or "Present"
|
||||||
|
|
||||||
if payroll_based_on == "Attendance" and consider_unmarked_attendance_as =="Absent":
|
if payroll_based_on == "Attendance" and consider_unmarked_attendance_as =="Absent":
|
||||||
|
unmarked_days = self.get_unmarked_days(include_holidays_in_total_working_days)
|
||||||
self.absent_days += unmarked_days #will be treated as absent
|
self.absent_days += unmarked_days #will be treated as absent
|
||||||
self.payment_days -= unmarked_days
|
self.payment_days -= unmarked_days
|
||||||
if include_holidays_in_total_working_days:
|
|
||||||
for holiday in holidays:
|
|
||||||
if not frappe.db.exists("Attendance", {"employee": self.employee, "attendance_date": holiday, "docstatus": 1 }):
|
|
||||||
self.payment_days += 1
|
|
||||||
else:
|
else:
|
||||||
self.payment_days = 0
|
self.payment_days = 0
|
||||||
|
|
||||||
def get_unmarked_days(self):
|
def get_unmarked_days(self, include_holidays_in_total_working_days):
|
||||||
marked_days = frappe.get_all("Attendance", filters = {
|
unmarked_days = self.total_working_days
|
||||||
"attendance_date": ["between", [self.start_date, self.end_date]],
|
joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
|
||||||
"employee": self.employee,
|
["date_of_joining", "relieving_date"])
|
||||||
"docstatus": 1
|
start_date = self.start_date
|
||||||
}, fields = ["COUNT(*) as marked_days"])[0].marked_days
|
end_date = self.end_date
|
||||||
|
|
||||||
return self.total_working_days - marked_days
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# exclude days for which attendance has been marked
|
||||||
|
unmarked_days -= frappe.get_all("Attendance", filters = {
|
||||||
|
"attendance_date": ["between", [start_date, end_date]],
|
||||||
|
"employee": self.employee,
|
||||||
|
"docstatus": 1
|
||||||
|
}, fields = ["COUNT(*) as marked_days"])[0].marked_days
|
||||||
|
|
||||||
|
return unmarked_days
|
||||||
|
|
||||||
|
def get_unmarked_days_based_on_doj_or_relieving(self, unmarked_days,
|
||||||
|
include_holidays_in_total_working_days, start_date, end_date):
|
||||||
|
"""
|
||||||
|
Exclude days before DOJ or after
|
||||||
|
Relieving Date from unmarked days
|
||||||
|
"""
|
||||||
|
from erpnext.hr.doctype.employee.employee import is_holiday
|
||||||
|
|
||||||
|
if include_holidays_in_total_working_days:
|
||||||
|
unmarked_days -= date_diff(end_date, start_date)
|
||||||
|
else:
|
||||||
|
# exclude only if not holidays
|
||||||
|
for days in range(date_diff(end_date, start_date)):
|
||||||
|
date = add_days(end_date, -days)
|
||||||
|
if not is_holiday(self.employee, date):
|
||||||
|
unmarked_days -= 1
|
||||||
|
|
||||||
|
return unmarked_days
|
||||||
|
|
||||||
def get_payment_days(self, joining_date, relieving_date, include_holidays_in_total_working_days):
|
def get_payment_days(self, joining_date, relieving_date, include_holidays_in_total_working_days):
|
||||||
if not joining_date:
|
if not joining_date:
|
||||||
|
|||||||
@ -7,10 +7,12 @@ import unittest
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
from frappe.tests.utils import change_settings
|
||||||
from frappe.utils import (
|
from frappe.utils import (
|
||||||
add_days,
|
add_days,
|
||||||
add_months,
|
add_months,
|
||||||
cstr,
|
cstr,
|
||||||
|
date_diff,
|
||||||
flt,
|
flt,
|
||||||
get_first_day,
|
get_first_day,
|
||||||
get_last_day,
|
get_last_day,
|
||||||
@ -21,6 +23,7 @@ from frappe.utils.make_random import get_random
|
|||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.accounts.utils import get_fiscal_year
|
from erpnext.accounts.utils import get_fiscal_year
|
||||||
|
from erpnext.hr.doctype.attendance.attendance import mark_attendance
|
||||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||||
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
|
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
|
||||||
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
|
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
|
||||||
@ -37,17 +40,17 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
setup_test()
|
setup_test()
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
|
frappe.db.rollback()
|
||||||
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0)
|
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0)
|
||||||
frappe.set_user("Administrator")
|
frappe.set_user("Administrator")
|
||||||
|
|
||||||
|
@change_settings("Payroll Settings", {
|
||||||
|
"payroll_based_on": "Attendance",
|
||||||
|
"daily_wages_fraction_for_half_day": 0.75
|
||||||
|
})
|
||||||
def test_payment_days_based_on_attendance(self):
|
def test_payment_days_based_on_attendance(self):
|
||||||
from erpnext.hr.doctype.attendance.attendance import mark_attendance
|
|
||||||
no_of_days = self.get_no_of_days()
|
no_of_days = self.get_no_of_days()
|
||||||
|
|
||||||
# Payroll based on attendance
|
|
||||||
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance")
|
|
||||||
frappe.db.set_value("Payroll Settings", None, "daily_wages_fraction_for_half_day", 0.75)
|
|
||||||
|
|
||||||
emp_id = make_employee("test_payment_days_based_on_attendance@salary.com")
|
emp_id = make_employee("test_payment_days_based_on_attendance@salary.com")
|
||||||
frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
|
frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
|
||||||
|
|
||||||
@ -85,14 +88,78 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(ss.gross_pay, gross_pay)
|
self.assertEqual(ss.gross_pay, gross_pay)
|
||||||
|
|
||||||
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
|
@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(self):
|
||||||
|
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
|
||||||
|
|
||||||
|
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)
|
||||||
|
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):
|
||||||
|
date = add_days(joining_date, days)
|
||||||
|
if not is_holiday("Salary Slip Test Holiday List", date):
|
||||||
|
mark_attendance(new_emp_id, date, 'Present', ignore_validate=True)
|
||||||
|
else:
|
||||||
|
holidays += 1
|
||||||
|
|
||||||
|
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] - holidays - 8)
|
||||||
|
|
||||||
|
@change_settings("Payroll Settings", {
|
||||||
|
"payroll_based_on": "Attendance",
|
||||||
|
"consider_unmarked_attendance_as": "Absent",
|
||||||
|
"include_holidays_in_total_working_days": False
|
||||||
|
})
|
||||||
|
def test_payment_days_for_mid_joinee_excluding_holidays(self):
|
||||||
|
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
|
||||||
|
|
||||||
|
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)
|
||||||
|
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):
|
||||||
|
date = add_days(joining_date, days)
|
||||||
|
if not is_holiday("Salary Slip Test Holiday List", date):
|
||||||
|
mark_attendance(new_emp_id, date, 'Present', ignore_validate=True)
|
||||||
|
else:
|
||||||
|
holidays += 1
|
||||||
|
|
||||||
|
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] - no_of_days[1])
|
||||||
|
self.assertEqual(new_ss.payment_days, no_of_days[0] - holidays - 8)
|
||||||
|
|
||||||
|
@change_settings("Payroll Settings", {
|
||||||
|
"payroll_based_on": "Leave"
|
||||||
|
})
|
||||||
def test_payment_days_based_on_leave_application(self):
|
def test_payment_days_based_on_leave_application(self):
|
||||||
no_of_days = self.get_no_of_days()
|
no_of_days = self.get_no_of_days()
|
||||||
|
|
||||||
# Payroll based on attendance
|
|
||||||
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
|
|
||||||
|
|
||||||
emp_id = make_employee("test_payment_days_based_on_leave_application@salary.com")
|
emp_id = make_employee("test_payment_days_based_on_leave_application@salary.com")
|
||||||
frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
|
frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
|
||||||
|
|
||||||
@ -133,8 +200,9 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 4)
|
self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 4)
|
||||||
|
|
||||||
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
|
@change_settings("Payroll Settings", {
|
||||||
|
"payroll_based_on": "Attendance"
|
||||||
|
})
|
||||||
def test_payment_days_in_salary_slip_based_on_timesheet(self):
|
def test_payment_days_in_salary_slip_based_on_timesheet(self):
|
||||||
from erpnext.hr.doctype.attendance.attendance import mark_attendance
|
from erpnext.hr.doctype.attendance.attendance import mark_attendance
|
||||||
from erpnext.projects.doctype.timesheet.test_timesheet import (
|
from erpnext.projects.doctype.timesheet.test_timesheet import (
|
||||||
@ -145,9 +213,6 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
make_salary_slip as make_salary_slip_for_timesheet,
|
make_salary_slip as make_salary_slip_for_timesheet,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Payroll based on attendance
|
|
||||||
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance")
|
|
||||||
|
|
||||||
emp = make_employee("test_employee_timesheet@salary.com", company="_Test Company", holiday_list="Salary Slip Test Holiday List")
|
emp = make_employee("test_employee_timesheet@salary.com", company="_Test Company", holiday_list="Salary Slip Test Holiday List")
|
||||||
frappe.db.set_value("Employee", emp, {"relieving_date": None, "status": "Active"})
|
frappe.db.set_value("Employee", emp, {"relieving_date": None, "status": "Active"})
|
||||||
|
|
||||||
@ -185,17 +250,15 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(salary_slip.gross_pay, flt(gross_pay, 2))
|
self.assertEqual(salary_slip.gross_pay, flt(gross_pay, 2))
|
||||||
|
|
||||||
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
|
@change_settings("Payroll Settings", {
|
||||||
|
"payroll_based_on": "Attendance"
|
||||||
|
})
|
||||||
def test_component_amount_dependent_on_another_payment_days_based_component(self):
|
def test_component_amount_dependent_on_another_payment_days_based_component(self):
|
||||||
from erpnext.hr.doctype.attendance.attendance import mark_attendance
|
from erpnext.hr.doctype.attendance.attendance import mark_attendance
|
||||||
from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
|
from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
|
||||||
create_salary_structure_assignment,
|
create_salary_structure_assignment,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Payroll based on attendance
|
|
||||||
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance")
|
|
||||||
|
|
||||||
salary_structure = make_salary_structure_for_payment_days_based_component_dependency()
|
salary_structure = make_salary_structure_for_payment_days_based_component_dependency()
|
||||||
employee = make_employee("test_payment_days_based_component@salary.com", company="_Test Company")
|
employee = make_employee("test_payment_days_based_component@salary.com", company="_Test Company")
|
||||||
|
|
||||||
@ -238,11 +301,12 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
expected_amount = flt((flt(ss.gross_pay) - payment_days_based_comp_amount) * 0.12, precision)
|
expected_amount = flt((flt(ss.gross_pay) - payment_days_based_comp_amount) * 0.12, precision)
|
||||||
|
|
||||||
self.assertEqual(actual_amount, expected_amount)
|
self.assertEqual(actual_amount, expected_amount)
|
||||||
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
|
|
||||||
|
|
||||||
|
@change_settings("Payroll Settings", {
|
||||||
|
"include_holidays_in_total_working_days": 1
|
||||||
|
})
|
||||||
def test_salary_slip_with_holidays_included(self):
|
def test_salary_slip_with_holidays_included(self):
|
||||||
no_of_days = self.get_no_of_days()
|
no_of_days = self.get_no_of_days()
|
||||||
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 1)
|
|
||||||
make_employee("test_salary_slip_with_holidays_included@salary.com")
|
make_employee("test_salary_slip_with_holidays_included@salary.com")
|
||||||
frappe.db.set_value("Employee", frappe.get_value("Employee",
|
frappe.db.set_value("Employee", frappe.get_value("Employee",
|
||||||
{"employee_name":"test_salary_slip_with_holidays_included@salary.com"}, "name"), "relieving_date", None)
|
{"employee_name":"test_salary_slip_with_holidays_included@salary.com"}, "name"), "relieving_date", None)
|
||||||
@ -256,9 +320,11 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
self.assertEqual(ss.earnings[1].amount, 3000)
|
self.assertEqual(ss.earnings[1].amount, 3000)
|
||||||
self.assertEqual(ss.gross_pay, 78000)
|
self.assertEqual(ss.gross_pay, 78000)
|
||||||
|
|
||||||
|
@change_settings("Payroll Settings", {
|
||||||
|
"include_holidays_in_total_working_days": 0
|
||||||
|
})
|
||||||
def test_salary_slip_with_holidays_excluded(self):
|
def test_salary_slip_with_holidays_excluded(self):
|
||||||
no_of_days = self.get_no_of_days()
|
no_of_days = self.get_no_of_days()
|
||||||
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0)
|
|
||||||
make_employee("test_salary_slip_with_holidays_excluded@salary.com")
|
make_employee("test_salary_slip_with_holidays_excluded@salary.com")
|
||||||
frappe.db.set_value("Employee", frappe.get_value("Employee",
|
frappe.db.set_value("Employee", frappe.get_value("Employee",
|
||||||
{"employee_name":"test_salary_slip_with_holidays_excluded@salary.com"}, "name"), "relieving_date", None)
|
{"employee_name":"test_salary_slip_with_holidays_excluded@salary.com"}, "name"), "relieving_date", None)
|
||||||
@ -273,14 +339,15 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
self.assertEqual(ss.earnings[1].amount, 3000)
|
self.assertEqual(ss.earnings[1].amount, 3000)
|
||||||
self.assertEqual(ss.gross_pay, 78000)
|
self.assertEqual(ss.gross_pay, 78000)
|
||||||
|
|
||||||
|
@change_settings("Payroll Settings", {
|
||||||
|
"include_holidays_in_total_working_days": 1
|
||||||
|
})
|
||||||
def test_payment_days(self):
|
def test_payment_days(self):
|
||||||
from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
|
from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
|
||||||
create_salary_structure_assignment,
|
create_salary_structure_assignment,
|
||||||
)
|
)
|
||||||
|
|
||||||
no_of_days = self.get_no_of_days()
|
no_of_days = self.get_no_of_days()
|
||||||
# Holidays not included in working days
|
|
||||||
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 1)
|
|
||||||
|
|
||||||
# set joinng date in the same month
|
# set joinng date in the same month
|
||||||
employee = make_employee("test_payment_days@salary.com")
|
employee = make_employee("test_payment_days@salary.com")
|
||||||
@ -338,11 +405,12 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
frappe.set_user("test_employee_salary_slip_read_permission@salary.com")
|
frappe.set_user("test_employee_salary_slip_read_permission@salary.com")
|
||||||
self.assertTrue(salary_slip_test_employee.has_permission("read"))
|
self.assertTrue(salary_slip_test_employee.has_permission("read"))
|
||||||
|
|
||||||
|
@change_settings("Payroll Settings", {
|
||||||
|
"email_salary_slip_to_employee": 1
|
||||||
|
})
|
||||||
def test_email_salary_slip(self):
|
def test_email_salary_slip(self):
|
||||||
frappe.db.sql("delete from `tabEmail Queue`")
|
frappe.db.sql("delete from `tabEmail Queue`")
|
||||||
|
|
||||||
frappe.db.set_value("Payroll Settings", None, "email_salary_slip_to_employee", 1)
|
|
||||||
|
|
||||||
make_employee("test_email_salary_slip@salary.com")
|
make_employee("test_email_salary_slip@salary.com")
|
||||||
ss = make_employee_salary_slip("test_email_salary_slip@salary.com", "Monthly", "Test Salary Slip Email")
|
ss = make_employee_salary_slip("test_email_salary_slip@salary.com", "Monthly", "Test Salary Slip Email")
|
||||||
ss.company = "_Test Company"
|
ss.company = "_Test Company"
|
||||||
|
|||||||
@ -116,7 +116,7 @@ frappe.ui.form.on("Timesheet", {
|
|||||||
|
|
||||||
currency: function(frm) {
|
currency: function(frm) {
|
||||||
let base_currency = frappe.defaults.get_global_default('currency');
|
let base_currency = frappe.defaults.get_global_default('currency');
|
||||||
if (base_currency != frm.doc.currency) {
|
if (frm.doc.currency && (base_currency != frm.doc.currency)) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "erpnext.setup.utils.get_exchange_rate",
|
method: "erpnext.setup.utils.get_exchange_rate",
|
||||||
args: {
|
args: {
|
||||||
|
|||||||
@ -23,9 +23,5 @@
|
|||||||
"StateCesAmt": "{item.state_cess_amount}",
|
"StateCesAmt": "{item.state_cess_amount}",
|
||||||
"StateCesNonAdvlAmt": "{item.state_cess_nadv_amount}",
|
"StateCesNonAdvlAmt": "{item.state_cess_nadv_amount}",
|
||||||
"OthChrg": "{item.other_charges}",
|
"OthChrg": "{item.other_charges}",
|
||||||
"TotItemVal": "{item.total_value}",
|
"TotItemVal": "{item.total_value}"
|
||||||
"BchDtls": {{
|
|
||||||
"Nm": "{item.batch_no}",
|
|
||||||
"ExpDt": "{item.batch_expiry_date}"
|
|
||||||
}}
|
|
||||||
}}
|
}}
|
||||||
@ -214,8 +214,6 @@ def get_item_list(invoice):
|
|||||||
item.taxable_value = abs(item.taxable_value)
|
item.taxable_value = abs(item.taxable_value)
|
||||||
item.discount_amount = 0
|
item.discount_amount = 0
|
||||||
|
|
||||||
item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None
|
|
||||||
item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None
|
|
||||||
item.is_service_item = 'Y' if item.gst_hsn_code and item.gst_hsn_code[:2] == "99" else 'N'
|
item.is_service_item = 'Y' if item.gst_hsn_code and item.gst_hsn_code[:2] == "99" else 'N'
|
||||||
item.serial_no = ""
|
item.serial_no = ""
|
||||||
|
|
||||||
|
|||||||
@ -40,7 +40,6 @@ frappe.ui.form.on('Quotation', {
|
|||||||
|
|
||||||
erpnext.selling.QuotationController = class QuotationController extends erpnext.selling.SellingController {
|
erpnext.selling.QuotationController = class QuotationController extends erpnext.selling.SellingController {
|
||||||
onload(doc, dt, dn) {
|
onload(doc, dt, dn) {
|
||||||
var me = this;
|
|
||||||
super.onload(doc, dt, dn);
|
super.onload(doc, dt, dn);
|
||||||
}
|
}
|
||||||
party_name() {
|
party_name() {
|
||||||
|
|||||||
@ -562,6 +562,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
|||||||
var me = this;
|
var me = this;
|
||||||
var dialog = new frappe.ui.Dialog({
|
var dialog = new frappe.ui.Dialog({
|
||||||
title: __("Select Items"),
|
title: __("Select Items"),
|
||||||
|
size: "large",
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
@ -663,7 +664,8 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
|||||||
} else {
|
} else {
|
||||||
let po_items = [];
|
let po_items = [];
|
||||||
me.frm.doc.items.forEach(d => {
|
me.frm.doc.items.forEach(d => {
|
||||||
let pending_qty = (flt(d.stock_qty) - flt(d.ordered_qty)) / flt(d.conversion_factor);
|
let ordered_qty = me.get_ordered_qty(d, me.frm.doc);
|
||||||
|
let pending_qty = (flt(d.stock_qty) - ordered_qty) / flt(d.conversion_factor);
|
||||||
if (pending_qty > 0) {
|
if (pending_qty > 0) {
|
||||||
po_items.push({
|
po_items.push({
|
||||||
"doctype": "Sales Order Item",
|
"doctype": "Sales Order Item",
|
||||||
@ -689,6 +691,24 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
|||||||
dialog.show();
|
dialog.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get_ordered_qty(item, so) {
|
||||||
|
let ordered_qty = item.ordered_qty;
|
||||||
|
if (so.packed_items) {
|
||||||
|
// calculate ordered qty based on packed items in case of product bundle
|
||||||
|
let packed_items = so.packed_items.filter(
|
||||||
|
(pi) => pi.parent_detail_docname == item.name
|
||||||
|
);
|
||||||
|
if (packed_items) {
|
||||||
|
ordered_qty = packed_items.reduce(
|
||||||
|
(sum, pi) => sum + flt(pi.ordered_qty),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
ordered_qty = ordered_qty / packed_items.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ordered_qty;
|
||||||
|
}
|
||||||
|
|
||||||
hold_sales_order(){
|
hold_sales_order(){
|
||||||
var me = this;
|
var me = this;
|
||||||
var d = new frappe.ui.Dialog({
|
var d = new frappe.ui.Dialog({
|
||||||
|
|||||||
@ -877,6 +877,9 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
|
|||||||
target.stock_qty = (flt(source.stock_qty) - flt(source.ordered_qty))
|
target.stock_qty = (flt(source.stock_qty) - flt(source.ordered_qty))
|
||||||
target.project = source_parent.project
|
target.project = source_parent.project
|
||||||
|
|
||||||
|
def update_item_for_packed_item(source, target, source_parent):
|
||||||
|
target.qty = flt(source.qty) - flt(source.ordered_qty)
|
||||||
|
|
||||||
# po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")})
|
# po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")})
|
||||||
doc = get_mapped_doc("Sales Order", source_name, {
|
doc = get_mapped_doc("Sales Order", source_name, {
|
||||||
"Sales Order": {
|
"Sales Order": {
|
||||||
@ -920,6 +923,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
|
|||||||
"Packed Item": {
|
"Packed Item": {
|
||||||
"doctype": "Purchase Order Item",
|
"doctype": "Purchase Order Item",
|
||||||
"field_map": [
|
"field_map": [
|
||||||
|
["name", "sales_order_packed_item"],
|
||||||
["parent", "sales_order"],
|
["parent", "sales_order"],
|
||||||
["uom", "uom"],
|
["uom", "uom"],
|
||||||
["conversion_factor", "conversion_factor"],
|
["conversion_factor", "conversion_factor"],
|
||||||
@ -934,6 +938,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
|
|||||||
"supplier",
|
"supplier",
|
||||||
"pricing_rules"
|
"pricing_rules"
|
||||||
],
|
],
|
||||||
|
"postprocess": update_item_for_packed_item,
|
||||||
"condition": lambda doc: doc.parent_item in items_to_map
|
"condition": lambda doc: doc.parent_item in items_to_map
|
||||||
}
|
}
|
||||||
}, target_doc, set_missing_values)
|
}, target_doc, set_missing_values)
|
||||||
|
|||||||
@ -959,6 +959,42 @@ class TestSalesOrder(FrappeTestCase):
|
|||||||
self.assertEqual(purchase_order.items[0].item_code, "_Test Bundle Item 1")
|
self.assertEqual(purchase_order.items[0].item_code, "_Test Bundle Item 1")
|
||||||
self.assertEqual(purchase_order.items[1].item_code, "_Test Bundle Item 2")
|
self.assertEqual(purchase_order.items[1].item_code, "_Test Bundle Item 2")
|
||||||
|
|
||||||
|
def test_purchase_order_updates_packed_item_ordered_qty(self):
|
||||||
|
"""
|
||||||
|
Tests if the packed item's `ordered_qty` is updated with the quantity of the Purchase Order
|
||||||
|
"""
|
||||||
|
from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order
|
||||||
|
|
||||||
|
product_bundle = make_item("_Test Product Bundle", {"is_stock_item": 0})
|
||||||
|
make_item("_Test Bundle Item 1", {"is_stock_item": 1})
|
||||||
|
make_item("_Test Bundle Item 2", {"is_stock_item": 1})
|
||||||
|
|
||||||
|
make_product_bundle("_Test Product Bundle",
|
||||||
|
["_Test Bundle Item 1", "_Test Bundle Item 2"])
|
||||||
|
|
||||||
|
so_items = [
|
||||||
|
{
|
||||||
|
"item_code": product_bundle.item_code,
|
||||||
|
"warehouse": "",
|
||||||
|
"qty": 2,
|
||||||
|
"rate": 400,
|
||||||
|
"delivered_by_supplier": 1,
|
||||||
|
"supplier": '_Test Supplier'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
so = make_sales_order(item_list=so_items)
|
||||||
|
|
||||||
|
purchase_order = make_purchase_order(so.name, selected_items=so_items)
|
||||||
|
purchase_order.supplier = "_Test Supplier"
|
||||||
|
purchase_order.set_warehouse = "_Test Warehouse - _TC"
|
||||||
|
purchase_order.save()
|
||||||
|
purchase_order.submit()
|
||||||
|
|
||||||
|
so.reload()
|
||||||
|
self.assertEqual(so.packed_items[0].ordered_qty, 2)
|
||||||
|
self.assertEqual(so.packed_items[1].ordered_qty, 2)
|
||||||
|
|
||||||
def test_reserved_qty_for_closing_so(self):
|
def test_reserved_qty_for_closing_so(self):
|
||||||
bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"},
|
bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"},
|
||||||
fields=["reserved_qty"])
|
fields=["reserved_qty"])
|
||||||
|
|||||||
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
import frappe.defaults
|
import frappe.defaults
|
||||||
@ -422,14 +421,14 @@ def get_name_with_abbr(name, company):
|
|||||||
return " - ".join(parts)
|
return " - ".join(parts)
|
||||||
|
|
||||||
def install_country_fixtures(company, country):
|
def install_country_fixtures(company, country):
|
||||||
path = frappe.get_app_path('erpnext', 'regional', frappe.scrub(country))
|
try:
|
||||||
if os.path.exists(path.encode("utf-8")):
|
module_name = f"erpnext.regional.{frappe.scrub(country)}.setup.setup"
|
||||||
try:
|
frappe.get_attr(module_name)(company, False)
|
||||||
module_name = "erpnext.regional.{0}.setup.setup".format(frappe.scrub(country))
|
except ImportError:
|
||||||
frappe.get_attr(module_name)(company, False)
|
pass
|
||||||
except Exception as e:
|
except Exception:
|
||||||
frappe.log_error()
|
frappe.log_error()
|
||||||
frappe.throw(_("Failed to setup defaults for country {0}. Please contact support.").format(frappe.bold(country)))
|
frappe.throw(_("Failed to setup defaults for country {0}. Please contact support.").format(frappe.bold(country)))
|
||||||
|
|
||||||
|
|
||||||
def update_company_current_month_sales(company):
|
def update_company_current_month_sales(company):
|
||||||
|
|||||||
@ -11,6 +11,7 @@ from erpnext.accounts.doctype.account.test_account import create_account, get_in
|
|||||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||||
from erpnext.accounts.utils import update_gl_entries_after
|
from erpnext.accounts.utils import update_gl_entries_after
|
||||||
from erpnext.assets.doctype.asset.test_asset import create_asset_category, create_fixed_asset_item
|
from erpnext.assets.doctype.asset.test_asset import create_asset_category, create_fixed_asset_item
|
||||||
|
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
|
||||||
get_gl_entries,
|
get_gl_entries,
|
||||||
make_purchase_receipt,
|
make_purchase_receipt,
|
||||||
@ -177,6 +178,53 @@ class TestLandedCostVoucher(FrappeTestCase):
|
|||||||
self.assertEqual(serial_no.purchase_rate - serial_no_rate, 5.0)
|
self.assertEqual(serial_no.purchase_rate - serial_no_rate, 5.0)
|
||||||
self.assertEqual(serial_no.warehouse, "Stores - TCP1")
|
self.assertEqual(serial_no.warehouse, "Stores - TCP1")
|
||||||
|
|
||||||
|
def test_serialized_lcv_delivered(self):
|
||||||
|
"""In some cases you'd want to deliver before you can know all the
|
||||||
|
landed costs, this should be allowed for serial nos too.
|
||||||
|
|
||||||
|
Case:
|
||||||
|
- receipt a serial no @ X rate
|
||||||
|
- delivery the serial no @ X rate
|
||||||
|
- add LCV to receipt X + Y
|
||||||
|
- LCV should be successful
|
||||||
|
- delivery should reflect X+Y valuation.
|
||||||
|
"""
|
||||||
|
serial_no = "LCV_TEST_SR_NO"
|
||||||
|
item_code = "_Test Serialized Item"
|
||||||
|
warehouse = "Stores - TCP1"
|
||||||
|
|
||||||
|
pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
|
||||||
|
warehouse=warehouse, qty=1, rate=200,
|
||||||
|
item_code=item_code, serial_no=serial_no)
|
||||||
|
|
||||||
|
serial_no_rate = frappe.db.get_value("Serial No", serial_no, "purchase_rate")
|
||||||
|
|
||||||
|
# deliver it before creating LCV
|
||||||
|
dn = create_delivery_note(item_code=item_code,
|
||||||
|
company='_Test Company with perpetual inventory', warehouse='Stores - TCP1',
|
||||||
|
serial_no=serial_no, qty=1, rate=500,
|
||||||
|
cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1")
|
||||||
|
|
||||||
|
charges = 10
|
||||||
|
create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=charges)
|
||||||
|
|
||||||
|
new_purchase_rate = serial_no_rate + charges
|
||||||
|
|
||||||
|
serial_no = frappe.db.get_value("Serial No", serial_no,
|
||||||
|
["warehouse", "purchase_rate"], as_dict=1)
|
||||||
|
|
||||||
|
self.assertEqual(serial_no.purchase_rate, new_purchase_rate)
|
||||||
|
|
||||||
|
stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
|
||||||
|
filters={
|
||||||
|
"voucher_no": dn.name,
|
||||||
|
"voucher_type": dn.doctype,
|
||||||
|
"is_cancelled": 0 # LCV cancels with same name.
|
||||||
|
},
|
||||||
|
fieldname="stock_value_difference")
|
||||||
|
|
||||||
|
# reposting should update the purchase rate in future delivery
|
||||||
|
self.assertEqual(stock_value_difference, -new_purchase_rate)
|
||||||
|
|
||||||
def test_landed_cost_voucher_for_odd_numbers (self):
|
def test_landed_cost_voucher_for_odd_numbers (self):
|
||||||
pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", do_not_save=True)
|
pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", do_not_save=True)
|
||||||
|
|||||||
@ -626,13 +626,13 @@ class TestMaterialRequest(FrappeTestCase):
|
|||||||
mr.schedule_date = today()
|
mr.schedule_date = today()
|
||||||
|
|
||||||
if not frappe.db.get_value('UOM Conversion Detail',
|
if not frappe.db.get_value('UOM Conversion Detail',
|
||||||
{'parent': item.item_code, 'uom': 'Kg'}):
|
{'parent': item.item_code, 'uom': 'Kg'}):
|
||||||
item_doc = frappe.get_doc('Item', item.item_code)
|
item_doc = frappe.get_doc('Item', item.item_code)
|
||||||
item_doc.append('uoms', {
|
item_doc.append('uoms', {
|
||||||
'uom': 'Kg',
|
'uom': 'Kg',
|
||||||
'conversion_factor': 5
|
'conversion_factor': 5
|
||||||
})
|
})
|
||||||
item_doc.save(ignore_permissions=True)
|
item_doc.save(ignore_permissions=True)
|
||||||
|
|
||||||
item.uom = 'Kg'
|
item.uom = 'Kg'
|
||||||
for item in mr.items:
|
for item in mr.items:
|
||||||
|
|||||||
@ -26,6 +26,7 @@
|
|||||||
"section_break_13",
|
"section_break_13",
|
||||||
"actual_qty",
|
"actual_qty",
|
||||||
"projected_qty",
|
"projected_qty",
|
||||||
|
"ordered_qty",
|
||||||
"column_break_16",
|
"column_break_16",
|
||||||
"incoming_rate",
|
"incoming_rate",
|
||||||
"page_break",
|
"page_break",
|
||||||
@ -224,13 +225,21 @@
|
|||||||
"label": "Rate",
|
"label": "Rate",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
|
"fieldname": "ordered_qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Ordered Qty",
|
||||||
|
"no_copy": 1,
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-01-28 16:03:30.780111",
|
"modified": "2022-02-22 12:57:45.325488",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Packed Item",
|
"name": "Packed Item",
|
||||||
|
|||||||
@ -161,6 +161,15 @@ class TestPurchaseReceipt(FrappeTestCase):
|
|||||||
qty=abs(existing_bin_qty)
|
qty=abs(existing_bin_qty)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
existing_bin_qty, existing_bin_stock_value = frappe.db.get_value(
|
||||||
|
"Bin",
|
||||||
|
{
|
||||||
|
"item_code": "_Test Item",
|
||||||
|
"warehouse": "_Test Warehouse - _TC"
|
||||||
|
},
|
||||||
|
["actual_qty", "stock_value"]
|
||||||
|
)
|
||||||
|
|
||||||
pr = make_purchase_receipt()
|
pr = make_purchase_receipt()
|
||||||
|
|
||||||
stock_value_difference = frappe.db.get_value(
|
stock_value_difference = frappe.db.get_value(
|
||||||
|
|||||||
@ -389,10 +389,13 @@ class TestStockLedgerEntry(FrappeTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def assertSLEs(self, doc, expected_sles):
|
def assertSLEs(self, doc, expected_sles, sle_filters=None):
|
||||||
""" Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line"""
|
""" Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line"""
|
||||||
sles = frappe.get_all("Stock Ledger Entry", fields=["*"],
|
|
||||||
filters={"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled":0},
|
filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled": 0}
|
||||||
|
if sle_filters:
|
||||||
|
filters.update(sle_filters)
|
||||||
|
sles = frappe.get_all("Stock Ledger Entry", fields=["*"], filters=filters,
|
||||||
order_by="timestamp(posting_date, posting_time), creation")
|
order_by="timestamp(posting_date, posting_time), creation")
|
||||||
|
|
||||||
for exp_sle, act_sle in zip(expected_sles, sles):
|
for exp_sle, act_sle in zip(expected_sles, sles):
|
||||||
@ -665,6 +668,78 @@ class TestStockLedgerEntry(FrappeTestCase):
|
|||||||
{"actual_qty": -10, "stock_value_difference": -10*40, "stock_queue": []},
|
{"actual_qty": -10, "stock_value_difference": -10*40, "stock_queue": []},
|
||||||
]))
|
]))
|
||||||
|
|
||||||
|
def test_fifo_dependent_consumption(self):
|
||||||
|
item = make_item("_TestFifoTransferRates")
|
||||||
|
source = "_Test Warehouse - _TC"
|
||||||
|
target = "Stores - _TC"
|
||||||
|
|
||||||
|
rates = [10 * i for i in range(1, 20)]
|
||||||
|
|
||||||
|
receipt = make_stock_entry(item_code=item.name, target=source, qty=10, do_not_save=True, rate=10)
|
||||||
|
for rate in rates[1:]:
|
||||||
|
row = frappe.copy_doc(receipt.items[0], ignore_no_copy=False)
|
||||||
|
row.basic_rate = rate
|
||||||
|
receipt.append("items", row)
|
||||||
|
|
||||||
|
receipt.save()
|
||||||
|
receipt.submit()
|
||||||
|
|
||||||
|
expected_queues = []
|
||||||
|
for idx, rate in enumerate(rates, start=1):
|
||||||
|
expected_queues.append(
|
||||||
|
{"stock_queue": [[10, 10 * i] for i in range(1, idx + 1)]}
|
||||||
|
)
|
||||||
|
self.assertSLEs(receipt, expected_queues)
|
||||||
|
|
||||||
|
transfer = make_stock_entry(item_code=item.name, source=source, target=target, qty=10, do_not_save=True, rate=10)
|
||||||
|
for rate in rates[1:]:
|
||||||
|
row = frappe.copy_doc(transfer.items[0], ignore_no_copy=False)
|
||||||
|
transfer.append("items", row)
|
||||||
|
|
||||||
|
transfer.save()
|
||||||
|
transfer.submit()
|
||||||
|
|
||||||
|
# same exact queue should be transferred
|
||||||
|
self.assertSLEs(transfer, expected_queues, sle_filters={"warehouse": target})
|
||||||
|
|
||||||
|
def test_fifo_multi_item_repack_consumption(self):
|
||||||
|
rm = make_item("_TestFifoRepackRM")
|
||||||
|
packed = make_item("_TestFifoRepackFinished")
|
||||||
|
warehouse = "_Test Warehouse - _TC"
|
||||||
|
|
||||||
|
rates = [10 * i for i in range(1, 5)]
|
||||||
|
|
||||||
|
receipt = make_stock_entry(item_code=rm.name, target=warehouse, qty=10, do_not_save=True, rate=10)
|
||||||
|
for rate in rates[1:]:
|
||||||
|
row = frappe.copy_doc(receipt.items[0], ignore_no_copy=False)
|
||||||
|
row.basic_rate = rate
|
||||||
|
receipt.append("items", row)
|
||||||
|
|
||||||
|
receipt.save()
|
||||||
|
receipt.submit()
|
||||||
|
|
||||||
|
repack = make_stock_entry(item_code=rm.name, source=warehouse, qty=10,
|
||||||
|
do_not_save=True, rate=10, purpose="Repack")
|
||||||
|
for rate in rates[1:]:
|
||||||
|
row = frappe.copy_doc(repack.items[0], ignore_no_copy=False)
|
||||||
|
repack.append("items", row)
|
||||||
|
|
||||||
|
repack.append("items", {
|
||||||
|
"item_code": packed.name,
|
||||||
|
"t_warehouse": warehouse,
|
||||||
|
"qty": 1,
|
||||||
|
"transfer_qty": 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
repack.save()
|
||||||
|
repack.submit()
|
||||||
|
|
||||||
|
# same exact queue should be transferred
|
||||||
|
self.assertSLEs(repack, [
|
||||||
|
{"incoming_rate": sum(rates) * 10}
|
||||||
|
], sle_filters={"item_code": packed.name})
|
||||||
|
|
||||||
|
|
||||||
def create_repack_entry(**args):
|
def create_repack_entry(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
repack = frappe.new_doc("Stock Entry")
|
repack = frappe.new_doc("Stock Entry")
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
|||||||
@ -21,6 +21,7 @@ SLE_FIELDS = (
|
|||||||
"stock_value",
|
"stock_value",
|
||||||
"stock_value_difference",
|
"stock_value_difference",
|
||||||
"valuation_rate",
|
"valuation_rate",
|
||||||
|
"voucher_detail_no",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -66,7 +67,9 @@ def add_invariant_check_fields(sles):
|
|||||||
balance_qty += sle.actual_qty
|
balance_qty += sle.actual_qty
|
||||||
balance_stock_value += sle.stock_value_difference
|
balance_stock_value += sle.stock_value_difference
|
||||||
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
|
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
|
||||||
balance_qty = sle.qty_after_transaction
|
balance_qty = frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "qty")
|
||||||
|
if balance_qty is None:
|
||||||
|
balance_qty = sle.qty_after_transaction
|
||||||
|
|
||||||
sle.fifo_queue_qty = fifo_qty
|
sle.fifo_queue_qty = fifo_qty
|
||||||
sle.fifo_stock_value = fifo_value
|
sle.fifo_stock_value = fifo_value
|
||||||
|
|||||||
@ -28,6 +28,16 @@ class SerialNoExistsInFutureTransaction(frappe.ValidationError):
|
|||||||
|
|
||||||
|
|
||||||
def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
|
def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
|
||||||
|
""" Create SL entries from SL entry dicts
|
||||||
|
|
||||||
|
args:
|
||||||
|
- allow_negative_stock: disable negative stock valiations if true
|
||||||
|
- via_landed_cost_voucher: landed cost voucher cancels and reposts
|
||||||
|
entries of purchase document. This flag is used to identify if
|
||||||
|
cancellation and repost is happening via landed cost voucher, in
|
||||||
|
such cases certain validations need to be ignored (like negative
|
||||||
|
stock)
|
||||||
|
"""
|
||||||
from erpnext.controllers.stock_controller import future_sle_exists
|
from erpnext.controllers.stock_controller import future_sle_exists
|
||||||
if sl_entries:
|
if sl_entries:
|
||||||
cancel = sl_entries[0].get("is_cancelled")
|
cancel = sl_entries[0].get("is_cancelled")
|
||||||
@ -39,7 +49,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
|
|||||||
future_sle_exists(args, sl_entries)
|
future_sle_exists(args, sl_entries)
|
||||||
|
|
||||||
for sle in sl_entries:
|
for sle in sl_entries:
|
||||||
if sle.serial_no:
|
if sle.serial_no and not via_landed_cost_voucher:
|
||||||
validate_serial_no(sle)
|
validate_serial_no(sle)
|
||||||
|
|
||||||
if cancel:
|
if cancel:
|
||||||
@ -819,7 +829,7 @@ class update_entries_after(object):
|
|||||||
if msg_list:
|
if msg_list:
|
||||||
message = "\n\n".join(msg_list)
|
message = "\n\n".join(msg_list)
|
||||||
if self.verbose:
|
if self.verbose:
|
||||||
frappe.throw(message, NegativeStockError, title='Insufficient Stock')
|
frappe.throw(message, NegativeStockError, title=_('Insufficient Stock'))
|
||||||
else:
|
else:
|
||||||
raise NegativeStockError(message)
|
raise NegativeStockError(message)
|
||||||
|
|
||||||
@ -1147,7 +1157,7 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
|
|||||||
neg_sle[0]["posting_date"], neg_sle[0]["posting_time"],
|
neg_sle[0]["posting_date"], neg_sle[0]["posting_time"],
|
||||||
frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]))
|
frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]))
|
||||||
|
|
||||||
frappe.throw(message, NegativeStockError, title='Insufficient Stock')
|
frappe.throw(message, NegativeStockError, title=_('Insufficient Stock'))
|
||||||
|
|
||||||
|
|
||||||
if not args.batch_no:
|
if not args.batch_no:
|
||||||
@ -1161,7 +1171,7 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
|
|||||||
frappe.get_desk_link('Warehouse', args.warehouse),
|
frappe.get_desk_link('Warehouse', args.warehouse),
|
||||||
neg_batch_sle[0]["posting_date"], neg_batch_sle[0]["posting_time"],
|
neg_batch_sle[0]["posting_date"], neg_batch_sle[0]["posting_time"],
|
||||||
frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]))
|
frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]))
|
||||||
frappe.throw(message, NegativeStockError, title="Insufficient Stock for Batch")
|
frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
|
||||||
|
|
||||||
|
|
||||||
def get_future_sle_with_negative_qty(args):
|
def get_future_sle_with_negative_qty(args):
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user