Merge branch 'develop' into gstr_3b_nil_exempt_fixed

This commit is contained in:
Deepesh Garg 2022-03-06 18:09:38 +05:30 committed by GitHub
commit c6c51d7312
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 792 additions and 116 deletions

View File

@ -120,6 +120,7 @@ def get_booking_dates(doc, item, posting_date=None):
prev_gl_entry = frappe.db.sql(''' prev_gl_entry = frappe.db.sql('''
select name, posting_date from `tabGL Entry` where company=%s and account=%s and select name, posting_date from `tabGL Entry` where company=%s and account=%s and
voucher_type=%s and voucher_no=%s and voucher_detail_no=%s voucher_type=%s and voucher_no=%s and voucher_detail_no=%s
and is_cancelled = 0
order by posting_date desc limit 1 order by posting_date desc limit 1
''', (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True) ''', (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True)
@ -227,6 +228,7 @@ def get_already_booked_amount(doc, item):
gl_entries_details = frappe.db.sql(''' gl_entries_details = frappe.db.sql('''
select sum({0}) as total_credit, sum({1}) as total_credit_in_account_currency, voucher_detail_no select sum({0}) as total_credit, sum({1}) as total_credit_in_account_currency, voucher_detail_no
from `tabGL Entry` where company=%s and account=%s and voucher_type=%s and voucher_no=%s and voucher_detail_no=%s from `tabGL Entry` where company=%s and account=%s and voucher_type=%s and voucher_no=%s and voucher_detail_no=%s
and is_cancelled = 0
group by voucher_detail_no group by voucher_detail_no
'''.format(total_credit_debit, total_credit_debit_currency), '''.format(total_credit_debit, total_credit_debit_currency),
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True) (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True)
@ -282,7 +284,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
return return
# check if books nor frozen till endate: # check if books nor frozen till endate:
if getdate(end_date) >= getdate(accounts_frozen_upto): if accounts_frozen_upto and (end_date) <= getdate(accounts_frozen_upto):
end_date = get_last_day(add_days(accounts_frozen_upto, 1)) end_date = get_last_day(add_days(accounts_frozen_upto, 1))
if via_journal_entry: if via_journal_entry:

View File

@ -194,8 +194,14 @@ frappe.ui.form.on('Payment Entry', {
frm.doc.paid_from_account_currency != frm.doc.paid_to_account_currency)); frm.doc.paid_from_account_currency != frm.doc.paid_to_account_currency));
frm.toggle_display("base_paid_amount", frm.doc.paid_from_account_currency != company_currency); frm.toggle_display("base_paid_amount", frm.doc.paid_from_account_currency != company_currency);
frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges &&
(frm.doc.paid_from_account_currency != company_currency)); if (frm.doc.payment_type == "Pay") {
frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges &&
(frm.doc.paid_to_account_currency != company_currency));
} else {
frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges &&
(frm.doc.paid_from_account_currency != company_currency));
}
frm.toggle_display("base_received_amount", ( frm.toggle_display("base_received_amount", (
frm.doc.paid_to_account_currency != company_currency frm.doc.paid_to_account_currency != company_currency
@ -230,7 +236,8 @@ frappe.ui.form.on('Payment Entry', {
var company_currency = frm.doc.company? frappe.get_doc(":Company", frm.doc.company).default_currency: ""; var company_currency = frm.doc.company? frappe.get_doc(":Company", frm.doc.company).default_currency: "";
frm.set_currency_labels(["base_paid_amount", "base_received_amount", "base_total_allocated_amount", frm.set_currency_labels(["base_paid_amount", "base_received_amount", "base_total_allocated_amount",
"difference_amount", "base_paid_amount_after_tax", "base_received_amount_after_tax"], company_currency); "difference_amount", "base_paid_amount_after_tax", "base_received_amount_after_tax",
"base_total_taxes_and_charges"], company_currency);
frm.set_currency_labels(["paid_amount"], frm.doc.paid_from_account_currency); frm.set_currency_labels(["paid_amount"], frm.doc.paid_from_account_currency);
frm.set_currency_labels(["received_amount"], frm.doc.paid_to_account_currency); frm.set_currency_labels(["received_amount"], frm.doc.paid_to_account_currency);
@ -339,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: {
@ -372,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)
]); ]);
} }
} }
@ -476,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: {
@ -503,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);

View File

