Merge branch 'version-13-hotfix' into patch-fixes

This commit is contained in:
Ankush Menat
2022-01-21 16:21:31 +05:30
committed by GitHub
105 changed files with 1929 additions and 955 deletions

View File

@@ -5,9 +5,14 @@ on:
paths-ignore: paths-ignore:
- '**.js' - '**.js'
- '**.md' - '**.md'
types: [opened, unlabeled, synchronize, reopened]
workflow_dispatch: workflow_dispatch:
concurrency:
group: patch-mariadb-v13-${{ github.event.number }}
cancel-in-progress: true
jobs: jobs:
test: test:
runs-on: ubuntu-18.04 runs-on: ubuntu-18.04
@@ -25,6 +30,11 @@ jobs:
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps: steps:
- name: Check for merge conficts label
if: ${{ contains(github.event.pull_request.labels.*.name, 'conflicts') }}
run: |
echo "Remove merge conflicts and remove conflict label to run CI"
exit 1
- name: Clone - name: Clone
uses: actions/checkout@v2 uses: actions/checkout@v2

View File

@@ -5,6 +5,7 @@ on:
paths-ignore: paths-ignore:
- '**.js' - '**.js'
- '**.md' - '**.md'
types: [opened, unlabeled, synchronize, reopened]
workflow_dispatch: workflow_dispatch:
push: push:
branches: [ develop ] branches: [ develop ]
@@ -12,6 +13,10 @@ on:
- '**.js' - '**.js'
- '**.md' - '**.md'
concurrency:
group: server-mariadb-v13-${{ github.event.number }}
cancel-in-progress: true
jobs: jobs:
test: test:
runs-on: ubuntu-18.04 runs-on: ubuntu-18.04
@@ -35,6 +40,12 @@ jobs:
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps: steps:
- name: Check for merge conficts label
if: ${{ contains(github.event.pull_request.labels.*.name, 'conflicts') }}
run: |
echo "Remove merge conflicts and remove conflict label to run CI"
exit 1
- name: Clone - name: Clone
uses: actions/checkout@v2 uses: actions/checkout@v2
@@ -89,39 +100,8 @@ jobs:
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
- name: Run Tests - name: Run Tests
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --use-orchestrator --with-coverage run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --use-orchestrator
env: env:
TYPE: server TYPE: server
CI_BUILD_ID: ${{ github.run_id }} CI_BUILD_ID: ${{ github.run_id }}
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
- name: Upload Coverage Data
run: |
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
cd ${GITHUB_WORKSPACE}
pip3 install coverage==5.5
pip3 install coveralls==3.0.1
coveralls
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_FLAG_NAME: run-${{ matrix.container }}
COVERALLS_SERVICE_NAME: ${{ github.event_name == 'pull_request' && 'github' || 'github-actions' }}
COVERALLS_PARALLEL: true
coveralls:
name: Coverage Wrap Up
needs: test
container: python:3-slim
runs-on: ubuntu-18.04
steps:
- name: Clone
uses: actions/checkout@v2
- name: Coveralls Finished
run: |
cd ${GITHUB_WORKSPACE}
pip3 install coverage==5.5
pip3 install coveralls==3.0.1
coveralls --finish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -6,6 +6,10 @@ on:
- '**.md' - '**.md'
workflow_dispatch: workflow_dispatch:
concurrency:
group: ui-v13-${{ github.event.number }}
cancel-in-progress: true
jobs: jobs:
test: test:
runs-on: ubuntu-18.04 runs-on: ubuntu-18.04

View File

@@ -255,11 +255,13 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
enable_check = "enable_deferred_revenue" \ enable_check = "enable_deferred_revenue" \
if doc.doctype=="Sales Invoice" else "enable_deferred_expense" 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): 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) start_date, end_date, last_gl_entry = get_booking_dates(doc, item, posting_date=posting_date)
if not (start_date and end_date): return 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": if doc.doctype == "Sales Invoice":
against, project = doc.customer, doc.project against, project = doc.customer, doc.project
credit_account, debit_account = item.income_account, item.deferred_revenue_account 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: if not amount:
return 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: if via_journal_entry:
book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount, 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) 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, 'account': credit_account,
'credit': base_amount, 'credit': base_amount,
'credit_in_account_currency': amount, 'credit_in_account_currency': amount,
'party_type': 'Customer' if doc.doctype == 'Sales Invoice' else 'Supplier',
'party': against,
'account_currency': account_currency, 'account_currency': account_currency,
'reference_name': doc.name, 'reference_name': doc.name,
'reference_type': doc.doctype, 'reference_type': doc.doctype,
@@ -421,8 +425,6 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
'account': debit_account, 'account': debit_account,
'debit': base_amount, 'debit': base_amount,
'debit_in_account_currency': amount, 'debit_in_account_currency': amount,
'party_type': 'Customer' if doc.doctype == 'Sales Invoice' else 'Supplier',
'party': against,
'account_currency': account_currency, 'account_currency': account_currency,
'reference_name': doc.name, 'reference_name': doc.name,
'reference_type': doc.doctype, 'reference_type': doc.doctype,

View File

@@ -17,6 +17,7 @@ from openpyxl.styles import Font
from openpyxl.utils import get_column_letter from openpyxl.utils import get_column_letter
from six import string_types from six import string_types
INVALID_VALUES = ("", None)
class BankStatementImport(DataImport): class BankStatementImport(DataImport):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@@ -96,6 +97,18 @@ def download_errored_template(data_import_name):
data_import = frappe.get_doc("Bank Statement Import", data_import_name) data_import = frappe.get_doc("Bank Statement Import", data_import_name)
data_import.export_errored_rows() data_import.export_errored_rows()
def parse_data_from_template(raw_data):
data = []
for i, row in enumerate(raw_data):
if all(v in INVALID_VALUES for v in row):
# empty row
continue
data.append(row)
return data
def start_import(data_import, bank_account, import_file_path, google_sheets_url, bank, template_options): def start_import(data_import, bank_account, import_file_path, google_sheets_url, bank, template_options):
"""This method runs in background job""" """This method runs in background job"""
@@ -105,7 +118,8 @@ def start_import(data_import, bank_account, import_file_path, google_sheets_url,
file = import_file_path if import_file_path else google_sheets_url file = import_file_path if import_file_path else google_sheets_url
import_file = ImportFile("Bank Transaction", file = file, import_type="Insert New Records") import_file = ImportFile("Bank Transaction", file = file, import_type="Insert New Records")
data = import_file.raw_data
data = parse_data_from_template(import_file.raw_data)
if import_file_path: if import_file_path:
add_bank_account(data, bank_account) add_bank_account(data, bank_account)

View File

@@ -397,13 +397,14 @@ class JournalEntry(AccountsController):
debit_or_credit = 'Debit' if d.debit else 'Credit' debit_or_credit = 'Debit' if d.debit else 'Credit'
party_account = get_deferred_booking_accounts(d.reference_type, d.reference_detail_no, party_account = get_deferred_booking_accounts(d.reference_type, d.reference_detail_no,
debit_or_credit) debit_or_credit)
against_voucher = ['', against_voucher[1]]
else: else:
if d.reference_type == "Sales Invoice": if d.reference_type == "Sales Invoice":
party_account = get_party_account_based_on_invoice_discounting(d.reference_name) or against_voucher[1] party_account = get_party_account_based_on_invoice_discounting(d.reference_name) or against_voucher[1]
else: else:
party_account = against_voucher[1] 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}") 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], .format(d.idx, field_dict.get(d.reference_type)[0], field_dict.get(d.reference_type)[1],
d.reference_type, d.reference_name)) d.reference_type, d.reference_name))
@@ -468,13 +469,22 @@ class JournalEntry(AccountsController):
def set_against_account(self): def set_against_account(self):
accounts_debited, accounts_credited = [], [] accounts_debited, accounts_credited = [], []
for d in self.get("accounts"): if self.voucher_type in ('Deferred Revenue', 'Deferred Expense'):
if flt(d.debit > 0): accounts_debited.append(d.party or d.account) for d in self.get('accounts'):
if flt(d.credit) > 0: accounts_credited.append(d.party or d.account) if d.reference_type == 'Sales Invoice':
field = 'customer'
else:
field = 'supplier'
for d in self.get("accounts"): d.against_account = frappe.db.get_value(d.reference_type, d.reference_name, field)
if flt(d.debit > 0): d.against_account = ", ".join(list(set(accounts_credited))) else:
if flt(d.credit > 0): d.against_account = ", ".join(list(set(accounts_debited))) 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): def validate_debit_credit_amount(self):
for d in self.get('accounts'): for d in self.get('accounts'):

View File

@@ -3,6 +3,7 @@
import json import json
from functools import reduce
import frappe import frappe
from frappe import ValidationError, _, scrub, throw 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.received_amount = received_amount
pe.letter_head = doc.get("letter_head") 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"]: if pe.party_type in ["Customer", "Supplier"]:
bank_account = get_party_bank_account(pe.party_type, pe.party) bank_account = get_party_bank_account(pe.party_type, pe.party)
pe.set("bank_account", bank_account) 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): def apply_early_payment_discount(paid_amount, received_amount, doc):
total_discount = 0 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: for term in doc.payment_schedule:
if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date: if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date:
if term.discount_type == 'Percentage': if term.discount_type == 'Percentage':

View File

@@ -354,7 +354,6 @@ class POSInvoice(SalesInvoice):
if not for_validate and not self.customer: if not for_validate and not self.customer:
self.customer = profile.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.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 self.set_warehouse = profile.get('warehouse') or self.set_warehouse

View File

@@ -556,6 +556,37 @@ class TestPOSInvoice(unittest.TestCase):
batch.cancel() batch.cancel()
batch.delete() 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): def create_pos_invoice(**args):
args = frappe._dict(args) args = frappe._dict(args)
pos_profile = None pos_profile = None

View File

