Merge remote-tracking branch 'upstream/version-13-hotfix' into fix-ecommerce-cart-badge

This commit is contained in:
Devin Slauenwhite
2022-01-17 09:15:14 -05:00
90 changed files with 1596 additions and 822 deletions

View File

@@ -255,11 +255,13 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
enable_check = "enable_deferred_revenue" \
if doc.doctype=="Sales Invoice" else "enable_deferred_expense"
accounts_frozen_upto = frappe.get_cached_value('Accounts Settings', 'None', 'acc_frozen_upto')
def _book_deferred_revenue_or_expense(item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on):
start_date, end_date, last_gl_entry = get_booking_dates(doc, item, posting_date=posting_date)
if not (start_date and end_date): return
account_currency = get_account_currency(item.expense_account)
account_currency = get_account_currency(item.expense_account or item.income_account)
if doc.doctype == "Sales Invoice":
against, project = doc.customer, doc.project
credit_account, debit_account = item.income_account, item.deferred_revenue_account
@@ -280,6 +282,10 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
if not amount:
return
# check if books nor frozen till endate:
if getdate(end_date) >= getdate(accounts_frozen_upto):
end_date = get_last_day(add_days(accounts_frozen_upto, 1))
if via_journal_entry:
book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount,
base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process, submit_journal_entry)
@@ -407,8 +413,6 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
'account': credit_account,
'credit': base_amount,
'credit_in_account_currency': amount,
'party_type': 'Customer' if doc.doctype == 'Sales Invoice' else 'Supplier',
'party': against,
'account_currency': account_currency,
'reference_name': doc.name,
'reference_type': doc.doctype,
@@ -421,8 +425,6 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
'account': debit_account,
'debit': base_amount,
'debit_in_account_currency': amount,
'party_type': 'Customer' if doc.doctype == 'Sales Invoice' else 'Supplier',
'party': against,
'account_currency': account_currency,
'reference_name': doc.name,
'reference_type': doc.doctype,

View File

@@ -43,12 +43,12 @@ frappe.ui.form.on('Account', {
frm.trigger('add_toolbar_buttons');
}
if (frm.has_perm('write')) {
frm.add_custom_button(__('Update Account Name / Number'), function () {
frm.trigger("update_account_number");
});
frm.add_custom_button(__('Merge Account'), function () {
frm.trigger("merge_account");
});
}, __('Actions'));
frm.add_custom_button(__('Update Account Name / Number'), function () {
frm.trigger("update_account_number");
}, __('Actions'));
}
}
},
@@ -59,11 +59,12 @@ frappe.ui.form.on('Account', {
}
},
add_toolbar_buttons: function(frm) {
frm.add_custom_button(__('Chart of Accounts'),
function () { frappe.set_route("Tree", "Account"); });
frm.add_custom_button(__('Chart of Accounts'), () => {
frappe.set_route("Tree", "Account");
}, __('View'));
if (frm.doc.is_group == 1) {
frm.add_custom_button(__('Group to Non-Group'), function () {
frm.add_custom_button(__('Convert to Non-Group'), function () {
return frappe.call({
doc: frm.doc,
method: 'convert_group_to_ledger',
@@ -71,10 +72,11 @@ frappe.ui.form.on('Account', {
frm.refresh();
}
});
});
}, __('Actions'));
} else if (cint(frm.doc.is_group) == 0
&& frappe.boot.user.can_read.indexOf("GL Entry") !== -1) {
frm.add_custom_button(__('Ledger'), function () {
frm.add_custom_button(__('General Ledger'), function () {
frappe.route_options = {
"account": frm.doc.name,
"from_date": frappe.sys_defaults.year_start_date,
@@ -82,9 +84,9 @@ frappe.ui.form.on('Account', {
"company": frm.doc.company
};
frappe.set_route("query-report", "General Ledger");
});
}, __('View'));
frm.add_custom_button(__('Non-Group to Group'), function () {
frm.add_custom_button(__('Convert to Group'), function () {
return frappe.call({
doc: frm.doc,
method: 'convert_ledger_to_group',
@@ -92,7 +94,7 @@ frappe.ui.form.on('Account', {
frm.refresh();
}
});
});
}, __('Actions'));
}
},

View File

@@ -7,7 +7,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
frm.set_query("bank_account", function () {
return {
filters: {
company: ["in", frm.doc.company],
company: frm.doc.company,
'is_company_account': 1
},
};

View File

@@ -239,7 +239,8 @@ frappe.ui.form.on("Bank Statement Import", {
"withdrawal",
"description",
"reference_number",
"bank_account"
"bank_account",
"currency"
],
},
});

View File

@@ -31,7 +31,7 @@ frappe.ui.form.on("Journal Entry", {
if(frm.doc.docstatus==1) {
frm.add_custom_button(__('Reverse Journal Entry'), function() {
return erpnext.journal_entry.reverse_journal_entry(frm);
}, __('Make'));
}, __('Actions'));
}
if (frm.doc.__islocal) {

View File

@@ -13,6 +13,7 @@
"voucher_type",
"naming_series",
"finance_book",
"reversal_of",
"tax_withholding_category",
"column_break1",
"from_template",
@@ -515,13 +516,21 @@
"fieldname": "apply_tds",
"fieldtype": "Check",
"label": "Apply Tax Withholding Amount "
},
{
"depends_on": "eval:doc.docstatus",
"fieldname": "reversal_of",
"fieldtype": "Link",
"label": "Reversal Of",
"options": "Journal Entry",
"read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 176,
"is_submittable": 1,
"links": [],
"modified": "2021-09-09 15:31:14.484029",
"modified": "2022-01-04 13:39:36.485954",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",

View File

@@ -397,13 +397,14 @@ class JournalEntry(AccountsController):
debit_or_credit = 'Debit' if d.debit else 'Credit'
party_account = get_deferred_booking_accounts(d.reference_type, d.reference_detail_no,
debit_or_credit)
against_voucher = ['', against_voucher[1]]
else:
if d.reference_type == "Sales Invoice":
party_account = get_party_account_based_on_invoice_discounting(d.reference_name) or against_voucher[1]
else:
party_account = against_voucher[1]
if (against_voucher[0] != d.party or party_account != d.account):
if (against_voucher[0] != cstr(d.party) or party_account != d.account):
frappe.throw(_("Row {0}: Party / Account does not match with {1} / {2} in {3} {4}")
.format(d.idx, field_dict.get(d.reference_type)[0], field_dict.get(d.reference_type)[1],
d.reference_type, d.reference_name))
@@ -468,13 +469,22 @@ class JournalEntry(AccountsController):
def set_against_account(self):
accounts_debited, accounts_credited = [], []
for d in self.get("accounts"):
if flt(d.debit > 0): accounts_debited.append(d.party or d.account)
if flt(d.credit) > 0: accounts_credited.append(d.party or d.account)
if self.voucher_type in ('Deferred Revenue', 'Deferred Expense'):
for d in self.get('accounts'):
if d.reference_type == 'Sales Invoice':
field = 'customer'
else:
field = 'supplier'
for d in self.get("accounts"):
if flt(d.debit > 0): d.against_account = ", ".join(list(set(accounts_credited)))
if flt(d.credit > 0): d.against_account = ", ".join(list(set(accounts_debited)))
d.against_account = frappe.db.get_value(d.reference_type, d.reference_name, field)
else:
for d in self.get("accounts"):
if flt(d.debit > 0): accounts_debited.append(d.party or d.account)
if flt(d.credit) > 0: accounts_credited.append(d.party or d.account)
for d in self.get("accounts"):
if flt(d.debit > 0): d.against_account = ", ".join(list(set(accounts_credited)))
if flt(d.credit > 0): d.against_account = ", ".join(list(set(accounts_debited)))
def validate_debit_credit_amount(self):
for d in self.get('accounts'):
@@ -1147,9 +1157,8 @@ def make_inter_company_journal_entry(name, voucher_type, company):
def make_reverse_journal_entry(source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc
def update_accounts(source, target, source_parent):
target.reference_type = "Journal Entry"
target.reference_name = source_parent.name
def post_process(source, target):
target.reversal_of = source.name
doclist = get_mapped_doc("Journal Entry", source_name, {
"Journal Entry": {
@@ -1167,9 +1176,8 @@ def make_reverse_journal_entry(source_name, target_doc=None):
"debit": "credit",
"credit_in_account_currency": "debit_in_account_currency",
"credit": "debit",
},
"postprocess": update_accounts,
}
},
}, target_doc)
}, target_doc, post_process)
return doclist

View File

@@ -75,7 +75,7 @@
],
"hide_toolbar": 1,
"issingle": 1,
"modified": "2019-07-25 14:57:33.187689",
"modified": "2022-01-04 15:25:06.053187",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Opening Invoice Creation Tool",

View File

@@ -159,7 +159,8 @@ class OpeningInvoiceCreationTool(Document):
frappe.scrub(row.party_type): row.party,
"is_pos": 0,
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice",
"update_stock": 0
"update_stock": 0,
"invoice_number": row.invoice_number
})
accounting_dimension = get_accounting_dimensions()
@@ -200,10 +201,13 @@ def start_import(invoices):
names = []
for idx, d in enumerate(invoices):
try:
invoice_number = None
if d.invoice_number:
invoice_number = d.invoice_number
publish(idx, len(invoices), d.doctype)
doc = frappe.get_doc(d)
doc.flags.ignore_mandatory = True
doc.insert()
doc.insert(set_name=invoice_number)
doc.submit()
frappe.db.commit()
names.append(doc.name)

View File

@@ -18,10 +18,10 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
make_company()
def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None):
def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None):
doc = frappe.get_single("Opening Invoice Creation Tool")
args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company,
party_1=party_1, party_2=party_2)
party_1=party_1, party_2=party_2, invoice_number=invoice_number)
doc.update(args)
return doc.make_invoices()
@@ -92,6 +92,20 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
# teardown
frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account)
def test_renaming_of_invoice_using_invoice_number_field(self):
company = "_Test Opening Invoice Company"
party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
self.make_invoices(company=company, party_1=party_1, party_2=party_2, invoice_number="TEST-NEW-INV-11")
sales_inv1 = frappe.get_all('Sales Invoice', filters={'customer':'Customer A'})[0].get("name")
sales_inv2 = frappe.get_all('Sales Invoice', filters={'customer':'Customer B'})[0].get("name")
self.assertEqual(sales_inv1, "TEST-NEW-INV-11")
#teardown
for inv in [sales_inv1, sales_inv2]:
doc = frappe.get_doc('Sales Invoice', inv)
doc.cancel()
def get_opening_invoice_creation_dict(**args):
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
company = args.get("company", "_Test Company")
@@ -107,7 +121,8 @@ def get_opening_invoice_creation_dict(**args):
"item_name": "Opening Item",
"due_date": "2016-09-10",
"posting_date": "2016-09-05",
"temporary_opening_account": get_temporary_opening_account(company)
"temporary_opening_account": get_temporary_opening_account(company),
"invoice_number": args.get("invoice_number")
},
{
"qty": 2.0,
@@ -116,7 +131,8 @@ def get_opening_invoice_creation_dict(**args):
"item_name": "Opening Item",
"due_date": "2016-09-10",
"posting_date": "2016-09-05",
"temporary_opening_account": get_temporary_opening_account(company)
"temporary_opening_account": get_temporary_opening_account(company),
"invoice_number": None
}
]
})

View File

@@ -1,9 +1,11 @@
{
"actions": [],
"creation": "2017-08-29 04:26:36.159247",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"invoice_number",
"party_type",
"party",
"temporary_opening_account",
@@ -114,7 +116,7 @@
],
"istable": 1,
"links": [],
"modified": "2021-12-17 19:25:06.053187",
"modified": "2022-01-04 18:40:15.927675",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Opening Invoice Creation Tool Item",

View File

@@ -3,6 +3,7 @@
import json
from functools import reduce
import frappe
from frappe import ValidationError, _, scrub, throw
@@ -1524,6 +1525,10 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
pe.received_amount = received_amount
pe.letter_head = doc.get("letter_head")
if dt in ['Purchase Order', 'Sales Order', 'Sales Invoice', 'Purchase Invoice']:
pe.project = (doc.get('project') or
reduce(lambda prev,cur: prev or cur, [x.get('project') for x in doc.get('items')], None)) # get first non-empty project from items
if pe.party_type in ["Customer", "Supplier"]:
bank_account = get_party_bank_account(pe.party_type, pe.party)
pe.set("bank_account", bank_account)
@@ -1709,7 +1714,10 @@ def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outsta
def apply_early_payment_discount(paid_amount, received_amount, doc):
total_discount = 0
if doc.doctype in ['Sales Invoice', 'Purchase Invoice'] and doc.payment_schedule:
eligible_for_payments = ['Sales Order', 'Sales Invoice', 'Purchase Order', 'Purchase Invoice']
has_payment_schedule = hasattr(doc, 'payment_schedule') and doc.payment_schedule
if doc.doctype in eligible_for_payments and has_payment_schedule:
for term in doc.payment_schedule:
if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date:
if term.discount_type == 'Percentage':

View File