@ -66,7 +66,9 @@
"tax_withholding_category", "tax_withholding_category",
"section_break_56", "section_break_56",
"taxes", "taxes",
"section_break_60",
"base_total_taxes_and_charges", "base_total_taxes_and_charges",
"column_break_61",
"total_taxes_and_charges", "total_taxes_and_charges",
"deductions_or_loss_section", "deductions_or_loss_section",
"deductions", "deductions",
@ -715,12 +717,21 @@
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"label": "Paid To Account Type" "label": "Paid To Account Type"
},
{
"fieldname": "column_break_61",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_60",
"fieldtype": "Section Break",
"hide_border": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-11-24 18:58:24.919764", "modified": "2022-02-23 20:08:39.559814",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Entry", "name": "Payment Entry",
@ -763,6 +774,7 @@
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"title_field": "title", "title_field": "title",
"track_changes": 1 "track_changes": 1
} }

View File

@ -934,8 +934,12 @@ class PaymentEntry(AccountsController):
tax.base_total = tax.total * self.source_exchange_rate tax.base_total = tax.total * self.source_exchange_rate
self.total_taxes_and_charges += current_tax_amount if self.payment_type == 'Pay':
self.base_total_taxes_and_charges += current_tax_amount * self.source_exchange_rate self.base_total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate)
self.total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate)
else:
self.base_total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate)
self.total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate)
if self.get('taxes'): if self.get('taxes'):
self.paid_amount_after_tax = self.get('taxes')[-1].base_total self.paid_amount_after_tax = self.get('taxes')[-1].base_total

View File

@ -633,6 +633,45 @@ class TestPaymentEntry(unittest.TestCase):
self.assertEqual(flt(expected_party_balance), party_balance) self.assertEqual(flt(expected_party_balance), party_balance)
self.assertEqual(flt(expected_party_account_balance), party_account_balance) self.assertEqual(flt(expected_party_account_balance), party_account_balance)
def test_multi_currency_payment_entry_with_taxes(self):
payment_entry = create_payment_entry(party='_Test Supplier USD', paid_to = '_Test Payable USD - _TC',
save=True)
payment_entry.append('taxes', {
'account_head': '_Test Account Service Tax - _TC',
'charge_type': 'Actual',
'tax_amount': 10,
'add_deduct_tax': 'Add',
'description': 'Test'
})
payment_entry.save()
self.assertEqual(payment_entry.base_total_taxes_and_charges, 10)
self.assertEqual(flt(payment_entry.total_taxes_and_charges, 2), flt(10 / payment_entry.target_exchange_rate, 2))
def create_payment_entry(**args):
payment_entry = frappe.new_doc('Payment Entry')
payment_entry.company = args.get('company') or '_Test Company'
payment_entry.payment_type = args.get('payment_type') or 'Pay'
payment_entry.party_type = args.get('party_type') or 'Supplier'
payment_entry.party = args.get('party') or '_Test Supplier'
payment_entry.paid_from = args.get('paid_from') or '_Test Bank - _TC'
payment_entry.paid_to = args.get('paid_to') or 'Creditors - _TC'
payment_entry.paid_amount = args.get('paid_amount') or 1000
payment_entry.setup_party_account_field()
payment_entry.set_missing_values()
payment_entry.set_exchange_rate()
payment_entry.received_amount = payment_entry.paid_amount / payment_entry.target_exchange_rate
payment_entry.reference_no = 'Test001'
payment_entry.reference_date = nowdate()
if args.get('save'):
payment_entry.save()
if args.get('submit'):
payment_entry.submit()
return payment_entry
def create_payment_terms_template(): def create_payment_terms_template():
create_payment_term('Basic Amount Receivable') create_payment_term('Basic Amount Receivable')

View File