@@ -652,7 +652,7 @@ def make_pricing_rule(**args):
"rate": args.rate or 0.0, "rate": args.rate or 0.0,
"margin_rate_or_amount": args.margin_rate_or_amount or 0.0, "margin_rate_or_amount": args.margin_rate_or_amount or 0.0,
"condition": args.condition or '', "condition": args.condition or '',
"priority": 1, "priority": args.priority or 1,
"discount_amount": args.discount_amount or 0.0, "discount_amount": args.discount_amount or 0.0,
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 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): if args.get(applicable_for):
doc.db_set(applicable_for, args.get(applicable_for)) doc.db_set(applicable_for, args.get(applicable_for))
return doc
def setup_pricing_rule_data(): def setup_pricing_rule_data():
if not frappe.db.exists('Campaign', '_Test Campaign'): if not frappe.db.exists('Campaign', '_Test Campaign'):
frappe.get_doc({ frappe.get_doc({

View File

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

View File

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

View File

@@ -45,6 +45,7 @@ from erpnext.setup.doctype.company.company import update_company_current_month_s
from erpnext.stock.doctype.batch.batch import set_batch_nos from erpnext.stock.doctype.batch.batch import set_batch_nos
from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so
from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no, get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no, get_serial_nos
from erpnext.stock.utils import calculate_mapped_packed_items_return
form_grid_templates = { form_grid_templates = {
"items": "templates/form_grid/item_grid.html" "items": "templates/form_grid/item_grid.html"
@@ -745,8 +746,11 @@ class SalesInvoice(SellingController):
def update_packing_list(self): def update_packing_list(self):
if cint(self.update_stock) == 1: if cint(self.update_stock) == 1:
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list if cint(self.is_return) and self.return_against:
make_packing_list(self) calculate_mapped_packed_items_return(self)
else:
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
make_packing_list(self)
else: else:
self.set('packed_items', []) self.set('packed_items', [])
@@ -879,11 +883,11 @@ class SalesInvoice(SellingController):
# Checked both rounding_adjustment and rounded_total # Checked both rounding_adjustment and rounded_total
# because rounded_total had value even before introcution of posting GLE based on 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 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(): if grand_total and not self.is_internal_transfer():
# Didnot use base_grand_total to book rounding loss gle # 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( gl_entries.append(
self.get_gl_dict({ self.get_gl_dict({
"account": self.debit_to, "account": self.debit_to,
@@ -891,8 +895,8 @@ class SalesInvoice(SellingController):
"party": self.customer, "party": self.customer,
"due_date": self.due_date, "due_date": self.due_date,
"against": self.against_income_account, "against": self.against_income_account,
"debit": grand_total_in_company_currency, "debit": base_grand_total,
"debit_in_account_currency": grand_total_in_company_currency \ "debit_in_account_currency": base_grand_total \
if self.party_account_currency==self.company_currency else 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": self.return_against if cint(self.is_return) and self.return_against else self.name,
"against_voucher_type": self.doctype, "against_voucher_type": self.doctype,

View File

@@ -1789,47 +1789,6 @@ class TestSalesInvoice(unittest.TestCase):
check_gl_entries(self, si.name, expected_gle, "2019-01-30") 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): def test_fixed_deferred_revenue(self):
deferred_account = create_account(account_name="Deferred Revenue", deferred_account = create_account(account_name="Deferred Revenue",
parent_account="Current Liabilities - _TC", company="_Test Company") parent_account="Current Liabilities - _TC", company="_Test Company")
@@ -2206,9 +2165,9 @@ class TestSalesInvoice(unittest.TestCase):
asset.load_from_db() asset.load_from_db()
expected_values = [ expected_values = [
["2020-06-30", 1311.48, 1311.48], ["2020-06-30", 1366.12, 1366.12],
["2021-06-30", 20000.0, 21311.48], ["2021-06-30", 20000.0, 21366.12],
["2021-09-30", 5041.1, 26352.58] ["2021-09-30", 5041.1, 26407.22]
] ]
for i, schedule in enumerate(asset.schedules): for i, schedule in enumerate(asset.schedules):
@@ -2256,12 +2215,12 @@ class TestSalesInvoice(unittest.TestCase):
asset.load_from_db() asset.load_from_db()
expected_values = [ expected_values = [
["2020-06-30", 1311.48, 1311.48, True], ["2020-06-30", 1366.12, 1366.12, True],
["2021-06-30", 20000.0, 21311.48, True], ["2021-06-30", 20000.0, 21366.12, True],
["2022-06-30", 20000.0, 41311.48, False], ["2022-06-30", 20000.0, 41366.12, False],
["2023-06-30", 20000.0, 61311.48, False], ["2023-06-30", 20000.0, 61366.12, False],
["2024-06-30", 20000.0, 81311.48, False], ["2024-06-30", 20000.0, 81366.12, False],
["2025-06-06", 18688.52, 100000.0, False] ["2025-06-06", 18633.88, 100000.0, False]
] ]
for i, schedule in enumerate(asset.schedules): for i, schedule in enumerate(asset.schedules):
@@ -2455,6 +2414,74 @@ class TestSalesInvoice(unittest.TestCase):
frappe.db.set_value('Accounts Settings', None, 'over_billing_allowance', over_billing_allowance) 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(): def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill() si = make_sales_invoice_for_ewaybill()
si.naming_series = 'INV-2020-.#####' si.naming_series = 'INV-2020-.#####'

View File

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

View File

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

View File

@@ -4,7 +4,7 @@
import frappe import frappe
from frappe import _, scrub from frappe import _, scrub
from frappe.utils import cint from frappe.utils import cint, flt
from six import iteritems from six import iteritems
from erpnext.accounts.party import get_partywise_advanced_payment_amount 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, party_advance_amount = get_partywise_advanced_payment_amount(self.party_type,
self.filters.report_date, self.filters.show_future_payments, self.filters.company) or {} 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): for party, party_dict in iteritems(self.party_total):
if party_dict.outstanding == 0: if party_dict.outstanding == 0:
continue continue
@@ -56,6 +59,10 @@ class AccountsReceivableSummary(ReceivablePayableReport):
# but in summary report advance shown in separate column # but in summary report advance shown in separate column
row.paid -= row.advance 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) self.data.append(row)
def get_party_total(self, args): 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(_(credit_debit_label), fieldname='credit_note')
self.add_column(_('Outstanding Amount'), fieldname='outstanding') 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() self.setup_ageing_columns()
if self.party_type == "Customer": if self.party_type == "Customer":
@@ -141,3 +152,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
# Add column for total due amount # Add column for total due amount
self.add_column(label="Total Amount Due", fieldname='total_due') 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. 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: if self.service_start_date != self.service_end_date:
self.estimate_for_period_list = get_period_list( if add_days(self.last_entry_date, 1) < self.period_list[-1].to_date:
self.filters.from_fiscal_year, self.estimate_for_period_list = get_period_list(
self.filters.to_fiscal_year, self.filters.from_fiscal_year,
add_days(self.last_entry_date, 1), self.filters.to_fiscal_year,
self.period_list[-1].to_date, add_days(self.last_entry_date, 1),
"Date Range", self.period_list[-1].to_date,
"Monthly", "Date Range",
company=self.filters.company, "Monthly",
) company=self.filters.company,
for period in self.estimate_for_period_list: )
amount = self.calculate_amount(period.from_date, period.to_date) for period in self.estimate_for_period_list:
gle = self.make_dummy_gle(period.key, period.to_date, amount) amount = self.calculate_amount(period.from_date, period.to_date)
self.gle_entries.append(gle) gle = self.make_dummy_gle(period.key, period.to_date, amount)
self.gle_entries.append(gle)
def calculate_item_revenue_expense_for_period(self): def calculate_item_revenue_expense_for_period(self):
""" """

View File

@@ -167,7 +167,7 @@ frappe.query_reports["General Ledger"] = {
"fieldname": "include_dimensions", "fieldname": "include_dimensions",
"label": __("Consider Accounting Dimensions"), "label": __("Consider Accounting Dimensions"),
"fieldtype": "Check", "fieldtype": "Check",
"default": 0 "default": 1
}, },
{ {
"fieldname": "show_opening_entries", "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: elif group_by_voucher_consolidated:
keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")] keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")]
for dim in accounting_dimensions: if filters.get("include_dimensions"):
keylist.append(gle.get(dim)) for dim in accounting_dimensions:
keylist.append(gle.get("cost_center")) keylist.append(gle.get(dim))
keylist.append(gle.get("cost_center"))
key = tuple(keylist) key = tuple(keylist)
if key not in consolidated_gle: if key not in consolidated_gle:
consolidated_gle.setdefault(key, gle) consolidated_gle.setdefault(key, gle)
@@ -595,14 +597,14 @@ def get_columns(filters):
"fieldname": dim.fieldname, "fieldname": dim.fieldname,
"width": 100 "width": 100
}) })
columns.append({
columns.extend([
{
"label": _("Cost Center"), "label": _("Cost Center"),
"options": "Cost Center", "options": "Cost Center",
"fieldname": "cost_center", "fieldname": "cost_center",
"width": 100 "width": 100
}, })
columns.extend([
{ {
"label": _("Against Voucher Type"), "label": _("Against Voucher Type"),
"fieldname": "against_voucher_type", "fieldname": "against_voucher_type",

View File

@@ -186,83 +186,85 @@ class Asset(AccountsController):
if not self.available_for_use_date: if not self.available_for_use_date:
return return
for d in self.get('finance_books'): start = self.clear_depreciation_schedule()
self.validate_asset_finance_books(d)
start = self.clear_depreciation_schedule() for finance_book in self.get('finance_books'):
self.validate_asset_finance_books(finance_book)
# value_after_depreciation - current Asset value # value_after_depreciation - current Asset value
if self.docstatus == 1 and d.value_after_depreciation: if self.docstatus == 1 and finance_book.value_after_depreciation:
value_after_depreciation = flt(d.value_after_depreciation) value_after_depreciation = flt(finance_book.value_after_depreciation)
else: else:
value_after_depreciation = (flt(self.gross_purchase_amount) - value_after_depreciation = (flt(self.gross_purchase_amount) -
flt(self.opening_accumulated_depreciation)) flt(self.opening_accumulated_depreciation))
d.value_after_depreciation = value_after_depreciation finance_book.value_after_depreciation = value_after_depreciation
number_of_pending_depreciations = cint(d.total_number_of_depreciations) - \ number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - \
cint(self.number_of_depreciations_booked) cint(self.number_of_depreciations_booked)
has_pro_rata = self.check_is_pro_rata(d) has_pro_rata = self.check_is_pro_rata(finance_book)
if has_pro_rata: if has_pro_rata:
number_of_pending_depreciations += 1 number_of_pending_depreciations += 1
skip_row = False skip_row = False
for n in range(start, number_of_pending_depreciations):
for n in range(start[finance_book.idx-1], number_of_pending_depreciations):
# If depreciation is already completed (for double declining balance) # If depreciation is already completed (for double declining balance)
if skip_row: continue if skip_row: continue
depreciation_amount = get_depreciation_amount(self, value_after_depreciation, d) depreciation_amount = get_depreciation_amount(self, value_after_depreciation, finance_book)
if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1: if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1:
schedule_date = add_months(d.depreciation_start_date, schedule_date = add_months(finance_book.depreciation_start_date,
n * cint(d.frequency_of_depreciation)) n * cint(finance_book.frequency_of_depreciation))
# schedule date will be a year later from start date # schedule date will be a year later from start date
# so monthly schedule date is calculated by removing 11 months from it # so monthly schedule date is calculated by removing 11 months from it
monthly_schedule_date = add_months(schedule_date, - d.frequency_of_depreciation + 1) monthly_schedule_date = add_months(schedule_date, - finance_book.frequency_of_depreciation + 1)
# if asset is being sold # if asset is being sold
if date_of_sale: if date_of_sale:
from_date = self.get_from_date(d.finance_book) from_date = self.get_from_date(finance_book.finance_book)
depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount, depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
from_date, date_of_sale) from_date, date_of_sale)
if depreciation_amount > 0: if depreciation_amount > 0:
self.append("schedules", { self.append("schedules", {
"schedule_date": date_of_sale, "schedule_date": date_of_sale,
"depreciation_amount": depreciation_amount, "depreciation_amount": depreciation_amount,
"depreciation_method": d.depreciation_method, "depreciation_method": finance_book.depreciation_method,
"finance_book": d.finance_book, "finance_book": finance_book.finance_book,
"finance_book_id": d.idx "finance_book_id": finance_book.idx
}) })
break break
# For first row # For first row
if has_pro_rata and not self.opening_accumulated_depreciation and n==0: if has_pro_rata and not self.opening_accumulated_depreciation and n==0:
depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount, from_date = add_days(self.available_for_use_date, -1) # needed to calc depr amount for available_for_use_date too
self.available_for_use_date, d.depreciation_start_date) depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
from_date, finance_book.depreciation_start_date)
# For first depr schedule date will be the start date # For first depr schedule date will be the start date
# so monthly schedule date is calculated by removing month difference between use date and start date # so monthly schedule date is calculated by removing month difference between use date and start date
monthly_schedule_date = add_months(d.depreciation_start_date, - months + 1) monthly_schedule_date = add_months(finance_book.depreciation_start_date, - months + 1)
# For last row # For last row
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1: elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
if not self.flags.increase_in_asset_life: if not self.flags.increase_in_asset_life:
# In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission # In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission
self.to_date = add_months(self.available_for_use_date, self.to_date = add_months(self.available_for_use_date,
(n + self.number_of_depreciations_booked) * cint(d.frequency_of_depreciation)) (n + self.number_of_depreciations_booked) * cint(finance_book.frequency_of_depreciation))
depreciation_amount_without_pro_rata = depreciation_amount depreciation_amount_without_pro_rata = depreciation_amount
depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount, days, months = self.get_pro_rata_amt(finance_book,
depreciation_amount, schedule_date, self.to_date) depreciation_amount, schedule_date, self.to_date)
depreciation_amount = self.get_adjusted_depreciation_amount(depreciation_amount_without_pro_rata, depreciation_amount = self.get_adjusted_depreciation_amount(depreciation_amount_without_pro_rata,
depreciation_amount, d.finance_book) depreciation_amount, finance_book.finance_book)
monthly_schedule_date = add_months(schedule_date, 1) monthly_schedule_date = add_months(schedule_date, 1)
schedule_date = add_days(schedule_date, days) schedule_date = add_days(schedule_date, days)
@@ -273,10 +275,10 @@ class Asset(AccountsController):
self.precision("gross_purchase_amount")) self.precision("gross_purchase_amount"))
# Adjust depreciation amount in the last period based on the expected value after useful life # Adjust depreciation amount in the last period based on the expected value after useful life
if d.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1 if finance_book.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1
and value_after_depreciation != d.expected_value_after_useful_life) and value_after_depreciation != finance_book.expected_value_after_useful_life)
or value_after_depreciation < d.expected_value_after_useful_life): or value_after_depreciation < finance_book.expected_value_after_useful_life):
depreciation_amount += (value_after_depreciation - d.expected_value_after_useful_life) depreciation_amount += (value_after_depreciation - finance_book.expected_value_after_useful_life)
skip_row = True skip_row = True
if depreciation_amount > 0: if depreciation_amount > 0:
@@ -286,7 +288,7 @@ class Asset(AccountsController):
# In pro rata case, for first and last depreciation, month range would be different # In pro rata case, for first and last depreciation, month range would be different
month_range = months \ month_range = months \
if (has_pro_rata and n==0) or (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) \ if (has_pro_rata and n==0) or (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) \
else d.frequency_of_depreciation else finance_book.frequency_of_depreciation
for r in range(month_range): for r in range(month_range):
if (has_pro_rata and n == 0): if (has_pro_rata and n == 0):
@@ -312,27 +314,52 @@ class Asset(AccountsController):
self.append("schedules", { self.append("schedules", {
"schedule_date": date, "schedule_date": date,
"depreciation_amount": amount, "depreciation_amount": amount,
"depreciation_method": d.depreciation_method, "depreciation_method": finance_book.depreciation_method,
"finance_book": d.finance_book, "finance_book": finance_book.finance_book,
"finance_book_id": d.idx "finance_book_id": finance_book.idx
}) })
else: else:
self.append("schedules", { self.append("schedules", {
"schedule_date": schedule_date, "schedule_date": schedule_date,
"depreciation_amount": depreciation_amount, "depreciation_amount": depreciation_amount,
"depreciation_method": d.depreciation_method, "depreciation_method": finance_book.depreciation_method,
"finance_book": d.finance_book, "finance_book": finance_book.finance_book,
"finance_book_id": d.idx "finance_book_id": finance_book.idx
}) })
# used when depreciation schedule needs to be modified due to increase in asset life # depreciation schedules need to be cleared before modification due to increase in asset life/asset sales
# JE: Journal Entry, FB: Finance Book
def clear_depreciation_schedule(self): def clear_depreciation_schedule(self):
start = 0 start = []
for n in range(len(self.schedules)): num_of_depreciations_completed = 0
if not self.schedules[n].journal_entry: depr_schedule = []
del self.schedules[n:]
start = n for schedule in self.get('schedules'):
break
# to update start when there are JEs linked with all the schedule rows corresponding to an FB
if len(start) == (int(schedule.finance_book_id) - 2):
start.append(num_of_depreciations_completed)
num_of_depreciations_completed = 0
# to ensure that start will only be updated once for each FB
if len(start) == (int(schedule.finance_book_id) - 1):
if schedule.journal_entry:
num_of_depreciations_completed += 1
depr_schedule.append(schedule)
else:
start.append(num_of_depreciations_completed)
num_of_depreciations_completed = 0
# to update start when all the schedule rows corresponding to the last FB are linked with JEs
if len(start) == (len(self.finance_books) - 1):
start.append(num_of_depreciations_completed)
# when the Depreciation Schedule is being created for the first time
if start == []:
start = [0] * len(self.finance_books)
else:
self.schedules = depr_schedule
return start return start
def get_from_date(self, finance_book): def get_from_date(self, finance_book):
@@ -349,7 +376,9 @@ class Asset(AccountsController):
if from_date: if from_date:
return from_date return from_date
return self.available_for_use_date
# since depr for available_for_use_date is not yet booked
return add_days(self.available_for_use_date, -1)
# if it returns True, depreciation_amount will not be equal for the first and last rows # if it returns True, depreciation_amount will not be equal for the first and last rows
def check_is_pro_rata(self, row): def check_is_pro_rata(self, row):
@@ -583,7 +612,17 @@ class Asset(AccountsController):
return purchase_document return purchase_document
def get_fixed_asset_account(self): 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): def get_cwip_account(self, cwip_enabled=False):
cwip_account = None cwip_account = None