@@ -16,6 +16,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
update_multi_mode_option,
)
from erpnext.accounts.party import get_due_date, get_party_account
from erpnext.stock.doctype.batch.batch import get_batch_qty, get_pos_reserved_batch_qty
from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos, get_serial_nos
@@ -125,9 +126,26 @@ class POSInvoice(SalesInvoice):
frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no.")
.format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable"))
elif invalid_serial_nos:
frappe.throw(_("Row #{}: Serial Nos. {} has already been transacted into another POS Invoice. Please select valid serial no.")
frappe.throw(_("Row #{}: Serial Nos. {} have already been transacted into another POS Invoice. Please select valid serial no.")
.format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable"))
def validate_pos_reserved_batch_qty(self, item):
filters = {"item_code": item.item_code, "warehouse": item.warehouse, "batch_no":item.batch_no}
available_batch_qty = get_batch_qty(item.batch_no, item.warehouse, item.item_code)
reserved_batch_qty = get_pos_reserved_batch_qty(filters)
bold_item_name = frappe.bold(item.item_name)
bold_extra_batch_qty_needed = frappe.bold(abs(available_batch_qty - reserved_batch_qty - item.qty))
bold_invalid_batch_no = frappe.bold(item.batch_no)
if (available_batch_qty - reserved_batch_qty) == 0:
frappe.throw(_("Row #{}: Batch No. {} of item {} has no stock available. Please select valid batch no.")
.format(item.idx, bold_invalid_batch_no, bold_item_name), title=_("Item Unavailable"))
elif (available_batch_qty - reserved_batch_qty - item.qty) < 0:
frappe.throw(_("Row #{}: Batch No. {} of item {} has less than required stock available, {} more required")
.format(item.idx, bold_invalid_batch_no, bold_item_name, bold_extra_batch_qty_needed), title=_("Item Unavailable"))
def validate_delivered_serial_nos(self, item):
serial_nos = get_serial_nos(item.serial_no)
delivered_serial_nos = frappe.db.get_list('Serial No', {
@@ -150,6 +168,8 @@ class POSInvoice(SalesInvoice):
if d.serial_no:
self.validate_pos_reserved_serial_nos(d)
self.validate_delivered_serial_nos(d)
elif d.batch_no:
self.validate_pos_reserved_batch_qty(d)
else:
if allow_negative_stock:
return
@@ -334,7 +354,6 @@ class POSInvoice(SalesInvoice):
if not for_validate and not self.customer:
self.customer = profile.customer
self.ignore_pricing_rule = profile.ignore_pricing_rule
self.account_for_change_amount = profile.get('account_for_change_amount') or self.account_for_change_amount
self.set_warehouse = profile.get('warehouse') or self.set_warehouse

View File

@@ -521,6 +521,72 @@ class TestPOSInvoice(unittest.TestCase):
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total")
self.assertEqual(rounded_total, 400)
def test_pos_batch_item_qty_validation(self):
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_batch_item_with_batch,
)
create_batch_item_with_batch('_BATCH ITEM', 'TestBatch 01')
item = frappe.get_doc('Item', '_BATCH ITEM')
batch = frappe.get_doc('Batch', 'TestBatch 01')
batch.submit()
item.batch_no = 'TestBatch 01'
item.save()
se = make_stock_entry(target="_Test Warehouse - _TC", item_code="_BATCH ITEM", qty=2, basic_rate=100, batch_no='TestBatch 01')
pos_inv1 = create_pos_invoice(item=item.name, rate=300, qty=1, do_not_submit=1)
pos_inv1.items[0].batch_no = 'TestBatch 01'
pos_inv1.save()
pos_inv1.submit()
pos_inv2 = create_pos_invoice(item=item.name, rate=300, qty=2, do_not_submit=1)
pos_inv2.items[0].batch_no = 'TestBatch 01'
pos_inv2.save()
self.assertRaises(frappe.ValidationError, pos_inv2.submit)
#teardown
pos_inv1.reload()
pos_inv1.cancel()
pos_inv1.delete()
pos_inv2.reload()
pos_inv2.delete()
se.cancel()
batch.reload()
batch.cancel()
batch.delete()
def test_ignore_pricing_rule(self):
from erpnext.accounts.doctype.pricing_rule.test_pricing_rule import make_pricing_rule
item_price = frappe.get_doc({
'doctype': 'Item Price',
'item_code': '_Test Item',
'price_list': '_Test Price List',
'price_list_rate': '450',
})
item_price.insert()
pr = make_pricing_rule(selling=1, priority=5, discount_percentage=10)
pr.save()
pos_inv = create_pos_invoice(qty=1, do_not_submit=1)
pos_inv.items[0].rate = 300
pos_inv.save()
self.assertEquals(pos_inv.items[0].discount_percentage, 10)
# rate shouldn't change
self.assertEquals(pos_inv.items[0].rate, 405)
pos_inv.ignore_pricing_rule = 1
pos_inv.items[0].rate = 300
pos_inv.save()
self.assertEquals(pos_inv.ignore_pricing_rule, 1)
# rate should change since pricing rules are ignored
self.assertEquals(pos_inv.items[0].rate, 300)
item_price.delete()
pos_inv.delete()
pr.delete()
def create_pos_invoice(**args):
args = frappe._dict(args)
pos_profile = None
@@ -557,7 +623,8 @@ def create_pos_invoice(**args):
"income_account": args.income_account or "Sales - _TC",
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
"cost_center": args.cost_center or "_Test Cost Center - _TC",
"serial_no": args.serial_no
"serial_no": args.serial_no,
"batch_no": args.batch_no
})
if not args.do_not_save:
@@ -570,3 +637,8 @@ def create_pos_invoice(**args):
pos_inv.payment_schedule = []
return pos_inv
def make_batch_item(item_name):
from erpnext.stock.doctype.item.test_item import make_item
if not frappe.db.exists(item_name):
return make_item(item_name, dict(has_batch_no = 1, create_new_batch = 1, is_stock_item=1))

View File

@@ -652,7 +652,7 @@ def make_pricing_rule(**args):
"rate": args.rate or 0.0,
"margin_rate_or_amount": args.margin_rate_or_amount or 0.0,
"condition": args.condition or '',
"priority": 1,
"priority": args.priority or 1,
"discount_amount": args.discount_amount or 0.0,
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0
})
@@ -678,6 +678,8 @@ def make_pricing_rule(**args):
if args.get(applicable_for):
doc.db_set(applicable_for, args.get(applicable_for))
return doc
def setup_pricing_rule_data():
if not frappe.db.exists('Campaign', '_Test Campaign'):
frappe.get_doc({

View File

@@ -503,11 +503,11 @@ class PurchaseInvoice(BuyingController):
# Checked both rounding_adjustment and rounded_total
# because rounded_total had value even before introcution of posting GLE based on rounded total
grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
base_grand_total = flt(self.base_rounded_total if (self.base_rounding_adjustment and self.base_rounded_total)
else self.base_grand_total, self.precision("base_grand_total"))
if grand_total and not self.is_internal_transfer():
# Did not use base_grand_total to book rounding loss gle
grand_total_in_company_currency = flt(grand_total * self.conversion_rate,
self.precision("grand_total"))
gl_entries.append(
self.get_gl_dict({
"account": self.credit_to,
@@ -515,8 +515,8 @@ class PurchaseInvoice(BuyingController):
"party": self.supplier,
"due_date": self.due_date,
"against": self.against_expense_account,
"credit": grand_total_in_company_currency,
"credit_in_account_currency": grand_total_in_company_currency \
"credit": base_grand_total,
"credit_in_account_currency": base_grand_total \
if self.party_account_currency==self.company_currency else grand_total,
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
"against_voucher_type": self.doctype,

View File

@@ -1213,7 +1213,7 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
def update_tax_witholding_category(company, account):
from erpnext.accounts.utils import get_fiscal_year
fiscal_year = get_fiscal_year(fiscal_year='_Test Fiscal Year 2021')
fiscal_year = get_fiscal_year(date=nowdate())
if not frappe.db.get_value('Tax Withholding Rate',
{'parent': 'TDS - 194 - Dividends - Individual', 'from_date': ('>=', fiscal_year[1]),

View File

@@ -651,7 +651,7 @@
"hide_seconds": 1,
"label": "Ignore Pricing Rule",
"no_copy": 1,
"permlevel": 1,
"permlevel": 0,
"print_hide": 1
},
{
@@ -2038,7 +2038,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2021-10-21 20:19:38.667508",
"modified": "2021-12-23 20:19:38.667508",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -879,11 +879,11 @@ class SalesInvoice(SellingController):
# Checked both rounding_adjustment and rounded_total
# because rounded_total had value even before introcution of posting GLE based on rounded total
grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
base_grand_total = flt(self.base_rounded_total if (self.base_rounding_adjustment and self.base_rounded_total)
else self.base_grand_total, self.precision("base_grand_total"))
if grand_total and not self.is_internal_transfer():
# Didnot use base_grand_total to book rounding loss gle
grand_total_in_company_currency = flt(grand_total * self.conversion_rate,
self.precision("grand_total"))
gl_entries.append(
self.get_gl_dict({
"account": self.debit_to,
@@ -891,8 +891,8 @@ class SalesInvoice(SellingController):
"party": self.customer,
"due_date": self.due_date,
"against": self.against_income_account,
"debit": grand_total_in_company_currency,
"debit_in_account_currency": grand_total_in_company_currency \
"debit": base_grand_total,
"debit_in_account_currency": base_grand_total \
if self.party_account_currency==self.company_currency else grand_total,
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
"against_voucher_type": self.doctype,

View File

@@ -19,6 +19,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_comp
from erpnext.accounts.utils import PaymentEntryUnlinkError
from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries
from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
from erpnext.controllers.accounts_controller import update_invoice_status
from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
from erpnext.regional.india.utils import get_ewb_data
@@ -1788,47 +1789,6 @@ class TestSalesInvoice(unittest.TestCase):
check_gl_entries(self, si.name, expected_gle, "2019-01-30")
def test_deferred_revenue_post_account_freeze_upto_by_admin(self):
frappe.set_user("Administrator")
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', None)
deferred_account = create_account(account_name="Deferred Revenue",
parent_account="Current Liabilities - _TC", company="_Test Company")
item = create_item("_Test Item for Deferred Accounting")
item.enable_deferred_revenue = 1
item.deferred_revenue_account = deferred_account
item.no_of_months = 12
item.save()
si = create_sales_invoice(item=item.name, posting_date="2019-01-10", do_not_save=True)
si.items[0].enable_deferred_revenue = 1
si.items[0].service_start_date = "2019-01-10"
si.items[0].service_end_date = "2019-03-15"
si.items[0].deferred_revenue_account = deferred_account
si.save()
si.submit()
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', getdate('2019-01-31'))
frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', 'System Manager')
pda1 = frappe.get_doc(dict(
doctype='Process Deferred Accounting',
posting_date=nowdate(),
start_date="2019-01-01",
end_date="2019-03-31",
type="Income",
company="_Test Company"
))
pda1.insert()
self.assertRaises(frappe.ValidationError, pda1.submit)
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', None)
def test_fixed_deferred_revenue(self):
deferred_account = create_account(account_name="Deferred Revenue",
parent_account="Current Liabilities - _TC", company="_Test Company")
@@ -2358,6 +2318,41 @@ class TestSalesInvoice(unittest.TestCase):
si.reload()
self.assertEqual(si.status, "Paid")
def test_update_invoice_status(self):
today = nowdate()
# Sales Invoice without Payment Schedule
si = create_sales_invoice(posting_date=add_days(today, -5))
# Sales Invoice with Payment Schedule
si_with_payment_schedule = create_sales_invoice(do_not_submit=True)
si_with_payment_schedule.extend("payment_schedule", [
{
"due_date": add_days(today, -5),
"invoice_portion": 50,
"payment_amount": si_with_payment_schedule.grand_total / 2
},
{
"due_date": add_days(today, 5),
"invoice_portion": 50,
"payment_amount": si_with_payment_schedule.grand_total / 2
}
])
si_with_payment_schedule.submit()
for invoice in (si, si_with_payment_schedule):
invoice.db_set("status", "Unpaid")
update_invoice_status()
invoice.reload()
self.assertEqual(invoice.status, "Overdue")
invoice.db_set("status", "Unpaid and Discounted")
update_invoice_status()
invoice.reload()
self.assertEqual(invoice.status, "Overdue and Discounted")
def test_sales_commission(self):
si = frappe.copy_doc(test_records[0])
item = copy.deepcopy(si.get('items')[0])
@@ -2419,6 +2414,74 @@ class TestSalesInvoice(unittest.TestCase):
frappe.db.set_value('Accounts Settings', None, 'over_billing_allowance', over_billing_allowance)
def test_multi_currency_deferred_revenue_via_journal_entry(self):
deferred_account = create_account(account_name="Deferred Revenue",
parent_account="Current Liabilities - _TC", company="_Test Company")
acc_settings = frappe.get_single('Accounts Settings')
acc_settings.book_deferred_entries_via_journal_entry = 1
acc_settings.submit_journal_entries = 1
acc_settings.save()
item = create_item("_Test Item for Deferred Accounting")
item.enable_deferred_expense = 1
item.deferred_revenue_account = deferred_account
item.save()
si = create_sales_invoice(customer='_Test Customer USD', currency='USD',
item=item.name, qty=1, rate=100, conversion_rate=60, do_not_save=True)
si.set_posting_time = 1
si.posting_date = '2019-01-01'
si.debit_to = '_Test Receivable USD - _TC'
si.items[0].enable_deferred_revenue = 1
si.items[0].service_start_date = "2019-01-01"
si.items[0].service_end_date = "2019-03-30"
si.items[0].deferred_expense_account = deferred_account
si.save()
si.submit()
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', getdate('2019-01-31'))
pda1 = frappe.get_doc(dict(
doctype='Process Deferred Accounting',
posting_date=nowdate(),
start_date="2019-01-01",
end_date="2019-03-31",
type="Income",
company="_Test Company"
))
pda1.insert()
pda1.submit()
expected_gle = [
["Sales - _TC", 0.0, 2089.89, "2019-01-28"],
[deferred_account, 2089.89, 0.0, "2019-01-28"],
["Sales - _TC", 0.0, 1887.64, "2019-02-28"],
[deferred_account, 1887.64, 0.0, "2019-02-28"],
["Sales - _TC", 0.0, 2022.47, "2019-03-15"],
[deferred_account, 2022.47, 0.0, "2019-03-15"]
]
gl_entries = gl_entries = frappe.db.sql("""select account, debit, credit, posting_date
from `tabGL Entry`
where voucher_type='Journal Entry' and voucher_detail_no=%s and posting_date <= %s
order by posting_date asc, account asc""", (si.items[0].name, si.posting_date), as_dict=1)
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_gle[i][0], gle.account)
self.assertEqual(expected_gle[i][1], gle.credit)
self.assertEqual(expected_gle[i][2], gle.debit)
self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
acc_settings = frappe.get_single('Accounts Settings')
acc_settings.book_deferred_entries_via_journal_entry = 0
acc_settings.submit_journal_entriessubmit_journal_entries = 0
acc_settings.save()
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill()
si.naming_series = 'INV-2020-.#####'

View File

@@ -28,14 +28,14 @@
{
"columns": 2,
"fieldname": "single_threshold",
"fieldtype": "Currency",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Single Transaction Threshold"
},
{
"columns": 3,
"fieldname": "cumulative_threshold",
"fieldtype": "Currency",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Cumulative Transaction Threshold"
},
@@ -59,7 +59,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-08-31 11:42:12.213977",
"modified": "2022-01-13 12:04:42.904263",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Withholding Rate",
@@ -68,5 +68,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -117,6 +117,11 @@ frappe.query_reports["Accounts Receivable Summary"] = {
"label": __("Show Future Payments"),
"fieldtype": "Check",
},
{
"fieldname":"show_gl_balance",
"label": __("Show GL Balance"),
"fieldtype": "Check",
},
],
onload: function(report) {

View File

@@ -4,7 +4,7 @@
import frappe
from frappe import _, scrub
from frappe.utils import cint
from frappe.utils import cint, flt
from six import iteritems
from erpnext.accounts.party import get_partywise_advanced_payment_amount
@@ -37,6 +37,9 @@ class AccountsReceivableSummary(ReceivablePayableReport):
party_advance_amount = get_partywise_advanced_payment_amount(self.party_type,
self.filters.report_date, self.filters.show_future_payments, self.filters.company) or {}
if self.filters.show_gl_balance:
gl_balance_map = get_gl_balance(self.filters.report_date)
for party, party_dict in iteritems(self.party_total):
if party_dict.outstanding == 0:
continue
@@ -56,6 +59,10 @@ class AccountsReceivableSummary(ReceivablePayableReport):
# but in summary report advance shown in separate column
row.paid -= row.advance
if self.filters.show_gl_balance:
row.gl_balance = gl_balance_map.get(party)
row.diff = flt(row.outstanding) - flt(row.gl_balance)
self.data.append(row)
def get_party_total(self, args):
@@ -115,6 +122,10 @@ class AccountsReceivableSummary(ReceivablePayableReport):
self.add_column(_(credit_debit_label), fieldname='credit_note')
self.add_column(_('Outstanding Amount'), fieldname='outstanding')
if self.filters.show_gl_balance:
self.add_column(_('GL Balance'), fieldname='gl_balance')
self.add_column(_('Difference'), fieldname='diff')
self.setup_ageing_columns()
if self.party_type == "Customer":
@@ -141,3 +152,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
# Add column for total due amount
self.add_column(label="Total Amount Due", fieldname='total_due')
def get_gl_balance(report_date):
return frappe._dict(frappe.db.get_all("GL Entry", fields=['party', 'sum(debit - credit)'],
filters={'posting_date': ("<=", report_date), 'is_cancelled': 0}, group_by='party', as_list=1))

View File

@@ -121,20 +121,21 @@ class Deferred_Item(object):
"""
simulate future posting by creating dummy gl entries. starts from the last posting date.
"""
if add_days(self.last_entry_date, 1) < self.period_list[-1].to_date:
self.estimate_for_period_list = get_period_list(
self.filters.from_fiscal_year,
self.filters.to_fiscal_year,
add_days(self.last_entry_date, 1),
self.period_list[-1].to_date,
"Date Range",
"Monthly",
company=self.filters.company,
)
for period in self.estimate_for_period_list:
amount = self.calculate_amount(period.from_date, period.to_date)
gle = self.make_dummy_gle(period.key, period.to_date, amount)
self.gle_entries.append(gle)
if self.service_start_date != self.service_end_date:
if add_days(self.last_entry_date, 1) < self.period_list[-1].to_date:
self.estimate_for_period_list = get_period_list(
self.filters.from_fiscal_year,
self.filters.to_fiscal_year,
add_days(self.last_entry_date, 1),
self.period_list[-1].to_date,
"Date Range",
"Monthly",
company=self.filters.company,
)
for period in self.estimate_for_period_list:
amount = self.calculate_amount(period.from_date, period.to_date)
gle = self.make_dummy_gle(period.key, period.to_date, amount)
self.gle_entries.append(gle)
def calculate_item_revenue_expense_for_period(self):
"""

View File

@@ -167,7 +167,7 @@ frappe.query_reports["General Ledger"] = {
"fieldname": "include_dimensions",
"label": __("Consider Accounting Dimensions"),
"fieldtype": "Check",
"default": 0
"default": 1
},
{
"fieldname": "show_opening_entries",

View File

@@ -449,9 +449,11 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
elif group_by_voucher_consolidated:
keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")]
for dim in accounting_dimensions:
keylist.append(gle.get(dim))
keylist.append(gle.get("cost_center"))
if filters.get("include_dimensions"):
for dim in accounting_dimensions:
keylist.append(gle.get(dim))
keylist.append(gle.get("cost_center"))
key = tuple(keylist)
if key not in consolidated_gle:
consolidated_gle.setdefault(key, gle)
@@ -595,14 +597,14 @@ def get_columns(filters):
"fieldname": dim.fieldname,
"width": 100
})
columns.extend([
{
columns.append({
"label": _("Cost Center"),
"options": "Cost Center",
"fieldname": "cost_center",
"width": 100
},
})
columns.extend([
{
"label": _("Against Voucher Type"),
"fieldname": "against_voucher_type",

View File

@@ -583,7 +583,17 @@ class Asset(AccountsController):
return purchase_document
def get_fixed_asset_account(self):
return get_asset_category_account('fixed_asset_account', None, self.name, None, self.asset_category, self.company)
fixed_asset_account = get_asset_category_account('fixed_asset_account', None, self.name, None, self.asset_category, self.company)
if not fixed_asset_account:
frappe.throw(
_("Set {0} in asset category {1} for company {2}").format(
frappe.bold("Fixed Asset Account"),
frappe.bold(self.asset_category),
frappe.bold(self.company),
),
title=_("Account not Found"),
)
return fixed_asset_account
def get_cwip_account(self, cwip_enabled=False):
cwip_account = None

View File

@@ -7,6 +7,7 @@ import json
import frappe
from frappe import _, throw
from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied
from frappe.query_builder.functions import Sum
from frappe.utils import (
add_days,
add_months,
@@ -113,7 +114,7 @@ class AccountsController(TransactionBase):
_('{0} is blocked so this transaction cannot proceed').format(supplier_name), raise_exception=1)
def validate(self):
if not self.get('is_return'):
if not self.get('is_return') and not self.get('is_debit_note'):
self.validate_qty_is_not_zero()
if self.get("_action") and self._action != "update_after_submit":
@@ -190,8 +191,6 @@ class AccountsController(TransactionBase):
frappe.throw(_("Row #{0}: Service Start Date cannot be greater than Service End Date").format(d.idx))
elif getdate(self.posting_date) > getdate(d.service_end_date):
frappe.throw(_("Row #{0}: Service End Date cannot be before Invoice Posting Date").format(d.idx))
elif getdate(self.posting_date) > getdate(d.service_start_date):
frappe.throw(_("Row #{0}: Service Start Date cannot be before Invoice Posting Date").format(d.idx))
def validate_invoice_documents_schedule(self):
self.validate_payment_schedule_dates()
@@ -1692,58 +1691,69 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype,
def update_invoice_status():
"""Updates status as Overdue for applicable invoices. Runs daily."""
today = getdate()
payment_schedule = frappe.qb.DocType("Payment Schedule")
for doctype in ("Sales Invoice", "Purchase Invoice"):
frappe.db.sql("""
UPDATE `tab{doctype}` invoice SET invoice.status = 'Overdue'
WHERE invoice.docstatus = 1
AND invoice.status REGEXP '^Unpaid|^Partly Paid'
AND invoice.outstanding_amount > 0
AND (
{or_condition}
(
(
CASE
WHEN invoice.party_account_currency = invoice.currency
THEN (
CASE
WHEN invoice.disable_rounded_total
THEN invoice.grand_total
ELSE invoice.rounded_total
END
)
ELSE (
CASE
WHEN invoice.disable_rounded_total
THEN invoice.base_grand_total
ELSE invoice.base_rounded_total
END
)
END
) - invoice.outstanding_amount
) < (
SELECT SUM(
CASE
WHEN invoice.party_account_currency = invoice.currency
THEN ps.payment_amount
ELSE ps.base_payment_amount
END
)
FROM `tabPayment Schedule` ps
WHERE ps.parent = invoice.name
AND ps.due_date < %(today)s
)
)
""".format(
doctype=doctype,
or_condition=(
"invoice.is_pos AND invoice.due_date < %(today)s OR"
if doctype == "Sales Invoice"
else ""
)
), {"today": today}
invoice = frappe.qb.DocType(doctype)
consider_base_amount = invoice.party_account_currency != invoice.currency
payment_amount = (
frappe.qb.terms.Case()
.when(consider_base_amount, payment_schedule.base_payment_amount)
.else_(payment_schedule.payment_amount)
)
payable_amount = (
frappe.qb.from_(payment_schedule)
.select(Sum(payment_amount))
.where(
(payment_schedule.parent == invoice.name)
& (payment_schedule.due_date < today)
)
)
total = (
frappe.qb.terms.Case()
.when(invoice.disable_rounded_total, invoice.grand_total)
.else_(invoice.rounded_total)
)
base_total = (
frappe.qb.terms.Case()
.when(invoice.disable_rounded_total, invoice.base_grand_total)
.else_(invoice.base_rounded_total)
)
total_amount = (
frappe.qb.terms.Case()
.when(consider_base_amount, base_total)
.else_(total)
)
is_overdue = total_amount - invoice.outstanding_amount < payable_amount
conditions = (
(invoice.docstatus == 1)
& (invoice.outstanding_amount > 0)
& (
invoice.status.like("Unpaid%")
| invoice.status.like("Partly Paid%")
)
& (
((invoice.is_pos & invoice.due_date < today) | is_overdue)
if doctype == "Sales Invoice"
else is_overdue
)
)
status = (
frappe.qb.terms.Case()
.when(invoice.status.like("%Discounted"), "Overdue and Discounted")
.else_("Overdue")
)
frappe.qb.update(invoice).set("status", status).where(conditions).run()
@frappe.whitelist()
def get_payment_terms(terms_template, posting_date=None, grand_total=None, base_grand_total=None, bill_date=None):
if not terms_template:
@@ -2113,6 +2123,11 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
parent.update_status_updater()
else:
parent.check_credit_limit()
# reset index of child table
for idx, row in enumerate(parent.get(child_docname), start=1):
row.idx = idx
parent.save()
if parent_doctype == 'Purchase Order':

View File

@@ -385,7 +385,7 @@ class SellingController(StockController):
# Get incoming rate based on original item cost based on valuation method
qty = flt(d.get('stock_qty') or d.get('actual_qty'))
if not d.incoming_rate:
if not (self.get("is_return") and d.incoming_rate):
d.incoming_rate = get_incoming_rate({
"item_code": d.item_code,
"warehouse": d.warehouse,

View File

@@ -17,7 +17,7 @@ from erpnext.accounts.general_ledger import (
from erpnext.accounts.utils import get_fiscal_year
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.stock import get_warehouse_account_map
from erpnext.stock.stock_ledger import get_items_to_be_repost, get_valuation_rate
from erpnext.stock.stock_ledger import get_items_to_be_repost
class QualityInspectionRequiredError(frappe.ValidationError): pass
@@ -111,17 +111,6 @@ class StockController(AccountsController):
self.check_expense_account(item_row)
# If the item does not have the allow zero valuation rate flag set
# and ( valuation rate not mentioned in an incoming entry
# or incoming entry not found while delivering the item),
# try to pick valuation rate from previous sle or Item master and update in SLE
# Otherwise, throw an exception
if not sle.stock_value_difference and self.doctype != "Stock Reconciliation" \
and not item_row.get("allow_zero_valuation_rate"):
sle = self.update_stock_ledger_entries(sle)
# expense account/ target_warehouse / source_warehouse
if item_row.get('target_warehouse'):
warehouse = item_row.get('target_warehouse')
@@ -164,26 +153,6 @@ class StockController(AccountsController):
return frappe.flags.debit_field_precision
def update_stock_ledger_entries(self, sle):
sle.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
self.doctype, self.name, currency=self.company_currency, company=self.company)
sle.stock_value = flt(sle.qty_after_transaction) * flt(sle.valuation_rate)
sle.stock_value_difference = flt(sle.actual_qty) * flt(sle.valuation_rate)
if sle.name:
frappe.db.sql("""
update
`tabStock Ledger Entry`
set
stock_value = %(stock_value)s,
valuation_rate = %(valuation_rate)s,
stock_value_difference = %(stock_value_difference)s
where
name = %(name)s""", (sle))
return sle
def get_voucher_details(self, default_expense_account, default_cost_center, sle_map):
if self.doctype == "Stock Reconciliation":
reconciliation_purpose = frappe.db.get_value(self.doctype, self.name, "purpose")

View File

@@ -139,6 +139,8 @@ class calculate_taxes_and_totals(object):
if not item.qty and self.doc.get("is_return"):
item.amount = flt(-1 * item.rate, item.precision("amount"))
elif not item.qty and self.doc.get("is_debit_note"):
item.amount = flt(item.rate, item.precision("amount"))
else:
item.amount = flt(item.rate * item.qty, item.precision("amount"))
@@ -594,13 +596,14 @@ class calculate_taxes_and_totals(object):
if self.doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
grand_total = self.doc.rounded_total or self.doc.grand_total
base_grand_total = self.doc.base_rounded_total or self.doc.base_grand_total
if self.doc.party_account_currency == self.doc.currency:
total_amount_to_pay = flt(grand_total - self.doc.total_advance
- flt(self.doc.write_off_amount), self.doc.precision("grand_total"))
else:
total_amount_to_pay = flt(flt(grand_total *
self.doc.conversion_rate, self.doc.precision("grand_total")) - self.doc.total_advance
- flt(self.doc.base_write_off_amount), self.doc.precision("grand_total"))
total_amount_to_pay = flt(flt(base_grand_total, self.doc.precision("base_grand_total")) - self.doc.total_advance
- flt(self.doc.base_write_off_amount), self.doc.precision("base_grand_total"))
self.doc.round_floats_in(self.doc, ["paid_amount"])
change_amount = 0

View File

@@ -1,6 +1,8 @@
import unittest
from functools import partial
import frappe
from erpnext.controllers import queries
@@ -85,3 +87,6 @@ class TestQueries(unittest.TestCase):
wh = query(filters=[["Bin", "item_code", "=", "_Test Item"]])
self.assertGreaterEqual(len(wh), 1)
def test_default_uoms(self):
self.assertGreaterEqual(frappe.db.count("UOM", {"enabled": 1}), 10)

View File

@@ -4,19 +4,72 @@ import frappe
class TestUtils(unittest.TestCase):
def test_reset_default_field_value(self):
doc = frappe.get_doc({
"doctype": "Purchase Receipt",
"set_warehouse": "Warehouse 1",
})
def test_reset_default_field_value(self):
doc = frappe.get_doc({
"doctype": "Purchase Receipt",
"set_warehouse": "Warehouse 1",
})
# Same values
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}]
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
self.assertEqual(doc.set_warehouse, "Warehouse 1")
# Same values
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}]
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
self.assertEqual(doc.set_warehouse, "Warehouse 1")
# Mixed values
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}]
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
self.assertEqual(doc.set_warehouse, None)
# Mixed values
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}]
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
self.assertEqual(doc.set_warehouse, None)
def test_reset_default_field_value_in_mfg_stock_entry(self):
# manufacture stock entry with rows having blank source/target wh
se = frappe.get_doc(
doctype="Stock Entry",
purpose="Manufacture",
stock_entry_type="Manufacture",
company="_Test Company",
from_warehouse="_Test Warehouse - _TC",
to_warehouse="_Test Warehouse 1 - _TC",
items=[
frappe._dict(item_code="_Test Item", qty=1, basic_rate=200, s_warehouse="_Test Warehouse - _TC"),
frappe._dict(item_code="_Test FG Item", qty=4, t_warehouse="_Test Warehouse 1 - _TC", is_finished_item=1)
]
)
se.save()
# default fields must be untouched
self.assertEqual(se.from_warehouse, "_Test Warehouse - _TC")
self.assertEqual(se.to_warehouse, "_Test Warehouse 1 - _TC")
se.delete()
def test_reset_default_field_value_in_transfer_stock_entry(self):
doc = frappe.get_doc({
"doctype": "Stock Entry",
"purpose": "Material Receipt",
"from_warehouse": "Warehouse 1",
"to_warehouse": "Warehouse 2",
})
# Same values
doc.items = [
{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"},
{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"},
{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"}
]
doc.reset_default_field_value("from_warehouse", "items", "s_warehouse")
doc.reset_default_field_value("to_warehouse", "items", "t_warehouse")
self.assertEqual(doc.from_warehouse, "Warehouse 1")
self.assertEqual(doc.to_warehouse, "Warehouse 2")
# Mixed values in source wh
doc.items = [
{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"},
{"s_warehouse": "Warehouse 3", "t_warehouse": "Warehouse 2"},
{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"}
]
doc.reset_default_field_value("from_warehouse", "items", "s_warehouse")
doc.reset_default_field_value("to_warehouse", "items", "t_warehouse")
self.assertEqual(doc.from_warehouse, None)
self.assertEqual(doc.to_warehouse, "Warehouse 2")

View File

@@ -1,7 +1,6 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import itertools
import json
import frappe
@@ -203,16 +202,15 @@ class WebsiteItem(WebsiteGenerator):
context.body_class = "product-page"
context.parents = get_parent_item_groups(self.item_group, from_item=True) # breadcumbs
self.attributes = frappe.get_all("Item Variant Attribute",
self.attributes = frappe.get_all(
"Item Variant Attribute",
fields=["attribute", "attribute_value"],
filters={"parent": self.item_code})
filters={"parent": self.item_code}
)
if self.slideshow:
context.update(get_slideshow(self))
self.set_variant_context(context)
self.set_attribute_context(context)
self.set_disabled_attributes(context)
self.set_metatags(context)
self.set_shopping_cart_data(context)
@@ -237,61 +235,6 @@ class WebsiteItem(WebsiteGenerator):
return context
def set_variant_context(self, context):
if not self.has_variants:
return
context.no_cache = True
variant = frappe.form_dict.variant
# load variants
# also used in set_attribute_context
context.variants = frappe.get_all(
"Item",
filters={
"variant_of": self.item_code,
"published_in_website": 1
},
order_by="name asc")
# the case when the item is opened for the first time from its list
if not variant and context.variants:
variant = context.variants[0]
if variant:
context.variant = frappe.get_doc("Item", variant)
fields = ("website_image", "website_image_alt", "web_long_description", "description",
"website_specifications")
for fieldname in fields:
if context.variant.get(fieldname):
value = context.variant.get(fieldname)
if isinstance(value, list):
value = [d.as_dict() for d in value]
context[fieldname] = value
if self.slideshow and context.variant and context.variant.slideshow:
context.update(get_slideshow(context.variant))
def set_attribute_context(self, context):
if not self.has_variants:
return
attribute_values_available = {}
context.attribute_values = {}
context.selected_attributes = {}
# load attributes
self.set_selected_attributes(context.variants, context, attribute_values_available)
# filter attributes, order based on attribute table
item = frappe.get_cached_doc("Item", self.item_code)
self.set_attribute_values(item.attributes, context, attribute_values_available)
context.variant_info = json.dumps(context.variants)
def set_selected_attributes(self, variants, context, attribute_values_available):
for variant in variants:
variant.attributes = frappe.get_all(
@@ -328,50 +271,6 @@ class WebsiteItem(WebsiteGenerator):
if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []):
values.append(attr_value.attribute_value)
def set_disabled_attributes(self, context):
"""Disable selection options of attribute combinations that do not result in a variant"""
if not self.attributes or not self.has_variants:
return
context.disabled_attributes = {}
attributes = [attr.attribute for attr in self.attributes]
def find_variant(combination):
for variant in context.variants:
if len(variant.attributes) < len(attributes):
continue
if "combination" not in variant:
ref_combination = []
for attr in variant.attributes:
idx = attributes.index(attr.attribute)
ref_combination.insert(idx, attr.attribute_value)
variant["combination"] = ref_combination
if not (set(combination) - set(variant["combination"])):
# check if the combination is a subset of a variant combination
# eg. [Blue, 0.5] is a possible combination if exists [Blue, Large, 0.5]
return True
for i, attr in enumerate(self.attributes):
if i == 0:
continue
combination_source = []
# loop through previous attributes
for prev_attr in self.attributes[:i]:
combination_source.append([context.selected_attributes.get(prev_attr.attribute)])
combination_source.append(context.attribute_values[attr.attribute])
for combination in itertools.product(*combination_source):
if not find_variant(combination):
context.disabled_attributes.setdefault(attr.attribute, []).append(combination[-1])
def set_metatags(self, context):
context.metatags = frappe._dict({})

View File

@@ -197,7 +197,10 @@ class ProductQuery:
website_item_groups = frappe.db.get_all(
"Website Item",
fields=self.fields + ["`tabWebsite Item Group`.parent as wig_parent"],
filters=[["Website Item Group", "item_group", "=", item_group]]
filters=[
["Website Item Group", "item_group", "=", item_group],
["published", "=", 1]
]
)
return website_item_groups

View File

@@ -44,7 +44,7 @@ class ItemVariantsCacheManager:
val = frappe.cache().get_value('ordered_attribute_values_map')
if val: return val
all_attribute_values = frappe.db.get_all('Item Attribute Value',
all_attribute_values = frappe.get_all('Item Attribute Value',
['attribute_value', 'idx', 'parent'], order_by='idx asc')
ordered_attribute_values_map = frappe._dict({})
@@ -57,25 +57,34 @@ class ItemVariantsCacheManager:
def build_cache(self):
parent_item_code = self.item_code
attributes = [a.attribute for a in frappe.db.get_all('Item Variant Attribute',
{'parent': parent_item_code}, ['attribute'], order_by='idx asc')
attributes = [
a.attribute for a in frappe.get_all(
'Item Variant Attribute',
{'parent': parent_item_code},
['attribute'],
order_by='idx asc'
)
]
item_variants_data = frappe.db.get_all('Item Variant Attribute',
{'variant_of': parent_item_code}, ['parent', 'attribute', 'attribute_value'],
# join with Website Item
item_variants_data = frappe.get_all(
'Item Variant Attribute',
{'variant_of': parent_item_code},
['parent', 'attribute', 'attribute_value'],
order_by='name',
as_list=1
)
unpublished_items = set([i.item_code for i in frappe.db.get_all('Website Item', filters={'published': 0}, fields=["item_code"])])
disabled_items = set(
[i.name for i in frappe.db.get_all('Item', {'disabled': 1})]
)
attribute_value_item_map = frappe._dict({})
item_attribute_value_map = frappe._dict({})
attribute_value_item_map = frappe._dict()
item_attribute_value_map = frappe._dict()
# dont consider variants that are unpublished
# (either have no Website Item or are unpublished in Website Item)
item_variants_data = [r for r in item_variants_data if r[0] not in unpublished_items]
item_variants_data = [r for r in item_variants_data if frappe.db.exists("Website Item", {"item_code": r[0]})]
# dont consider variants that are disabled
# pull all other variants
item_variants_data = [r for r in item_variants_data if r[0] not in disabled_items]
for row in item_variants_data:
item_code, attribute, attribute_value = row

View File

@@ -1,11 +1,96 @@
# import frappe
import unittest
# from erpnext.e_commerce.product_data_engine.query import ProductQuery
# from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
import frappe
from erpnext.controllers.item_variant import create_variant
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
from erpnext.stock.doctype.item.test_item import make_item
test_dependencies = ["Item"]
class TestVariantSelector(unittest.TestCase):
# TODO: Variant Selector Tests
pass
def setUp(self) -> None:
self.template_item = make_item("Test-Tshirt-Temp", {
"has_variant": 1,
"variant_based_on": "Item Attribute",
"attributes": [
{
"attribute": "Test Size"
},
{
"attribute": "Test Colour"
}
]
})
# create L-R, L-G, M-R, M-G and S-R
for size in ("Large", "Medium",):
for colour in ("Red", "Green",):
variant = create_variant("Test-Tshirt-Temp", {
"Test Size": size,
"Test Colour": colour
})
variant.save()
variant = create_variant("Test-Tshirt-Temp", {
"Test Size": "Small",
"Test Colour": "Red"
})
variant.save()
def tearDown(self):
frappe.db.rollback()
def test_item_attributes(self):
"""
Test if the right attributes are fetched in the popup.
(Attributes must only come from active items)
Attribute selection must not be linked to Website Items.
"""
from erpnext.e_commerce.variant_selector.utils import get_attributes_and_values
make_website_item(self.template_item) # publish template not variants
attr_data = get_attributes_and_values("Test-Tshirt-Temp")
self.assertEqual(attr_data[0]["attribute"], "Test Size")
self.assertEqual(attr_data[1]["attribute"], "Test Colour")
self.assertEqual(len(attr_data[0]["values"]), 3) # ['Small', 'Medium', 'Large']
self.assertEqual(len(attr_data[1]["values"]), 2) # ['Red', 'Green']
# disable small red tshirt, now there are no small tshirts.
# but there are some red tshirts
small_variant = frappe.get_doc("Item", "Test-Tshirt-Temp-S-R")
small_variant.disabled = 1
small_variant.save() # trigger cache rebuild
attr_data = get_attributes_and_values("Test-Tshirt-Temp")
# Only L and M attribute values must be fetched since S is disabled
self.assertEqual(len(attr_data[0]["values"]), 2) # ['Medium', 'Large']
# teardown
small_variant.disabled = 1
small_variant.save()
def test_next_item_variant_values(self):
"""
Test if on selecting an attribute value, the next possible values
are filtered accordingly.
Values that dont apply should not be fetched.
E.g.
There is a ** Small-Red ** Tshirt. No other colour in this size.
On selecting ** Small **, only ** Red ** should be selectable next.
"""
from erpnext.e_commerce.variant_selector.utils import get_next_attribute_and_values
next_values = get_next_attribute_and_values("Test-Tshirt-Temp", selected_attributes={"Test Size": "Small"})
next_colours = next_values["valid_options_for_attributes"]["Test Colour"]
filtered_items = next_values["filtered_items"]
self.assertEqual(len(next_colours), 1)
self.assertEqual(next_colours.pop(), "Red")
self.assertEqual(len(filtered_items), 1)
self.assertEqual(filtered_items.pop(), "Test-Tshirt-Temp-S-R")

View File

@@ -143,7 +143,7 @@ class Patient(Document):
age = self.age
if not age:
return
age_str = str(age.years) + ' ' + _("Years(s)") + ' ' + str(age.months) + ' ' + _("Month(s)") + ' ' + str(age.days) + ' ' + _("Day(s)")
age_str = str(age.years) + ' ' + _("Year(s)") + ' ' + str(age.months) + ' ' + _("Month(s)") + ' ' + str(age.days) + ' ' + _("Day(s)")
return age_str
@frappe.whitelist()

View File

@@ -5,9 +5,9 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import cstr, formatdate, get_datetime, getdate, nowdate
from frappe.utils import cint, cstr, formatdate, get_datetime, getdate, nowdate
from erpnext.hr.utils import validate_active_employee
from erpnext.hr.utils import get_holiday_dates_for_employee, validate_active_employee
class Attendance(Document):
@@ -171,7 +171,7 @@ def get_month_map():
})
@frappe.whitelist()
def get_unmarked_days(employee, month):
def get_unmarked_days(employee, month, exclude_holidays=0):
import calendar
month_map = get_month_map()
@@ -191,6 +191,11 @@ def get_unmarked_days(employee, month):
])
marked_days = [get_datetime(record.attendance_date) for record in records]
if cint(exclude_holidays):
holiday_dates = get_holiday_dates_for_employee(employee, month_start, month_end)
holidays = [get_datetime(record) for record in holiday_dates]
marked_days.extend(holidays)
unmarked_days = []
for date in dates_of_month:

View File

@@ -28,6 +28,7 @@ frappe.listview_settings['Attendance'] = {
onchange: function() {
dialog.set_df_property("unmarked_days", "hidden", 1);
dialog.set_df_property("status", "hidden", 1);
dialog.set_df_property("exclude_holidays", "hidden", 1);
dialog.set_df_property("month", "value", '');
dialog.set_df_property("unmarked_days", "options", []);
dialog.no_unmarked_days_left = false;
@@ -42,9 +43,14 @@ frappe.listview_settings['Attendance'] = {
onchange: function() {
if (dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
dialog.set_df_property("status", "hidden", 0);
dialog.set_df_property("exclude_holidays", "hidden", 0);
dialog.set_df_property("unmarked_days", "options", []);
dialog.no_unmarked_days_left = false;
me.get_multi_select_options(dialog.fields_dict.employee.value, dialog.fields_dict.month.value).then(options => {
me.get_multi_select_options(
dialog.fields_dict.employee.value,
dialog.fields_dict.month.value,
dialog.fields_dict.exclude_holidays.get_value()
).then(options => {
if (options.length > 0) {
dialog.set_df_property("unmarked_days", "hidden", 0);
dialog.set_df_property("unmarked_days", "options", options);
@@ -64,6 +70,31 @@ frappe.listview_settings['Attendance'] = {
reqd: 1,
},
{
label: __("Exclude Holidays"),
fieldtype: "Check",
fieldname: "exclude_holidays",
hidden: 1,
onchange: function() {
if (dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
dialog.set_df_property("status", "hidden", 0);
dialog.set_df_property("unmarked_days", "options", []);
dialog.no_unmarked_days_left = false;
me.get_multi_select_options(
dialog.fields_dict.employee.value,
dialog.fields_dict.month.value,
dialog.fields_dict.exclude_holidays.get_value()
).then(options => {
if (options.length > 0) {
dialog.set_df_property("unmarked_days", "hidden", 0);
dialog.set_df_property("unmarked_days", "options", options);
} else {
dialog.no_unmarked_days_left = true;
}
});
}
}
},
{
label: __("Unmarked Attendance for days"),
fieldname: "unmarked_days",
@@ -105,7 +136,7 @@ frappe.listview_settings['Attendance'] = {
});
},
get_multi_select_options: function(employee, month) {
get_multi_select_options: function(employee, month, exclude_holidays) {
return new Promise(resolve => {
frappe.call({
method: 'erpnext.hr.doctype.attendance.attendance.get_unmarked_days',
@@ -113,6 +144,7 @@ frappe.listview_settings['Attendance'] = {
args: {
employee: employee,
month: month,
exclude_holidays: exclude_holidays
}
}).then(r => {
var options = [];

View File

@@ -17,7 +17,10 @@ class TestEmployeeOnboarding(unittest.TestCase):
def test_employee_onboarding_incomplete_task(self):
if frappe.db.exists('Employee Onboarding', {'employee_name': 'Test Researcher'}):
frappe.delete_doc('Employee Onboarding', {'employee_name': 'Test Researcher'})
_set_up()
frappe.db.sql("delete from `tabEmployee Onboarding`")
project = "Employee Onboarding : test@researcher.com"
frappe.db.sql("delete from tabProject where name=%s", project)
frappe.db.sql("delete from tabTask where project=%s", project)
applicant = get_job_applicant()
job_offer = create_job_offer(job_applicant=applicant.name)
@@ -42,7 +45,7 @@ class TestEmployeeOnboarding(unittest.TestCase):
onboarding.submit()
project_name = frappe.db.get_value("Project", onboarding.project, "project_name")
self.assertEqual(project_name, 'Employee Onboarding : Test Researcher - test@researcher.com')
self.assertEqual(project_name, 'Employee Onboarding : test@researcher.com')
# don't allow making employee if onboarding is not complete
self.assertRaises(IncompleteTaskError, make_employee, onboarding.name)
@@ -65,8 +68,8 @@ class TestEmployeeOnboarding(unittest.TestCase):
self.assertEqual(employee.employee_name, 'Test Researcher')
def get_job_applicant():
if frappe.db.exists('Job Applicant', 'Test Researcher - test@researcher.com'):
return frappe.get_doc('Job Applicant', 'Test Researcher - test@researcher.com')
if frappe.db.exists('Job Applicant', 'test@researcher.com'):
return frappe.get_doc('Job Applicant', 'test@researcher.com')
applicant = frappe.new_doc('Job Applicant')
applicant.applicant_name = 'Test Researcher'
applicant.email_id = 'test@researcher.com'

View File

@@ -192,10 +192,11 @@
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-09-29 23:06:10.904260",
"modified": "2022-01-12 16:28:53.196881",
"modified_by": "Administrator",
"module": "HR",
"name": "Job Applicant",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -210,10 +211,11 @@
"write": 1
}
],
"search_fields": "applicant_name",
"search_fields": "applicant_name, email_id, job_title, phone_number",
"sender_field": "email_id",
"sort_field": "modified",
"sort_order": "ASC",
"states": [],
"subject_field": "notes",
"title_field": "applicant_name"
}

View File

@@ -7,6 +7,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.model.naming import append_number_if_name_exists
from frappe.utils import validate_email_address
from erpnext.hr.doctype.interview.interview import get_interviewers
@@ -21,10 +22,11 @@ class JobApplicant(Document):
self.get("__onload").job_offer = job_offer[0].name
def autoname(self):
keys = filter(None, (self.applicant_name, self.email_id, self.job_title))
if not keys:
frappe.throw(_("Name or Email is mandatory"), frappe.NameError)
self.name = " - ".join(keys)
self.name = self.email_id
# applicant can apply more than once for a different job title or reapply
if frappe.db.exists("Job Applicant", self.name):
self.name = append_number_if_name_exists("Job Applicant", self.name)
def validate(self):
if self.email_id:

View File

@@ -9,7 +9,26 @@ from erpnext.hr.doctype.designation.test_designation import create_designation
class TestJobApplicant(unittest.TestCase):
pass
def test_job_applicant_naming(self):
applicant = frappe.get_doc({
"doctype": "Job Applicant",
"status": "Open",
"applicant_name": "_Test Applicant",
"email_id": "job_applicant_naming@example.com"
}).insert()
self.assertEqual(applicant.name, 'job_applicant_naming@example.com')
applicant = frappe.get_doc({
"doctype": "Job Applicant",
"status": "Open",
"applicant_name": "_Test Applicant",
"email_id": "job_applicant_naming@example.com"
}).insert()
self.assertEqual(applicant.name, 'job_applicant_naming@example.com-1')
def tearDown(self):
frappe.db.rollback()
def create_job_applicant(**args):
args = frappe._dict(args)

View File

@@ -1,294 +1,108 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "HR-LPR-.YYYY.-.#####",
"beta": 0,
"creation": "2018-04-13 15:20:52.864288",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"from_date",
"to_date",
"is_active",
"column_break_3",
"company",
"optional_holiday_list"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "from_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "From Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "to_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "To Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "is_active",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Is Active",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Is Active"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_3",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "company",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Company",
"length": 0,
"no_copy": 0,
"options": "Company",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "optional_holiday_list",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Holiday List for Optional Leave",
"length": 0,
"no_copy": 0,
"options": "Holiday List",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"options": "Holiday List"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-05-30 16:15:43.305502",
"links": [],
"modified": "2022-01-13 13:28:12.951025",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Period",
"name_case": "",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"search_fields": "from_date, to_date, company",
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
"states": [],
"track_changes": 1
}

View File

@@ -113,10 +113,11 @@
],
"is_submittable": 1,
"links": [],
"modified": "2021-03-01 17:54:01.014509",
"modified": "2022-01-13 13:37:11.218882",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Policy Assignment",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -164,5 +165,7 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "employee_name",
"track_changes": 1
}

View File

@@ -129,6 +129,8 @@ class LeavePolicyAssignment(Document):
monthly_earned_leave = get_monthly_earned_leave(new_leaves_allocated,
leave_type_details.get(leave_type).earned_leave_frequency, leave_type_details.get(leave_type).rounding)
new_leaves_allocated = monthly_earned_leave * months_passed
else:
new_leaves_allocated = 0
return new_leaves_allocated

View File

@@ -48,7 +48,16 @@ frappe.listview_settings['Leave Policy Assignment'] = {
if (cur_dialog.fields_dict.leave_period.value) {
me.set_effective_date();
}
}
},
get_query() {
let filters = {"is_active": 1};
if (cur_dialog.fields_dict.company.value)
filters["company"] = cur_dialog.fields_dict.company.value;
return {
filters: filters
};
},
},
{
fieldtype: "Column Break"

View File

@@ -4,6 +4,7 @@
import unittest
import frappe
from frappe.utils import add_months, get_first_day, getdate
from erpnext.hr.doctype.leave_application.test_leave_application import (
get_employee,
@@ -17,9 +18,8 @@ from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
test_dependencies = ["Employee"]
class TestLeavePolicyAssignment(unittest.TestCase):
def setUp(self):
for doctype in ["Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]:
for doctype in ["Leave Period", "Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]:
frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec
def test_grant_leaves(self):
@@ -54,8 +54,8 @@ class TestLeavePolicyAssignment(unittest.TestCase):
self.assertEqual(leave_alloc_doc.new_leaves_allocated, 10)
self.assertEqual(leave_alloc_doc.leave_type, "_Test Leave Type")
self.assertEqual(leave_alloc_doc.from_date, leave_period.from_date)
self.assertEqual(leave_alloc_doc.to_date, leave_period.to_date)
self.assertEqual(getdate(leave_alloc_doc.from_date), getdate(leave_period.from_date))
self.assertEqual(getdate(leave_alloc_doc.to_date), getdate(leave_period.to_date))
self.assertEqual(leave_alloc_doc.leave_policy, leave_policy.name)
self.assertEqual(leave_alloc_doc.leave_policy_assignment, leave_policy_assignments[0])
@@ -101,6 +101,55 @@ class TestLeavePolicyAssignment(unittest.TestCase):
# User are now allowed to grant leave
self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 0)
def test_earned_leave_allocation(self):
leave_period = create_leave_period("Test Earned Leave Period")
employee = get_employee()
leave_type = create_earned_leave_type("Test Earned Leave")
leave_policy = frappe.get_doc({
"doctype": "Leave Policy",
"leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 6}]
}).insert()
data = {
"assignment_based_on": "Leave Period",
"leave_policy": leave_policy.name,
"leave_period": leave_period.name
}
leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
# leaves allocated should be 0 since it is an earned leave and allocation happens via scheduler based on set frequency
leaves_allocated = frappe.db.get_value("Leave Allocation", {
"leave_policy_assignment": leave_policy_assignments[0]
}, "total_leaves_allocated")
self.assertEqual(leaves_allocated, 0)
def tearDown(self):
for doctype in ["Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]:
frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec
frappe.db.rollback()
def create_earned_leave_type(leave_type):
frappe.delete_doc_if_exists("Leave Type", leave_type, force=1)
return frappe.get_doc(dict(
leave_type_name=leave_type,
doctype="Leave Type",
is_earned_leave=1,
earned_leave_frequency="Monthly",
rounding=0.5,
max_leaves_allowed=6
)).insert()
def create_leave_period(name):
frappe.delete_doc_if_exists("Leave Period", name, force=1)
start_date = get_first_day(getdate())
return frappe.get_doc(dict(
name=name,
doctype="Leave Period",
from_date=start_date,
to_date=add_months(start_date, 12),
company="_Test Company",
is_active=1
)).insert()

View File

@@ -4,15 +4,32 @@
frappe.ui.form.on('Shift Type', {
refresh: function(frm) {
frm.add_custom_button(
'Mark Attendance',
() => frm.call({
doc: frm.doc,
method: 'process_auto_attendance',
freeze: true,
callback: () => {
frappe.msgprint(__("Attendance has been marked as per employee check-ins"));
__('Mark Attendance'),
() => {
if (!frm.doc.enable_auto_attendance) {
frm.scroll_to_field('enable_auto_attendance');
frappe.throw(__('Please Enable Auto Attendance and complete the setup first.'));
}
})
if (!frm.doc.process_attendance_after) {
frm.scroll_to_field('process_attendance_after');
frappe.throw(__('Please set {0}.', [__('Process Attendance After').bold()]));
}
if (!frm.doc.last_sync_of_checkin) {
frm.scroll_to_field('last_sync_of_checkin');
frappe.throw(__('Please set {0}.', [__('Last Sync of Checkin').bold()]));
}
frm.call({
doc: frm.doc,
method: 'process_auto_attendance',
freeze: true,
callback: () => {
frappe.msgprint(__('Attendance has been marked as per employee check-ins'));
}
});
}
);
}
});

View File

@@ -13,8 +13,10 @@
"column_break_3",
"company",
"posting_date",
"is_term_loan",
"rate_of_interest",
"payroll_payable_account",
"is_term_loan",
"repay_from_salary",
"payment_details_section",
"due_date",
"pending_principal_amount",
@@ -243,15 +245,31 @@
"label": "Total Penalty Paid",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"depends_on": "eval:doc.repay_from_salary",
"fieldname": "payroll_payable_account",
"fieldtype": "Link",
"label": "Payroll Payable Account",
"mandatory_depends_on": "eval:doc.repay_from_salary",
"options": "Account"
},
{
"default": "0",
"fetch_from": "against_loan.repay_from_salary",
"fieldname": "repay_from_salary",
"fieldtype": "Check",
"label": "Repay From Salary"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-04-19 18:10:00.935364",
"modified": "2022-01-06 01:51:06.707782",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Repayment",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -287,5 +305,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -321,74 +321,79 @@ class LoanRepayment(AccountsController):
else:
remarks = _("Repayment against Loan: ") + self.against_loan
if not loan_details.repay_from_salary:
if self.total_penalty_paid:
gle_map.append(
self.get_gl_dict({
"account": loan_details.loan_account,
"against": loan_details.payment_account,
"debit": self.total_penalty_paid,
"debit_in_account_currency": self.total_penalty_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": _("Penalty against loan:") + self.against_loan,
"cost_center": self.cost_center,
"party_type": self.applicant_type,
"party": self.applicant,
"posting_date": getdate(self.posting_date)
})
)
gle_map.append(
self.get_gl_dict({
"account": loan_details.penalty_income_account,
"against": loan_details.payment_account,
"credit": self.total_penalty_paid,
"credit_in_account_currency": self.total_penalty_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": _("Penalty against loan:") + self.against_loan,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date)
})
)
gle_map.append(
self.get_gl_dict({
"account": loan_details.payment_account,
"against": loan_details.loan_account + ", " + loan_details.interest_income_account
+ ", " + loan_details.penalty_income_account,
"debit": self.amount_paid,
"debit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": remarks,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date)
})
)
if self.repay_from_salary:
payment_account = self.payroll_payable_account
else:
payment_account = loan_details.payment_account
if self.total_penalty_paid:
gle_map.append(
self.get_gl_dict({
"account": loan_details.loan_account,
"party_type": loan_details.applicant_type,
"party": loan_details.applicant,
"against": loan_details.payment_account,
"credit": self.amount_paid,
"credit_in_account_currency": self.amount_paid,
"debit": self.total_penalty_paid,
"debit_in_account_currency": self.total_penalty_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": remarks,
"remarks": _("Penalty against loan:") + self.against_loan,
"cost_center": self.cost_center,
"party_type": self.applicant_type,
"party": self.applicant,
"posting_date": getdate(self.posting_date)
})
)
gle_map.append(
self.get_gl_dict({
"account": loan_details.penalty_income_account,
"against": payment_account,
"credit": self.total_penalty_paid,
"credit_in_account_currency": self.total_penalty_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": _("Penalty against loan:") + self.against_loan,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date)
})
)
if gle_map:
make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False)
gle_map.append(
self.get_gl_dict({
"account": payment_account,
"against": loan_details.loan_account + ", " + loan_details.interest_income_account
+ ", " + loan_details.penalty_income_account,
"debit": self.amount_paid,
"debit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": remarks,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date)
})
)
gle_map.append(
self.get_gl_dict({
"account": loan_details.loan_account,
"party_type": loan_details.applicant_type,
"party": loan_details.applicant,
"against": payment_account,
"credit": self.amount_paid,
"credit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": remarks,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date)
})
)
if gle_map:
make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False)
def create_repayment_entry(loan, applicant, company, posting_date, loan_type,
payment_type, interest_payable, payable_principal_amount, amount_paid, penalty_amount=None):
payment_type, interest_payable, payable_principal_amount, amount_paid, penalty_amount=None,
payroll_payable_account=None):
lr = frappe.get_doc({
"doctype": "Loan Repayment",
@@ -401,7 +406,8 @@ def create_repayment_entry(loan, applicant, company, posting_date, loan_type,
"interest_payable": interest_payable,
"payable_principal_amount": payable_principal_amount,
"amount_paid": amount_paid,
"loan_type": loan_type
"loan_type": loan_type,
"payroll_payable_account": payroll_payable_account
}).insert()
return lr

View File

@@ -531,16 +531,6 @@ class BOM(WebsiteGenerator):
row.hour_rate = (hour_rate / flt(self.conversion_rate)
if self.conversion_rate and hour_rate else hour_rate)
if self.routing:
time_in_mins = flt(frappe.db.get_value("BOM Operation", {
"workstation": row.workstation,
"operation": row.operation,
"parent": self.routing
}, ["time_in_mins"]))
if time_in_mins:
row.time_in_mins = time_in_mins
if row.hour_rate and row.time_in_mins:
row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate)
row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0

View File

@@ -46,6 +46,7 @@ class TestRouting(ERPNextTestCase):
wo_doc.delete()
def test_update_bom_operation_time(self):
"""Update cost shouldn't update routing times."""
operations = [
{
"operation": "Test Operation A",
@@ -85,8 +86,8 @@ class TestRouting(ERPNextTestCase):
routing_doc.save()
bom_doc.update_cost()
bom_doc.reload()
self.assertEqual(bom_doc.operations[0].time_in_mins, 90)
self.assertEqual(bom_doc.operations[1].time_in_mins, 42.2)
self.assertEqual(bom_doc.operations[0].time_in_mins, 30)
self.assertEqual(bom_doc.operations[1].time_in_mins, 20)
def setup_operations(rows):

View File

@@ -2,7 +2,7 @@
# License: GNU General Public License v3. See license.txt
import frappe
from frappe.utils import add_months, cint, flt, now, today
from frappe.utils import add_days, add_months, cint, flt, now, today
from erpnext.manufacturing.doctype.job_card.job_card import JobCardCancelError
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
@@ -12,6 +12,7 @@ from erpnext.manufacturing.doctype.work_order.work_order import (
OverProductionError,
StockOverProductionError,
close_work_order,
make_job_card,
make_stock_entry,
stop_unstop,
)
@@ -801,6 +802,34 @@ class TestWorkOrder(ERPNextTestCase):
if row.is_scrap_item:
self.assertEqual(row.qty, 1)
# Partial Job Card 1 with qty 10
wo_order = make_wo_order_test_record(item=item, company=company, planned_start_date=add_days(now(), 60), qty=20, skip_transfer=1)
job_card = frappe.db.get_value('Job Card', {'work_order': wo_order.name}, 'name')
update_job_card(job_card, 10)
stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10))
for row in stock_entry.items:
if row.is_scrap_item:
self.assertEqual(row.qty, 2)
# Partial Job Card 2 with qty 10
operations = []
wo_order.load_from_db()
for row in wo_order.operations:
n_dict = row.as_dict()
n_dict['qty'] = 10
n_dict['pending_qty'] = 10
operations.append(n_dict)
make_job_card(wo_order.name, operations)
job_card = frappe.db.get_value('Job Card', {'work_order': wo_order.name, 'docstatus': 0}, 'name')
update_job_card(job_card, 10)
stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10))
for row in stock_entry.items:
if row.is_scrap_item:
self.assertEqual(row.qty, 2)
def test_close_work_order(self):
items = ['Test FG Item for Closed WO', 'Test RM Item 1 for Closed WO',
'Test RM Item 2 for Closed WO']
@@ -841,7 +870,9 @@ class TestWorkOrder(ERPNextTestCase):
close_work_order(wo_order, "Closed")
self.assertEqual(wo_order.get('status'), "Closed")
def update_job_card(job_card):
def update_job_card(job_card, jc_qty=None):
employee = frappe.db.get_value('Employee', {'status': 'Active'}, 'name')
job_card_doc = frappe.get_doc('Job Card', job_card)
job_card_doc.set('scrap_items', [
{
@@ -854,15 +885,18 @@ def update_job_card(job_card):
},
])
if jc_qty:
job_card_doc.for_quantity = jc_qty
job_card_doc.append('time_logs', {
'from_time': now(),
'employee': employee,
'time_in_mins': 60,
'completed_qty': job_card_doc.for_quantity
})
job_card_doc.submit()
def get_scrap_item_details(bom_no):
scrap_items = {}
for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item`

View File

@@ -131,16 +131,14 @@ frappe.ui.form.on("Work Order", {
erpnext.work_order.set_custom_buttons(frm);
frm.set_intro("");
if (frm.doc.docstatus === 0 && !frm.doc.__islocal) {
if (frm.doc.docstatus === 0 && !frm.is_new()) {
frm.set_intro(__("Submit this Work Order for further processing."));
} else {
frm.trigger("show_progress_for_items");
frm.trigger("show_progress_for_operations");
}
if (frm.doc.status != "Closed") {
if (frm.doc.docstatus===1) {
frm.trigger('show_progress_for_items');
frm.trigger('show_progress_for_operations');
}
if (frm.doc.docstatus === 1
&& frm.doc.operations && frm.doc.operations.length) {

View File

@@ -178,7 +178,6 @@ erpnext.patches.v12_0.set_updated_purpose_in_pick_list
erpnext.patches.v12_0.set_default_payroll_based_on
erpnext.patches.v12_0.repost_stock_ledger_entries_for_target_warehouse
erpnext.patches.v12_0.update_end_date_and_status_in_email_campaign
erpnext.patches.v13_0.validate_options_for_data_field
erpnext.patches.v13_0.move_tax_slabs_from_payroll_period_to_income_tax_slab #123
erpnext.patches.v12_0.fix_quotation_expired_status
erpnext.patches.v12_0.update_appointment_reminder_scheduler_entry
@@ -306,6 +305,7 @@ erpnext.patches.v13_0.shopify_deprecation_warning
erpnext.patches.v13_0.add_custom_field_for_south_africa #2
erpnext.patches.v13_0.rename_discharge_ordered_date_in_ip_record
erpnext.patches.v13_0.remove_bad_selling_defaults
erpnext.patches.v13_0.trim_whitespace_from_serial_nos # 16-01-2022
erpnext.patches.v13_0.migrate_stripe_api
erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries
execute:frappe.reload_doc("erpnext_integrations", "doctype", "TaxJar Settings")
@@ -331,6 +331,7 @@ erpnext.patches.v13_0.enable_scheduler_job_for_item_reposting
erpnext.patches.v13_0.requeue_failed_reposts
erpnext.patches.v13_0.fetch_thumbnail_in_website_items
erpnext.patches.v13_0.update_job_card_status
erpnext.patches.v13_0.enable_uoms
erpnext.patches.v12_0.update_production_plan_status
erpnext.patches.v13_0.item_naming_series_not_mandatory
erpnext.patches.v13_0.update_category_in_ltds_certificate
@@ -339,3 +340,4 @@ erpnext.patches.v13_0.rename_ksa_qr_field
erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021
erpnext.patches.v13_0.update_tax_category_for_rcm
erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template
erpnext.patches.v13_0.agriculture_deprecation_warning

View File

@@ -0,0 +1,10 @@
import click
def execute():
click.secho(
"Agriculture Domain is moved to a separate app and will be removed from ERPNext in version-14.\n"
"When upgrading to ERPNext version-14, please install the app to continue using the Agriculture domain: https://github.com/frappe/agriculture",
fg="yellow",
)

View File

@@ -0,0 +1,13 @@
import frappe
def execute():
frappe.reload_doc('setup', 'doctype', 'uom')
uom = frappe.qb.DocType("UOM")
(frappe.qb
.update(uom)
.set(uom.enabled, 1)
.where(uom.creation >= "2021-10-18") # date when this field was released
).run()

View File

@@ -0,0 +1,67 @@
import frappe
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
def execute():
broken_sles = frappe.db.sql("""
select name, serial_no
from `tabStock Ledger Entry`
where
is_cancelled = 0
and ( serial_no like %s or serial_no like %s or serial_no like %s or serial_no like %s
or serial_no = %s )
""",
(
" %", # leading whitespace
"% ", # trailing whitespace
"%\n %", # leading whitespace on newline
"% \n%", # trailing whitespace on newline
"\n", # just new line
),
as_dict=True,
)
frappe.db.MAX_WRITES_PER_TRANSACTION += len(broken_sles)
if not broken_sles:
return
broken_serial_nos = set()
# patch SLEs
for sle in broken_sles:
serial_no_list = get_serial_nos(sle.serial_no)
correct_sr_no = "\n".join(serial_no_list)
if correct_sr_no == sle.serial_no:
continue
frappe.db.set_value("Stock Ledger Entry", sle.name, "serial_no", correct_sr_no, update_modified=False)
broken_serial_nos.update(serial_no_list)
if not broken_serial_nos:
return
# Patch serial No documents if they don't have purchase info
# Purchase info is used for fetching incoming rate
broken_sr_no_records = frappe.get_list("Serial No",
filters={
"status":"Active",
"name": ("in", broken_serial_nos),
"purchase_document_type": ("is", "not set")
},
pluck="name",
)
frappe.db.MAX_WRITES_PER_TRANSACTION += len(broken_sr_no_records)
patch_savepoint = "serial_no_patch"
for serial_no in broken_sr_no_records:
try:
frappe.db.savepoint(patch_savepoint)
sn = frappe.get_doc("Serial No", serial_no)
sn.update_serial_no_reference()
sn.db_update()
except Exception:
frappe.db.rollback(save_point=patch_savepoint)

View File

@@ -1,26 +0,0 @@
# Copyright (c) 2021, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from frappe.model import data_field_options
def execute():
for field in frappe.get_all('Custom Field',
fields = ['name'],
filters = {
'fieldtype': 'Data',
'options': ['!=', None]
}):
if field not in data_field_options:
frappe.db.sql("""
UPDATE
`tabCustom Field`
SET
options=NULL
WHERE
name=%s
""", (field))

View File

@@ -60,6 +60,8 @@ class PayrollEntry(Document):
def on_cancel(self):
frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip`
where payroll_entry=%s """, (self.name)))
self.db_set("salary_slips_created", 0)
self.db_set("salary_slips_submitted", 0)
def get_emp_list(self):
"""
@@ -350,23 +352,24 @@ class PayrollEntry(Document):
currencies = []
multi_currency = 0
company_currency = erpnext.get_company_currency(self.company)
accounting_dimensions = get_accounting_dimensions() or []
exchange_rate, amount = self.get_amount_and_exchange_rate_for_journal_entry(self.payment_account, je_payment_amount, company_currency, currencies)
accounts.append({
accounts.append(self.update_accounting_dimensions({
"account": self.payment_account,
"bank_account": self.bank_account,
"credit_in_account_currency": flt(amount, precision),
"exchange_rate": flt(exchange_rate),
})
}, accounting_dimensions))
exchange_rate, amount = self.get_amount_and_exchange_rate_for_journal_entry(payroll_payable_account, je_payment_amount, company_currency, currencies)
accounts.append({
accounts.append(self.update_accounting_dimensions({
"account": payroll_payable_account,
"debit_in_account_currency": flt(amount, precision),
"exchange_rate": flt(exchange_rate),
"reference_type": self.doctype,
"reference_name": self.name
})
}, accounting_dimensions))
if len(currencies) > 1:
multi_currency = 1

View File

@@ -936,8 +936,11 @@ class SalarySlip(TransactionBase):
def get_future_recurring_additional_amount(self, additional_salary, monthly_additional_amount):
future_recurring_additional_amount = 0
to_date = frappe.db.get_value("Additional Salary", additional_salary, 'to_date')
# future month count excluding current
future_recurring_period = (getdate(to_date).month - getdate(self.start_date).month)
from_date, to_date = getdate(self.start_date), getdate(to_date)
future_recurring_period = ((to_date.year - from_date.year) * 12) + (to_date.month - from_date.month)
if future_recurring_period > 0:
future_recurring_additional_amount = monthly_additional_amount * future_recurring_period # Used earning.additional_amount to consider the amount for the full month
return future_recurring_additional_amount
@@ -1036,7 +1039,8 @@ class SalarySlip(TransactionBase):
data.update({"annual_taxable_earning": annual_taxable_earning})
tax_amount = 0
for slab in tax_slab.slabs:
if slab.condition and not self.eval_tax_slab_condition(slab.condition, data):
cond = cstr(slab.condition).strip()
if cond and not self.eval_tax_slab_condition(cond, data):
continue
if not slab.to_amount and annual_taxable_earning >= slab.from_amount:
tax_amount += (annual_taxable_earning - slab.from_amount + 1) * slab.percent_deduction *.01
@@ -1142,15 +1146,17 @@ class SalarySlip(TransactionBase):
})
def make_loan_repayment_entry(self):
payroll_payable_account = get_payroll_payable_account(self.company, self.payroll_entry)
for loan in self.loans:
repayment_entry = create_repayment_entry(loan.loan, self.employee,
self.company, self.posting_date, loan.loan_type, "Regular Payment", loan.interest_amount,
loan.principal_amount, loan.total_payment)
if loan.total_payment:
repayment_entry = create_repayment_entry(loan.loan, self.employee,
self.company, self.posting_date, loan.loan_type, "Regular Payment", loan.interest_amount,
loan.principal_amount, loan.total_payment, payroll_payable_account=payroll_payable_account)
repayment_entry.save()
repayment_entry.submit()
repayment_entry.save()
repayment_entry.submit()
frappe.db.set_value("Salary Slip Loan", loan.name, "loan_repayment_entry", repayment_entry.name)
frappe.db.set_value("Salary Slip Loan", loan.name, "loan_repayment_entry", repayment_entry.name)
def cancel_loan_repayment_entry(self):
for loan in self.loans:
@@ -1384,3 +1390,11 @@ def get_salary_component_data(component):
],
as_dict=1,
)
def get_payroll_payable_account(company, payroll_entry):
if payroll_entry:
payroll_payable_account = frappe.db.get_value('Payroll Entry', payroll_entry, 'payroll_payable_account')
else:
payroll_payable_account = frappe.db.get_value('Company', company, 'default_payroll_payable_account')
return payroll_payable_account

View File

@@ -384,7 +384,7 @@ class TestSalarySlip(unittest.TestCase):
make_salary_structure("Test Loan Repayment Salary Structure", "Monthly", employee=applicant, currency='INR',
payroll_period=payroll_period)
frappe.db.sql("delete from tabLoan")
frappe.db.sql("delete from tabLoan where applicant = 'test_loan_repayment_salary_slip@salary.com'")
loan = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1))
loan.repay_from_salary = 1
loan.submit()

View File

@@ -105,7 +105,7 @@ class Task(NestedSet):
frappe.throw(_("Completed On cannot be greater than Today"))
def update_depends_on(self):
depends_on_tasks = self.depends_on_tasks or ""
depends_on_tasks = ""
for d in self.depends_on:
if d.task and d.task not in depends_on_tasks:
depends_on_tasks += d.task + ","

View File

@@ -114,6 +114,8 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
if ((!item.qty) && me.frm.doc.is_return) {
item.amount = flt(item.rate * -1, precision("amount", item));
} else if ((!item.qty) && me.frm.doc.is_debit_note) {
item.amount = flt(item.rate, precision("amount", item));
} else {
item.amount = flt(item.rate * item.qty, precision("amount", item));
}
@@ -708,14 +710,15 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
frappe.model.round_floats_in(this.frm.doc, ["grand_total", "total_advance", "write_off_amount"]);
if(in_list(["Sales Invoice", "POS Invoice", "Purchase Invoice"], this.frm.doc.doctype)) {
var grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total;
let grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total;
let base_grand_total = this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total;
if(this.frm.doc.party_account_currency == this.frm.doc.currency) {
var total_amount_to_pay = flt((grand_total - this.frm.doc.total_advance
- this.frm.doc.write_off_amount), precision("grand_total"));
} else {
var total_amount_to_pay = flt(
(flt(grand_total*this.frm.doc.conversion_rate, precision("grand_total"))
(flt(base_grand_total, precision("base_grand_total"))
- this.frm.doc.total_advance - this.frm.doc.base_write_off_amount),
precision("base_grand_total")
);
@@ -746,14 +749,15 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
},
set_total_amount_to_default_mop: function() {
var grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total;
let grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total;
let base_grand_total = this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total;
if(this.frm.doc.party_account_currency == this.frm.doc.currency) {
var total_amount_to_pay = flt((grand_total - this.frm.doc.total_advance
- this.frm.doc.write_off_amount), precision("grand_total"));
} else {
var total_amount_to_pay = flt(
(flt(grand_total*this.frm.doc.conversion_rate, precision("grand_total"))
(flt(base_grand_total, precision("base_grand_total"))
- this.frm.doc.total_advance - this.frm.doc.base_write_off_amount),
precision("base_grand_total")
);

View File

@@ -2,7 +2,13 @@
// For license information, please see license.txt
frappe.ui.form.on('UAE VAT Settings', {
// refresh: function(frm) {
// }
onload: function(frm) {
frm.set_query('account', 'uae_vat_accounts', function() {
return {
filters: {
'company': frm.doc.company
}
};
});
}
});

View File

@@ -217,7 +217,7 @@ def get_regional_address_details(party_details, doctype, company):
if tax_template_by_category:
party_details['taxes_and_charges'] = tax_template_by_category
return
return party_details
if not party_details.place_of_supply: return party_details
if not party_details.company_gstin: return party_details

View File

@@ -53,7 +53,8 @@ frappe.query_reports["GSTR-1"] = {
{ "value": "CDNR-REG", "label": __("Credit/Debit Notes (Registered) - 9B") },
{ "value": "CDNR-UNREG", "label": __("Credit/Debit Notes (Unregistered) - 9B") },
{ "value": "EXPORT", "label": __("Export Invoice - 6A") },
{ "value": "Advances", "label": __("Tax Liability (Advances Received) - 11A(1), 11A(2)") }
{ "value": "Advances", "label": __("Tax Liability (Advances Received) - 11A(1), 11A(2)") },
{ "value": "NIL Rated", "label": __("NIL RATED/EXEMPTED Invoices") }
],
"default": "B2B"
}

View File

@@ -41,7 +41,8 @@ class Gstr1Report(object):
port_code,
shipping_bill_number,
shipping_bill_date,
reason_for_issuing_document
reason_for_issuing_document,
company_gstin
"""
def run(self):
@@ -63,6 +64,8 @@ class Gstr1Report(object):
self.get_b2c_data()
elif self.filters.get("type_of_business") == "Advances":
self.get_advance_data()
elif self.filters.get("type_of_business") == "NIL Rated":
self.get_nil_rated_invoices()
elif self.invoices:
for inv, items_based_on_rate in self.items_based_on_tax_rate.items():
invoice_details = self.invoices.get(inv)
@@ -92,6 +95,57 @@ class Gstr1Report(object):
row= [key[0], key[1], value[0], value[1]]
self.data.append(row)
def get_nil_rated_invoices(self):
nil_exempt_output = [
{
"description": "Inter-State supplies to registered persons",
"nil_rated": 0.0,
"exempted": 0.0,
"non_gst": 0.0
},
{
"description": "Intra-State supplies to registered persons",
"nil_rated": 0.0,
"exempted": 0.0,
"non_gst": 0.0
},
{
"description": "Inter-State supplies to unregistered persons",
"nil_rated": 0.0,
"exempted": 0.0,
"non_gst": 0.0
},
{
"description": "Intra-State supplies to unregistered persons",
"nil_rated": 0.0,
"exempted": 0.0,
"non_gst": 0.0
}
]
for invoice, details in self.nil_exempt_non_gst.items():
invoice_detail = self.invoices.get(invoice)
if invoice_detail.get('gst_category') in ("Registered Regular", "Deemed Export", "SEZ"):
if is_inter_state(invoice_detail):
nil_exempt_output[0]["nil_rated"] += details[0]
nil_exempt_output[0]["exempted"] += details[1]
nil_exempt_output[0]["non_gst"] += details[2]
else:
nil_exempt_output[1]["nil_rated"] += details[0]
nil_exempt_output[1]["exempted"] += details[1]
nil_exempt_output[1]["non_gst"] += details[2]
else:
if is_inter_state(invoice_detail):
nil_exempt_output[2]["nil_rated"] += details[0]
nil_exempt_output[2]["exempted"] += details[1]
nil_exempt_output[2]["non_gst"] += details[2]
else:
nil_exempt_output[3]["nil_rated"] += details[0]
nil_exempt_output[3]["exempted"] += details[1]
nil_exempt_output[3]["non_gst"] += details[2]
self.data = nil_exempt_output
def get_b2c_data(self):
b2cs_output = {}
@@ -241,10 +295,11 @@ class Gstr1Report(object):
def get_invoice_items(self):
self.invoice_items = frappe._dict()
self.item_tax_rate = frappe._dict()
self.nil_exempt_non_gst = {}
items = frappe.db.sql("""
select item_code, parent, taxable_value, base_net_amount, item_tax_rate
from `tab%s Item`
select item_code, parent, taxable_value, base_net_amount, item_tax_rate, is_nil_exempt,
is_non_gst from `tab%s Item`
where parent in (%s)
""" % (self.doctype, ', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1)
@@ -261,6 +316,16 @@ class Gstr1Report(object):
tax_rate_dict = self.item_tax_rate.setdefault(d.parent, {}).setdefault(d.item_code, [])
tax_rate_dict.append(rate)
if d.is_nil_exempt:
self.nil_exempt_non_gst.setdefault(d.parent, [0.0, 0.0, 0.0])
if item_tax_rate:
self.nil_exempt_non_gst[d.parent][0] += d.get('taxable_value', 0)
else:
self.nil_exempt_non_gst[d.parent][1] += d.get('taxable_value', 0)
elif d.is_non_gst:
self.nil_exempt_non_gst.setdefault(d.parent, [0.0, 0.0, 0.0])
self.nil_exempt_non_gst[d.parent][2] += d.get('taxable_value', 0)
def get_items_based_on_tax_rate(self):
self.tax_details = frappe.db.sql("""
select
@@ -323,21 +388,24 @@ class Gstr1Report(object):
self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys())
def get_columns(self):
self.tax_columns = [
{
"fieldname": "rate",
"label": "Rate",
"fieldtype": "Int",
"width": 60
},
{
"fieldname": "taxable_value",
"label": "Taxable Value",
"fieldtype": "Currency",
"width": 100
}
]
self.other_columns = []
self.tax_columns = []
if self.filters.get("type_of_business") != "NIL Rated":
self.tax_columns = [
{
"fieldname": "rate",
"label": "Rate",
"fieldtype": "Int",
"width": 60
},
{
"fieldname": "taxable_value",
"label": "Taxable Value",
"fieldtype": "Currency",
"width": 100
}
]
if self.filters.get("type_of_business") == "B2B":
self.invoice_columns = [
@@ -706,6 +774,33 @@ class Gstr1Report(object):
"width": 100
}
]
elif self.filters.get("type_of_business") == "NIL Rated":
self.invoice_columns = [
{
"fieldname": "description",
"label": "Description",
"fieldtype": "Data",
"width": 420
},
{
"fieldname": "nil_rated",
"label": "Nil Rated",
"fieldtype": "Currency",
"width": 200
},
{
"fieldname": "exempted",
"label": "Exempted",
"fieldtype": "Currency",
"width": 200
},
{
"fieldname": "non_gst",
"label": "Non GST",
"fieldtype": "Currency",
"width": 200
}
]
self.columns = self.invoice_columns + self.tax_columns + self.other_columns
@@ -769,6 +864,11 @@ def get_json(filters, report_name, data):
out = get_advances_json(res, gstin)
gst_json["at"] = out
elif filters["type_of_business"] == "NIL Rated":
res = report_data[:-1]
out = get_exempted_json(res)
gst_json["nil"] = out
return {
'report_name': report_name,
'report_type': filters['type_of_business'],
@@ -981,6 +1081,36 @@ def get_cdnr_unreg_json(res, gstin):
return out
def get_exempted_json(data):
out = {
"inv": [
{
"sply_ty": "INTRB2B"
},
{
"sply_ty": "INTRAB2B"
},
{
"sply_ty": "INTRB2C"
},
{
"sply_ty": "INTRAB2C"
}
]
}
for i, v in enumerate(data):
if data[i].get('nil_rated'):
out['inv'][i]['nil_amt'] = data[i]['nil_rated']
if data[i].get('exempted'):
out['inv'][i]['expt_amt'] = data[i]['exempted']
if data[i].get('non_gst'):
out['inv'][i]['ngsup_amt'] = data[i]['non_gst']
return out
def get_invoice_type_for_cdnr(row):
if row.get('gst_category') == 'SEZ':
if row.get('export_type') == 'WPAY':
@@ -1065,3 +1195,9 @@ def download_json_file():
frappe.response['filecontent'] = data['data']
frappe.response['content_type'] = 'application/json'
frappe.response['type'] = 'download'
def is_inter_state(invoice_detail):
if invoice_detail.place_of_supply.split("-")[0] != invoice_detail.company_gstin[:2]:
return True
else:
return False

View File

@@ -20,25 +20,35 @@ def get_columns():
"fieldname": "title",
"label": _("Title"),
"fieldtype": "Data",
"width": 300
"width": 300,
},
{
"fieldname": "amount",
"label": _("Amount (SAR)"),
"fieldtype": "Currency",
"options": "currency",
"width": 150,
},
{
"fieldname": "adjustment_amount",
"label": _("Adjustment (SAR)"),
"fieldtype": "Currency",
"options": "currency",
"width": 150,
},
{
"fieldname": "vat_amount",
"label": _("VAT Amount (SAR)"),
"fieldtype": "Currency",
"options": "currency",
"width": 150,
},
{
"fieldname": "currency",
"label": _("Currency"),
"fieldtype": "Currency",
"width": 150,
"hidden": 1
}
]
@@ -47,6 +57,8 @@ def get_data(filters):
# Validate if vat settings exist
company = filters.get('company')
company_currency = frappe.get_cached_value('Company', company, "default_currency")
if frappe.db.exists('KSA VAT Setting', company) is None:
url = get_url_to_list('KSA VAT Setting')
frappe.msgprint(_('Create <a href="{}">KSA VAT Setting</a> for this company').format(url))
@@ -55,7 +67,7 @@ def get_data(filters):
ksa_vat_setting = frappe.get_doc('KSA VAT Setting', company)
# Sales Heading
append_data(data, 'VAT on Sales', '', '', '')
append_data(data, 'VAT on Sales', '', '', '', company_currency)
grand_total_taxable_amount = 0
grand_total_taxable_adjustment_amount = 0
@@ -67,7 +79,7 @@ def get_data(filters):
# Adding results to data
append_data(data, vat_setting.title, total_taxable_amount,
total_taxable_adjustment_amount, total_tax)
total_taxable_adjustment_amount, total_tax, company_currency)
grand_total_taxable_amount += total_taxable_amount
grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount
@@ -75,13 +87,13 @@ def get_data(filters):
# Sales Grand Total
append_data(data, 'Grand Total', grand_total_taxable_amount,
grand_total_taxable_adjustment_amount, grand_total_tax)
grand_total_taxable_adjustment_amount, grand_total_tax, company_currency)
# Blank Line
append_data(data, '', '', '', '')
append_data(data, '', '', '', '', company_currency)
# Purchase Heading
append_data(data, 'VAT on Purchases', '', '', '')
append_data(data, 'VAT on Purchases', '', '', '', company_currency)
grand_total_taxable_amount = 0
grand_total_taxable_adjustment_amount = 0
@@ -93,7 +105,7 @@ def get_data(filters):
# Adding results to data
append_data(data, vat_setting.title, total_taxable_amount,
total_taxable_adjustment_amount, total_tax)
total_taxable_adjustment_amount, total_tax, company_currency)
grand_total_taxable_amount += total_taxable_amount
grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount
@@ -101,7 +113,7 @@ def get_data(filters):
# Purchase Grand Total
append_data(data, 'Grand Total', grand_total_taxable_amount,
grand_total_taxable_adjustment_amount, grand_total_tax)
grand_total_taxable_adjustment_amount, grand_total_tax, company_currency)
return data
@@ -147,9 +159,10 @@ def get_tax_data_for_each_vat_setting(vat_setting, filters, doctype):
def append_data(data, title, amount, adjustment_amount, vat_amount):
def append_data(data, title, amount, adjustment_amount, vat_amount, company_currency):
"""Returns data with appended value."""
data.append({"title": _(title), "amount": amount, "adjustment_amount": adjustment_amount, "vat_amount": vat_amount})
data.append({"title": _(title), "amount": amount, "adjustment_amount": adjustment_amount, "vat_amount": vat_amount,
"currency": company_currency})
def get_tax_amount(item_code, account_head, doctype, parent):
if doctype == 'Sales Invoice':