@ -1595,6 +1595,56 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(expected_values[gle.account][1], gle.debit) self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit) self.assertEqual(expected_values[gle.account][2], gle.credit)
def test_rounding_adjustment_3(self):
si = create_sales_invoice(do_not_save=True)
si.items = []
for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]:
si.append("items", {
"item_code": "_Test Item",
"gst_hsn_code": "999800",
"warehouse": "_Test Warehouse - _TC",
"qty": d[1],
"rate": d[0],
"income_account": "Sales - _TC",
"cost_center": "_Test Cost Center - _TC"
})
for tax_account in ["_Test Account VAT - _TC", "_Test Account Service Tax - _TC"]:
si.append("taxes", {
"charge_type": "On Net Total",
"account_head": tax_account,
"description": tax_account,
"rate": 6,
"cost_center": "_Test Cost Center - _TC",
"included_in_print_rate": 1
})
si.save()
si.submit()
self.assertEqual(si.net_total, 4007.16)
self.assertEqual(si.grand_total, 4488.02)
self.assertEqual(si.total_taxes_and_charges, 480.86)
self.assertEqual(si.rounding_adjustment, -0.02)
expected_values = dict((d[0], d) for d in [
[si.debit_to, 4488.0, 0.0],
["_Test Account Service Tax - _TC", 0.0, 240.43],
["_Test Account VAT - _TC", 0.0, 240.43],
["Sales - _TC", 0.0, 4007.15],
["Round Off - _TC", 0.01, 0]
])
gl_entries = frappe.db.sql("""select account, debit, credit
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
order by account asc""", si.name, as_dict=1)
debit_credit_diff = 0
for gle in gl_entries:
self.assertEqual(expected_values[gle.account][0], gle.account)
self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit)
debit_credit_diff += (gle.debit - gle.credit)
self.assertEqual(debit_credit_diff, 0)
def test_sales_invoice_with_shipping_rule(self): def test_sales_invoice_with_shipping_rule(self):
from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule

View File

@ -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)))

View File

@ -274,7 +274,7 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
debit_credit_diff += flt(d.credit) debit_credit_diff += flt(d.credit)
round_off_account_exists = True round_off_account_exists = True
if round_off_account_exists and abs(debit_credit_diff) <= (1.0 / (10**precision)): if round_off_account_exists and abs(debit_credit_diff) < (1.0 / (10**precision)):
gl_map.remove(round_off_gle) gl_map.remove(round_off_gle)
return return

View File

@ -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"""

View File

@ -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
} }

View File

@ -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):

View File

@ -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
} }

View File

@ -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()

View File

@ -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

View File

@ -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']]
] ]
}; };
}); });

View File

@ -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()

View File

@ -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

View File

@ -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']
} }
] ]
} }

View File

@ -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();

View File

@ -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": [
{ {

View File

@ -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")

View File

@ -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

View File

@ -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);
} }
}); });

View File

@ -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

View File

@ -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

View File

@ -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',]
} }

View File

@ -357,3 +357,4 @@ 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

View 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()

View File

@ -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":

View File

@ -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: {

View File

@ -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({

View File

@ -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)

View File

@ -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"])

View File

@ -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):

View File

@ -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)

View File

@ -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",

View File

@ -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(

View File

@ -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")

View File

@ -244,7 +244,7 @@
"idx": 1, "idx": 1,
"is_tree": 1, "is_tree": 1,
"links": [], "links": [],
"modified": "2021-12-03 04:40:06.414630", "modified": "2022-03-01 02:37:48.034944",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Warehouse", "name": "Warehouse",
@ -301,5 +301,7 @@
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"title_field": "warehouse_name" "states": [],
"title_field": "warehouse_name",
"track_changes": 1
} }

View File

@ -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

View File

@ -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:

View File

@ -1,27 +1 @@
{% set domains = frappe.get_doc("Domain Settings").active_domains %} <a href="https://erpnext.com?source=website_footer" target="_blank" class="text-muted">Powered by ERPNext</a>
{% set links = {
'Manufacturing': '/manufacturing',
'Services': '/services',
'Retail': '/retail',
'Distribution': '/distribution',
'Non Profit': '/non-profit',
'Education': '/education',
'Healthcare': '/healthcare',
'Agriculture': '/agriculture',
'Hospitality': ''
} %}
{% set link = '' %}
{% set label = '' %}
{% if domains %}
{% set label = domains[0].domain %}
{% set link = links[label] %}
{% endif %}
{% if label == "Services" %}
{% set label = "Service" %}
{% endif %}
<a href="https://erpnext.com{{ link }}?source=website_footer" target="_blank" class="text-muted">Powered by ERPNext - {{ '' if domains else 'Open Source' }} ERP Software {{ ('for ' + label + ' Companies') if domains else '' }}</a>

View File

@ -25,7 +25,7 @@ class TestPointOfSale(unittest.TestCase):
Test Stock and Service Item Search. Test Stock and Service Item Search.
""" """
pos_profile = make_pos_profile() pos_profile = make_pos_profile(name="Test POS Profile for Search")
item1 = make_item("Test Search Stock Item", {"is_stock_item": 1}) item1 = make_item("Test Search Stock Item", {"is_stock_item": 1})
make_stock_entry( make_stock_entry(
item_code="Test Search Stock Item", item_code="Test Search Stock Item",