View File

@@ -207,9 +207,9 @@ class TestAsset(AssetSetup):
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold") self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
expected_gle = ( expected_gle = (
("_Test Accumulated Depreciations - _TC", 20392.16, 0.0), ("_Test Accumulated Depreciations - _TC", 20490.2, 0.0),
("_Test Fixed Asset - _TC", 0.0, 100000.0), ("_Test Fixed Asset - _TC", 0.0, 100000.0),
("_Test Gain/Loss on Asset Disposal - _TC", 54607.84, 0.0), ("_Test Gain/Loss on Asset Disposal - _TC", 54509.8, 0.0),
("Debtors - _TC", 25000.0, 0.0) ("Debtors - _TC", 25000.0, 0.0)
) )
@@ -491,10 +491,10 @@ class TestDepreciationMethods(AssetSetup):
) )
expected_schedules = [ expected_schedules = [
["2030-12-31", 27534.25, 27534.25], ['2030-12-31', 27616.44, 27616.44],
["2031-12-31", 30000.0, 57534.25], ['2031-12-31', 30000.0, 57616.44],
["2032-12-31", 30000.0, 87534.25], ['2032-12-31', 30000.0, 87616.44],
["2033-01-30", 2465.75, 90000.0] ['2033-01-30', 2383.56, 90000.0]
] ]
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)] schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)]
@@ -544,10 +544,10 @@ class TestDepreciationMethods(AssetSetup):
self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0) self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0)
expected_schedules = [ expected_schedules = [
["2030-12-31", 28493.15, 28493.15], ['2030-12-31', 28630.14, 28630.14],
["2031-12-31", 35753.43, 64246.58], ['2031-12-31', 35684.93, 64315.07],
["2032-12-31", 17876.71, 82123.29], ['2032-12-31', 17842.47, 82157.54],
["2033-06-06", 5376.71, 87500.0] ['2033-06-06', 5342.46, 87500.0]
] ]
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)] schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)]
@@ -580,10 +580,10 @@ class TestDepreciationMethods(AssetSetup):
self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0) self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0)
expected_schedules = [ expected_schedules = [
["2030-12-31", 11780.82, 11780.82], ["2030-12-31", 11849.32, 11849.32],
["2031-12-31", 44109.59, 55890.41], ["2031-12-31", 44075.34, 55924.66],
["2032-12-31", 22054.8, 77945.21], ["2032-12-31", 22037.67, 77962.33],
["2033-07-12", 9554.79, 87500.0] ["2033-07-12", 9537.67, 87500.0]
] ]
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)] schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)]
@@ -642,7 +642,7 @@ class TestDepreciationBasics(AssetSetup):
asset = create_asset( asset = create_asset(
item_code = "Macbook Pro", item_code = "Macbook Pro",
calculate_depreciation = 1, calculate_depreciation = 1,
available_for_use_date = getdate("2019-12-31"), available_for_use_date = getdate("2020-01-01"),
total_number_of_depreciations = 3, total_number_of_depreciations = 3,
expected_value_after_useful_life = 10000, expected_value_after_useful_life = 10000,
depreciation_start_date = getdate("2020-07-01"), depreciation_start_date = getdate("2020-07-01"),
@@ -653,7 +653,7 @@ class TestDepreciationBasics(AssetSetup):
["2020-07-01", 15000, 15000], ["2020-07-01", 15000, 15000],
["2021-07-01", 30000, 45000], ["2021-07-01", 30000, 45000],
["2022-07-01", 30000, 75000], ["2022-07-01", 30000, 75000],
["2022-12-31", 15000, 90000] ["2023-01-01", 15000, 90000]
] ]
for i, schedule in enumerate(asset.schedules): for i, schedule in enumerate(asset.schedules):
@@ -976,6 +976,82 @@ class TestDepreciationBasics(AssetSetup):
self.assertEqual(len(asset.schedules), 1) self.assertEqual(len(asset.schedules), 1)
def test_clear_depreciation_schedule_for_multiple_finance_books(self):
asset = create_asset(
item_code = "Macbook Pro",
available_for_use_date = "2019-12-31",
do_not_save = 1
)
asset.calculate_depreciation = 1
asset.append("finance_books", {
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 1,
"total_number_of_depreciations": 3,
"expected_value_after_useful_life": 10000,
"depreciation_start_date": "2020-01-31"
})
asset.append("finance_books", {
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 1,
"total_number_of_depreciations": 6,
"expected_value_after_useful_life": 10000,
"depreciation_start_date": "2020-01-31"
})
asset.append("finance_books", {
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 12,
"total_number_of_depreciations": 3,
"expected_value_after_useful_life": 10000,
"depreciation_start_date": "2020-12-31"
})
asset.submit()
post_depreciation_entries(date="2020-04-01")
asset.load_from_db()
asset.clear_depreciation_schedule()
self.assertEqual(len(asset.schedules), 6)
for schedule in asset.schedules:
if schedule.idx <= 3:
self.assertEqual(schedule.finance_book_id, "1")
else:
self.assertEqual(schedule.finance_book_id, "2")
def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self):
asset = create_asset(
item_code = "Macbook Pro",
available_for_use_date = "2019-12-31",
do_not_save = 1
)
asset.calculate_depreciation = 1
asset.append("finance_books", {
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 12,
"total_number_of_depreciations": 3,
"expected_value_after_useful_life": 10000,
"depreciation_start_date": "2020-12-31"
})
asset.append("finance_books", {
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 12,
"total_number_of_depreciations": 6,
"expected_value_after_useful_life": 10000,
"depreciation_start_date": "2020-12-31"
})
asset.save()
self.assertEqual(len(asset.schedules), 9)
for schedule in asset.schedules:
if schedule.idx <= 3:
self.assertEqual(schedule.finance_book_id, 1)
else:
self.assertEqual(schedule.finance_book_id, 2)
def test_depreciation_entry_cancellation(self): def test_depreciation_entry_cancellation(self):
asset = create_asset( asset = create_asset(
item_code = "Macbook Pro", item_code = "Macbook Pro",

View File

@@ -114,7 +114,7 @@ class AccountsController(TransactionBase):
_('{0} is blocked so this transaction cannot proceed').format(supplier_name), raise_exception=1) _('{0} is blocked so this transaction cannot proceed').format(supplier_name), raise_exception=1)
def validate(self): 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() self.validate_qty_is_not_zero()
if self.get("_action") and self._action != "update_after_submit": if self.get("_action") and self._action != "update_after_submit":
@@ -191,8 +191,6 @@ class AccountsController(TransactionBase):
frappe.throw(_("Row #{0}: Service Start Date cannot be greater than Service End Date").format(d.idx)) 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): 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)) 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): def validate_invoice_documents_schedule(self):
self.validate_payment_schedule_dates() self.validate_payment_schedule_dates()

View File

@@ -385,7 +385,7 @@ class SellingController(StockController):
# Get incoming rate based on original item cost based on valuation method # Get incoming rate based on original item cost based on valuation method
qty = flt(d.get('stock_qty') or d.get('actual_qty')) 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({ d.incoming_rate = get_incoming_rate({
"item_code": d.item_code, "item_code": d.item_code,
"warehouse": d.warehouse, "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.accounts.utils import get_fiscal_year
from erpnext.controllers.accounts_controller import AccountsController from erpnext.controllers.accounts_controller import AccountsController
from erpnext.stock import get_warehouse_account_map 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 class QualityInspectionRequiredError(frappe.ValidationError): pass
@@ -111,17 +111,6 @@ class StockController(AccountsController):
self.check_expense_account(item_row) 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 # expense account/ target_warehouse / source_warehouse
if item_row.get('target_warehouse'): if item_row.get('target_warehouse'):
warehouse = item_row.get('target_warehouse') warehouse = item_row.get('target_warehouse')
@@ -164,26 +153,6 @@ class StockController(AccountsController):
return frappe.flags.debit_field_precision 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): def get_voucher_details(self, default_expense_account, default_cost_center, sle_map):
if self.doctype == "Stock Reconciliation": if self.doctype == "Stock Reconciliation":
reconciliation_purpose = frappe.db.get_value(self.doctype, self.name, "purpose") reconciliation_purpose = frappe.db.get_value(self.doctype, self.name, "purpose")
@@ -287,11 +256,7 @@ class StockController(AccountsController):
for d in self.items: for d in self.items:
if not d.batch_no: continue if not d.batch_no: continue
serial_nos = [sr.name for sr in frappe.get_all("Serial No", frappe.db.set_value("Serial No", {"batch_no": d.batch_no, "status": "Inactive"}, "batch_no", None)
{'batch_no': d.batch_no, 'status': 'Inactive'})]
if serial_nos:
frappe.db.set_value("Serial No", { 'name': ['in', serial_nos] }, "batch_no", None)
d.batch_no = None d.batch_no = None
d.db_set("batch_no", None) d.db_set("batch_no", None)

View File

@@ -139,6 +139,8 @@ class calculate_taxes_and_totals(object):
if not item.qty and self.doc.get("is_return"): if not item.qty and self.doc.get("is_return"):
item.amount = flt(-1 * item.rate, item.precision("amount")) 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: else:
item.amount = flt(item.rate * item.qty, item.precision("amount")) 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"]: if self.doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
grand_total = self.doc.rounded_total or self.doc.grand_total 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: if self.doc.party_account_currency == self.doc.currency:
total_amount_to_pay = flt(grand_total - self.doc.total_advance total_amount_to_pay = flt(grand_total - self.doc.total_advance
- flt(self.doc.write_off_amount), self.doc.precision("grand_total")) - flt(self.doc.write_off_amount), self.doc.precision("grand_total"))
else: else:
total_amount_to_pay = flt(flt(grand_total * total_amount_to_pay = flt(flt(base_grand_total, self.doc.precision("base_grand_total")) - self.doc.total_advance
self.doc.conversion_rate, self.doc.precision("grand_total")) - self.doc.total_advance - flt(self.doc.base_write_off_amount), self.doc.precision("base_grand_total"))
- flt(self.doc.base_write_off_amount), self.doc.precision("grand_total"))
self.doc.round_floats_in(self.doc, ["paid_amount"]) self.doc.round_floats_in(self.doc, ["paid_amount"])
change_amount = 0 change_amount = 0

View File

@@ -1,6 +1,8 @@
import unittest import unittest
from functools import partial from functools import partial
import frappe
from erpnext.controllers import queries from erpnext.controllers import queries
@@ -85,3 +87,6 @@ class TestQueries(unittest.TestCase):
wh = query(filters=[["Bin", "item_code", "=", "_Test Item"]]) wh = query(filters=[["Bin", "item_code", "=", "_Test Item"]])
self.assertGreaterEqual(len(wh), 1) 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): class TestUtils(unittest.TestCase):
def test_reset_default_field_value(self): def test_reset_default_field_value(self):
doc = frappe.get_doc({ doc = frappe.get_doc({
"doctype": "Purchase Receipt", "doctype": "Purchase Receipt",
"set_warehouse": "Warehouse 1", "set_warehouse": "Warehouse 1",
}) })
# Same values # Same values
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}] doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}]
doc.reset_default_field_value("set_warehouse", "items", "warehouse") doc.reset_default_field_value("set_warehouse", "items", "warehouse")
self.assertEqual(doc.set_warehouse, "Warehouse 1") self.assertEqual(doc.set_warehouse, "Warehouse 1")
# Mixed values # Mixed values
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}] doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}]
doc.reset_default_field_value("set_warehouse", "items", "warehouse") doc.reset_default_field_value("set_warehouse", "items", "warehouse")
self.assertEqual(doc.set_warehouse, None) 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 # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
import itertools
import json import json
import frappe import frappe
@@ -203,16 +202,15 @@ class WebsiteItem(WebsiteGenerator):
context.body_class = "product-page" context.body_class = "product-page"
context.parents = get_parent_item_groups(self.item_group, from_item=True) # breadcumbs 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"], fields=["attribute", "attribute_value"],
filters={"parent": self.item_code}) filters={"parent": self.item_code}
)
if self.slideshow: if self.slideshow:
context.update(get_slideshow(self)) 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_metatags(context)
self.set_shopping_cart_data(context) self.set_shopping_cart_data(context)
@@ -237,61 +235,6 @@ class WebsiteItem(WebsiteGenerator):
return context 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): def set_selected_attributes(self, variants, context, attribute_values_available):
for variant in variants: for variant in variants:
variant.attributes = frappe.get_all( variant.attributes = frappe.get_all(
@@ -328,50 +271,6 @@ class WebsiteItem(WebsiteGenerator):
if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []): if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []):
values.append(attr_value.attribute_value) 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): def set_metatags(self, context):
context.metatags = frappe._dict({}) context.metatags = frappe._dict({})

View File