View File

@@ -643,7 +643,7 @@ erpnext.PointOfSale.Controller = class {
message: __('Item Code: {0} is not available under warehouse {1}.', [bold_item_code, bold_warehouse])
})
} else if (available_qty < qty_needed) {
frappe.show_alert({
frappe.throw({
message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', [bold_item_code, bold_warehouse, bold_available_qty]),
indicator: 'orange'
});

View File

@@ -113,7 +113,7 @@ erpnext.PointOfSale.ItemSelector = class {
`<div class="item-wrapper"
data-item-code="${escape(item.item_code)}" data-serial-no="${escape(serial_no)}"
data-batch-no="${escape(batch_no)}" data-uom="${escape(stock_uom)}"
data-rate="${escape(price_list_rate)}"
data-rate="${escape(price_list_rate || 0)}"
title="${item.item_name}">
${get_item_image_html()}

View File

@@ -67,7 +67,8 @@ def get_data(conditions, filters):
(soi.billed_amt * IFNULL(so.conversion_rate, 1)) as billed_amount,
(soi.base_amount - (soi.billed_amt * IFNULL(so.conversion_rate, 1))) as pending_amount,
soi.warehouse as warehouse,
so.company, soi.name
so.company, soi.name,
soi.description as description
FROM
`tabSales Order` so,
`tabSales Order Item` soi
@@ -179,6 +180,12 @@ def get_columns(filters):
"options": "Item",
"width": 100
})
columns.append({
"label":_("Description"),
"fieldname": "description",
"fieldtype": "Small Text",
"width": 100
})
columns.extend([
{

View File

@@ -79,14 +79,11 @@ frappe.ui.form.on("Company", {
},
refresh: function(frm) {
if(!frm.doc.__islocal) {
frm.doc.abbr && frm.set_df_property("abbr", "read_only", 1);
frm.set_df_property("parent_company", "read_only", 1);
disbale_coa_fields(frm);
}
frm.toggle_display('address_html', !frm.is_new());
frm.toggle_display('address_html', !frm.doc.__islocal);
if(!frm.doc.__islocal) {
if (!frm.is_new()) {
frm.doc.abbr && frm.set_df_property("abbr", "read_only", 1);
disbale_coa_fields(frm);
frappe.contacts.render_address_and_contact(frm);
frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Company'}
@@ -216,6 +213,9 @@ erpnext.company.setup_queries = function(frm) {
["default_payroll_payable_account", {"root_type": "Liability"}],
["round_off_account", {"root_type": "Expense"}],
["write_off_account", {"root_type": "Expense"}],
["default_deferred_expense_account", {}],
["default_deferred_revenue_account", {}],
["default_expense_claim_payable_account", {}],
["default_discount_account", {}],
["discount_allowed_account", {"root_type": "Expense"}],
["discount_received_account", {"root_type": "Income"}],

View File

@@ -49,6 +49,7 @@ class Company(NestedSet):
self.validate_perpetual_inventory()
self.validate_perpetual_inventory_for_non_stock_items()
self.check_country_change()
self.check_parent_changed()
self.set_chart_of_accounts()
self.validate_parent_company()
@@ -132,6 +133,10 @@ class Company(NestedSet):
self.name in frappe.local.enable_perpetual_inventory:
frappe.local.enable_perpetual_inventory[self.name] = self.enable_perpetual_inventory
if frappe.flags.parent_company_changed:
from frappe.utils.nestedset import rebuild_tree
rebuild_tree("Company", "parent_company")
frappe.clear_cache()
def create_default_warehouses(self):
@@ -193,7 +198,7 @@ class Company(NestedSet):
def check_country_change(self):
frappe.flags.country_change = False
if not self.get('__islocal') and \
if not self.is_new() and \
self.country != frappe.get_cached_value('Company', self.name, 'country'):
frappe.flags.country_change = True
@@ -398,6 +403,13 @@ class Company(NestedSet):
if not frappe.db.get_value('GL Entry', {'company': self.name}):
frappe.db.sql("delete from `tabProcess Deferred Accounting` where company=%s", self.name)
def check_parent_changed(self):
frappe.flags.parent_company_changed = False
if not self.is_new() and \
self.parent_company != frappe.db.get_value("Company", self.name, "parent_company"):
frappe.flags.parent_company_changed = True
def get_name_with_abbr(name, company):
company_abbr = frappe.get_cached_value('Company', company, "abbr")
parts = name.split(" - ")

View File

@@ -93,6 +93,61 @@ class TestCompany(unittest.TestCase):
frappe.db.sql(""" delete from `tabMode of Payment Account`
where company =%s """, (company))
def test_basic_tree(self, records=None):
min_lft = 1
max_rgt = frappe.db.sql("select max(rgt) from `tabCompany`")[0][0]
if not records:
records = test_records[2:]
for company in records:
lft, rgt, parent_company = frappe.db.get_value("Company", company["company_name"],
["lft", "rgt", "parent_company"])
if parent_company:
parent_lft, parent_rgt = frappe.db.get_value("Company", parent_company,
["lft", "rgt"])
else:
# root
parent_lft = min_lft - 1
parent_rgt = max_rgt + 1
self.assertTrue(lft)
self.assertTrue(rgt)
self.assertTrue(lft < rgt)
self.assertTrue(parent_lft < parent_rgt)
self.assertTrue(lft > parent_lft)
self.assertTrue(rgt < parent_rgt)
self.assertTrue(lft >= min_lft)
self.assertTrue(rgt <= max_rgt)
def get_no_of_children(self, company):
def get_no_of_children(companies, no_of_children):
children = []
for company in companies:
children += frappe.db.sql_list("""select name from `tabCompany`
where ifnull(parent_company, '')=%s""", company or '')
if len(children):
return get_no_of_children(children, no_of_children + len(children))
else:
return no_of_children
return get_no_of_children([company], 0)
def test_change_parent_company(self):
child_company = frappe.get_doc("Company", "_Test Company 5")
# changing parent of company
child_company.parent_company = "_Test Company 3"
child_company.save()
self.test_basic_tree()
# move it back
child_company.parent_company = "_Test Company 4"
child_company.save()
self.test_basic_tree()
def create_company_communication(doctype, docname):
comm = frappe.get_doc({
"doctype": "Communication",

View File

@@ -350,7 +350,8 @@ def add_uom_data():
"doctype": "UOM",
"uom_name": _(d.get("uom_name")),
"name": _(d.get("uom_name")),
"must_be_whole_number": d.get("must_be_whole_number")
"must_be_whole_number": d.get("must_be_whole_number"),
"enabled": 1,
}).db_insert()
# bootstrap uom conversion factors

View File

@@ -293,6 +293,7 @@ def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None):
join `tabStock Ledger Entry` ignore index (item_code, warehouse)
on (`tabBatch`.batch_id = `tabStock Ledger Entry`.batch_no )
where `tabStock Ledger Entry`.item_code = %s and `tabStock Ledger Entry`.warehouse = %s
and `tabStock Ledger Entry`.is_cancelled = 0
and (`tabBatch`.expiry_date >= CURDATE() or `tabBatch`.expiry_date IS NULL) {0}
group by batch_id
order by `tabBatch`.expiry_date ASC, `tabBatch`.creation ASC
@@ -313,3 +314,30 @@ def make_batch(args):
if frappe.db.get_value("Item", args.item, "has_batch_no"):
args.doctype = "Batch"
frappe.get_doc(args).insert().name
@frappe.whitelist()
def get_pos_reserved_batch_qty(filters):
import json
from frappe.query_builder.functions import Sum
if isinstance(filters, str):
filters = json.loads(filters)
p = frappe.qb.DocType("POS Invoice").as_("p")
item = frappe.qb.DocType("POS Invoice Item").as_("item")
sum_qty = Sum(item.qty).as_("qty")
reserved_batch_qty = frappe.qb.from_(p).from_(item).select(sum_qty).where(
(p.name == item.parent) &
(p.consolidated_invoice.isnull()) &
(p.status != "Consolidated") &
(p.docstatus == 1) &
(item.docstatus == 1) &
(item.item_code == filters.get('item_code')) &
(item.warehouse == filters.get('warehouse')) &
(item.batch_no == filters.get('batch_no'))
).run()
flt_reserved_batch_qty = flt(reserved_batch_qty[0][0])
return flt_reserved_batch_qty

View File

@@ -421,10 +421,16 @@ def update_serial_nos(sle, item_det):
def get_auto_serial_nos(serial_no_series, qty):
serial_nos = []
for i in range(cint(qty)):
serial_nos.append(make_autoname(serial_no_series, "Serial No"))
serial_nos.append(get_new_serial_number(serial_no_series))
return "\n".join(serial_nos)
def get_new_serial_number(series):
sr_no = make_autoname(series, "Serial No")
if frappe.db.exists("Serial No", sr_no):
sr_no = get_new_serial_number(series)
return sr_no
def auto_make_serial_nos(args):
serial_nos = get_serial_nos(args.get('serial_no'))
created_numbers = []

View File

@@ -8,8 +8,10 @@
import frappe
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
@@ -21,6 +23,10 @@ from erpnext.tests.utils import ERPNextTestCase
class TestSerialNo(ERPNextTestCase):
def tearDown(self):
frappe.db.rollback()
def test_cannot_create_direct(self):
frappe.delete_doc_if_exists("Serial No", "_TCSER0001")
@@ -176,6 +182,24 @@ class TestSerialNo(ERPNextTestCase):
self.assertEqual(sn_doc.warehouse, "_Test Warehouse - _TC")
self.assertEqual(sn_doc.purchase_document_no, se.name)
def test_auto_creation_of_serial_no(self):
"""
Test if auto created Serial No excludes existing serial numbers
"""
item_code = make_item("_Test Auto Serial Item ", {
"has_serial_no": 1,
"serial_no_series": "XYZ.###"
}).item_code
# Reserve XYZ005
pr_1 = make_purchase_receipt(item_code=item_code, qty=1, serial_no="XYZ005")
# XYZ005 is already used and will throw an error if used again
pr_2 = make_purchase_receipt(item_code=item_code, qty=10)
self.assertEqual(get_serial_nos(pr_1.get("items")[0].serial_no)[0], "XYZ005")
for serial_no in get_serial_nos(pr_2.get("items")[0].serial_no):
self.assertNotEqual(serial_no, "XYZ005")
def test_serial_no_sanitation(self):
"Test if Serial No input is sanitised before entering the DB."
item_code = "_Test Serialized Item"
@@ -192,7 +216,28 @@ class TestSerialNo(ERPNextTestCase):
self.assertEqual(se.get("items")[0].serial_no, "_TS1\n_TS2\n_TS3\n_TS4 - 2021")
frappe.db.rollback()
def test_correct_serial_no_incoming_rate(self):
""" Check correct consumption rate based on serial no record.
"""
item_code = "_Test Serialized Item"
warehouse = "_Test Warehouse - _TC"
serial_nos = ["LOWVALUATION", "HIGHVALUATION"]
in1 = make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=1, rate=42,
serial_no=serial_nos[0])
in2 = make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=1, rate=113,
serial_no=serial_nos[1])
out = create_delivery_note(item_code=item_code, qty=1, serial_no=serial_nos[0], do_not_submit=True)
# change serial no
out.items[0].serial_no = serial_nos[1]
out.save()
out.submit()
value_diff = frappe.db.get_value("Stock Ledger Entry",
{"voucher_no": out.name, "voucher_type": "Delivery Note"},
"stock_value_difference"
)
self.assertEqual(value_diff, -113)
def tearDown(self):
frappe.db.rollback()

View File

@@ -8,6 +8,7 @@ from collections import defaultdict
import frappe
from frappe import _
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder.functions import Sum
from frappe.utils import cint, comma_or, cstr, flt, format_time, formatdate, getdate, nowdate
from six import iteritems, itervalues, string_types
@@ -86,8 +87,11 @@ class StockEntry(StockController):
self.validate_warehouse()
self.validate_work_order()
self.validate_bom()
self.mark_finished_and_scrap_items()
self.validate_finished_goods()
if self.purpose in ("Manufacture", "Repack"):
self.mark_finished_and_scrap_items()
self.validate_finished_goods()
self.validate_with_material_request()
self.validate_batch()
self.validate_inspection()
@@ -110,8 +114,12 @@ class StockEntry(StockController):
self.set_actual_qty()
self.calculate_rate_and_amount()
self.validate_putaway_capacity()
self.reset_default_field_value("from_warehouse", "items", "s_warehouse")
self.reset_default_field_value("to_warehouse", "items", "t_warehouse")
if not self.get("purpose") == "Manufacture":
# ignore scrap item wh difference and empty source/target wh
# in Manufacture Entry
self.reset_default_field_value("from_warehouse", "items", "s_warehouse")
self.reset_default_field_value("to_warehouse", "items", "t_warehouse")
def on_submit(self):
self.update_stock_ledger()
@@ -702,26 +710,25 @@ class StockEntry(StockController):
validate_bom_no(item_code, d.bom_no)
def mark_finished_and_scrap_items(self):
if self.purpose in ("Repack", "Manufacture"):
if any([d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]):
return
if any([d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]):
return
finished_item = self.get_finished_item()
finished_item = self.get_finished_item()
if not finished_item and self.purpose == "Manufacture":
# In case of independent Manufacture entry, don't auto set
# user must decide and set
return
if not finished_item and self.purpose == "Manufacture":
# In case of independent Manufacture entry, don't auto set
# user must decide and set
return
for d in self.items:
if d.t_warehouse and not d.s_warehouse:
if self.purpose=="Repack" or d.item_code == finished_item:
d.is_finished_item = 1
else:
d.is_scrap_item = 1
for d in self.items:
if d.t_warehouse and not d.s_warehouse:
if self.purpose=="Repack" or d.item_code == finished_item:
d.is_finished_item = 1
else:
d.is_finished_item = 0
d.is_scrap_item = 0
d.is_scrap_item = 1
else:
d.is_finished_item = 0
d.is_scrap_item = 0
def get_finished_item(self):
finished_item = None
@@ -734,9 +741,9 @@ class StockEntry(StockController):
def validate_finished_goods(self):
"""
1. Check if FG exists
2. Check if Multiple FG Items are present
3. Check FG Item and Qty against WO if present
1. Check if FG exists (mfg, repack)
2. Check if Multiple FG Items are present (mfg)
3. Check FG Item and Qty against WO if present (mfg)
"""
production_item, wo_qty, finished_items = None, 0, []
@@ -749,8 +756,9 @@ class StockEntry(StockController):
for d in self.get('items'):
if d.is_finished_item:
if not self.work_order:
# Independent MFG Entry/ Repack Entry, no WO to match against
finished_items.append(d.item_code)
continue # Independent Manufacture Entry, no WO to match against
continue
if d.item_code != production_item:
frappe.throw(_("Finished Item {0} does not match with Work Order {1}")
@@ -763,19 +771,17 @@ class StockEntry(StockController):
finished_items.append(d.item_code)
if len(set(finished_items)) > 1:
if not finished_items:
frappe.throw(
msg=_("Multiple items cannot be marked as finished item"),
title=_("Note"),
exc=FinishedGoodError
msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name),
title=_("Missing Finished Good"), exc=FinishedGoodError
)
if self.purpose == "Manufacture":
if not finished_items:
if len(set(finished_items)) > 1:
frappe.throw(
msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name),
title=_("Missing Finished Good"),
exc=FinishedGoodError
msg=_("Multiple items cannot be marked as finished item"),
title=_("Note"), exc=FinishedGoodError
)
allowance_percentage = flt(
@@ -1276,22 +1282,29 @@ class StockEntry(StockController):
if not self.pro_doc:
self.set_work_order_details()
scrap_items = frappe.db.sql('''
SELECT
JCSI.item_code, JCSI.item_name, SUM(JCSI.stock_qty) as stock_qty, JCSI.stock_uom, JCSI.description
FROM
`tabJob Card` JC, `tabJob Card Scrap Item` JCSI
WHERE
JCSI.parent = JC.name AND JC.docstatus = 1
AND JCSI.item_code IS NOT NULL AND JC.work_order = %s
GROUP BY
JCSI.item_code
''', self.work_order, as_dict=1)
pending_qty = flt(self.pro_doc.qty) - flt(self.pro_doc.produced_qty)
if pending_qty <=0:
if not self.pro_doc.operations:
return []
job_card = frappe.qb.DocType('Job Card')
job_card_scrap_item = frappe.qb.DocType('Job Card Scrap Item')
scrap_items = (
frappe.qb.from_(job_card)
.select(
Sum(job_card_scrap_item.stock_qty).as_('stock_qty'),
job_card_scrap_item.item_code, job_card_scrap_item.item_name,
job_card_scrap_item.description, job_card_scrap_item.stock_uom)
.join(job_card_scrap_item)
.on(job_card_scrap_item.parent == job_card.name)
.where(
(job_card_scrap_item.item_code.isnotnull())
& (job_card.work_order == self.work_order)
& (job_card.docstatus == 1))
.groupby(job_card_scrap_item.item_code)
).run(as_dict=1)
pending_qty = flt(self.get_completed_job_card_qty()) - flt(self.pro_doc.produced_qty)
used_scrap_items = self.get_used_scrap_items()
for row in scrap_items:
row.stock_qty -= flt(used_scrap_items.get(row.item_code))
@@ -1305,6 +1318,9 @@ class StockEntry(StockController):
return scrap_items
def get_completed_job_card_qty(self):
return flt(min([d.completed_qty for d in self.pro_doc.operations]))
def get_used_scrap_items(self):
used_scrap_items = defaultdict(float)
data = frappe.get_all(

View File

@@ -227,9 +227,47 @@ class TestStockEntry(ERPNextTestCase):
mtn.cancel()
def test_repack_no_change_in_valuation(self):
company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company')
def test_repack_multiple_fg(self):
"Test `is_finished_item` for one item repacked into two items."
make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=100, basic_rate=100)
repack = frappe.copy_doc(test_records[3])
repack.posting_date = nowdate()
repack.posting_time = nowtime()
repack.items[0].qty = 100.0
repack.items[0].transfer_qty = 100.0
repack.items[1].qty = 50.0
repack.append("items", {
"conversion_factor": 1.0,
"cost_center": "_Test Cost Center - _TC",
"doctype": "Stock Entry Detail",
"expense_account": "Stock Adjustment - _TC",
"basic_rate": 150,
"item_code": "_Test Item 2",
"parentfield": "items",
"qty": 50.0,
"stock_uom": "_Test UOM",
"t_warehouse": "_Test Warehouse - _TC",
"transfer_qty": 50.0,
"uom": "_Test UOM"
})
repack.set_stock_entry_type()
repack.insert()
self.assertEqual(repack.items[1].is_finished_item, 1)
self.assertEqual(repack.items[2].is_finished_item, 1)
repack.items[1].is_finished_item = 0
repack.items[2].is_finished_item = 0
# must raise error if 0 fg in repack entry
self.assertRaises(FinishedGoodError, repack.validate_finished_goods)
repack.delete() # teardown
def test_repack_no_change_in_valuation(self):
make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, basic_rate=100)
make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC",
qty=50, basic_rate=100)
@@ -814,6 +852,34 @@ class TestStockEntry(ERPNextTestCase):
self.assertEqual(se.get("items")[0].allow_zero_valuation_rate, 1)
self.assertEqual(se.get("items")[0].amount, 0)
def test_zero_incoming_rate(self):
""" Make sure incoming rate of 0 is allowed while consuming.
qty | rate | valuation rate
1 | 100 | 100
1 | 0 | 50
-1 | 100 | 0
-1 | 0 <--- assert this
"""
item_code = "_TestZeroVal"
warehouse = "_Test Warehouse - _TC"
create_item('_TestZeroVal')
_receipt = make_stock_entry(item_code=item_code, qty=1, to_warehouse=warehouse, rate=100)
receipt2 = make_stock_entry(item_code=item_code, qty=1, to_warehouse=warehouse, rate=0, do_not_save=True)
receipt2.items[0].allow_zero_valuation_rate = 1
receipt2.save()
receipt2.submit()
issue = make_stock_entry(item_code=item_code, qty=1, from_warehouse=warehouse)
value_diff = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": issue.name, "voucher_type": "Stock Entry"}, "stock_value_difference")
self.assertEqual(value_diff, -100)
issue2 = make_stock_entry(item_code=item_code, qty=1, from_warehouse=warehouse)
value_diff = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": issue2.name, "voucher_type": "Stock Entry"}, "stock_value_difference")
self.assertEqual(value_diff, 0)
def test_gle_for_opening_stock_entry(self):
mr = make_stock_entry(item_code="_Test Item", target="Stores - TCP1",
company="_Test Company with perpetual inventory", qty=50, basic_rate=100,

View File

@@ -55,7 +55,8 @@ def get_stock_ledger_entries(filters):
return frappe.db.sql("""select item_code, batch_no, warehouse,
posting_date, actual_qty
from `tabStock Ledger Entry`
where docstatus < 2 and ifnull(batch_no, '') != '' %s order by item_code, warehouse""" %
where is_cancelled = 0
and docstatus < 2 and ifnull(batch_no, '') != '' %s order by item_code, warehouse""" %
conditions, as_dict=1)
def get_item_warehouse_batch_map(filters, float_precision):

View File

@@ -91,7 +91,7 @@ def get_stock_value_difference_list(filtered_entries: FilteredEntries) -> SVDLis
voucher_nos = [fe.get('voucher_no') for fe in filtered_entries]
svd_list = frappe.get_list(
'Stock Ledger Entry', fields=['item_code','stock_value_difference'],
filters=[('voucher_no', 'in', voucher_nos)]
filters=[('voucher_no', 'in', voucher_nos), ("is_cancelled", "=", 0)]
)
assign_item_groups_to_svd_list(svd_list)
return svd_list

View File

@@ -74,7 +74,7 @@ def get_stock_ledger_entries(report_filters):
fields = ['name', 'voucher_type', 'voucher_no', 'item_code', 'serial_no as serial_nos', 'actual_qty',
'posting_date', 'posting_time', 'company', 'warehouse', '(stock_value_difference / actual_qty) as valuation_rate']
filters = {'serial_no': ("is", "set")}
filters = {'serial_no': ("is", "set"), "is_cancelled": 0}
if report_filters.get('item_code'):
filters['item_code'] = report_filters.get('item_code')

View File

@@ -76,6 +76,7 @@ def get_consumed_items(condition):
on sle.voucher_no = se.name
where
actual_qty < 0
and is_cancelled = 0
and voucher_type not in ('Delivery Note', 'Sales Invoice')
%s
group by item_code""" % condition, as_dict=1)

View File

@@ -105,6 +105,7 @@ def get_args_for_future_sle(row):
def validate_serial_no(sle):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
for sn in get_serial_nos(sle.serial_no):
args = copy.deepcopy(sle)
args.serial_no = sn
@@ -415,6 +416,8 @@ class update_entries_after(object):
return sorted(entries_to_fix, key=lambda k: k['timestamp'])
def process_sle(self, sle):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
# previous sle data for this warehouse
self.wh_data = self.data[sle.warehouse]
@@ -429,7 +432,7 @@ class update_entries_after(object):
if not self.args.get("sle_id"):
self.get_dynamic_incoming_outgoing_rate(sle)
if sle.serial_no:
if get_serial_nos(sle.serial_no):
self.get_serialized_values(sle)
self.wh_data.qty_after_transaction += flt(sle.actual_qty)
if sle.voucher_type == "Stock Reconciliation":
@@ -441,8 +444,9 @@ class update_entries_after(object):
# assert
self.wh_data.valuation_rate = sle.valuation_rate
self.wh_data.qty_after_transaction = sle.qty_after_transaction
self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate)
if self.valuation_method != "Moving Average":
self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
else:
if self.valuation_method == "Moving Average":
self.get_moving_average_values(sle)
@@ -595,9 +599,9 @@ class update_entries_after(object):
incoming_rate = self.wh_data.valuation_rate
stock_value_change = 0
if incoming_rate:
if actual_qty > 0:
stock_value_change = actual_qty * incoming_rate
elif actual_qty < 0:
else:
# In case of delivery/stock issue, get average purchase rate
# of serial nos of current entry
if not sle.is_cancelled:
@@ -639,6 +643,7 @@ class update_entries_after(object):
where
company = %s
and actual_qty > 0
and is_cancelled = 0
and (serial_no = %s
or serial_no like %s
or serial_no like %s
@@ -942,6 +947,7 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
item_code = %s
AND warehouse = %s
AND valuation_rate >= 0
AND is_cancelled = 0
AND NOT (voucher_no = %s AND voucher_type = %s)
order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, warehouse, voucher_no, voucher_type))
@@ -952,6 +958,7 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
where
item_code = %s
AND valuation_rate > 0
AND is_cancelled = 0
AND NOT(voucher_no = %s AND voucher_type = %s)
order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, voucher_no, voucher_type))

View File

@@ -87,8 +87,8 @@ def get_stock_balance(item_code, warehouse, posting_date=None, posting_time=None
from erpnext.stock.stock_ledger import get_previous_sle
if not posting_date: posting_date = nowdate()
if not posting_time: posting_time = nowtime()
if posting_date is None: posting_date = nowdate()
if posting_time is None: posting_time = nowtime()
args = {
"item_code": item_code,

View File

@@ -182,8 +182,6 @@ class TransactionBase(StatusUpdater):
if len(child_table_values) > 1:
self.set(default_field, None)
else:
self.set(default_field, list(child_table_values)[0])
def delete_events(ref_type, ref_name):
events = frappe.db.sql_list(""" SELECT