@@ -197,7 +197,10 @@ class ProductQuery:
website_item_groups = frappe.db.get_all( website_item_groups = frappe.db.get_all(
"Website Item", "Website Item",
fields=self.fields + ["`tabWebsite Item Group`.parent as wig_parent"], 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 return website_item_groups

View File

@@ -276,10 +276,29 @@ def guess_territory():
def decorate_quotation_doc(doc): def decorate_quotation_doc(doc):
for d in doc.get("items", []): for d in doc.get("items", []):
item_code = d.item_code
fields = ["web_item_name", "thumbnail", "website_image", "description", "route"]
# Variant Item
if not frappe.db.exists("Website Item", {"item_code": item_code}):
variant_data = frappe.db.get_values(
"Item",
filters={"item_code": item_code},
fieldname=["variant_of", "item_name", "image"],
as_dict=True
)[0]
item_code = variant_data.variant_of
fields = fields[1:]
d.website_item_name = variant_data.item_name
if variant_data.image: # get image from variant or template web item
d.thumbnail = variant_data.image
fields = fields[2:]
d.update(frappe.db.get_value( d.update(frappe.db.get_value(
"Website Item", "Website Item",
{"item_code": d.item_code}, {"item_code": item_code},
["web_item_name", "thumbnail", "website_image", "description", "route"], fields,
as_dict=True) as_dict=True)
) )

View File

@@ -9,8 +9,13 @@ from frappe.utils import add_months, nowdate
from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
from erpnext.e_commerce.shopping_cart.cart import _get_cart_quotation, get_party, update_cart from erpnext.e_commerce.shopping_cart.cart import (
from erpnext.tests.utils import create_test_contact_and_address _get_cart_quotation,
get_cart_quotation,
get_party,
update_cart,
)
from erpnext.tests.utils import change_settings, create_test_contact_and_address
class TestShoppingCart(unittest.TestCase): class TestShoppingCart(unittest.TestCase):
@@ -34,6 +39,7 @@ class TestShoppingCart(unittest.TestCase):
make_website_item(frappe.get_cached_doc("Item", "_Test Item 2")) make_website_item(frappe.get_cached_doc("Item", "_Test Item 2"))
def tearDown(self): def tearDown(self):
frappe.db.rollback()
frappe.set_user("Administrator") frappe.set_user("Administrator")
self.disable_shopping_cart() self.disable_shopping_cart()
@@ -128,6 +134,40 @@ class TestShoppingCart(unittest.TestCase):
self.remove_test_quotation(quotation) self.remove_test_quotation(quotation)
@change_settings("E Commerce Settings",{
"company": "_Test Company",
"enabled": 1,
"default_customer_group": "_Test Customer Group",
"price_list": "_Test Price List India",
"show_price": 1
})
def test_add_item_variant_without_web_item_to_cart(self):
"Test adding Variants having no Website Items in cart via Template Web Item."
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
template_item = make_item("Test-Tshirt-Temp", {
"has_variant": 1,
"variant_based_on": "Item Attribute",
"attributes": [
{"attribute": "Test Size"},
{"attribute": "Test Colour"}
]
})
variant = create_variant("Test-Tshirt-Temp", {
"Test Size": "Small", "Test Colour": "Red"
})
variant.save()
make_website_item(template_item) # publish template not variant
update_cart("Test-Tshirt-Temp-S-R", 1)
cart = get_cart_quotation() # test if cart page gets data without errors
doc = cart.get("doc")
self.assertEqual(doc.get("items")[0].item_name, "Test-Tshirt-Temp-S-R")
def create_tax_rule(self): def create_tax_rule(self):
tax_rule = frappe.get_test_records("Tax Rule")[0] tax_rule = frappe.get_test_records("Tax Rule")[0]
try: try:

View File

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

View File

@@ -1,11 +1,114 @@
# import frappe
import unittest import unittest
# from erpnext.e_commerce.product_data_engine.query import ProductQuery import frappe
# from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
from erpnext.controllers.item_variant import create_variant
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
from erpnext.e_commerce.variant_selector.utils import get_next_attribute_and_values
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.tests.utils import ERPNextTestCase, change_settings
test_dependencies = ["Item"] test_dependencies = ["Item"]
class TestVariantSelector(unittest.TestCase): class TestVariantSelector(ERPNextTestCase):
# TODO: Variant Selector Tests
pass @classmethod
def setUpClass(cls):
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()
make_website_item(template_item) # publish template not variants
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
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 = 0
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.
"""
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")
@change_settings("E Commerce Settings",{
"company": "_Test Company",
"enabled": 1,
"default_customer_group": "_Test Customer Group",
"price_list": "_Test Price List India",
"show_price": 1
})
def test_exact_match_with_price(self):
"""
Test price fetching and matching of variant without Website Item
"""
from erpnext.e_commerce.doctype.website_item.test_website_item import make_web_item_price
frappe.set_user("Administrator")
make_web_item_price(item_code="Test-Tshirt-Temp-S-R", price_list_rate=100)
next_values = get_next_attribute_and_values(
"Test-Tshirt-Temp",
selected_attributes={"Test Size": "Small", "Test Colour": "Red"}
)
price_info = next_values["product_info"]["price"]
self.assertEqual(next_values["exact_match"][0],"Test-Tshirt-Temp-S-R")
self.assertEqual(next_values["exact_match"][0],"Test-Tshirt-Temp-S-R")
self.assertEqual(price_info["price_list_rate"], 100.0)
self.assertEqual(price_info["formatted_price_sales_uom"], "₹ 100.00")

View File

@@ -1,7 +1,12 @@
import frappe import frappe
from frappe.utils import cint from frappe.utils import cint
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
get_shopping_cart_settings,
)
from erpnext.e_commerce.shopping_cart.cart import _set_price_list
from erpnext.e_commerce.variant_selector.item_variants_cache import ItemVariantsCacheManager from erpnext.e_commerce.variant_selector.item_variants_cache import ItemVariantsCacheManager
from erpnext.utilities.product import get_price
def get_item_codes_by_attributes(attribute_filters, template_item_code=None): def get_item_codes_by_attributes(attribute_filters, template_item_code=None):
@@ -143,14 +148,13 @@ def get_next_attribute_and_values(item_code, selected_attributes):
filtered_items_count = len(filtered_items) filtered_items_count = len(filtered_items)
# get product info if exact match # get product info if exact match
from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website # from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website
if exact_match: if exact_match:
data = get_product_info_for_website(exact_match[0]) cart_settings = get_shopping_cart_settings()
product_info = data.product_info product_info = get_item_variant_price_dict(exact_match[0], cart_settings)
if product_info: if product_info:
product_info["allow_items_not_in_stock"] = cint(data.cart_settings.allow_items_not_in_stock) product_info["allow_items_not_in_stock"] = cint(cart_settings.allow_items_not_in_stock)
if not data.cart_settings.show_price:
product_info = None
else: else:
product_info = None product_info = None
@@ -195,3 +199,20 @@ def get_item_attributes(item_code):
return attributes return attributes
def get_item_variant_price_dict(item_code, cart_settings):
if cart_settings.enabled and cart_settings.show_price:
is_guest = frappe.session.user == "Guest"
# Show Price if logged in.
# If not logged in, check if price is hidden for guest.
if not is_guest or not cart_settings.hide_price_for_guest:
price_list = _set_price_list(cart_settings, None)
price = get_price(
item_code,
price_list,
cart_settings.default_customer_group,
cart_settings.company
)
return {"price": price}
return None

View File

@@ -201,8 +201,8 @@ def get_course_schedule_events(start, end, filters=None):
conditions = get_event_conditions("Course Schedule", filters) conditions = get_event_conditions("Course Schedule", filters)
data = frappe.db.sql("""select name, course, color, data = frappe.db.sql("""select name, course, color,
timestamp(schedule_date, from_time) as from_datetime, timestamp(schedule_date, from_time) as from_time,
timestamp(schedule_date, to_time) as to_datetime, timestamp(schedule_date, to_time) as to_time,
room, student_group, 0 as 'allDay' room, student_group, 0 as 'allDay'
from `tabCourse Schedule` from `tabCourse Schedule`
where ( schedule_date between %(start)s and %(end)s ) where ( schedule_date between %(start)s and %(end)s )

View File

@@ -3,6 +3,8 @@
# For license information, please see license.txt # For license information, please see license.txt
from datetime import datetime
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
@@ -30,6 +32,14 @@ class CourseSchedule(Document):
if self.from_time > self.to_time: if self.from_time > self.to_time:
frappe.throw(_("From Time cannot be greater than To Time.")) frappe.throw(_("From Time cannot be greater than To Time."))
"""Handles specicfic case to update schedule date in calendar """
if isinstance(self.from_time, str):
try:
datetime_obj = datetime.strptime(self.from_time, '%Y-%m-%d %H:%M:%S')
self.schedule_date = datetime_obj
except ValueError:
pass
def validate_overlap(self): def validate_overlap(self):
"""Validates overlap for Student Group, Instructor, Room""" """Validates overlap for Student Group, Instructor, Room"""
@@ -47,4 +57,4 @@ class CourseSchedule(Document):
validate_overlap_for(self, "Assessment Plan", "student_group") validate_overlap_for(self, "Assessment Plan", "student_group")
validate_overlap_for(self, "Assessment Plan", "room") validate_overlap_for(self, "Assessment Plan", "room")
validate_overlap_for(self, "Assessment Plan", "supervisor", self.instructor) validate_overlap_for(self, "Assessment Plan", "supervisor", self.instructor)

View File

@@ -1,11 +1,10 @@
frappe.views.calendar["Course Schedule"] = { frappe.views.calendar["Course Schedule"] = {
field_map: { field_map: {
// from_datetime and to_datetime don't exist as docfields but are used in onload "start": "from_time",
"start": "from_datetime", "end": "to_time",
"end": "to_datetime",
"id": "name", "id": "name",
"title": "course", "title": "course",
"allDay": "allDay" "allDay": "allDay",
}, },
gantt: false, gantt: false,
order_by: "schedule_date", order_by: "schedule_date",

View File

@@ -6,6 +6,7 @@ import unittest
import frappe import frappe
from frappe.utils import to_timedelta, today from frappe.utils import to_timedelta, today
from frappe.utils.data import add_to_date
from erpnext.education.utils import OverlapError from erpnext.education.utils import OverlapError
@@ -39,6 +40,11 @@ class TestCourseSchedule(unittest.TestCase):
make_course_schedule_test_record(from_time= cs1.from_time, to_time= cs1.to_time, make_course_schedule_test_record(from_time= cs1.from_time, to_time= cs1.to_time,
student_group="Course-TC102-2014-2015 (_Test Academic Term)", instructor="_Test Instructor 2", room=frappe.get_all("Room")[1].name) student_group="Course-TC102-2014-2015 (_Test Academic Term)", instructor="_Test Instructor 2", room=frappe.get_all("Room")[1].name)
def test_update_schedule_date(self):
doc = make_course_schedule_test_record(schedule_date= add_to_date(today(), days=1))
doc.schedule_date = add_to_date(doc.schedule_date, days=1)
doc.save()
def make_course_schedule_test_record(**args): def make_course_schedule_test_record(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@@ -143,7 +143,7 @@ class Patient(Document):
age = self.age age = self.age
if not age: if not age:
return 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 return age_str
@frappe.whitelist() @frappe.whitelist()

View File

@@ -5,9 +5,9 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import 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): class Attendance(Document):
@@ -171,7 +171,7 @@ def get_month_map():
}) })
@frappe.whitelist() @frappe.whitelist()
def get_unmarked_days(employee, month): def get_unmarked_days(employee, month, exclude_holidays=0):
import calendar import calendar
month_map = get_month_map() 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] 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 = [] unmarked_days = []
for date in dates_of_month: for date in dates_of_month:

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,26 @@ from erpnext.hr.doctype.designation.test_designation import create_designation
class TestJobApplicant(unittest.TestCase): 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): def create_job_applicant(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

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

View File

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

View File

@@ -48,7 +48,16 @@ frappe.listview_settings['Leave Policy Assignment'] = {
if (cur_dialog.fields_dict.leave_period.value) { if (cur_dialog.fields_dict.leave_period.value) {
me.set_effective_date(); 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" fieldtype: "Column Break"

View File

@@ -13,8 +13,10 @@
"column_break_3", "column_break_3",
"company", "company",
"posting_date", "posting_date",
"is_term_loan",
"rate_of_interest", "rate_of_interest",
"payroll_payable_account",
"is_term_loan",
"repay_from_salary",
"payment_details_section", "payment_details_section",
"due_date", "due_date",
"pending_principal_amount", "pending_principal_amount",
@@ -243,15 +245,31 @@
"label": "Total Penalty Paid", "label": "Total Penalty Paid",
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"read_only": 1 "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, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-04-19 18:10:00.935364", "modified": "2022-01-06 01:51:06.707782",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan Repayment", "name": "Loan Repayment",
"naming_rule": "Expression (old style)",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@@ -287,5 +305,6 @@
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

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

View File

@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import frappe import frappe
from frappe import _, throw from frappe import _, throw
from frappe.utils import add_days, cint, cstr, date_diff, formatdate, getdate from frappe.utils import add_days, cint, cstr, date_diff, formatdate, getdate
@@ -306,13 +305,18 @@ class MaintenanceSchedule(TransactionBase):
return schedule.name return schedule.name
@frappe.whitelist() @frappe.whitelist()
def update_serial_nos(s_id): def get_serial_nos_from_schedule(item_code, schedule=None):
serial_nos = frappe.db.get_value('Maintenance Schedule Detail', s_id, 'serial_no') serial_nos = []
if schedule:
serial_nos = frappe.db.get_value('Maintenance Schedule Item', {
'parent': schedule,
'item_code': item_code
}, 'serial_no')
if serial_nos: if serial_nos:
serial_nos = get_serial_nos(serial_nos) serial_nos = get_serial_nos(serial_nos)
return serial_nos
else: return serial_nos
return False
@frappe.whitelist() @frappe.whitelist()
def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=None): def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=None):
@@ -320,12 +324,9 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No
def update_status_and_detail(source, target, parent): def update_status_and_detail(source, target, parent):
target.maintenance_type = "Scheduled" target.maintenance_type = "Scheduled"
target.maintenance_schedule = source.name
target.maintenance_schedule_detail = s_id target.maintenance_schedule_detail = s_id
def update_sales_and_serial(source, target, parent): def update_serial(source, target, parent):
sales_person = frappe.db.get_value('Maintenance Schedule Detail', s_id, 'sales_person')
target.service_person = sales_person
serial_nos = get_serial_nos(target.serial_no) serial_nos = get_serial_nos(target.serial_no)
if len(serial_nos) == 1: if len(serial_nos) == 1:
target.serial_no = serial_nos[0] target.serial_no = serial_nos[0]
@@ -346,7 +347,10 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No
"Maintenance Schedule Item": { "Maintenance Schedule Item": {
"doctype": "Maintenance Visit Purpose", "doctype": "Maintenance Visit Purpose",
"condition": lambda doc: doc.item_name == item_name, "condition": lambda doc: doc.item_name == item_name,
"postprocess": update_sales_and_serial "field_map": {
"sales_person": "service_person"
},
"postprocess": update_serial
} }
}, target_doc) }, target_doc)

View File

@@ -4,11 +4,15 @@
import unittest import unittest
import frappe import frappe
from frappe.utils import format_date
from frappe.utils.data import add_days, formatdate, today from frappe.utils.data import add_days, formatdate, today
from erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule import ( from erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule import (
get_serial_nos_from_schedule,
make_maintenance_visit, make_maintenance_visit,
) )
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
# test_records = frappe.get_test_records('Maintenance Schedule') # test_records = frappe.get_test_records('Maintenance Schedule')
@@ -79,6 +83,49 @@ class TestMaintenanceSchedule(unittest.TestCase):
#checks if visit status is back updated in schedule #checks if visit status is back updated in schedule
self.assertTrue(ms.schedules[1].completion_status, "Partially Completed") self.assertTrue(ms.schedules[1].completion_status, "Partially Completed")
self.assertEqual(format_date(visit.mntc_date), format_date(ms.schedules[1].actual_date))
#checks if visit status is updated on cancel
visit.cancel()
ms.reload()
self.assertTrue(ms.schedules[1].completion_status, "Pending")
self.assertEqual(ms.schedules[1].actual_date, None)
def test_serial_no_filters(self):
# Without serial no. set in schedule -> returns None
item_code = "_Test Serial Item"
make_serial_item_with_serial(item_code)
ms = make_maintenance_schedule(item_code=item_code)
ms.submit()
s_item = ms.schedules[0]
mv = make_maintenance_visit(source_name=ms.name, item_name=item_code, s_id=s_item.name)
mvi = mv.purposes[0]
serial_nos = get_serial_nos_from_schedule(mvi.item_name, ms.name)
self.assertEqual(serial_nos, None)
# With serial no. set in schedule -> returns serial nos.
make_serial_item_with_serial(item_code)
ms = make_maintenance_schedule(item_code=item_code, serial_no="TEST001, TEST002")
ms.submit()
s_item = ms.schedules[0]
mv = make_maintenance_visit(source_name=ms.name, item_name=item_code, s_id=s_item.name)
mvi = mv.purposes[0]
serial_nos = get_serial_nos_from_schedule(mvi.item_name, ms.name)
self.assertEqual(serial_nos, ["TEST001", "TEST002"])
frappe.db.rollback()
def make_serial_item_with_serial(item_code):
serial_item_doc = create_item(item_code, is_stock_item=1)
if not serial_item_doc.has_serial_no or not serial_item_doc.serial_no_series:
serial_item_doc.has_serial_no = 1
serial_item_doc.serial_no_series = "TEST.###"
serial_item_doc.save(ignore_permissions=True)
active_serials = frappe.db.get_all('Serial No', {"status": "Active", "item_code": item_code})
if len(active_serials) < 2:
make_serialized_item(item_code=item_code)
def get_events(ms): def get_events(ms):
return frappe.get_all("Event Participants", filters={ return frappe.get_all("Event Participants", filters={
@@ -87,17 +134,18 @@ def get_events(ms):
"parenttype": "Event" "parenttype": "Event"
}) })
def make_maintenance_schedule(): def make_maintenance_schedule(**args):
ms = frappe.new_doc("Maintenance Schedule") ms = frappe.new_doc("Maintenance Schedule")
ms.company = "_Test Company" ms.company = "_Test Company"
ms.customer = "_Test Customer" ms.customer = "_Test Customer"
ms.transaction_date = today() ms.transaction_date = today()
ms.append("items", { ms.append("items", {
"item_code": "_Test Item", "item_code": args.get("item_code") or "_Test Item",
"start_date": today(), "start_date": today(),
"periodicity": "Weekly", "periodicity": "Weekly",
"no_of_visits": 4, "no_of_visits": 4,
"serial_no": args.get("serial_no"),
"sales_person": "Sales Team", "sales_person": "Sales Team",
}) })
ms.insert(ignore_permissions=True) ms.insert(ignore_permissions=True)

View File

@@ -2,52 +2,54 @@
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
frappe.provide("erpnext.maintenance"); frappe.provide("erpnext.maintenance");
var serial_nos = [];
frappe.ui.form.on('Maintenance Visit', { frappe.ui.form.on('Maintenance Visit', {
refresh: function (frm) {
//filters for serial_no based on item_code
frm.set_query('serial_no', 'purposes', function (frm, cdt, cdn) {
let item = locals[cdt][cdn];
if (serial_nos) {
return {
filters: {
'item_code': item.item_code,
'name': ["in", serial_nos]
}
};
} else {
return {
filters: {
'item_code': item.item_code
}
};
}
});
},
setup: function (frm) { setup: function (frm) {
frm.set_query('contact_person', erpnext.queries.contact_query); frm.set_query('contact_person', erpnext.queries.contact_query);
frm.set_query('customer_address', erpnext.queries.address_query); frm.set_query('customer_address', erpnext.queries.address_query);
frm.set_query('customer', erpnext.queries.customer); frm.set_query('customer', erpnext.queries.customer);
}, },
onload: function (frm, cdt, cdn) { onload: function (frm) {
let item = locals[cdt][cdn]; // filters for serial no based on item code
if (frm.doc.maintenance_type === "Scheduled") { if (frm.doc.maintenance_type === "Scheduled") {
const schedule_id = item.purposes[0].prevdoc_detail_docname || frm.doc.maintenance_schedule_detail; let item_code = frm.doc.purposes[0].item_code;
frappe.call({ frappe.call({
method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.update_serial_nos", method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.get_serial_nos_from_schedule",
args: { args: {
s_id: schedule_id schedule: frm.doc.maintenance_schedule,
}, item_code: item_code
callback: function (r) {
serial_nos = r.message;
} }
}).then((r) => {
let serial_nos = r.message;
frm.set_query('serial_no', 'purposes', () => {
if (serial_nos.length > 0) {
return {
filters: {
'item_code': item_code,
'name': ["in", serial_nos]
}
};
}
return {
filters: {
'item_code': item_code
}
};
});
});
} else {
frm.set_query('serial_no', 'purposes', (frm, cdt, cdn) => {
let row = locals[cdt][cdn];
return {
filters: {
'item_code': row.item_code
}
};
}); });
} }
if (!frm.doc.status) { if (!frm.doc.status) {
frm.set_value({ status: 'Draft' }); frm.set_value({ status: 'Draft' });
} }
if (frm.doc.__islocal) { if (frm.doc.__islocal) {
frm.doc.maintenance_type == 'Unscheduled' && frm.clear_table("purposes");
frm.set_value({ mntc_date: frappe.datetime.get_today() }); frm.set_value({ mntc_date: frappe.datetime.get_today() });
} }
}, },
@@ -60,7 +62,6 @@ frappe.ui.form.on('Maintenance Visit', {
contact_person: function (frm) { contact_person: function (frm) {
erpnext.utils.get_contact_details(frm); erpnext.utils.get_contact_details(frm);
} }
}) })
// TODO commonify this code // TODO commonify this code

View File

@@ -179,8 +179,7 @@
"label": "Purposes", "label": "Purposes",
"oldfieldname": "maintenance_visit_details", "oldfieldname": "maintenance_visit_details",
"oldfieldtype": "Table", "oldfieldtype": "Table",
"options": "Maintenance Visit Purpose", "options": "Maintenance Visit Purpose"
"reqd": 1
}, },
{ {
"fieldname": "more_info", "fieldname": "more_info",
@@ -294,10 +293,11 @@
"idx": 1, "idx": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-05-27 16:06:17.352572", "modified": "2021-12-17 03:10:27.608112",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Maintenance", "module": "Maintenance",
"name": "Maintenance Visit", "name": "Maintenance Visit",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {

View File

@@ -4,7 +4,7 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import get_datetime from frappe.utils import format_date, get_datetime
from erpnext.utilities.transaction_base import TransactionBase from erpnext.utilities.transaction_base import TransactionBase
@@ -18,25 +18,34 @@ class MaintenanceVisit(TransactionBase):
if d.serial_no and not frappe.db.exists("Serial No", d.serial_no): if d.serial_no and not frappe.db.exists("Serial No", d.serial_no):
frappe.throw(_("Serial No {0} does not exist").format(d.serial_no)) frappe.throw(_("Serial No {0} does not exist").format(d.serial_no))
def validate_purpose_table(self):
if not self.purposes:
frappe.throw(_("Add Items in the Purpose Table"), title="Purposes Required")
def validate_maintenance_date(self): def validate_maintenance_date(self):
if self.maintenance_type == "Scheduled" and self.maintenance_schedule_detail: if self.maintenance_type == "Scheduled" and self.maintenance_schedule_detail:
item_ref = frappe.db.get_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'item_reference') item_ref = frappe.db.get_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'item_reference')
if item_ref: if item_ref:
start_date, end_date = frappe.db.get_value('Maintenance Schedule Item', item_ref, ['start_date', 'end_date']) start_date, end_date = frappe.db.get_value('Maintenance Schedule Item', item_ref, ['start_date', 'end_date'])
if get_datetime(self.mntc_date) < get_datetime(start_date) or get_datetime(self.mntc_date) > get_datetime(end_date): if get_datetime(self.mntc_date) < get_datetime(start_date) or get_datetime(self.mntc_date) > get_datetime(end_date):
frappe.throw(_("Date must be between {0} and {1}").format(start_date, end_date)) frappe.throw(_("Date must be between {0} and {1}")
.format(format_date(start_date), format_date(end_date)))
def validate(self): def validate(self):
self.validate_serial_no() self.validate_serial_no()
self.validate_maintenance_date() self.validate_maintenance_date()
self.validate_purpose_table()
def update_completion_status(self): def update_status_and_actual_date(self, cancel=False):
status = "Pending"
actual_date = None
if not cancel:
status = self.completion_status
actual_date = self.mntc_date
if self.maintenance_schedule_detail: if self.maintenance_schedule_detail:
frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'completion_status', self.completion_status) frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'completion_status', status)
frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'actual_date', actual_date)
def update_actual_date(self):
if self.maintenance_schedule_detail:
frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'actual_date', self.mntc_date)
def update_customer_issue(self, flag): def update_customer_issue(self, flag):
if not self.maintenance_schedule: if not self.maintenance_schedule:
@@ -97,12 +106,12 @@ class MaintenanceVisit(TransactionBase):
def on_submit(self): def on_submit(self):
self.update_customer_issue(1) self.update_customer_issue(1)
frappe.db.set(self, 'status', 'Submitted') frappe.db.set(self, 'status', 'Submitted')
self.update_completion_status() self.update_status_and_actual_date()
self.update_actual_date()
def on_cancel(self): def on_cancel(self):
self.check_if_last_visit() self.check_if_last_visit()
frappe.db.set(self, 'status', 'Cancelled') frappe.db.set(self, 'status', 'Cancelled')
self.update_status_and_actual_date(cancel=True)
def on_update(self): def on_update(self):
pass pass

View File

@@ -531,16 +531,6 @@ class BOM(WebsiteGenerator):
row.hour_rate = (hour_rate / flt(self.conversion_rate) row.hour_rate = (hour_rate / flt(self.conversion_rate)
if self.conversion_rate and hour_rate else hour_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: if row.hour_rate and row.time_in_mins:
row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate) 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 row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0

View File

@@ -948,11 +948,8 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company):
locations = get_available_item_locations(item.get("item_code"), locations = get_available_item_locations(item.get("item_code"),
warehouses, item.get("quantity"), company, ignore_validation=True) warehouses, item.get("quantity"), company, ignore_validation=True)
if not locations:
new_mr_items.append(item)
return
required_qty = item.get("quantity") required_qty = item.get("quantity")
# get available material by transferring to production warehouse
for d in locations: for d in locations:
if required_qty <=0: return if required_qty <=0: return
@@ -963,14 +960,34 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company):
new_dict.update({ new_dict.update({
"quantity": quantity, "quantity": quantity,
"material_request_type": "Material Transfer", "material_request_type": "Material Transfer",
"uom": new_dict.get("stock_uom"), # internal transfer should be in stock UOM
"from_warehouse": d.get("warehouse") "from_warehouse": d.get("warehouse")
}) })
required_qty -= quantity required_qty -= quantity
new_mr_items.append(new_dict) new_mr_items.append(new_dict)
# raise purchase request for remaining qty
if required_qty: if required_qty:
stock_uom, purchase_uom = frappe.db.get_value(
'Item',
item['item_code'],
['stock_uom', 'purchase_uom']
)
if purchase_uom != stock_uom and purchase_uom == item['uom']:
conversion_factor = get_uom_conversion_factor(item['item_code'], item['uom'])
if not (conversion_factor or frappe.flags.show_qty_in_stock_uom):
frappe.throw(_("UOM Conversion factor ({0} -> {1}) not found for item: {2}")
.format(purchase_uom, stock_uom, item['item_code']))
required_qty = required_qty / conversion_factor
if frappe.db.get_value("UOM", purchase_uom, "must_be_whole_number"):
required_qty = ceil(required_qty)
item["quantity"] = required_qty item["quantity"] = required_qty
new_mr_items.append(item) new_mr_items.append(item)
@frappe.whitelist() @frappe.whitelist()

View File

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

View File

@@ -2,7 +2,7 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import frappe 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.job_card.job_card import JobCardCancelError
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom 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, OverProductionError,
StockOverProductionError, StockOverProductionError,
close_work_order, close_work_order,
make_job_card,
make_stock_entry, make_stock_entry,
stop_unstop, stop_unstop,
) )
@@ -801,6 +802,34 @@ class TestWorkOrder(ERPNextTestCase):
if row.is_scrap_item: if row.is_scrap_item:
self.assertEqual(row.qty, 1) 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): def test_close_work_order(self):
items = ['Test FG Item for Closed WO', 'Test RM Item 1 for Closed WO', items = ['Test FG Item for Closed WO', 'Test RM Item 1 for Closed WO',
'Test RM Item 2 for Closed WO'] 'Test RM Item 2 for Closed WO']
@@ -841,7 +870,9 @@ class TestWorkOrder(ERPNextTestCase):
close_work_order(wo_order, "Closed") close_work_order(wo_order, "Closed")
self.assertEqual(wo_order.get('status'), "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 = frappe.get_doc('Job Card', job_card)
job_card_doc.set('scrap_items', [ 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', { job_card_doc.append('time_logs', {
'from_time': now(), 'from_time': now(),
'employee': employee,
'time_in_mins': 60, 'time_in_mins': 60,
'completed_qty': job_card_doc.for_quantity 'completed_qty': job_card_doc.for_quantity
}) })
job_card_doc.submit() job_card_doc.submit()
def get_scrap_item_details(bom_no): def get_scrap_item_details(bom_no):
scrap_items = {} scrap_items = {}
for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item` 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); erpnext.work_order.set_custom_buttons(frm);
frm.set_intro(""); 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.")); 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.status != "Closed") {
if (frm.doc.docstatus===1) {
frm.trigger('show_progress_for_items');
frm.trigger('show_progress_for_operations');
}
if (frm.doc.docstatus === 1 if (frm.doc.docstatus === 1
&& frm.doc.operations && frm.doc.operations.length) { && frm.doc.operations && frm.doc.operations.length) {

View File

@@ -4,6 +4,39 @@
frappe.query_reports["BOM Operations Time"] = { frappe.query_reports["BOM Operations Time"] = {
"filters": [ "filters": [
{
"fieldname": "item_code",
"label": __("Item Code"),
"fieldtype": "Link",
"width": "100",
"options": "Item",
"get_query": () =>{
return {
filters: { "disabled": 0, "is_stock_item": 1 }
}
}
},
{
"fieldname": "bom_id",
"label": __("BOM ID"),
"fieldtype": "MultiSelectList",
"width": "100",
"options": "BOM",
"get_data": function(txt) {
return frappe.db.get_link_options("BOM", txt);
},
"get_query": () =>{
return {
filters: { "docstatus": 1, "is_active": 1, "with_operations": 1 }
}
}
},
{
"fieldname": "workstation",
"label": __("Workstation"),
"fieldtype": "Link",
"width": "100",
"options": "Workstation"
},
] ]
}; };

View File

@@ -1,14 +1,16 @@
{ {
"add_total_row": 0, "add_total_row": 1,
"columns": [],
"creation": "2020-03-03 01:41:20.862521", "creation": "2020-03-03 01:41:20.862521",
"disable_prepared_report": 0, "disable_prepared_report": 0,
"disabled": 0, "disabled": 0,
"docstatus": 0, "docstatus": 0,
"doctype": "Report", "doctype": "Report",
"filters": [],
"idx": 0, "idx": 0,
"is_standard": "Yes", "is_standard": "Yes",
"letter_head": "", "letter_head": "",
"modified": "2020-03-03 01:41:20.862521", "modified": "2022-01-20 14:21:47.771591",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM Operations Time", "name": "BOM Operations Time",

View File

@@ -12,19 +12,15 @@ def execute(filters=None):
return columns, data return columns, data
def get_data(filters): def get_data(filters):
data = [] bom_wise_data = {}
bom_data, report_data = [], []
bom_data = [] bom_operation_data = get_filtered_data(filters)
for d in frappe.db.sql("""
SELECT for d in bom_operation_data:
bom.name, bom.item, bom.item_name, bom.uom,
bomps.operation, bomps.workstation, bomps.time_in_mins
FROM `tabBOM` bom, `tabBOM Operation` bomps
WHERE
bom.docstatus = 1 and bom.is_active = 1 and bom.name = bomps.parent
""", as_dict=1):
row = get_args() row = get_args()
if d.name not in bom_data: if d.name not in bom_data:
bom_wise_data[d.name] = []
bom_data.append(d.name) bom_data.append(d.name)
row.update(d) row.update(d)
else: else:
@@ -34,14 +30,49 @@ def get_data(filters):
"time_in_mins": d.time_in_mins "time_in_mins": d.time_in_mins
}) })
data.append(row) # maintain BOM wise data for grouping such as:
# {"BOM A": [{Row1}, {Row2}], "BOM B": ...}
bom_wise_data[d.name].append(row)
used_as_subassembly_items = get_bom_count(bom_data) used_as_subassembly_items = get_bom_count(bom_data)
for d in data: for d in bom_wise_data:
d.used_as_subassembly_items = used_as_subassembly_items.get(d.name, 0) for row in bom_wise_data[d]:
row.used_as_subassembly_items = used_as_subassembly_items.get(row.name, 0)
report_data.append(row)
return data return report_data
def get_filtered_data(filters):
bom = frappe.qb.DocType("BOM")
bom_ops = frappe.qb.DocType("BOM Operation")
bom_ops_query = (
frappe.qb.from_(bom)
.join(bom_ops).on(bom.name == bom_ops.parent)
.select(
bom.name, bom.item, bom.item_name, bom.uom,
bom_ops.operation, bom_ops.workstation, bom_ops.time_in_mins
).where(
(bom.docstatus == 1)
& (bom.is_active == 1)
)
)
if filters.get("item_code"):
bom_ops_query = bom_ops_query.where(bom.item == filters.get("item_code"))
if filters.get("bom_id"):
bom_ops_query = bom_ops_query.where(bom.name.isin(filters.get("bom_id")))
if filters.get("workstation"):
bom_ops_query = bom_ops_query.where(
bom_ops.workstation == filters.get("workstation")
)
bom_operation_data = bom_ops_query.run(as_dict=True)
return bom_operation_data
def get_bom_count(bom_data): def get_bom_count(bom_data):
data = frappe.get_all("BOM Item", data = frappe.get_all("BOM Item",
@@ -68,13 +99,13 @@ def get_columns(filters):
"options": "BOM", "options": "BOM",
"fieldname": "name", "fieldname": "name",
"fieldtype": "Link", "fieldtype": "Link",
"width": 140 "width": 220
}, { }, {
"label": _("BOM Item Code"), "label": _("Item Code"),
"options": "Item", "options": "Item",
"fieldname": "item", "fieldname": "item",
"fieldtype": "Link", "fieldtype": "Link",
"width": 140 "width": 150
}, { }, {
"label": _("Item Name"), "label": _("Item Name"),
"fieldname": "item_name", "fieldname": "item_name",
@@ -85,13 +116,13 @@ def get_columns(filters):
"options": "UOM", "options": "UOM",
"fieldname": "uom", "fieldname": "uom",
"fieldtype": "Link", "fieldtype": "Link",
"width": 140 "width": 100
}, { }, {
"label": _("Operation"), "label": _("Operation"),
"options": "Operation", "options": "Operation",
"fieldname": "operation", "fieldname": "operation",
"fieldtype": "Link", "fieldtype": "Link",
"width": 120 "width": 140
}, { }, {
"label": _("Workstation"), "label": _("Workstation"),
"options": "Workstation", "options": "Workstation",
@@ -101,11 +132,11 @@ def get_columns(filters):
}, { }, {
"label": _("Time (In Mins)"), "label": _("Time (In Mins)"),
"fieldname": "time_in_mins", "fieldname": "time_in_mins",
"fieldtype": "Int", "fieldtype": "Float",
"width": 140 "width": 120
}, { }, {
"label": _("Sub-assembly BOM Count"), "label": _("Sub-assembly BOM Count"),
"fieldname": "used_as_subassembly_items", "fieldname": "used_as_subassembly_items",
"fieldtype": "Int", "fieldtype": "Int",
"width": 180 "width": 200
}] }]

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.set_default_payroll_based_on
erpnext.patches.v12_0.repost_stock_ledger_entries_for_target_warehouse 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.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.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.fix_quotation_expired_status
erpnext.patches.v12_0.update_appointment_reminder_scheduler_entry 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.add_custom_field_for_south_africa #2
erpnext.patches.v13_0.rename_discharge_ordered_date_in_ip_record erpnext.patches.v13_0.rename_discharge_ordered_date_in_ip_record
erpnext.patches.v13_0.remove_bad_selling_defaults 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.migrate_stripe_api
erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries
execute:frappe.reload_doc("erpnext_integrations", "doctype", "TaxJar Settings") execute:frappe.reload_doc("erpnext_integrations", "doctype", "TaxJar Settings")
@@ -331,12 +331,17 @@ erpnext.patches.v13_0.enable_scheduler_job_for_item_reposting
erpnext.patches.v13_0.requeue_failed_reposts erpnext.patches.v13_0.requeue_failed_reposts
erpnext.patches.v13_0.fetch_thumbnail_in_website_items erpnext.patches.v13_0.fetch_thumbnail_in_website_items
erpnext.patches.v13_0.update_job_card_status erpnext.patches.v13_0.update_job_card_status
erpnext.patches.v13_0.enable_uoms
erpnext.patches.v12_0.update_production_plan_status erpnext.patches.v12_0.update_production_plan_status
erpnext.patches.v13_0.item_naming_series_not_mandatory erpnext.patches.v13_0.item_naming_series_not_mandatory
erpnext.patches.v13_0.update_category_in_ltds_certificate erpnext.patches.v13_0.update_category_in_ltds_certificate
erpnext.patches.v13_0.create_ksa_vat_custom_fields erpnext.patches.v13_0.create_ksa_vat_custom_fields
erpnext.patches.v13_0.rename_ksa_qr_field erpnext.patches.v13_0.rename_ksa_qr_field
erpnext.patches.v13_0.wipe_serial_no_field_for_0_qty
erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021 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.update_tax_category_for_rcm
erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template
erpnext.patches.v13_0.agriculture_deprecation_warning erpnext.patches.v13_0.agriculture_deprecation_warning
erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit
erpnext.patches.v13_0.hospitality_deprecation_warning
erpnext.patches.v13_0.delete_bank_reconciliation_detail

View File

@@ -0,0 +1,13 @@
# Copyright (c) 2019, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
def execute():
if frappe.db.exists('DocType', 'Bank Reconciliation Detail') and \
frappe.db.exists('DocType', 'Bank Clearance Detail'):
frappe.delete_doc("DocType", 'Bank Reconciliation Detail', force=1)

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,10 @@
import click
def execute():
click.secho(
"Hospitality 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/hospitality",
fg="yellow",
)

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

@@ -0,0 +1,24 @@
import frappe
def execute():
frappe.reload_doc("maintenance", "doctype", "maintenance_visit")
# Updates the Maintenance Schedule link to fetch serial nos
from frappe.query_builder.functions import Coalesce
mvp = frappe.qb.DocType('Maintenance Visit Purpose')
mv = frappe.qb.DocType('Maintenance Visit')
frappe.qb.update(
mv
).join(
mvp
).on(mvp.parent == mv.name).set(
mv.maintenance_schedule,
Coalesce(mvp.prevdoc_docname, '')
).where(
(mv.maintenance_type == "Scheduled")
& (mvp.prevdoc_docname.notnull())
& (mv.docstatus < 2)
).run(as_dict=1)

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

@@ -0,0 +1,18 @@
import frappe
def execute():
doctype = "Stock Reconciliation Item"
if not frappe.db.has_column(doctype, "current_serial_no"):
# nothing to fix if column doesn't exist
return
sr_item = frappe.qb.DocType(doctype)
(frappe.qb
.update(sr_item)
.set(sr_item.current_serial_no, None)
.where(sr_item.current_qty == 0)
).run()

View File

@@ -60,6 +60,8 @@ class PayrollEntry(Document):
def on_cancel(self): def on_cancel(self):
frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip` frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip`
where payroll_entry=%s """, (self.name))) 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): def get_emp_list(self):
""" """

View File

@@ -1146,15 +1146,17 @@ class SalarySlip(TransactionBase):
}) })
def make_loan_repayment_entry(self): def make_loan_repayment_entry(self):
payroll_payable_account = get_payroll_payable_account(self.company, self.payroll_entry)
for loan in self.loans: for loan in self.loans:
repayment_entry = create_repayment_entry(loan.loan, self.employee, if loan.total_payment:
self.company, self.posting_date, loan.loan_type, "Regular Payment", loan.interest_amount, repayment_entry = create_repayment_entry(loan.loan, self.employee,
loan.principal_amount, loan.total_payment) 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.save()
repayment_entry.submit() 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): def cancel_loan_repayment_entry(self):
for loan in self.loans: for loan in self.loans:
@@ -1388,3 +1390,11 @@ def get_salary_component_data(component):
], ],
as_dict=1, 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', make_salary_structure("Test Loan Repayment Salary Structure", "Monthly", employee=applicant, currency='INR',
payroll_period=payroll_period) 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 = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1))
loan.repay_from_salary = 1 loan.repay_from_salary = 1
loan.submit() loan.submit()

View File

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

View File

@@ -5,7 +5,7 @@ import datetime
import unittest import unittest
import frappe import frappe
from frappe.utils import add_months, now_datetime, nowdate from frappe.utils import add_months, add_to_date, now_datetime, nowdate
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.employee.test_employee import make_employee
@@ -151,6 +151,27 @@ class TestTimesheet(unittest.TestCase):
settings.ignore_employee_time_overlap = initial_setting settings.ignore_employee_time_overlap = initial_setting
settings.save() settings.save()
def test_to_time(self):
emp = make_employee("test_employee_6@salary.com")
from_time = now_datetime()
timesheet = frappe.new_doc("Timesheet")
timesheet.employee = emp
timesheet.append(
'time_logs',
{
"billable": 1,
"activity_type": "_Test Activity Type",
"from_time": from_time,
"hours": 2,
"company": "_Test Company"
}
)
timesheet.save()
to_time = timesheet.time_logs[0].to_time
self.assertEqual(to_time, add_to_date(from_time, hours=2, as_datetime=True))
def make_salary_structure_for_timesheet(employee, company=None): def make_salary_structure_for_timesheet(employee, company=None):
salary_structure_name = "Timesheet Salary Structure Test" salary_structure_name = "Timesheet Salary Structure Test"

View File

@@ -7,7 +7,7 @@ import json
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import flt, getdate, time_diff_in_hours from frappe.utils import add_to_date, flt, getdate, time_diff_in_hours
from erpnext.controllers.queries import get_match_cond from erpnext.controllers.queries import get_match_cond
from erpnext.hr.utils import validate_active_employee from erpnext.hr.utils import validate_active_employee
@@ -136,10 +136,19 @@ class Timesheet(Document):
def validate_time_logs(self): def validate_time_logs(self):
for data in self.get('time_logs'): for data in self.get('time_logs'):
self.set_to_time(data)
self.validate_overlap(data) self.validate_overlap(data)
self.set_project(data) self.set_project(data)
self.validate_project(data) self.validate_project(data)
def set_to_time(self, data):
if not (data.from_time and data.hours):
return
_to_time = add_to_date(data.from_time, hours=data.hours, as_datetime=True)
if data.to_time != _to_time:
data.to_time = _to_time
def validate_overlap(self, data): def validate_overlap(self, data):
settings = frappe.get_single('Projects Settings') settings = frappe.get_single('Projects Settings')
self.validate_overlap_for("user", data, self.user, settings.ignore_user_time_overlap) self.validate_overlap_for("user", data, self.user, settings.ignore_user_time_overlap)

View File

@@ -114,6 +114,8 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
if ((!item.qty) && me.frm.doc.is_return) { if ((!item.qty) && me.frm.doc.is_return) {
item.amount = flt(item.rate * -1, precision("amount", item)); 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 { } else {
item.amount = flt(item.rate * item.qty, precision("amount", item)); 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"]); 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)) { 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) { if(this.frm.doc.party_account_currency == this.frm.doc.currency) {
var total_amount_to_pay = flt((grand_total - this.frm.doc.total_advance var total_amount_to_pay = flt((grand_total - this.frm.doc.total_advance
- this.frm.doc.write_off_amount), precision("grand_total")); - this.frm.doc.write_off_amount), precision("grand_total"));
} else { } else {
var total_amount_to_pay = flt( 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), - this.frm.doc.total_advance - this.frm.doc.base_write_off_amount),
precision("base_grand_total") precision("base_grand_total")
); );
@@ -746,14 +749,15 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
}, },
set_total_amount_to_default_mop: function() { 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) { if(this.frm.doc.party_account_currency == this.frm.doc.currency) {
var total_amount_to_pay = flt((grand_total - this.frm.doc.total_advance var total_amount_to_pay = flt((grand_total - this.frm.doc.total_advance
- this.frm.doc.write_off_amount), precision("grand_total")); - this.frm.doc.write_off_amount), precision("grand_total"));
} else { } else {
var total_amount_to_pay = flt( 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), - this.frm.doc.total_advance - this.frm.doc.base_write_off_amount),
precision("base_grand_total") precision("base_grand_total")
); );

View File

@@ -590,7 +590,6 @@ body.product-page {
top: -10px; top: -10px;
left: -12px; left: -12px;
background: var(--red-600); background: var(--red-600);
width: 16px;
align-items: center; align-items: center;
height: 16px; height: 16px;
font-size: 10px; font-size: 10px;

View File

@@ -53,7 +53,8 @@ frappe.query_reports["GSTR-1"] = {
{ "value": "CDNR-REG", "label": __("Credit/Debit Notes (Registered) - 9B") }, { "value": "CDNR-REG", "label": __("Credit/Debit Notes (Registered) - 9B") },
{ "value": "CDNR-UNREG", "label": __("Credit/Debit Notes (Unregistered) - 9B") }, { "value": "CDNR-UNREG", "label": __("Credit/Debit Notes (Unregistered) - 9B") },
{ "value": "EXPORT", "label": __("Export Invoice - 6A") }, { "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" "default": "B2B"
} }

View File

@@ -41,7 +41,8 @@ class Gstr1Report(object):
port_code, port_code,
shipping_bill_number, shipping_bill_number,
shipping_bill_date, shipping_bill_date,
reason_for_issuing_document reason_for_issuing_document,
company_gstin
""" """
def run(self): def run(self):
@@ -63,6 +64,8 @@ class Gstr1Report(object):
self.get_b2c_data() self.get_b2c_data()
elif self.filters.get("type_of_business") == "Advances": elif self.filters.get("type_of_business") == "Advances":
self.get_advance_data() self.get_advance_data()
elif self.filters.get("type_of_business") == "NIL Rated":
self.get_nil_rated_invoices()
elif self.invoices: elif self.invoices:
for inv, items_based_on_rate in self.items_based_on_tax_rate.items(): for inv, items_based_on_rate in self.items_based_on_tax_rate.items():
invoice_details = self.invoices.get(inv) invoice_details = self.invoices.get(inv)
@@ -92,6 +95,57 @@ class Gstr1Report(object):
row= [key[0], key[1], value[0], value[1]] row= [key[0], key[1], value[0], value[1]]
self.data.append(row) 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): def get_b2c_data(self):
b2cs_output = {} b2cs_output = {}
@@ -241,10 +295,11 @@ class Gstr1Report(object):
def get_invoice_items(self): def get_invoice_items(self):
self.invoice_items = frappe._dict() self.invoice_items = frappe._dict()
self.item_tax_rate = frappe._dict() self.item_tax_rate = frappe._dict()
self.nil_exempt_non_gst = {}
items = frappe.db.sql(""" items = frappe.db.sql("""
select item_code, parent, taxable_value, base_net_amount, item_tax_rate select item_code, parent, taxable_value, base_net_amount, item_tax_rate, is_nil_exempt,
from `tab%s Item` is_non_gst from `tab%s Item`
where parent in (%s) where parent in (%s)
""" % (self.doctype, ', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1) """ % (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 = self.item_tax_rate.setdefault(d.parent, {}).setdefault(d.item_code, [])
tax_rate_dict.append(rate) 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): def get_items_based_on_tax_rate(self):
self.tax_details = frappe.db.sql(""" self.tax_details = frappe.db.sql("""
select select
@@ -323,21 +388,24 @@ class Gstr1Report(object):
self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys()) self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys())
def get_columns(self): 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.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": if self.filters.get("type_of_business") == "B2B":
self.invoice_columns = [ self.invoice_columns = [
@@ -706,6 +774,33 @@ class Gstr1Report(object):
"width": 100 "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 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) out = get_advances_json(res, gstin)
gst_json["at"] = out 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 { return {
'report_name': report_name, 'report_name': report_name,
'report_type': filters['type_of_business'], 'report_type': filters['type_of_business'],
@@ -981,6 +1081,36 @@ def get_cdnr_unreg_json(res, gstin):
return out 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): def get_invoice_type_for_cdnr(row):
if row.get('gst_category') == 'SEZ': if row.get('gst_category') == 'SEZ':
if row.get('export_type') == 'WPAY': if row.get('export_type') == 'WPAY':
@@ -1065,3 +1195,9 @@ def download_json_file():
frappe.response['filecontent'] = data['data'] frappe.response['filecontent'] = data['data']
frappe.response['content_type'] = 'application/json' frappe.response['content_type'] = 'application/json'
frappe.response['type'] = 'download' 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", "fieldname": "title",
"label": _("Title"), "label": _("Title"),
"fieldtype": "Data", "fieldtype": "Data",
"width": 300 "width": 300,
}, },
{ {
"fieldname": "amount", "fieldname": "amount",
"label": _("Amount (SAR)"), "label": _("Amount (SAR)"),
"fieldtype": "Currency", "fieldtype": "Currency",
"options": "currency",
"width": 150, "width": 150,
}, },
{ {
"fieldname": "adjustment_amount", "fieldname": "adjustment_amount",
"label": _("Adjustment (SAR)"), "label": _("Adjustment (SAR)"),
"fieldtype": "Currency", "fieldtype": "Currency",
"options": "currency",
"width": 150, "width": 150,
}, },
{ {
"fieldname": "vat_amount", "fieldname": "vat_amount",
"label": _("VAT Amount (SAR)"), "label": _("VAT Amount (SAR)"),
"fieldtype": "Currency", "fieldtype": "Currency",
"options": "currency",
"width": 150, "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 # Validate if vat settings exist
company = filters.get('company') company = filters.get('company')
company_currency = frappe.get_cached_value('Company', company, "default_currency")
if frappe.db.exists('KSA VAT Setting', company) is None: if frappe.db.exists('KSA VAT Setting', company) is None:
url = get_url_to_list('KSA VAT Setting') url = get_url_to_list('KSA VAT Setting')
frappe.msgprint(_('Create <a href="{}">KSA VAT Setting</a> for this company').format(url)) 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) ksa_vat_setting = frappe.get_doc('KSA VAT Setting', company)
# Sales Heading # Sales Heading
append_data(data, 'VAT on Sales', '', '', '') append_data(data, 'VAT on Sales', '', '', '', company_currency)
grand_total_taxable_amount = 0 grand_total_taxable_amount = 0
grand_total_taxable_adjustment_amount = 0 grand_total_taxable_adjustment_amount = 0
@@ -67,7 +79,7 @@ def get_data(filters):
# Adding results to data # Adding results to data
append_data(data, vat_setting.title, total_taxable_amount, 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_amount += total_taxable_amount
grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount
@@ -75,13 +87,13 @@ def get_data(filters):
# Sales Grand Total # Sales Grand Total
append_data(data, 'Grand Total', grand_total_taxable_amount, 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 # Blank Line
append_data(data, '', '', '', '') append_data(data, '', '', '', '', company_currency)
# Purchase Heading # Purchase Heading
append_data(data, 'VAT on Purchases', '', '', '') append_data(data, 'VAT on Purchases', '', '', '', company_currency)
grand_total_taxable_amount = 0 grand_total_taxable_amount = 0
grand_total_taxable_adjustment_amount = 0 grand_total_taxable_adjustment_amount = 0
@@ -93,7 +105,7 @@ def get_data(filters):
# Adding results to data # Adding results to data
append_data(data, vat_setting.title, total_taxable_amount, 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_amount += total_taxable_amount
grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount
@@ -101,7 +113,7 @@ def get_data(filters):
# Purchase Grand Total # Purchase Grand Total
append_data(data, 'Grand Total', grand_total_taxable_amount, 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 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.""" """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): def get_tax_amount(item_code, account_head, doctype, parent):
if doctype == 'Sales Invoice': if doctype == 'Sales Invoice':

View File

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

View File

@@ -213,6 +213,9 @@ erpnext.company.setup_queries = function(frm) {
["default_payroll_payable_account", {"root_type": "Liability"}], ["default_payroll_payable_account", {"root_type": "Liability"}],
["round_off_account", {"root_type": "Expense"}], ["round_off_account", {"root_type": "Expense"}],
["write_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", {}], ["default_discount_account", {}],
["discount_allowed_account", {"root_type": "Expense"}], ["discount_allowed_account", {"root_type": "Expense"}],
["discount_received_account", {"root_type": "Income"}], ["discount_received_account", {"root_type": "Income"}],

View File

@@ -350,7 +350,8 @@ def add_uom_data():
"doctype": "UOM", "doctype": "UOM",
"uom_name": _(d.get("uom_name")), "uom_name": _(d.get("uom_name")),
"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() }).db_insert()
# bootstrap uom conversion factors # 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) join `tabStock Ledger Entry` ignore index (item_code, warehouse)
on (`tabBatch`.batch_id = `tabStock Ledger Entry`.batch_no ) on (`tabBatch`.batch_id = `tabStock Ledger Entry`.batch_no )
where `tabStock Ledger Entry`.item_code = %s and `tabStock Ledger Entry`.warehouse = %s 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} and (`tabBatch`.expiry_date >= CURDATE() or `tabBatch`.expiry_date IS NULL) {0}
group by batch_id group by batch_id
order by `tabBatch`.expiry_date ASC, `tabBatch`.creation ASC order by `tabBatch`.expiry_date ASC, `tabBatch`.creation ASC

View File

@@ -14,6 +14,7 @@ from erpnext.controllers.accounts_controller import get_taxes_and_charges
from erpnext.controllers.selling_controller import SellingController from erpnext.controllers.selling_controller import SellingController
from erpnext.stock.doctype.batch.batch import set_batch_nos from erpnext.stock.doctype.batch.batch import set_batch_nos
from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no
from erpnext.stock.utils import calculate_mapped_packed_items_return
form_grid_templates = { form_grid_templates = {
"items": "templates/form_grid/item_grid.html" "items": "templates/form_grid/item_grid.html"
@@ -128,8 +129,12 @@ class DeliveryNote(SellingController):
self.validate_uom_is_integer("uom", "qty") self.validate_uom_is_integer("uom", "qty")
self.validate_with_previous_doc() self.validate_with_previous_doc()
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list # Keeps mapped packed_items in case product bundle is updated.
make_packing_list(self) if self.is_return and self.return_against:
calculate_mapped_packed_items_return(self)
else:
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
make_packing_list(self)
if self._action != 'submit' and not self.is_return: if self._action != 'submit' and not self.is_return:
set_batch_nos(self, 'warehouse', throw=True) set_batch_nos(self, 'warehouse', throw=True)

View File

@@ -386,8 +386,7 @@ class TestDeliveryNote(ERPNextTestCase):
self.assertEqual(actual_qty, 25) self.assertEqual(actual_qty, 25)
# return bundled item # return bundled item
dn1 = create_delivery_note(item_code='_Test Product Bundle Item', is_return=1, dn1 = create_return_delivery_note(source_name=dn.name, rate=500, qty=-2)
return_against=dn.name, qty=-2, rate=500, company=company, warehouse="Stores - TCP1", expense_account="Cost of Goods Sold - TCP1", cost_center="Main - TCP1")
# qty after return # qty after return
actual_qty = get_qty_after_transaction(warehouse="Stores - TCP1") actual_qty = get_qty_after_transaction(warehouse="Stores - TCP1")
@@ -823,6 +822,15 @@ class TestDeliveryNote(ERPNextTestCase):
automatically_fetch_payment_terms(enable=0) automatically_fetch_payment_terms(enable=0)
def create_return_delivery_note(**args):
args = frappe._dict(args)
from erpnext.controllers.sales_and_purchase_return import make_return_doc
doc = make_return_doc("Delivery Note", args.source_name, None)
doc.items[0].rate = args.rate
doc.items[0].qty = args.qty
doc.submit()
return doc
def create_delivery_note(**args): def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note") dn = frappe.new_doc("Delivery Note")
args = frappe._dict(args) args = frappe._dict(args)

View File

@@ -1,7 +1,7 @@
{ {
"actions": [], "actions": [],
"autoname": "REPOST-ITEM-VAL-.######", "autoname": "REPOST-ITEM-VAL-.######",
"creation": "2020-10-22 22:27:07.742161", "creation": "2022-01-11 15:03:38.273179",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
@@ -129,7 +129,7 @@
"reqd": 1 "reqd": 1
}, },
{ {
"default": "0", "default": "1",
"fieldname": "allow_negative_stock", "fieldname": "allow_negative_stock",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Allow Negative Stock" "label": "Allow Negative Stock"
@@ -177,7 +177,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-11-24 02:18:10.524560", "modified": "2022-01-18 10:57:33.450907",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Repost Item Valuation", "name": "Repost Item Valuation",
@@ -227,5 +227,6 @@
} }
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC" "sort_order": "DESC",
} "states": []
}

View File

@@ -27,8 +27,7 @@ class RepostItemValuation(Document):
self.item_code = None self.item_code = None
self.warehouse = None self.warehouse = None
self.allow_negative_stock = self.allow_negative_stock or \ self.allow_negative_stock = 1
cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
def set_company(self): def set_company(self):
if self.based_on == "Transaction": if self.based_on == "Transaction":

View File

@@ -421,10 +421,16 @@ def update_serial_nos(sle, item_det):
def get_auto_serial_nos(serial_no_series, qty): def get_auto_serial_nos(serial_no_series, qty):
serial_nos = [] serial_nos = []
for i in range(cint(qty)): 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) 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): def auto_make_serial_nos(args):
serial_nos = get_serial_nos(args.get('serial_no')) serial_nos = get_serial_nos(args.get('serial_no'))
created_numbers = [] created_numbers = []

View File

@@ -8,8 +8,10 @@
import frappe import frappe
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note 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.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.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.stock_entry.test_stock_entry import make_serialized_item
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
@@ -21,6 +23,10 @@ from erpnext.tests.utils import ERPNextTestCase
class TestSerialNo(ERPNextTestCase): class TestSerialNo(ERPNextTestCase):
def tearDown(self):
frappe.db.rollback()
def test_cannot_create_direct(self): def test_cannot_create_direct(self):
frappe.delete_doc_if_exists("Serial No", "_TCSER0001") 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.warehouse, "_Test Warehouse - _TC")
self.assertEqual(sn_doc.purchase_document_no, se.name) 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): def test_serial_no_sanitation(self):
"Test if Serial No input is sanitised before entering the DB." "Test if Serial No input is sanitised before entering the DB."
item_code = "_Test Serialized Item" 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") 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 import frappe
from frappe import _ from frappe import _
from frappe.model.mapper import get_mapped_doc 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 frappe.utils import cint, comma_or, cstr, flt, format_time, formatdate, getdate, nowdate
from six import iteritems, itervalues, string_types from six import iteritems, itervalues, string_types
@@ -86,8 +87,11 @@ class StockEntry(StockController):
self.validate_warehouse() self.validate_warehouse()
self.validate_work_order() self.validate_work_order()
self.validate_bom() 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_with_material_request()
self.validate_batch() self.validate_batch()
self.validate_inspection() self.validate_inspection()
@@ -110,8 +114,12 @@ class StockEntry(StockController):
self.set_actual_qty() self.set_actual_qty()
self.calculate_rate_and_amount() self.calculate_rate_and_amount()
self.validate_putaway_capacity() 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): def on_submit(self):
self.update_stock_ledger() self.update_stock_ledger()
@@ -702,26 +710,25 @@ class StockEntry(StockController):
validate_bom_no(item_code, d.bom_no) validate_bom_no(item_code, d.bom_no)
def mark_finished_and_scrap_items(self): 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)]):
if any([d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]): return
return
finished_item = self.get_finished_item() finished_item = self.get_finished_item()
if not finished_item and self.purpose == "Manufacture": if not finished_item and self.purpose == "Manufacture":
# In case of independent Manufacture entry, don't auto set # In case of independent Manufacture entry, don't auto set
# user must decide and set # user must decide and set
return return
for d in self.items: for d in self.items:
if d.t_warehouse and not d.s_warehouse: if d.t_warehouse and not d.s_warehouse:
if self.purpose=="Repack" or d.item_code == finished_item: if self.purpose=="Repack" or d.item_code == finished_item:
d.is_finished_item = 1 d.is_finished_item = 1
else:
d.is_scrap_item = 1
else: else:
d.is_finished_item = 0 d.is_scrap_item = 1
d.is_scrap_item = 0 else:
d.is_finished_item = 0
d.is_scrap_item = 0
def get_finished_item(self): def get_finished_item(self):
finished_item = None finished_item = None
@@ -734,9 +741,9 @@ class StockEntry(StockController):
def validate_finished_goods(self): def validate_finished_goods(self):
""" """
1. Check if FG exists 1. Check if FG exists (mfg, repack)
2. Check if Multiple FG Items are present 2. Check if Multiple FG Items are present (mfg)
3. Check FG Item and Qty against WO if present 3. Check FG Item and Qty against WO if present (mfg)
""" """
production_item, wo_qty, finished_items = None, 0, [] production_item, wo_qty, finished_items = None, 0, []
@@ -749,8 +756,9 @@ class StockEntry(StockController):
for d in self.get('items'): for d in self.get('items'):
if d.is_finished_item: if d.is_finished_item:
if not self.work_order: if not self.work_order:
# Independent MFG Entry/ Repack Entry, no WO to match against
finished_items.append(d.item_code) finished_items.append(d.item_code)
continue # Independent Manufacture Entry, no WO to match against continue
if d.item_code != production_item: if d.item_code != production_item:
frappe.throw(_("Finished Item {0} does not match with Work Order {1}") 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) finished_items.append(d.item_code)
if len(set(finished_items)) > 1: if not finished_items:
frappe.throw( frappe.throw(
msg=_("Multiple items cannot be marked as finished item"), msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name),
title=_("Note"), title=_("Missing Finished Good"), exc=FinishedGoodError
exc=FinishedGoodError
) )
if self.purpose == "Manufacture": if self.purpose == "Manufacture":
if not finished_items: if len(set(finished_items)) > 1:
frappe.throw( frappe.throw(
msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name), msg=_("Multiple items cannot be marked as finished item"),
title=_("Missing Finished Good"), title=_("Note"), exc=FinishedGoodError
exc=FinishedGoodError
) )
allowance_percentage = flt( allowance_percentage = flt(
@@ -1276,22 +1282,29 @@ class StockEntry(StockController):
if not self.pro_doc: if not self.pro_doc:
self.set_work_order_details() self.set_work_order_details()
scrap_items = frappe.db.sql(''' if not self.pro_doc.operations:
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:
return [] 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() used_scrap_items = self.get_used_scrap_items()
for row in scrap_items: for row in scrap_items:
row.stock_qty -= flt(used_scrap_items.get(row.item_code)) row.stock_qty -= flt(used_scrap_items.get(row.item_code))
@@ -1305,6 +1318,9 @@ class StockEntry(StockController):
return scrap_items 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): def get_used_scrap_items(self):
used_scrap_items = defaultdict(float) used_scrap_items = defaultdict(float)
data = frappe.get_all( data = frappe.get_all(

View File

@@ -227,9 +227,47 @@ class TestStockEntry(ERPNextTestCase):
mtn.cancel() mtn.cancel()
def test_repack_no_change_in_valuation(self): def test_repack_multiple_fg(self):
company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company') "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", target="_Test Warehouse - _TC", qty=50, basic_rate=100)
make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC",
qty=50, basic_rate=100) 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].allow_zero_valuation_rate, 1)
self.assertEqual(se.get("items")[0].amount, 0) 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): def test_gle_for_opening_stock_entry(self):
mr = make_stock_entry(item_code="_Test Item", target="Stores - TCP1", mr = make_stock_entry(item_code="_Test Item", target="Stores - TCP1",
company="_Test Company with perpetual inventory", qty=50, basic_rate=100, company="_Test Company with perpetual inventory", qty=50, basic_rate=100,

View File

@@ -5,7 +5,10 @@ import frappe
from frappe.core.page.permission_manager.permission_manager import reset from frappe.core.page.permission_manager.permission_manager import reset
from frappe.utils import add_days, today from frappe.utils import add_days, today
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.delivery_note.test_delivery_note import (
create_delivery_note,
create_return_delivery_note,
)
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import ( from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
create_landed_cost_voucher, create_landed_cost_voucher,
@@ -232,8 +235,7 @@ class TestStockLedgerEntry(ERPNextTestCase):
self.assertEqual(outgoing_rate, 100) self.assertEqual(outgoing_rate, 100)
# Return Entry: Qty = -2, Rate = 150 # Return Entry: Qty = -2, Rate = 150
return_dn = create_delivery_note(is_return=1, return_against=dn.name, item_code=bundled_item, qty=-2, rate=150, return_dn = create_return_delivery_note(source_name=dn.name, rate=150, qty=-2)
company=company, warehouse="Stores - _TC", expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC")
# check incoming rate for Return entry # check incoming rate for Return entry
incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry",

View File

@@ -6,7 +6,7 @@
import frappe import frappe
from frappe.utils import add_days, flt, nowdate, nowtime, random_string from frappe.utils import add_days, cstr, flt, nowdate, nowtime, random_string
from erpnext.accounts.utils import get_stock_and_account_balance from erpnext.accounts.utils import get_stock_and_account_balance
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item
@@ -439,8 +439,8 @@ class TestStockReconciliation(ERPNextTestCase):
self.assertRaises(frappe.ValidationError, sr.submit) self.assertRaises(frappe.ValidationError, sr.submit)
def test_serial_no_cancellation(self): def test_serial_no_cancellation(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
item = create_item("Stock-Reco-Serial-Item-9", is_stock_item=1) item = create_item("Stock-Reco-Serial-Item-9", is_stock_item=1)
if not item.has_serial_no: if not item.has_serial_no:
item.has_serial_no = 1 item.has_serial_no = 1
@@ -466,6 +466,31 @@ class TestStockReconciliation(ERPNextTestCase):
self.assertEqual(len(active_sr_no), 10) self.assertEqual(len(active_sr_no), 10)
def test_serial_no_creation_and_inactivation(self):
item = create_item("_TestItemCreatedWithStockReco", is_stock_item=1)
if not item.has_serial_no:
item.has_serial_no = 1
item.save()
item_code = item.name
warehouse = "_Test Warehouse - _TC"
sr = create_stock_reconciliation(item_code=item.name, warehouse=warehouse,
serial_no="SR-CREATED-SR-NO", qty=1, do_not_submit=True, rate=100)
sr.save()
self.assertEqual(cstr(sr.items[0].current_serial_no), "")
sr.submit()
active_sr_no = frappe.get_all("Serial No",
filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"})
self.assertEqual(len(active_sr_no), 1)
sr.cancel()
active_sr_no = frappe.get_all("Serial No",
filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"})
self.assertEqual(len(active_sr_no), 0)
def create_batch_item_with_batch(item_name, batch_id): def create_batch_item_with_batch(item_name, batch_id):
batch_item_doc = create_item(item_name, is_stock_item=1) batch_item_doc = create_item(item_name, is_stock_item=1)
if not batch_item_doc.has_batch_no: if not batch_item_doc.has_batch_no:

View File

@@ -55,7 +55,8 @@ def get_stock_ledger_entries(filters):
return frappe.db.sql("""select item_code, batch_no, warehouse, return frappe.db.sql("""select item_code, batch_no, warehouse,
posting_date, actual_qty posting_date, actual_qty
from `tabStock Ledger Entry` 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) conditions, as_dict=1)
def get_item_warehouse_batch_map(filters, float_precision): 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] voucher_nos = [fe.get('voucher_no') for fe in filtered_entries]
svd_list = frappe.get_list( svd_list = frappe.get_list(
'Stock Ledger Entry', fields=['item_code','stock_value_difference'], '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) assign_item_groups_to_svd_list(svd_list)
return 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', 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'] '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'): if report_filters.get('item_code'):
filters['item_code'] = report_filters.get('item_code') filters['item_code'] = report_filters.get('item_code')

Some files were not shown because too many files have changed in this diff Show More